5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2011 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
32 require 'booh/libadds'
33 require 'booh/GtkAutoTable'
37 bindtextdomain("booh")
39 require 'booh/rexml/document'
42 require 'booh/booh-lib'
44 require 'booh/UndoHandler'
49 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
50 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
51 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
54 #- default values for some globals
58 $ignore_videos = false
59 $button1_pressed_autotable = false
60 $generated_outofline = false
63 puts _("Usage: %s [OPTION]...") % File.basename($0)
65 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
70 parser = GetoptLong.new
71 parser.set_options(*$options.collect { |ary| ary[0..2] })
73 parser.each_option do |name, arg|
80 puts _("Booh version %s
82 Copyright (c) 2005-2011 Guillaume Cottenceau.
83 This is free software; see the source for copying conditions. There is NO
84 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
88 when '--verbose-level'
89 $verbose_level = arg.to_i
102 for line in IO.readlines('/proc/cpuinfo') do
103 line =~ /^processor/ and cpus += 1
110 $config_file = File.expand_path('~/.booh-gui-rc')
111 if File.readable?($config_file)
113 xmldoc = REXML::Document.new(File.new($config_file))
115 #- encoding unsupported anymore? file edited manually? ignore then
116 msg 1, "Ignoring #{$config_file}, failed to parse it: #{$!}"
119 xmldoc.root.elements.each { |element|
120 txt = element.get_text
122 if txt.value =~ /~~~/ || element.name == 'last-opens'
123 $config[element.name] = txt.value.split(/~~~/)
125 $config[element.name] = txt.value
127 elsif element.elements.size == 0
128 $config[element.name] = ''
130 $config[element.name] = {}
131 element.each { |chld|
133 $config[element.name][chld.name] = txt ? txt.value : nil
139 $config['video-viewer'] ||= '/usr/bin/mplayer %f || /usr/bin/vlc %f'
140 $config['image-editor'] ||= '/usr/bin/gimp-remote %f || /usr/bin/gimp %f'
141 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f || /usr/bin/firefox -remote 'openURL(%f,new-window)' || /usr/bin/firefox %f"
142 $config['use-flv'] ||= "true"
143 $config['flv-generator'] ||= "/usr/bin/ffmpeg -i %f -b 800k -ar 22050 -ab 32k %o"
144 $config['comments-format'] ||= '%t'
145 if !FileTest.directory?(File.expand_path('~/.booh'))
146 system("mkdir ~/.booh")
148 if $config['mproc'].nil?
151 $config['mproc'] = cpus
154 $config['rotate-set-exif'] ||= 'true'
159 def check_config_preferences_dep
160 viewer_binary = $config['video-viewer'].split.first
161 if viewer_binary && !File.executable?(viewer_binary)
162 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
163 You should fix this in Edit/Preferences so that you can view videos.
165 Problem was: '%s' is not an executable file.
166 Hint: don't forget to specify the full path to the executable,
167 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
170 flv_generator_binary = $config['use-flv'] == 'true' && $config['flv-generator'].split.first
171 if flv_generator_binary && !File.executable?(flv_generator_binary)
172 show_popup($main_window, utf8(_("The configured .flv generator seems to be unavailable.
173 You should fix this in Edit/Preferences so that you can have working
174 embedded flash videos.
176 Problem was: '%s' is not an executable file.
177 Hint: don't forget to specify the full path to the executable,
178 e.g. '/usr/bin/ffmpeg' is correct but 'ffmpeg' only is not.") % flv_generator_binary), { :pos_centered => true, :not_transient => true })
183 if !system("which convert >/dev/null 2>/dev/null")
184 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
185 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
188 if !system("which identify >/dev/null 2>/dev/null")
189 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
190 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
192 if !system("which exif >/dev/null 2>/dev/null")
193 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
195 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
197 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
200 check_config_preferences_dep
204 if $config['cpus'] && cpus > $config['cpus'].to_i
205 show_popup($main_window, utf8(_("It seems you now have more CPUs available than last time booh was run.
206 You should probably increase the amount of CPUs configured in Edit/Preferences,
207 so that web-albums are generated as fast as possible on this computer.")), { :pos_centered => true, :not_transient => true })
209 $config['cpus'] = cpus
212 def check_image_editor
213 if last_failed_binary = check_multi_binaries($config['image-editor'])
214 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
215 You should fix this in Edit/Preferences so that you can edit photos externally.
217 Problem was: '%s' is not an executable file.
218 Hint: don't forget to specify the full path to the executable,
219 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
227 if $config['last-opens'] && $config['last-opens'].size > 10
228 $config['last-opens'] = $config['last-opens'][-10, 10]
231 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
232 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
233 $config.each_pair { |key, value|
234 elem = xmldoc.root.add_element key
236 $config[key].each_pair { |subkey, subvalue|
237 subelem = elem.add_element subkey
238 subelem.add_text subvalue.to_s
240 elsif value.is_a? Array
241 elem.add_text value.join('~~~')
246 elem.add_text value.to_s
250 ios = File.open($config_file, "w")
254 $tempfiles.each { |f|
261 def set_mousecursor(what, *widget)
262 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
263 if widget[0] && widget[0].window
264 widget[0].window.cursor = cursor
266 if $main_window && $main_window.window
267 $main_window.window.cursor = cursor
269 $current_cursor = what
271 def set_mousecursor_wait(*widget)
272 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
273 if Thread.current == Thread.main
274 Gtk.main_iteration while Gtk.events_pending?
277 def set_mousecursor_normal(*widget)
278 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
280 def push_mousecursor_wait(*widget)
281 if $current_cursor != Gdk::Cursor::WATCH
282 $save_cursor = $current_cursor
283 gtk_thread_protect { set_mousecursor_wait(*widget) }
286 def pop_mousecursor(*widget)
287 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
291 source = $xmldoc.root.attributes['source']
292 dest = $xmldoc.root.attributes['destination']
293 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
296 def full_src_dir_to_rel(path, source)
297 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
300 def build_full_dest_filename(filename)
301 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
304 def save_undo(name, closure, *params)
305 UndoHandler.save_undo(name, closure, [ *params ])
306 $undo_tb.sensitive = $undo_mb.sensitive = true
307 $redo_tb.sensitive = $redo_mb.sensitive = false
310 def view_element(filename, closures)
311 if entry2type(filename) == 'video'
312 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
318 w = create_window.set_title(filename)
320 msg 3, "filename: #{filename}"
321 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
322 #- typically this file won't exist in case of videos; try with the largest thumbnail around
323 if !File.exists?(dest_img)
324 if entry2type(filename) == 'video'
325 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
326 if not alternatives.empty?
327 dest_img = alternatives[-1]
330 push_mousecursor_wait
331 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
333 if !File.exists?(dest_img)
334 msg 2, _("Could not generate fullscreen thumbnail!")
339 aspect = utf8(_("Aspect: unknown"))
340 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
342 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
344 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
345 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(vbox).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
346 evt.signal_connect('button-press-event') { |this, event|
347 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
348 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
350 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
352 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
353 delete_item.signal_connect('activate') {
355 closures[:delete].call(false)
358 menu.popup(nil, nil, event.button, event.time)
361 evt.signal_connect('button-release-event') { |this, event|
363 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
364 msg 3, "gesture delete: click-drag right button to the bottom"
366 closures[:delete].call(false)
367 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
371 tooltips = Gtk::Tooltips.new
372 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
374 w.signal_connect('key-press-event') { |w,event|
375 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
377 closures[:delete].call(false)
381 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
382 b.signal_connect('clicked') { w.destroy }
385 vb.pack_start(evt, false, false)
386 vb.pack_end(bottom, false, false)
389 w.signal_connect('delete-event') { w.destroy }
390 w.window_position = Gtk::Window::POS_CENTER
394 def scroll_upper(scrolledwindow, ypos_top)
395 newval = scrolledwindow.vadjustment.value -
396 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
397 if newval < scrolledwindow.vadjustment.lower
398 newval = scrolledwindow.vadjustment.lower
400 scrolledwindow.vadjustment.value = newval
403 def scroll_lower(scrolledwindow, ypos_bottom)
404 newval = scrolledwindow.vadjustment.value +
405 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
406 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
407 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
409 scrolledwindow.vadjustment.value = newval
412 def autoscroll_if_needed(scrolledwindow, image, textview)
413 #- autoscroll if cursor or image is not visible, if possible
414 if image && image.window || textview.window
415 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
416 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
417 current_miny_visible = scrolledwindow.vadjustment.value
418 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
419 if ypos_top < current_miny_visible
420 scroll_upper(scrolledwindow, ypos_top)
421 elsif ypos_bottom > current_maxy_visible
422 scroll_lower(scrolledwindow, ypos_bottom)
427 def create_editzone(scrolledwindow, pagenum, image)
428 frame = Gtk::Frame.new
429 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
430 frame.set_shadow_type(Gtk::SHADOW_IN)
431 textview.signal_connect('key-press-event') { |w, event|
432 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
433 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
434 scrolledwindow.signal_emit('key-press-event', event)
436 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
437 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
438 if event.keyval == Gdk::Keyval::GDK_Up
439 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
440 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
442 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
445 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
446 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
448 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
455 candidate_undo_text = nil
456 textview.signal_connect('focus-in-event') { |w, event|
457 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
458 candidate_undo_text = textview.buffer.text
462 textview.signal_connect('key-release-event') { |w, event|
463 if candidate_undo_text && candidate_undo_text != textview.buffer.text
465 save_undo(_("text edit"),
467 save_text = textview.buffer.text
468 textview.buffer.text = text
470 $notebook.set_page(pagenum)
472 textview.buffer.text = save_text
474 $notebook.set_page(pagenum)
476 }, candidate_undo_text)
477 candidate_undo_text = nil
480 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
481 autoscroll_if_needed(scrolledwindow, image, textview)
486 return [ frame, textview ]
489 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
491 if !$modified_pixbufs[thumbnail_img]
492 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
493 elsif !$modified_pixbufs[thumbnail_img][:orig]
494 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
497 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
500 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
501 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
502 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
503 if pixbuf.height > desired_y
504 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
505 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
506 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
511 if $modified_pixbufs[thumbnail_img][:whitebalance]
512 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
515 #- fix gamma correction
516 if $modified_pixbufs[thumbnail_img][:gammacorrect]
517 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
520 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
523 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
526 #- update rotate attribute
527 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
528 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
530 #- change exif orientation if configured so (but forget in case of thumbnails caption)
531 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
532 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
535 $modified_pixbufs[thumbnail_img] ||= {}
536 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
537 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
539 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
542 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
545 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
547 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
549 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
550 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
552 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
553 $notebook.set_page(0)
554 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
559 def color_swap(xmldir, attributes_prefix)
561 rexml_thread_protect {
562 if xmldir.attributes["#{attributes_prefix}color-swap"]
563 xmldir.delete_attribute("#{attributes_prefix}color-swap")
565 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
570 def enhance(xmldir, attributes_prefix)
572 rexml_thread_protect {
573 if xmldir.attributes["#{attributes_prefix}enhance"]
574 xmldir.delete_attribute("#{attributes_prefix}enhance")
576 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
581 def change_seektime(xmldir, attributes_prefix, value)
583 rexml_thread_protect {
584 xmldir.add_attribute("#{attributes_prefix}seektime", value)
588 def ask_new_seektime(xmldir, attributes_prefix)
589 value = rexml_thread_protect {
591 xmldir.attributes["#{attributes_prefix}seektime"]
597 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
599 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
600 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
601 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
605 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
609 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
610 entry.signal_connect('key-press-event') { |w, event|
611 if event.keyval == Gdk::Keyval::GDK_Return
612 dialog.response(Gtk::Dialog::RESPONSE_OK)
614 elsif event.keyval == Gdk::Keyval::GDK_Escape
615 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
618 false #- propagate if needed
622 dialog.window_position = Gtk::Window::POS_MOUSE
625 dialog.run { |response|
628 if response == Gtk::Dialog::RESPONSE_OK
630 msg 3, "changing seektime to #{newval}"
631 return { :old => value, :new => newval }
638 def change_pano_amount(xmldir, attributes_prefix, value)
640 rexml_thread_protect {
642 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
644 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
649 def ask_new_pano_amount(xmldir, attributes_prefix)
650 value = rexml_thread_protect {
652 xmldir.attributes["#{attributes_prefix}pano-amount"]
658 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
660 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
661 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
662 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
666 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
667 of this panorama image compared to other regular images. For example, if the panorama
668 was taken out of four photos on one row, counting the necessary overlap, the width of
669 this panorama image should probably be roughly three times the width of regular images.
671 With this information, booh will be able to generate panorama thumbnails looking
672 the right 'size', since the height of the thumbnail for this image will be similar
673 to the height of other thumbnails.
676 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
677 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
678 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
679 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
680 spin.signal_connect('value-changed') {
683 dialog.window_position = Gtk::Window::POS_MOUSE
686 spin.value = value.to_f
693 dialog.run { |response|
697 newval = spin.value.to_f
700 if response == Gtk::Dialog::RESPONSE_OK
702 msg 3, "changing panorama amount to #{newval}"
703 return { :old => value, :new => newval }
710 def change_whitebalance(xmlelem, attributes_prefix, value)
712 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
715 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
717 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
718 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
719 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
720 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
721 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
722 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
723 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
724 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
725 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
726 $modified_pixbufs[thumbnail_img] ||= {}
727 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
728 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
730 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
731 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
733 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
736 $modified_pixbufs[thumbnail_img] ||= {}
737 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
739 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
742 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
743 #- init $modified_pixbufs correctly
744 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
746 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
748 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
750 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
751 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
752 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
756 _("You can fix the <b>white balance</b> of the image, if your image is too blue
757 or too yellow because the recorder didn't detect the light correctly. Drag the
758 slider below the image to the left for more blue, to the right for more yellow.
762 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
764 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
766 dialog.window_position = Gtk::Window::POS_MOUSE
770 timeout = Gtk.timeout_add(100) {
771 if hs.value != lastval
774 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
780 dialog.run { |response|
781 Gtk.timeout_remove(timeout)
782 if response == Gtk::Dialog::RESPONSE_OK
784 newval = hs.value.to_s
785 msg 3, "changing white balance to #{newval}"
787 return { :old => value, :new => newval }
790 $modified_pixbufs[thumbnail_img] ||= {}
791 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
792 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
800 def change_gammacorrect(xmlelem, attributes_prefix, value)
802 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
805 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
807 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
808 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
809 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
810 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
811 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
812 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
813 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
814 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
815 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
816 $modified_pixbufs[thumbnail_img] ||= {}
817 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
818 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
820 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
821 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
823 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
826 $modified_pixbufs[thumbnail_img] ||= {}
827 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
829 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
832 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
833 #- init $modified_pixbufs correctly
834 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
836 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
838 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
840 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
841 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
842 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
846 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
847 or too bright. Drag the slider below the image.
851 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
853 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
855 dialog.window_position = Gtk::Window::POS_MOUSE
859 timeout = Gtk.timeout_add(100) {
860 if hs.value != lastval
863 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
869 dialog.run { |response|
870 Gtk.timeout_remove(timeout)
871 if response == Gtk::Dialog::RESPONSE_OK
873 newval = hs.value.to_s
874 msg 3, "gamma correction to #{newval}"
876 return { :old => value, :new => newval }
879 $modified_pixbufs[thumbnail_img] ||= {}
880 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
881 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
889 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
890 if File.exists?(destfile)
891 File.delete(destfile)
893 #- type can be 'element' or 'subdir'
895 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
897 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
901 $max_gen_thumbnail_threads = nil
902 $current_gen_thumbnail_threads = 0
903 $gen_thumbnail_monitor = Monitor.new
905 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
906 if $max_gen_thumbnail_threads.nil?
907 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
910 push_mousecursor_wait
911 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
914 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
919 $gen_thumbnail_monitor.synchronize {
920 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
921 $current_gen_thumbnail_threads += 1
926 msg 3, "generate thumbnail from new thread"
929 $gen_thumbnail_monitor.synchronize {
930 $current_gen_thumbnail_threads -= 1
934 msg 3, "generate thumbnail from current thread"
939 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
940 distribute_multiple_call = Proc.new { |action, arg|
941 $selected_elements.each_key { |path|
942 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
944 if possible_actions[:can_multiple] && $selected_elements.length > 0
945 UndoHandler.begin_batch
946 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
947 UndoHandler.end_batch
949 closures[action].call(arg)
951 $selected_elements = {}
954 if optionals.include?('change_image')
955 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
956 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
957 changeimg.signal_connect('activate') { closures[:change].call }
958 menu.append(Gtk::SeparatorMenuItem.new)
960 if !possible_actions[:can_multiple] || $selected_elements.length == 0
963 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
964 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
965 view.signal_connect('activate') { closures[:view].call }
967 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
968 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
969 view.signal_connect('activate') { closures[:view].call }
970 menu.append(Gtk::SeparatorMenuItem.new)
973 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
974 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
975 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
976 exif.signal_connect('activate') { show_popup($main_window,
977 utf8(`exif -m '#{fullpath}'`),
978 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
979 menu.append(Gtk::SeparatorMenuItem.new)
982 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
983 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
984 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
985 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
986 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
987 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
988 if !possible_actions[:can_multiple] || $selected_elements.length == 0
989 menu.append(Gtk::SeparatorMenuItem.new)
990 if !possible_actions[:forbid_left]
991 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
992 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
993 moveleft.signal_connect('activate') { closures[:move].call('left') }
994 if !possible_actions[:can_left]
995 moveleft.sensitive = false
998 if !possible_actions[:forbid_right]
999 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
1000 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
1001 moveright.signal_connect('activate') { closures[:move].call('right') }
1002 if !possible_actions[:can_right]
1003 moveright.sensitive = false
1006 if optionals.include?('move_top')
1007 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
1008 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
1009 movetop.signal_connect('activate') { closures[:move].call('top') }
1010 if !possible_actions[:can_top]
1011 movetop.sensitive = false
1014 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
1015 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
1016 moveup.signal_connect('activate') { closures[:move].call('up') }
1017 if !possible_actions[:can_up]
1018 moveup.sensitive = false
1020 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
1021 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
1022 movedown.signal_connect('activate') { closures[:move].call('down') }
1023 if !possible_actions[:can_down]
1024 movedown.sensitive = false
1026 if optionals.include?('move_bottom')
1027 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
1028 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
1029 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
1030 if !possible_actions[:can_bottom]
1031 movebottom.sensitive = false
1036 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
1037 menu.append(Gtk::SeparatorMenuItem.new)
1038 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
1039 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
1040 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
1041 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1042 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1043 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1044 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1045 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1046 seektime.signal_connect('activate') {
1047 if possible_actions[:can_multiple] && $selected_elements.length > 0
1048 if values = ask_new_seektime(nil, '')
1049 distribute_multiple_call.call(:seektime, values)
1052 closures[:seektime].call
1057 menu.append( Gtk::SeparatorMenuItem.new)
1058 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1059 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1060 gammacorrect.signal_connect('activate') {
1061 if possible_actions[:can_multiple] && $selected_elements.length > 0
1062 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1063 distribute_multiple_call.call(:gammacorrect, values)
1066 closures[:gammacorrect].call
1069 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1070 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1071 whitebalance.signal_connect('activate') {
1072 if possible_actions[:can_multiple] && $selected_elements.length > 0
1073 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1074 distribute_multiple_call.call(:whitebalance, values)
1077 closures[:whitebalance].call
1080 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1081 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1082 _("Enhance constrast"))))
1084 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1086 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1087 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1088 if type == 'image' && possible_actions[:can_panorama]
1089 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1090 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1091 panorama.signal_connect('activate') {
1092 if possible_actions[:can_multiple] && $selected_elements.length > 0
1093 if values = ask_new_pano_amount(nil, '')
1094 distribute_multiple_call.call(:pano, values)
1097 distribute_multiple_call.call(:pano)
1101 menu.append( Gtk::SeparatorMenuItem.new)
1102 if optionals.include?('delete')
1103 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1104 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1105 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1106 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1107 paste_item.signal_connect('activate') { closures[:paste].call }
1108 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1109 clear_item.signal_connect('activate') { $cuts = [] }
1111 paste_item.sensitive = clear_item.sensitive = false
1114 menu.append( Gtk::SeparatorMenuItem.new)
1116 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1117 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1118 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1119 editexternally.signal_connect('activate') {
1120 if check_image_editor
1121 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1127 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1128 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1129 if optionals.include?('delete')
1130 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1131 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1134 menu.popup(nil, nil, event.button, event.time)
1137 def delete_current_subalbum
1139 sel = $albums_tv.selection.selected_rows
1140 $xmldir.elements.each { |e|
1141 if e.name == 'image' || e.name == 'video'
1142 e.add_attribute('deleted', 'true')
1145 #- branch if we have a non deleted subalbum
1146 if $xmldir.child_byname_notattr('dir', 'deleted')
1147 $xmldir.delete_attribute('thumbnails-caption')
1148 $xmldir.delete_attribute('thumbnails-captionfile')
1150 $xmldir.add_attribute('deleted', 'true')
1152 while moveup.parent.name == 'dir'
1153 moveup = moveup.parent
1154 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1155 moveup.add_attribute('deleted', 'true')
1162 save_changes('forced')
1163 populate_subalbums_treeview(false)
1164 $albums_tv.selection.select_path(sel[0])
1170 $current_path = nil #- prevent save_changes from being rerun again
1171 sel = $albums_tv.selection.selected_rows
1172 restore_one = proc { |xmldir|
1173 xmldir.elements.each { |e|
1174 if e.name == 'dir' && e.attributes['deleted']
1177 e.delete_attribute('deleted')
1180 restore_one.call($xmldir)
1181 populate_subalbums_treeview(false)
1182 $albums_tv.selection.select_path(sel[0])
1185 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1188 frame1 = Gtk::Frame.new
1189 fullpath = from_utf8("#{$current_path}/#{filename}")
1191 my_gen_real_thumbnail = proc {
1192 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1196 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1197 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1198 pack_start(img = Gtk::Image.new).
1199 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1200 px, mask = pxb.render_pixmap_and_mask(0)
1201 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1202 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1204 frame1.add(img = Gtk::Image.new)
1207 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1208 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1209 my_gen_real_thumbnail.call
1211 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1214 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1216 tooltips = Gtk::Tooltips.new
1217 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1218 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1220 frame2, textview = create_editzone($autotable_sw, 1, img)
1221 textview.buffer.text = caption
1222 textview.set_justification(Gtk::Justification::CENTER)
1224 vbox = Gtk::VBox.new(false, 5)
1225 vbox.pack_start(evtbox, false, false)
1226 vbox.pack_start(frame2, false, false)
1227 autotable.append(vbox, filename)
1229 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1230 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1232 #- to be able to find widgets by name
1233 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1235 cleanup_all_thumbnails = proc {
1236 #- remove out of sync images
1237 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1238 for sizeobj in $images_size
1239 #- cannot use sizeobj because panoramic images will have a larger width
1240 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1248 cleanup_all_thumbnails.call
1249 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1251 rexml_thread_protect {
1252 $xmldir.delete_attribute('already-generated')
1254 my_gen_real_thumbnail.call
1257 rotate_and_cleanup = proc { |angle|
1258 cleanup_all_thumbnails.call
1259 rexml_thread_protect {
1260 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1264 move = proc { |direction|
1265 do_method = "move_#{direction}"
1266 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1268 done = autotable.method(do_method).call(vbox)
1269 textview.grab_focus #- because if moving, focus is stolen
1273 save_undo(_("move %s") % direction,
1275 autotable.method(undo_method).call(vbox)
1276 textview.grab_focus #- because if moving, focus is stolen
1277 autoscroll_if_needed($autotable_sw, img, textview)
1278 $notebook.set_page(1)
1280 autotable.method(do_method).call(vbox)
1281 textview.grab_focus #- because if moving, focus is stolen
1282 autoscroll_if_needed($autotable_sw, img, textview)
1283 $notebook.set_page(1)
1289 color_swap_and_cleanup = proc {
1290 perform_color_swap_and_cleanup = proc {
1291 cleanup_all_thumbnails.call
1292 rexml_thread_protect {
1293 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1295 my_gen_real_thumbnail.call
1298 perform_color_swap_and_cleanup.call
1300 save_undo(_("color swap"),
1302 perform_color_swap_and_cleanup.call
1304 autoscroll_if_needed($autotable_sw, img, textview)
1305 $notebook.set_page(1)
1307 perform_color_swap_and_cleanup.call
1309 autoscroll_if_needed($autotable_sw, img, textview)
1310 $notebook.set_page(1)
1315 change_seektime_and_cleanup_real = proc { |values|
1316 perform_change_seektime_and_cleanup = proc { |val|
1317 cleanup_all_thumbnails.call
1318 rexml_thread_protect {
1319 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1321 my_gen_real_thumbnail.call
1323 perform_change_seektime_and_cleanup.call(values[:new])
1325 save_undo(_("specify seektime"),
1327 perform_change_seektime_and_cleanup.call(values[:old])
1329 autoscroll_if_needed($autotable_sw, img, textview)
1330 $notebook.set_page(1)
1332 perform_change_seektime_and_cleanup.call(values[:new])
1334 autoscroll_if_needed($autotable_sw, img, textview)
1335 $notebook.set_page(1)
1340 change_seektime_and_cleanup = proc {
1341 rexml_thread_protect {
1342 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1343 change_seektime_and_cleanup_real.call(values)
1348 change_pano_amount_and_cleanup_real = proc { |values|
1349 perform_change_pano_amount_and_cleanup = proc { |val|
1350 cleanup_all_thumbnails.call
1351 rexml_thread_protect {
1352 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1355 perform_change_pano_amount_and_cleanup.call(values[:new])
1357 save_undo(_("change panorama amount"),
1359 perform_change_pano_amount_and_cleanup.call(values[:old])
1361 autoscroll_if_needed($autotable_sw, img, textview)
1362 $notebook.set_page(1)
1364 perform_change_pano_amount_and_cleanup.call(values[:new])
1366 autoscroll_if_needed($autotable_sw, img, textview)
1367 $notebook.set_page(1)
1372 change_pano_amount_and_cleanup = proc {
1373 rexml_thread_protect {
1374 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1375 change_pano_amount_and_cleanup_real.call(values)
1380 whitebalance_and_cleanup_real = proc { |values|
1381 perform_change_whitebalance_and_cleanup = proc { |val|
1382 cleanup_all_thumbnails.call
1383 rexml_thread_protect {
1384 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1385 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1386 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1389 perform_change_whitebalance_and_cleanup.call(values[:new])
1391 save_undo(_("fix white balance"),
1393 perform_change_whitebalance_and_cleanup.call(values[:old])
1395 autoscroll_if_needed($autotable_sw, img, textview)
1396 $notebook.set_page(1)
1398 perform_change_whitebalance_and_cleanup.call(values[:new])
1400 autoscroll_if_needed($autotable_sw, img, textview)
1401 $notebook.set_page(1)
1406 whitebalance_and_cleanup = proc {
1407 rexml_thread_protect {
1408 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1409 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1410 whitebalance_and_cleanup_real.call(values)
1415 gammacorrect_and_cleanup_real = proc { |values|
1416 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1417 cleanup_all_thumbnails.call
1418 rexml_thread_protect {
1419 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1420 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1421 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1424 perform_change_gammacorrect_and_cleanup.call(values[:new])
1426 save_undo(_("gamma correction"),
1428 perform_change_gammacorrect_and_cleanup.call(values[:old])
1430 autoscroll_if_needed($autotable_sw, img, textview)
1431 $notebook.set_page(1)
1433 perform_change_gammacorrect_and_cleanup.call(values[:new])
1435 autoscroll_if_needed($autotable_sw, img, textview)
1436 $notebook.set_page(1)
1441 gammacorrect_and_cleanup = Proc.new {
1442 rexml_thread_protect {
1443 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1444 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1445 gammacorrect_and_cleanup_real.call(values)
1450 enhance_and_cleanup = proc {
1451 perform_enhance_and_cleanup = proc {
1452 cleanup_all_thumbnails.call
1453 rexml_thread_protect {
1454 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1456 my_gen_real_thumbnail.call
1459 cleanup_all_thumbnails.call
1460 perform_enhance_and_cleanup.call
1462 save_undo(_("enhance"),
1464 perform_enhance_and_cleanup.call
1466 autoscroll_if_needed($autotable_sw, img, textview)
1467 $notebook.set_page(1)
1469 perform_enhance_and_cleanup.call
1471 autoscroll_if_needed($autotable_sw, img, textview)
1472 $notebook.set_page(1)
1477 delete = proc { |isacut|
1478 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1481 perform_delete = proc {
1482 after = autotable.get_next_widget(vbox)
1484 after = autotable.get_previous_widget(vbox)
1486 if $config['deleteondisk'] && !isacut
1487 msg 3, "scheduling for delete: #{fullpath}"
1488 $todelete << fullpath
1490 autotable.remove_widget(vbox)
1492 $vbox2widgets[after][:textview].grab_focus
1493 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1497 previous_pos = autotable.get_current_number(vbox)
1501 delete_current_subalbum
1503 save_undo(_("delete"),
1505 autotable.reinsert(pos, vbox, filename)
1506 $notebook.set_page(1)
1507 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1509 msg 3, "removing deletion schedule of: #{fullpath}"
1510 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1513 $notebook.set_page(1)
1522 $cuts << { :vbox => vbox, :filename => filename }
1523 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1528 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1531 autotable.queue_draws << proc {
1532 $vbox2widgets[last[:vbox]][:textview].grab_focus
1533 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1535 save_undo(_("paste"),
1537 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1538 $notebook.set_page(1)
1541 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1543 $notebook.set_page(1)
1546 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1551 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1552 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1553 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1554 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1556 textview.signal_connect('key-press-event') { |w, event|
1559 x, y = autotable.get_current_pos(vbox)
1560 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1561 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1562 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1563 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1565 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1566 $vbox2widgets[widget_up][:textview].grab_focus
1573 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1575 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1576 $vbox2widgets[widget_down][:textview].grab_focus
1583 if event.keyval == Gdk::Keyval::GDK_Left
1586 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1593 rotate_and_cleanup.call(-90)
1596 if event.keyval == Gdk::Keyval::GDK_Right
1597 next_ = autotable.get_next_widget(vbox)
1598 if next_ && autotable.get_current_pos(next_)[0] > x
1600 $vbox2widgets[next_][:textview].grab_focus
1607 rotate_and_cleanup.call(90)
1610 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1613 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1614 view_element(filename, { :delete => delete })
1617 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1620 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1624 !propagate #- propagate if needed
1627 $ignore_next_release = false
1628 evtbox.signal_connect('button-press-event') { |w, event|
1629 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1630 if event.state & Gdk::Window::BUTTON3_MASK != 0
1631 #- gesture redo: hold right mouse button then click left mouse button
1632 $config['nogestures'] or perform_redo
1633 $ignore_next_release = true
1635 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1637 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1639 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1640 elsif $enhance.active?
1641 enhance_and_cleanup.call
1642 elsif $delete.active?
1646 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1649 $button1_pressed_autotable = true
1650 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1651 if event.state & Gdk::Window::BUTTON1_MASK != 0
1652 #- gesture undo: hold left mouse button then click right mouse button
1653 $config['nogestures'] or perform_undo
1654 $ignore_next_release = true
1656 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1657 view_element(filename, { :delete => delete })
1662 evtbox.signal_connect('button-release-event') { |w, event|
1663 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1664 if !$ignore_next_release
1665 x, y = autotable.get_current_pos(vbox)
1666 next_ = autotable.get_next_widget(vbox)
1667 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1668 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1669 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1670 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1671 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1672 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1673 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1675 $ignore_next_release = false
1676 $gesture_press = nil
1681 #- handle reordering with drag and drop
1682 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1683 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1684 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1685 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1688 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1690 #- mouse gesture first (dnd disables button-release-event)
1691 if $gesture_press && $gesture_press[:filename] == filename
1692 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1693 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1694 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1695 rotate_and_cleanup.call(angle)
1696 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1698 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1699 msg 3, "gesture delete: click-drag right button to the bottom"
1701 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1706 ctxt.targets.each { |target|
1707 if target.name == 'reorder-elements'
1708 move_dnd = proc { |from,to|
1711 autotable.move(from, to)
1712 save_undo(_("reorder"),
1715 autotable.move(to - 1, from)
1717 autotable.move(to, from + 1)
1719 $notebook.set_page(1)
1721 autotable.move(from, to)
1722 $notebook.set_page(1)
1727 if $multiple_dnd.size == 0
1728 move_dnd.call(selection_data.data.to_i,
1729 autotable.get_current_number(vbox))
1731 UndoHandler.begin_batch
1732 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1734 #- need to update current position between each call
1735 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1736 autotable.get_current_number(vbox))
1738 UndoHandler.end_batch
1749 def create_auto_table
1751 $autotable = Gtk::AutoTable.new(5)
1753 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1754 thumbnails_vb = Gtk::VBox.new(false, 5)
1756 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1757 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1758 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1759 thumbnails_vb.add($autotable)
1761 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1762 $autotable_sw.add_with_viewport(thumbnails_vb)
1764 #- follows stuff for handling multiple elements selection
1765 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1767 update_selected = proc {
1768 $autotable.current_order.each { |path|
1769 w = $name2widgets[path][:evtbox].window
1770 xm = w.position[0] + w.size[0]/2
1771 ym = w.position[1] + w.size[1]/2
1772 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1773 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1774 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1775 if $name2widgets[path][:img].pixbuf
1776 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1780 if $selected_elements[path] && ! $selected_elements[path][:keep]
1781 if ((xm < press_x && xm < pos_x || xm > pos_x && xm > press_x) || (ym < press_y && ym < pos_y || ym > pos_y && ym > press_y))
1782 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1783 $selected_elements.delete(path)
1788 $autotable.signal_connect('realize') { |w,e|
1789 gc = Gdk::GC.new($autotable.window)
1790 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1791 gc.function = Gdk::GC::INVERT
1792 #- autoscroll handling for DND and multiple selections
1793 Gtk.timeout_add(100) {
1794 if ! $autotable.window.nil?
1795 w, x, y, mask = $autotable.window.pointer
1796 if mask & Gdk::Window::BUTTON1_MASK != 0
1797 if y < $autotable_sw.vadjustment.value
1799 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1801 if $button1_pressed_autotable || press_x
1802 scroll_upper($autotable_sw, y)
1805 w, pos_x, pos_y = $autotable.window.pointer
1806 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1807 update_selected.call
1810 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1812 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1814 if $button1_pressed_autotable || press_x
1815 scroll_lower($autotable_sw, y)
1818 w, pos_x, pos_y = $autotable.window.pointer
1819 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1820 update_selected.call
1825 ! $autotable.window.nil?
1829 $autotable.signal_connect('button-press-event') { |w,e|
1831 if !$button1_pressed_autotable
1834 if e.state & Gdk::Window::SHIFT_MASK == 0
1835 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1836 $selected_elements = {}
1837 $statusbar.push(0, utf8(_("Nothing selected.")))
1839 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1841 set_mousecursor(Gdk::Cursor::TCROSS)
1845 $autotable.signal_connect('button-release-event') { |w,e|
1847 if $button1_pressed_autotable
1848 #- unselect all only now
1849 $multiple_dnd = $selected_elements.keys
1850 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1851 $selected_elements = {}
1852 $button1_pressed_autotable = false
1855 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1856 if $selected_elements.length > 0
1857 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1860 press_x = press_y = pos_x = pos_y = nil
1861 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1865 $autotable.signal_connect('motion-notify-event') { |w,e|
1868 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1872 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1873 update_selected.call
1879 def create_subalbums_page
1881 subalbums_hb = Gtk::HBox.new
1882 $subalbums_vb = Gtk::VBox.new(false, 5)
1883 subalbums_hb.pack_start($subalbums_vb, false, false)
1884 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1885 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1886 $subalbums_sw.add_with_viewport(subalbums_hb)
1889 def save_current_file
1895 ios = File.open($filename, "w")
1898 rescue Iconv::IllegalSequence
1899 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1900 if ! ios.nil? && ! ios.closed?
1903 $xmldoc.xml_decl.encoding = 'UTF-8'
1904 ios = File.open($filename, "w")
1916 def save_current_file_user
1917 save_tempfilename = $filename
1918 $filename = $orig_filename
1919 if ! save_current_file
1920 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1921 $filename = save_tempfilename
1925 $generated_outofline = false
1926 $filename = save_tempfilename
1928 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1929 $todelete.each { |f|
1933 puts "Failed to delete #{f}: #{$!}"
1939 def mark_document_as_dirty
1940 $xmldoc.elements.each('//dir') { |elem|
1941 elem.delete_attribute('already-generated')
1945 #- ret: true => ok false => cancel
1946 def ask_save_modifications(msg1, msg2, *options)
1948 options = options.size > 0 ? options[0] : {}
1950 if options[:disallow_cancel]
1951 dialog = Gtk::Dialog.new(msg1,
1953 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1954 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1955 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1957 dialog = Gtk::Dialog.new(msg1,
1959 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1960 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1961 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1962 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1964 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1965 dialog.vbox.add(Gtk::Label.new(msg2))
1966 dialog.window_position = Gtk::Window::POS_CENTER
1969 dialog.run { |response|
1971 if response == Gtk::Dialog::RESPONSE_YES
1972 if ! save_current_file_user
1973 return ask_save_modifications(msg1, msg2, options)
1976 #- if we have generated an album but won't save modifications, we must remove
1977 #- already-generated markers in original file
1978 if $generated_outofline
1980 $xmldoc = REXML::Document.new(File.new($orig_filename))
1981 mark_document_as_dirty
1982 ios = File.open($orig_filename, "w")
1986 puts "exception: #{$!}"
1990 if response == Gtk::Dialog::RESPONSE_CANCEL
1998 def try_quit(*options)
1999 if ask_save_modifications(utf8(_("Save before quitting?")),
2000 utf8(_("Do you want to save your changes before quitting?")),
2006 def show_popup(parent, msg, *options)
2007 dialog = Gtk::Dialog.new
2008 if options[0] && options[0][:title]
2009 dialog.title = options[0][:title]
2011 dialog.title = utf8(_("Booh message"))
2013 lbl = Gtk::Label.new
2014 if options[0] && options[0][:nomarkup]
2019 if options[0] && options[0][:centered]
2020 lbl.set_justify(Gtk::Justification::CENTER)
2022 if options[0] && options[0][:selectable]
2023 lbl.selectable = true
2025 if options[0] && options[0][:scrolled]
2026 sw = Gtk::ScrolledWindow.new(nil, nil)
2027 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
2028 sw.add_with_viewport(lbl)
2030 dialog.set_default_size(500, 600)
2032 dialog.vbox.add(lbl)
2033 dialog.set_default_size(200, 120)
2035 if options[0] && options[0][:okcancel]
2036 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2038 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2040 if options[0] && options[0][:pos_centered]
2041 dialog.window_position = Gtk::Window::POS_CENTER
2043 dialog.window_position = Gtk::Window::POS_MOUSE
2046 if options[0] && options[0][:linkurl]
2047 linkbut = Gtk::Button.new('')
2048 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2049 linkbut.signal_connect('clicked') {
2050 open_url(options[0][:linkurl])
2051 dialog.response(Gtk::Dialog::RESPONSE_OK)
2052 set_mousecursor_normal
2054 linkbut.relief = Gtk::RELIEF_NONE
2055 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2056 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2057 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2062 if !options[0] || !options[0][:not_transient]
2063 dialog.transient_for = parent
2064 dialog.run { |response|
2066 if options[0] && options[0][:okcancel]
2067 return response == Gtk::Dialog::RESPONSE_OK
2071 dialog.signal_connect('response') { dialog.destroy }
2075 def set_mainwindow_title(progress)
2076 filename = $orig_filename || $filename
2079 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2081 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2085 $main_window.title = 'booh - ' + File.basename(filename)
2087 $main_window.title = 'booh'
2092 def backend_wait_message(parent, msg, infopipe_path, mode)
2094 w.set_transient_for(parent)
2097 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2098 vb.pack_start(Gtk::Label.new(msg), false, false)
2100 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2101 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2102 if mode != 'one dir scan'
2103 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2105 if mode == 'web-album'
2106 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2107 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2109 vb.pack_start(Gtk::HSeparator.new, false, false)
2111 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2112 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2113 vb.pack_end(bottom, false, false)
2116 update_progression_title_pb1 = proc {
2117 if mode == 'web-album'
2118 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2119 elsif mode != 'one dir scan'
2120 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2122 set_mainwindow_title(pb1_1.fraction)
2126 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2127 refresh_thread = Thread.new {
2128 directories_counter = 0
2129 while line = infopipe.gets
2130 msg 3, "infopipe got data: #{line}"
2131 if line =~ /^directories: (\d+), sizes: (\d+)/
2132 directories = $1.to_f + 1
2134 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2135 elements = $3.to_f + 1
2136 if mode == 'web-album'
2140 gtk_thread_protect { pb1_1.fraction = 0 }
2141 if mode != 'one dir scan'
2142 newtext = utf8(full_src_dir_to_rel($1, $2))
2143 newtext = '/' if newtext == ''
2144 gtk_thread_protect { pb1_2.text = newtext }
2145 directories_counter += 1
2146 gtk_thread_protect {
2147 pb1_2.fraction = directories_counter / directories
2148 update_progression_title_pb1.call
2151 elsif line =~ /^processing element$/
2152 element_counter += 1
2153 gtk_thread_protect {
2154 pb1_1.fraction = element_counter / elements
2155 update_progression_title_pb1.call
2157 elsif line =~ /^processing size$/
2158 element_counter += 1
2159 gtk_thread_protect {
2160 pb1_1.fraction = element_counter / elements
2161 update_progression_title_pb1.call
2163 elsif line =~ /^finished processing sizes$/
2164 gtk_thread_protect { pb1_1.fraction = 1 }
2165 elsif line =~ /^creating index.html$/
2166 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2167 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2168 directories_counter = 0
2169 elsif line =~ /^index.html: (.+)\|(.+)/
2170 newtext = utf8(full_src_dir_to_rel($1, $2))
2171 newtext = '/' if newtext == ''
2172 gtk_thread_protect { pb2.text = newtext }
2173 directories_counter += 1
2174 gtk_thread_protect {
2175 pb2.fraction = directories_counter / directories
2176 set_mainwindow_title(0.9 + pb2.fraction / 10)
2178 elsif line =~ /^die: (.*)$/
2185 w.signal_connect('delete-event') { w.destroy }
2186 w.signal_connect('destroy') {
2187 Thread.kill(refresh_thread)
2188 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2191 File.delete(infopipe_path)
2193 set_mainwindow_title(nil)
2195 w.window_position = Gtk::Window::POS_CENTER
2201 def call_backend(cmd, waitmsg, mode, params)
2202 pipe = Tempfile.new("boohpipe")
2205 system("mkfifo #{path}")
2206 cmd += " --info-pipe #{path}"
2207 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2212 id, exitstatus = Process.waitpid2(pid)
2213 gtk_thread_protect { w8.destroy }
2215 if params[:successmsg]
2216 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2218 if params[:closure_after]
2219 gtk_thread_protect(¶ms[:closure_after])
2221 elsif exitstatus == 15
2222 #- say nothing, user aborted
2224 gtk_thread_protect { show_popup($main_window,
2225 utf8($diemsg ? _("Unexpected internal error, sorry:\n\n%s") % $diemsg :
2226 _("Unexpected internal error, sorry.\nCheck console for error message."))) }
2233 button.signal_connect('clicked') {
2234 Process.kill('SIGTERM', pid)
2238 def save_changes(*forced)
2239 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2243 $xmldir.delete_attribute('already-generated')
2245 propagate_children = proc { |xmldir|
2246 if xmldir.attributes['subdirs-caption']
2247 xmldir.delete_attribute('already-generated')
2249 xmldir.elements.each('dir') { |element|
2250 propagate_children.call(element)
2254 if $xmldir.child_byname_notattr('dir', 'deleted')
2255 new_title = $subalbums_title.buffer.text
2256 if new_title != $xmldir.attributes['subdirs-caption']
2257 parent = $xmldir.parent
2258 if parent.name == 'dir'
2259 parent.delete_attribute('already-generated')
2261 propagate_children.call($xmldir)
2263 $xmldir.add_attribute('subdirs-caption', new_title)
2264 $xmldir.elements.each('dir') { |element|
2265 if !element.attributes['deleted']
2266 path = element.attributes['path']
2267 newtext = $subalbums_edits[path][:editzone].buffer.text
2268 if element.attributes['subdirs-caption']
2269 if element.attributes['subdirs-caption'] != newtext
2270 propagate_children.call(element)
2272 element.add_attribute('subdirs-caption', newtext)
2273 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2275 if element.attributes['thumbnails-caption'] != newtext
2276 element.delete_attribute('already-generated')
2278 element.add_attribute('thumbnails-caption', newtext)
2279 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2285 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2286 if $xmldir.attributes['thumbnails-caption']
2287 path = $xmldir.attributes['path']
2288 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2290 elsif $xmldir.attributes['thumbnails-caption']
2291 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2294 if $xmldir.attributes['thumbnails-caption']
2295 if edit = $subalbums_edits[$xmldir.attributes['path']]
2296 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2300 #- remove and reinsert elements to reflect new ordering
2303 $xmldir.elements.each { |element|
2304 if element.name == 'image' || element.name == 'video'
2305 saves[element.attributes['filename']] = element.remove
2309 $autotable.current_order.each { |path|
2310 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2311 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2314 saves.each_key { |path|
2315 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2316 chld.add_attribute('deleted', 'true')
2320 def sort_by_exif_date
2324 rexml_thread_protect {
2325 $xmldir.elements.each { |element|
2326 if element.name == 'image' || element.name == 'video'
2327 current_order << element.attributes['filename']
2332 #- look for EXIF dates
2335 if current_order.size > 20
2337 w.set_transient_for($main_window)
2339 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2340 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2341 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2342 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2343 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2344 vb.pack_end(bottom, false, false)
2346 w.signal_connect('delete-event') { w.destroy }
2347 w.window_position = Gtk::Window::POS_CENTER
2351 b.signal_connect('clicked') { aborted = true }
2353 current_order.each { |f|
2355 if entry2type(f) == 'image'
2357 pb.fraction = i.to_f / current_order.size
2358 Gtk.main_iteration while Gtk.events_pending?
2359 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2361 dates[f] = date_time
2374 current_order.each { |f|
2375 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2377 dates[f] = date_time
2383 rexml_thread_protect {
2384 $xmldir.elements.each { |element|
2385 if element.name == 'image' || element.name == 'video'
2386 saves[element.attributes['filename']] = element.remove
2391 neworder = smartsort(current_order, dates)
2393 rexml_thread_protect {
2395 $xmldir.add_element(saves[f].name, saves[f].attributes)
2399 #- let the auto-table reflect new ordering
2403 def remove_all_captions
2406 $autotable.current_order.each { |path|
2407 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2408 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2410 save_undo(_("remove all captions"),
2412 texts.each_key { |key|
2413 $name2widgets[key][:textview].buffer.text = texts[key]
2415 $notebook.set_page(1)
2417 texts.each_key { |key|
2418 $name2widgets[key][:textview].buffer.text = ''
2420 $notebook.set_page(1)
2426 $selected_elements.each_key { |path|
2427 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2433 $selected_elements = {}
2437 $undo_tb.sensitive = $undo_mb.sensitive = false
2438 $redo_tb.sensitive = $redo_mb.sensitive = false
2444 $subalbums_vb.children.each { |chld|
2445 $subalbums_vb.remove(chld)
2447 $subalbums = Gtk::Table.new(0, 0, true)
2448 current_y_sub_albums = 0
2450 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2451 $subalbums_edits = {}
2452 subalbums_counter = 0
2453 subalbums_edits_bypos = {}
2455 add_subalbum = proc { |xmldir, counter|
2456 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2457 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2458 if xmldir == $xmldir
2459 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2460 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2461 caption = xmldir.attributes['thumbnails-caption']
2462 infotype = 'thumbnails'
2464 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2465 captionfile, caption = find_subalbum_caption_info(xmldir)
2466 infotype = find_subalbum_info_type(xmldir)
2468 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2469 hbox = Gtk::HBox.new
2470 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2472 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2475 my_gen_real_thumbnail = proc {
2476 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2479 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2480 f.add(img = Gtk::Image.new)
2481 my_gen_real_thumbnail.call
2483 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2485 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2486 $subalbums.attach(hbox,
2487 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2489 frame, textview = create_editzone($subalbums_sw, 0, img)
2490 textview.buffer.text = caption
2491 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2492 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2494 change_image = proc {
2495 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2497 Gtk::FileChooser::ACTION_OPEN,
2499 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2500 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2501 fc.transient_for = $main_window
2502 fc.preview_widget = preview = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(f = Gtk::Frame.new.set_shadow_type(Gtk::SHADOW_ETCHED_OUT))
2503 f.add(preview_img = Gtk::Image.new)
2505 fc.signal_connect('update-preview') { |w|
2506 if fc.preview_filename
2507 if entry2type(fc.preview_filename) == 'video'
2511 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2513 fc.preview_widget_active = false
2515 tmpimage = "#{tmpdir}/00000001.jpg"
2517 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2518 fc.preview_widget_active = true
2519 rescue Gdk::PixbufError
2520 fc.preview_widget_active = false
2522 File.delete(tmpimage)
2529 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2530 fc.preview_widget_active = true
2531 rescue Gdk::PixbufError
2532 fc.preview_widget_active = false
2537 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2539 old_file = captionfile
2540 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2541 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2542 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2543 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2545 new_file = fc.filename
2546 msg 3, "new captionfile is: #{fc.filename}"
2547 perform_changefile = proc {
2548 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2549 $modified_pixbufs.delete(thumbnail_file)
2550 xmldir.delete_attribute("#{infotype}-rotate")
2551 xmldir.delete_attribute("#{infotype}-color-swap")
2552 xmldir.delete_attribute("#{infotype}-enhance")
2553 xmldir.delete_attribute("#{infotype}-seektime")
2554 my_gen_real_thumbnail.call
2556 perform_changefile.call
2558 save_undo(_("change caption file for sub-album"),
2560 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2561 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2562 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2563 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2564 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2565 my_gen_real_thumbnail.call
2566 $notebook.set_page(0)
2568 perform_changefile.call
2569 $notebook.set_page(0)
2577 if File.exists?(thumbnail_file)
2578 File.delete(thumbnail_file)
2580 my_gen_real_thumbnail.call
2583 rotate_and_cleanup = proc { |angle|
2584 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2585 if File.exists?(thumbnail_file)
2586 File.delete(thumbnail_file)
2590 move = proc { |direction|
2593 save_changes('forced')
2594 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2595 if direction == 'up'
2596 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2597 subalbums_edits_bypos[oldpos - 1][:position] += 1
2599 if direction == 'down'
2600 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2601 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2603 if direction == 'top'
2604 for i in 1 .. oldpos - 1
2605 subalbums_edits_bypos[i][:position] += 1
2607 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2609 if direction == 'bottom'
2610 for i in oldpos + 1 .. subalbums_counter
2611 subalbums_edits_bypos[i][:position] -= 1
2613 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2617 $xmldir.elements.each('dir') { |element|
2618 if (!element.attributes['deleted'])
2619 elems << [ element.attributes['path'], element.remove ]
2622 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2623 each { |e| $xmldir.add_element(e[1]) }
2624 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2625 $xmldir.elements.each('descendant::dir') { |elem|
2626 elem.delete_attribute('already-generated')
2629 sel = $albums_tv.selection.selected_rows
2631 populate_subalbums_treeview(false)
2632 $albums_tv.selection.select_path(sel[0])
2635 color_swap_and_cleanup = proc {
2636 perform_color_swap_and_cleanup = proc {
2637 color_swap(xmldir, "#{infotype}-")
2638 my_gen_real_thumbnail.call
2640 perform_color_swap_and_cleanup.call
2642 save_undo(_("color swap"),
2644 perform_color_swap_and_cleanup.call
2645 $notebook.set_page(0)
2647 perform_color_swap_and_cleanup.call
2648 $notebook.set_page(0)
2653 change_seektime_and_cleanup = proc {
2654 if values = ask_new_seektime(xmldir, "#{infotype}-")
2655 perform_change_seektime_and_cleanup = proc { |val|
2656 change_seektime(xmldir, "#{infotype}-", val)
2657 my_gen_real_thumbnail.call
2659 perform_change_seektime_and_cleanup.call(values[:new])
2661 save_undo(_("specify seektime"),
2663 perform_change_seektime_and_cleanup.call(values[:old])
2664 $notebook.set_page(0)
2666 perform_change_seektime_and_cleanup.call(values[:new])
2667 $notebook.set_page(0)
2673 whitebalance_and_cleanup = proc {
2674 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2675 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2676 perform_change_whitebalance_and_cleanup = proc { |val|
2677 change_whitebalance(xmldir, "#{infotype}-", val)
2678 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2679 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2680 if File.exists?(thumbnail_file)
2681 File.delete(thumbnail_file)
2684 perform_change_whitebalance_and_cleanup.call(values[:new])
2686 save_undo(_("fix white balance"),
2688 perform_change_whitebalance_and_cleanup.call(values[:old])
2689 $notebook.set_page(0)
2691 perform_change_whitebalance_and_cleanup.call(values[:new])
2692 $notebook.set_page(0)
2698 gammacorrect_and_cleanup = proc {
2699 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2700 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2701 perform_change_gammacorrect_and_cleanup = proc { |val|
2702 change_gammacorrect(xmldir, "#{infotype}-", val)
2703 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2704 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2705 if File.exists?(thumbnail_file)
2706 File.delete(thumbnail_file)
2709 perform_change_gammacorrect_and_cleanup.call(values[:new])
2711 save_undo(_("gamma correction"),
2713 perform_change_gammacorrect_and_cleanup.call(values[:old])
2714 $notebook.set_page(0)
2716 perform_change_gammacorrect_and_cleanup.call(values[:new])
2717 $notebook.set_page(0)
2723 enhance_and_cleanup = proc {
2724 perform_enhance_and_cleanup = proc {
2725 enhance(xmldir, "#{infotype}-")
2726 my_gen_real_thumbnail.call
2729 perform_enhance_and_cleanup.call
2731 save_undo(_("enhance"),
2733 perform_enhance_and_cleanup.call
2734 $notebook.set_page(0)
2736 perform_enhance_and_cleanup.call
2737 $notebook.set_page(0)
2742 evtbox.signal_connect('button-press-event') { |w, event|
2743 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2745 rotate_and_cleanup.call(90)
2747 rotate_and_cleanup.call(-90)
2748 elsif $enhance.active?
2749 enhance_and_cleanup.call
2752 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2753 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2754 { :forbid_left => true, :forbid_right => true,
2755 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2756 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2757 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2758 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2759 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2761 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2766 evtbox.signal_connect('button-press-event') { |w, event|
2767 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2771 evtbox.signal_connect('button-release-event') { |w, event|
2772 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2773 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2774 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2775 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2776 msg 3, "gesture rotate: #{angle}"
2777 rotate_and_cleanup.call(angle)
2780 $gesture_press = nil
2783 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2784 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2785 current_y_sub_albums += 1
2788 if $xmldir.child_byname_notattr('dir', 'deleted')
2790 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2791 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2792 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2793 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2794 #- this album image/caption
2795 if $xmldir.attributes['thumbnails-caption']
2796 add_subalbum.call($xmldir, 0)
2799 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2800 $xmldir.elements.each { |element|
2801 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2802 #- element (image or video) of this album
2803 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2804 msg 3, "dest_img: #{dest_img}"
2805 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2806 total[element.name] += 1
2808 if element.name == 'dir' && !element.attributes['deleted']
2809 #- sub-album image/caption
2810 add_subalbum.call(element, subalbums_counter += 1)
2811 total[element.name] += 1
2814 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2815 total['image'], total['video'], total['dir'] ]))
2816 $subalbums_vb.add($subalbums)
2817 $subalbums_vb.show_all
2819 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2820 $notebook.get_tab_label($autotable_sw).sensitive = false
2821 $notebook.set_page(0)
2822 $thumbnails_title.buffer.text = ''
2824 $notebook.get_tab_label($autotable_sw).sensitive = true
2825 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2828 if !$xmldir.child_byname_notattr('dir', 'deleted')
2829 $notebook.get_tab_label($subalbums_sw).sensitive = false
2830 $notebook.set_page(1)
2832 $notebook.get_tab_label($subalbums_sw).sensitive = true
2836 def pixbuf_or_nil(filename)
2838 return Gdk::Pixbuf.new(filename)
2844 def theme_choose(current)
2845 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2847 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2848 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2849 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2851 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2852 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2853 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2854 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2855 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2856 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2857 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2858 treeview.signal_connect('button-press-event') { |w, event|
2859 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2860 dialog.response(Gtk::Dialog::RESPONSE_OK)
2864 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2866 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.split("\n").find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2869 iter[0] = File.basename(dir)
2870 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2871 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2872 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2873 if File.basename(dir) == current
2874 treeview.selection.select_iter(iter)
2877 dialog.set_default_size(-1, 500)
2878 dialog.vbox.show_all
2880 dialog.run { |response|
2881 iter = treeview.selection.selected
2883 if response == Gtk::Dialog::RESPONSE_OK && iter
2884 return model.get_value(iter, 0)
2890 def show_password_protections
2891 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2892 child_iter = $albums_iters[xmldir.attributes['path']]
2893 if xmldir.attributes['password-protect']
2894 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2895 already_protected = true
2896 elsif already_protected
2897 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2899 pix = pix.saturate_and_pixelate(1, true)
2905 xmldir.elements.each('dir') { |elem|
2906 if !elem.attributes['deleted']
2907 examine_dir_elem.call(child_iter, elem, already_protected)
2911 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2914 def populate_subalbums_treeview(select_first)
2918 $subalbums_vb.children.each { |chld|
2919 $subalbums_vb.remove(chld)
2922 source = $xmldoc.root.attributes['source']
2923 msg 3, "source: #{source}"
2925 xmldir = $xmldoc.elements['//dir']
2926 if !xmldir || xmldir.attributes['path'] != source
2927 msg 1, _("Corrupted booh file...")
2931 append_dir_elem = proc { |parent_iter, xmldir|
2932 child_iter = $albums_ts.append(parent_iter)
2933 child_iter[0] = File.basename(xmldir.attributes['path'])
2934 child_iter[1] = xmldir.attributes['path']
2935 $albums_iters[xmldir.attributes['path']] = child_iter
2936 msg 3, "puttin location: #{xmldir.attributes['path']}"
2937 xmldir.elements.each('dir') { |elem|
2938 if !elem.attributes['deleted']
2939 append_dir_elem.call(child_iter, elem)
2943 append_dir_elem.call(nil, xmldir)
2944 show_password_protections
2946 $albums_tv.expand_all
2948 $albums_tv.selection.select_iter($albums_ts.iter_first)
2952 def select_current_theme
2953 select_theme($xmldoc.root.attributes['theme'],
2954 $xmldoc.root.attributes['limit-sizes'],
2955 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2956 $xmldoc.root.attributes['thumbnails-per-row'])
2959 def open_file(filename)
2963 $current_path = nil #- invalidate
2964 $modified_pixbufs = {}
2967 $subalbums_vb.children.each { |chld|
2968 $subalbums_vb.remove(chld)
2971 if !File.exists?(filename)
2972 return utf8(_("File not found."))
2976 $xmldoc = REXML::Document.new(File.new(filename))
2981 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2982 if entry2type(filename).nil?
2983 return utf8(_("Not a booh file!"))
2985 return utf8(_("Not a booh file!\n\nHint: you cannot import directly a photo or video with File/Open.\nUse File/New to create a new album."))
2989 if !source = $xmldoc.root.attributes['source']
2990 return utf8(_("Corrupted booh file..."))
2993 if !dest = $xmldoc.root.attributes['destination']
2994 return utf8(_("Corrupted booh file..."))
2997 if !theme = $xmldoc.root.attributes['theme']
2998 return utf8(_("Corrupted booh file..."))
3001 if $xmldoc.root.attributes['version'] < $VERSION
3002 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
3003 mark_document_as_dirty
3004 if $xmldoc.root.attributes['version'] < '0.8.4'
3005 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
3006 `find '#{source}' -type d -follow`.split("\n").sort.collect { |v| v.chomp }.each { |dir|
3007 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
3008 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
3009 if old_dest_dir != new_dest_dir
3010 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
3012 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
3013 xmldir.elements.each { |element|
3014 if %w(image video).include?(element.name) && !element.attributes['deleted']
3015 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3016 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3017 Dir[old_name + '*'].each { |file|
3018 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
3019 file != new_file and sys("mv '#{file}' '#{new_file}'")
3022 if element.name == 'dir' && !element.attributes['deleted']
3023 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3024 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3025 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
3029 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3033 $xmldoc.root.add_attribute('version', $VERSION)
3036 select_current_theme
3038 $filename = filename
3039 set_mainwindow_title(nil)
3040 $default_size['thumbnails'] =~ /(.*)x(.*)/
3041 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3042 $albums_thumbnail_size =~ /(.*)x(.*)/
3043 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3045 populate_subalbums_treeview(true)
3047 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $upload.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
3051 def open_file_user(filename)
3052 result = open_file(filename)
3054 $config['last-opens'] ||= []
3055 if $config['last-opens'][-1] != utf8(filename)
3056 $config['last-opens'] << utf8(filename)
3058 $orig_filename = $filename
3059 $main_window.title = 'booh - ' + File.basename($orig_filename)
3060 tmp = Tempfile.new("boohtemp")
3061 $filename = tmp.path
3064 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3066 $tempfiles << $filename << "#{$filename}.backup"
3068 $orig_filename = nil
3074 if !ask_save_modifications(utf8(_("Save this album?")),
3075 utf8(_("Do you want to save the changes to this album?")),
3076 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3079 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3081 Gtk::FileChooser::ACTION_OPEN,
3083 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3084 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3085 fc.set_current_folder(File.expand_path("~/.booh"))
3086 fc.transient_for = $main_window
3087 fc.preview_widget = previewlabel = Gtk::Label.new.show
3088 fc.signal_connect('update-preview') { |w|
3089 if fc.preview_filename
3091 push_mousecursor_wait(fc)
3092 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3096 xmldoc.elements.each('//*') { |elem|
3097 if elem.name == 'dir'
3099 elsif elem.name == 'image'
3101 elsif elem.name == 'video'
3109 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3110 fc.preview_widget_active = false
3112 previewlabel.markup = utf8(_("<i>Source:</i> %s\n<i>Destination:</i> %s\n<i>Subalbums:</i> %s\n<i>Images:</i> %s\n<i>Videos:</i> %s") %
3113 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3114 fc.preview_widget_active = true
3120 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3121 push_mousecursor_wait(fc)
3122 msg = open_file_user(fc.filename)
3137 def additional_booh_options
3140 options += "--mproc #{$config['mproc'].to_i} "
3142 options += "--comments-format '#{$config['comments-format']}' "
3143 if $config['transcode-videos']
3144 options += "--transcode-videos '#{$config['transcode-videos']}' "
3146 if $config['use-flv'] == 'true'
3147 options += "--flv-generator '#{$config['flv-generator']}' "
3152 def ask_multi_languages(value)
3154 spl = value.split(',')
3155 value = [ spl[0..-2], spl[-1] ]
3158 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3161 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3162 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3164 lbl = Gtk::Label.new
3166 _("You can choose to activate <b>multi-languages</b> support for this web-album
3167 (it will work only if you publish your web-album on an Apache web-server). This will
3168 use the MultiViews feature of Apache; the pages will be served according to the
3169 value of the Accept-Language HTTP header sent by the web browsers, so that people
3170 with different languages preferences will be able to browse your web-album with
3171 navigation in their language (if language is available).
3174 dialog.vbox.add(lbl)
3175 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3176 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3177 add(languages = Gtk::Button.new))))
3179 pick_languages = proc {
3180 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3183 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3184 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3186 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3187 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3189 SUPPORTED_LANGUAGES.each { |lang|
3190 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3191 if ! value.nil? && value[0].include?(lang)
3197 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3198 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3199 fallback_language = nil
3200 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3201 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3202 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3203 fbl_rb.active = true
3204 fallback_language = SUPPORTED_LANGUAGES[0]
3206 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3207 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3208 rb.signal_connect('clicked') { fallback_language = lang }
3209 if ! value.nil? && value[1] == lang
3214 dialog2.window_position = Gtk::Window::POS_MOUSE
3218 dialog2.run { |response|
3220 if resp == Gtk::Dialog::RESPONSE_OK
3222 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3223 value[1] = fallback_language
3224 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3231 languages.signal_connect('clicked') {
3234 dialog.window_position = Gtk::Window::POS_MOUSE
3238 rb_yes.active = true
3239 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3241 rb_no.signal_connect('clicked') {
3245 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3258 dialog.run { |response|
3263 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3265 return [ true, nil ]
3267 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3276 if !ask_save_modifications(utf8(_("Save this album?")),
3277 utf8(_("Do you want to save the changes to this album?")),
3278 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3281 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3283 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3284 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3285 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3287 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3288 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3289 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3290 tbl.attach(src = Gtk::Entry.new,
3291 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3292 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3293 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3294 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3295 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3296 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3297 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3298 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3299 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3300 tbl.attach(dest = Gtk::Entry.new,
3301 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3302 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3303 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3304 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3305 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3306 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3307 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3308 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3309 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3311 tooltips = Gtk::Tooltips.new
3312 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3313 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3314 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'gradient'), false, false, 0))
3315 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3316 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3317 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3318 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
3319 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3320 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3321 nperpage_model = Gtk::ListStore.new(String, String)
3322 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3323 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3324 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3325 nperpagecombo.set_attributes(crt, { :markup => 0 })
3326 iter = nperpage_model.append
3327 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3329 [ 12, 20, 30, 40, 50 ].each { |v|
3330 iter = nperpage_model.append
3331 iter[0] = iter[1] = v.to_s
3333 nperpagecombo.active = 0
3335 multilanguages_value = nil
3336 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3337 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3338 tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3339 multilanguages.signal_connect('clicked') {
3340 retval = ask_multi_languages(multilanguages_value)
3342 multilanguages_value = retval[1]
3344 if multilanguages_value
3345 ml_label.text = utf8(_("Multi-languages: enabled."))
3347 ml_label.text = utf8(_("Multi-languages: disabled."))
3350 if $config['default-multi-languages']
3351 multilanguages_value = $config['default-multi-languages']
3352 ml_label.text = utf8(_("Multi-languages: enabled."))
3354 ml_label.text = utf8(_("Multi-languages: disabled."))
3357 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3358 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3359 tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3360 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3361 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3362 tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
3363 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3364 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3365 tooltips.set_tip(quotehtml, utf8(_("If checked, text using markup special characters such as '<grin>' will be shown properly; if unchecked, markup such as '<a href..' links will be interpreted by the browser properly")), nil)
3367 src_nb_calculated_for = ''
3368 src_nb_process = nil
3369 process_src_nb = proc {
3370 if src.text != src_nb_calculated_for
3371 src_nb_calculated_for = src.text
3374 Process.kill(9, src_nb_process)
3376 #- process doesn't exist anymore - race condition
3379 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3380 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3382 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3383 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3386 while src_nb_process
3387 msg 3, "sleeping for completion of previous process"
3390 gtk_thread_flush #- flush to avoid race condition in src_nb markup update
3392 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3393 total = { 'image' => 0, 'video' => 0, nil => 0 }
3394 if src_nb_process = fork
3395 msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3399 rd.readlines.each { |dir|
3400 if File.basename(dir) =~ /^\./
3404 Dir.entries(dir.chomp).each { |file|
3405 total[entry2type(file)] += 1
3407 rescue Errno::EACCES, Errno::ENOENT
3412 msg 3, "ripping #{src_nb_process}"
3413 dummy, exitstatus = Process.waitpid2(src_nb_process)
3415 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3417 src_nb_process = nil
3423 wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3424 Process.exit!(0) #- _exit
3427 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3430 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3436 timeout_src_nb = Gtk.timeout_add(100) {
3440 src_browse.signal_connect('clicked') {
3441 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3443 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3445 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3446 fc.transient_for = $main_window
3447 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3448 src.text = utf8(fc.filename)
3450 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3455 dest_browse.signal_connect('clicked') {
3456 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3458 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3460 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3461 fc.transient_for = $main_window
3462 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3463 dest.text = utf8(fc.filename)
3468 conf_browse.signal_connect('clicked') {
3469 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3471 Gtk::FileChooser::ACTION_SAVE,
3473 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3474 fc.transient_for = $main_window
3475 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3476 fc.set_current_folder(File.expand_path("~/.booh"))
3477 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3478 conf.text = utf8(fc.filename)
3485 recreate_theme_config = proc {
3486 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3488 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3489 $images_size.each { |s|
3490 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3494 tooltips.set_tip(cb, utf8(s['description']), nil)
3495 theme_sizes << { :widget => cb, :value => s['name'] }
3497 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3498 tooltips = Gtk::Tooltips.new
3499 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3500 theme_sizes << { :widget => cb, :value => 'original' }
3503 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3506 $allowed_N_values.each { |n|
3508 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3510 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3512 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3516 nperrows << { :widget => rb, :value => n }
3518 nperrowradios.show_all
3520 recreate_theme_config.call
3522 theme_button.signal_connect('clicked') {
3523 if newtheme = theme_choose(theme_button.label)
3524 theme_button.label = newtheme
3525 recreate_theme_config.call
3529 dialog.vbox.add(frame1)
3530 dialog.vbox.add(frame2)
3536 dialog.run { |response|
3537 if response == Gtk::Dialog::RESPONSE_OK
3538 srcdir = from_utf8_safe(src.text)
3539 destdir = from_utf8_safe(dest.text)
3540 confpath = from_utf8_safe(conf.text)
3541 if src.text != '' && srcdir == ''
3542 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3544 elsif !File.directory?(srcdir)
3545 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3547 elsif dest.text != '' && destdir == ''
3548 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3550 elsif destdir != make_dest_filename(destdir)
3551 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3553 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3554 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3555 inside it will be permanently removed before creating the web-album!
3556 Are you sure you want to continue?")), { :okcancel => true })
3558 elsif File.exists?(destdir) && !File.directory?(destdir)
3559 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3561 elsif conf.text == ''
3562 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3564 elsif conf.text != '' && confpath == ''
3565 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3567 elsif File.directory?(confpath)
3568 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3570 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3571 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3573 system("mkdir '#{destdir}'")
3574 if !File.directory?(destdir)
3575 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3587 srcdir = from_utf8(src.text)
3588 destdir = from_utf8(dest.text)
3589 configskel = File.expand_path(from_utf8(conf.text))
3590 theme = theme_button.label
3591 #- some sort of automatic theme preference
3592 $config['default-theme'] = theme
3593 $config['default-multi-languages'] = multilanguages_value
3594 $config['default-optimize32'] = optimize432.active?.to_s
3595 $config['default-addthis'] = addthis.active?.to_s
3596 $config['default-quotehtml'] = quotehtml.active?.to_s
3597 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3598 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3599 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3600 opt432 = optimize432.active?
3601 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3602 indexlink = indexlinkentry.text.gsub('\'', ''')
3603 athis = addthis.active?
3604 qhtml = quotehtml.active?
3608 Process.kill(9, src_nb_process)
3609 while src_nb_process
3610 msg 3, "sleeping for completion of previous process"
3614 #- process doesn't exist
3616 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3619 Gtk.timeout_remove(timeout_src_nb)
3622 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3623 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3624 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3625 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3626 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' " +
3627 "#{athis ? '--addthis' : ''} #{qhtml ? '--quote-html' : ''} #{additional_booh_options}",
3628 utf8(_("Please wait while scanning source directory...")),
3630 { :closure_after => proc {
3631 open_file_user(configskel)
3632 $main_window.urgency_hint = true
3638 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3640 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3641 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3642 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3644 source = $xmldoc.root.attributes['source']
3645 dest = $xmldoc.root.attributes['destination']
3646 theme = $xmldoc.root.attributes['theme']
3647 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3648 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3649 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3650 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3652 limit_sizes = limit_sizes.split(/,/)
3654 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3655 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3656 athis = !$xmldoc.root.attributes['addthis'].nil?
3657 qhtml = !$xmldoc.root.attributes['quote-html'].nil?
3658 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3660 tooltips = Gtk::Tooltips.new
3661 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3662 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3663 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3664 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3665 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3666 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3667 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3668 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3669 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3670 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3671 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3672 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3673 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3675 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3676 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3677 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3678 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3679 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3680 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3681 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
3682 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3683 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3684 nperpage_model = Gtk::ListStore.new(String, String)
3685 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3686 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3687 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3688 nperpagecombo.set_attributes(crt, { :markup => 0 })
3689 iter = nperpage_model.append
3690 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3692 [ 12, 20, 30, 40, 50 ].each { |v|
3693 iter = nperpage_model.append
3694 iter[0] = iter[1] = v.to_s
3695 if nperpage && nperpage == v.to_s
3696 nperpagecombo.active_iter = iter
3699 if nperpagecombo.active_iter.nil?
3700 nperpagecombo.active = 0
3703 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3704 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3705 tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3707 if save_multilanguages_value
3708 ml_label.text = utf8(_("Multi-languages: enabled."))
3710 ml_label.text = utf8(_("Multi-languages: disabled."))
3714 multilanguages.signal_connect('clicked') {
3715 retval = ask_multi_languages(save_multilanguages_value)
3717 save_multilanguages_value = retval[1]
3722 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3723 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3725 indexlinkentry.text = indexlink
3727 tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3728 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3729 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3731 madewithentry.text = madewith
3733 tooltips.set_tip(madewithentry, utf8(_('Optional HTML markup to use on pages bottom for a small \'made with\' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!')), nil)
3734 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active(athis))
3735 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active(qhtml))
3736 tooltips.set_tip(quotehtml, utf8(_("If checked, text using markup special characters such as '<grin>' will be shown properly; if unchecked, markup such as '<a href..' links will be interpreted by the browser properly")), nil)
3740 recreate_theme_config = proc {
3741 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3743 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3745 $images_size.each { |s|
3746 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3748 if limit_sizes.include?(s['name'])
3756 tooltips.set_tip(cb, utf8(s['description']), nil)
3757 theme_sizes << { :widget => cb, :value => s['name'] }
3759 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3760 tooltips = Gtk::Tooltips.new
3761 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3762 if limit_sizes && limit_sizes.include?('original')
3765 theme_sizes << { :widget => cb, :value => 'original' }
3768 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3771 $allowed_N_values.each { |n|
3773 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3775 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3777 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3778 nperrowradios.add(Gtk::Label.new(' '))
3779 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3782 nperrows << { :widget => rb, :value => n.to_s }
3784 nperrowradios.show_all
3786 recreate_theme_config.call
3788 theme_button.signal_connect('clicked') {
3789 if newtheme = theme_choose(theme_button.label)
3792 theme_button.label = newtheme
3793 recreate_theme_config.call
3797 dialog.vbox.add(frame1)
3798 dialog.vbox.add(frame2)
3804 dialog.run { |response|
3805 if response == Gtk::Dialog::RESPONSE_OK
3806 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3807 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3816 save_theme = theme_button.label
3817 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3818 save_opt432 = optimize432.active?
3819 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3820 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3821 save_madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3822 save_indexlink = indexlinkentry.text.gsub('\'', ''')
3823 save_addthis = addthis.active?
3824 save_quotehtml = quotehtml.active?
3827 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlink || save_multilanguages_value != multilanguages_value || save_quotehtml != qhtml || save_addthis != athis)
3828 #- some sort of automatic preferences
3829 if save_theme != theme
3830 $config['default-theme'] = save_theme
3832 if save_multilanguages_value != multilanguages_value
3833 $config['default-multi-languages'] = save_multilanguages_value
3835 if save_opt432 != opt432
3836 $config['default-optimize32'] = save_opt432.to_s
3838 if save_addthis != athis
3839 $config['default-addthis'] = save_addthis.to_s
3841 if save_quotehtml != qhtml
3842 $config['default-quotehtml'] = save_quotehtml.to_s
3844 mark_document_as_dirty
3846 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3847 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3848 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3849 (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
3850 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' " +
3851 "#{save_addthis ? '--addthis' : ''} #{save_quotehtml ? '--quote-html' : ''} #{additional_booh_options}",
3852 utf8(_("Please wait while scanning source directory...")),
3854 { :closure_after => proc {
3855 open_file($filename)
3857 $main_window.urgency_hint = true
3860 #- select_theme merges global variables, need to return to current choices
3861 select_current_theme
3868 sel = $albums_tv.selection.selected_rows
3870 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3871 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3872 utf8(_("Please wait while scanning source directory...")),
3874 { :closure_after => proc {
3875 open_file($filename)
3876 $albums_tv.selection.select_path(sel[0])
3878 $main_window.urgency_hint = true
3885 sel = $albums_tv.selection.selected_rows
3887 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3888 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3889 utf8(_("Please wait while scanning source directory...")),
3891 { :closure_after => proc {
3892 open_file($filename)
3893 $albums_tv.selection.select_path(sel[0])
3895 $main_window.urgency_hint = true
3902 theme = $xmldoc.root.attributes['theme']
3903 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3905 limit_sizes = "--sizes #{limit_sizes}"
3907 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3908 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3909 utf8(_("Please wait while scanning source directory...")),
3911 { :closure_after => proc {
3912 open_file($filename)
3914 $main_window.urgency_hint = true
3919 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3921 Gtk::FileChooser::ACTION_SAVE,
3923 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3924 fc.transient_for = $main_window
3925 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3926 fc.set_current_folder(File.expand_path("~/.booh"))
3927 fc.filename = $orig_filename
3928 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3929 $orig_filename = fc.filename
3930 if ! save_current_file_user
3934 $config['last-opens'] ||= []
3935 $config['last-opens'] << $orig_filename
3941 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3943 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3944 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3945 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3948 dialog.vbox.add(notebook = Gtk::Notebook.new)
3949 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3950 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3951 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3952 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
3953 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3954 tooltips = Gtk::Tooltips.new
3955 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3956 for example: /usr/bin/mplayer %f")), nil)
3959 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3960 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3961 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3962 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3963 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3964 for example: /usr/bin/gimp-remote %f")), nil)
3967 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3968 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3969 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3970 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3971 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3972 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3975 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(flv_check = Gtk::CheckButton.new(utf8(_("Use embedded flash player for videos,\nand use this .flv generator:")))),
3976 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3977 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(flv_generator_entry = Gtk::Entry.new.set_text($config['flv-generator']).set_sensitive(false)),
3978 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3979 tooltips.set_tip(flv_check, utf8(_("Flowplayer will be used for embedded video playback")), nil)
3980 tooltips.set_tip(flv_generator_entry, utf8(_("Use %f to specify the input filename, %o the output filename;
3981 for example: /usr/bin/ffmpeg -i %f -b ${i}k -ar 22050 -ab 32k %o")), nil)
3984 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3985 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3986 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
3987 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3988 tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
3991 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3992 0, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3993 tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
3996 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
3997 0, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3998 tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting a photo or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
4000 flv_check.signal_connect('toggled') {
4001 flv_generator_entry.sensitive = flv_check.active?
4003 if $config['use-flv'] == 'true'
4004 flv_check.active = true
4006 smp_check.signal_connect('toggled') {
4007 smp_hbox.sensitive = smp_check.active?
4010 smp_check.active = true
4011 smp_spin.value = $config['mproc'].to_i
4013 nogestures_check.active = $config['nogestures']
4014 deleteondisk_check.active = $config['deleteondisk']
4016 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
4017 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
4018 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
4019 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
4020 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
4021 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nphotos in new albums:"))),
4022 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
4023 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
4024 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
4025 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
4026 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
4027 tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for photos and videos in new albums. Use this entry to use something else.")), nil)
4028 commentsformat_help.signal_connect('clicked') {
4029 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
4030 hence you should look at ImageMagick/identify documentation for the most
4031 accurate and up-to-date documentation. Last time I checked, documentation
4034 Print information about the image in a format of your choosing. You can
4035 include the image filename, type, width, height, Exif data, or other image
4036 attributes by embedding special format characters:
4039 %P page width and height
4043 %e filename extension
4048 %k number of unique colors
4055 %r image class and colorspace
4058 %u unique temporary filename
4063 %@ bounding box