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-2008 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
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'booh/rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
40 require 'booh/Synchronizator'
45 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
47 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
50 #- default values for some globals
52 $xmlaccesslock = Object.new
55 $ignore_videos = false
56 $button1_pressed_autotable = false
57 $generated_outofline = false
60 puts _("Usage: %s [OPTION]...") % File.basename($0)
62 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
67 parser = GetoptLong.new
68 parser.set_options(*$options.collect { |ary| ary[0..2] })
70 parser.each_option do |name, arg|
77 puts _("Booh version %s
79 Copyright (c) 2005-2008 Guillaume Cottenceau.
80 This is free software; see the source for copying conditions. There is NO
81 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
85 when '--verbose-level'
86 $verbose_level = arg.to_i
99 $config_file = File.expand_path('~/.booh-gui-rc')
100 if File.readable?($config_file)
101 $xmldoc = Synchronizator.new(REXML::Document.new(File.new($config_file)), $xmlaccesslock)
102 $xmldoc.root.elements.each { |element|
103 txt = element.get_text
105 if txt.value =~ /~~~/ || element.name == 'last-opens'
106 $config[element.name] = txt.value.split(/~~~/)
108 $config[element.name] = txt.value
110 elsif element.elements.size == 0
111 $config[element.name] = ''
113 $config[element.name] = {}
114 element.each { |chld|
116 $config[element.name][chld.name] = txt ? txt.value : nil
121 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
122 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
123 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
124 $config['comments-format'] ||= '%t'
125 if !FileTest.directory?(File.expand_path('~/.booh'))
126 system("mkdir ~/.booh")
128 if $config['mproc'].nil?
130 for line in IO.readlines('/proc/cpuinfo') do
131 line =~ /^processor/ and cpus += 1
134 $config['mproc'] = cpus
137 $config['rotate-set-exif'] ||= 'true'
143 if !system("which convert >/dev/null 2>/dev/null")
144 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
145 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
148 if !system("which identify >/dev/null 2>/dev/null")
149 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
150 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
152 if !system("which exif >/dev/null 2>/dev/null")
153 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
155 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
157 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
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 })
171 def check_image_editor
172 image_editor_binary = $config['image-editor'].split.first
173 if image_editor_binary && !File.executable?(image_editor_binary)
174 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
175 You should fix this in Edit/Preferences so that you can edit photos externally.
177 Problem was: '%s' is not an executable file.
178 Hint: don't forget to specify the full path to the executable,
179 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
187 if $config['last-opens'] && $config['last-opens'].size > 10
188 $config['last-opens'] = $config['last-opens'][-10, 10]
191 $xmldoc = Synchronizator.new(Document.new("<booh-gui-rc version='#{$VERSION}'/>"), $xmlaccesslock)
192 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
193 $config.each_pair { |key, value|
194 elem = $xmldoc.root.add_element key
196 $config[key].each_pair { |subkey, subvalue|
197 subelem = elem.add_element subkey
198 subelem.add_text subvalue.to_s
200 elsif value.is_a? Array
201 elem.add_text value.join('~~~')
206 elem.add_text value.to_s
210 ios = File.open($config_file, "w")
211 $xmldoc.write(ios, 0)
214 $tempfiles.each { |f|
221 def set_mousecursor(what, *widget)
222 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
223 if widget[0] && widget[0].window
224 widget[0].window.cursor = cursor
226 if $main_window && $main_window.window
227 $main_window.window.cursor = cursor
229 $current_cursor = what
231 def set_mousecursor_wait(*widget)
232 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
233 if Thread.current == Thread.main
234 Gtk.main_iteration while Gtk.events_pending?
237 def set_mousecursor_normal(*widget)
238 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
240 def push_mousecursor_wait(*widget)
241 if $current_cursor != Gdk::Cursor::WATCH
242 $save_cursor = $current_cursor
243 gtk_thread_protect { set_mousecursor_wait(*widget) }
246 def pop_mousecursor(*widget)
247 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
251 source = $xmldoc.root.attributes['source']
252 dest = $xmldoc.root.attributes['destination']
253 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
256 def full_src_dir_to_rel(path, source)
257 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
260 def build_full_dest_filename(filename)
261 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
264 def save_undo(name, closure, *params)
265 UndoHandler.save_undo(name, closure, [ *params ])
266 $undo_tb.sensitive = $undo_mb.sensitive = true
267 $redo_tb.sensitive = $redo_mb.sensitive = false
270 def view_element(filename, closures)
271 if entry2type(filename) == 'video'
272 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
278 w = create_window.set_title(filename)
280 msg 3, "filename: #{filename}"
281 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
282 #- typically this file won't exist in case of videos; try with the largest thumbnail around
283 if !File.exists?(dest_img)
284 if entry2type(filename) == 'video'
285 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
286 if not alternatives.empty?
287 dest_img = alternatives[-1]
290 push_mousecursor_wait
291 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
293 if !File.exists?(dest_img)
294 msg 2, _("Could not generate fullscreen thumbnail!")
299 aspect = utf8(_("Aspect: unknown"))
300 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
302 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
304 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
305 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)))
306 evt.signal_connect('button-press-event') { |this, event|
307 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
308 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
310 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
312 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
313 delete_item.signal_connect('activate') {
315 closures[:delete].call(false)
318 menu.popup(nil, nil, event.button, event.time)
321 evt.signal_connect('button-release-event') { |this, event|
323 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
324 msg 3, "gesture delete: click-drag right button to the bottom"
326 closures[:delete].call(false)
327 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
331 tooltips = Gtk::Tooltips.new
332 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
334 w.signal_connect('key-press-event') { |w,event|
335 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
337 closures[:delete].call(false)
341 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
342 b.signal_connect('clicked') { w.destroy }
345 vb.pack_start(evt, false, false)
346 vb.pack_end(bottom, false, false)
349 w.signal_connect('delete-event') { w.destroy }
350 w.window_position = Gtk::Window::POS_CENTER
354 def scroll_upper(scrolledwindow, ypos_top)
355 newval = scrolledwindow.vadjustment.value -
356 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
357 if newval < scrolledwindow.vadjustment.lower
358 newval = scrolledwindow.vadjustment.lower
360 scrolledwindow.vadjustment.value = newval
363 def scroll_lower(scrolledwindow, ypos_bottom)
364 newval = scrolledwindow.vadjustment.value +
365 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
366 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
367 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
369 scrolledwindow.vadjustment.value = newval
372 def autoscroll_if_needed(scrolledwindow, image, textview)
373 #- autoscroll if cursor or image is not visible, if possible
374 if image && image.window || textview.window
375 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
376 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
377 current_miny_visible = scrolledwindow.vadjustment.value
378 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
379 if ypos_top < current_miny_visible
380 scroll_upper(scrolledwindow, ypos_top)
381 elsif ypos_bottom > current_maxy_visible
382 scroll_lower(scrolledwindow, ypos_bottom)
387 def create_editzone(scrolledwindow, pagenum, image)
388 frame = Gtk::Frame.new
389 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
390 frame.set_shadow_type(Gtk::SHADOW_IN)
391 textview.signal_connect('key-press-event') { |w, event|
392 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
393 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
394 scrolledwindow.signal_emit('key-press-event', event)
396 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
397 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
398 if event.keyval == Gdk::Keyval::GDK_Up
399 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
400 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
402 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
405 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
406 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
408 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
415 candidate_undo_text = nil
416 textview.signal_connect('focus-in-event') { |w, event|
417 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
418 candidate_undo_text = textview.buffer.text
422 textview.signal_connect('key-release-event') { |w, event|
423 if candidate_undo_text && candidate_undo_text != textview.buffer.text
425 save_undo(_("text edit"),
427 save_text = textview.buffer.text
428 textview.buffer.text = text
430 $notebook.set_page(pagenum)
432 textview.buffer.text = save_text
434 $notebook.set_page(pagenum)
436 }, candidate_undo_text)
437 candidate_undo_text = nil
440 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)
441 autoscroll_if_needed(scrolledwindow, image, textview)
446 return [ frame, textview ]
449 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
451 if !$modified_pixbufs[thumbnail_img]
452 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
453 elsif !$modified_pixbufs[thumbnail_img][:orig]
454 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
457 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
460 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
461 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
462 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
463 if pixbuf.height > desired_y
464 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
465 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
466 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
471 if $modified_pixbufs[thumbnail_img][:whitebalance]
472 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
475 #- fix gamma correction
476 if $modified_pixbufs[thumbnail_img][:gammacorrect]
477 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
480 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
483 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
486 #- update rotate attribute
487 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
488 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
490 if $config['rotate-set-exif'] == 'true'
491 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
494 $modified_pixbufs[thumbnail_img] ||= {}
495 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
496 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
498 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
501 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
504 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
506 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
508 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
509 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
511 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
512 $notebook.set_page(0)
513 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
518 def color_swap(xmldir, attributes_prefix)
520 if xmldir.attributes["#{attributes_prefix}color-swap"]
521 xmldir.delete_attribute("#{attributes_prefix}color-swap")
523 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
527 def enhance(xmldir, attributes_prefix)
529 if xmldir.attributes["#{attributes_prefix}enhance"]
530 xmldir.delete_attribute("#{attributes_prefix}enhance")
532 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
536 def change_seektime(xmldir, attributes_prefix, value)
538 xmldir.add_attribute("#{attributes_prefix}seektime", value)
541 def ask_new_seektime(xmldir, attributes_prefix)
543 value = xmldir.attributes["#{attributes_prefix}seektime"]
548 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
550 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
551 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
552 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
556 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
560 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
561 entry.signal_connect('key-press-event') { |w, event|
562 if event.keyval == Gdk::Keyval::GDK_Return
563 dialog.response(Gtk::Dialog::RESPONSE_OK)
565 elsif event.keyval == Gdk::Keyval::GDK_Escape
566 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
569 false #- propagate if needed
573 dialog.window_position = Gtk::Window::POS_MOUSE
576 dialog.run { |response|
579 if response == Gtk::Dialog::RESPONSE_OK
581 msg 3, "changing seektime to #{newval}"
582 return { :old => value, :new => newval }
589 def change_pano_amount(xmldir, attributes_prefix, value)
592 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
594 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
598 def ask_new_pano_amount(xmldir, attributes_prefix)
600 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
605 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
607 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
608 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
609 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
613 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
614 of this panorama image compared to other regular images. For example, if the panorama
615 was taken out of four photos on one row, counting the necessary overlap, the width of
616 this panorama image should probably be roughly three times the width of regular images.
618 With this information, booh will be able to generate panorama thumbnails looking
619 the right 'size', since the height of the thumbnail for this image will be similar
620 to the height of other thumbnails.
623 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)")))).
624 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
625 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
626 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
627 spin.signal_connect('value-changed') {
630 dialog.window_position = Gtk::Window::POS_MOUSE
633 spin.value = value.to_f
640 dialog.run { |response|
644 newval = spin.value.to_f
647 if response == Gtk::Dialog::RESPONSE_OK
649 msg 3, "changing panorama amount to #{newval}"
650 return { :old => value, :new => newval }
657 def change_whitebalance(xmlelem, attributes_prefix, value)
659 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
662 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
664 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
665 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
666 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
667 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
668 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
669 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
670 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
671 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
672 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
673 $modified_pixbufs[thumbnail_img] ||= {}
674 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
675 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
677 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
678 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
680 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
683 $modified_pixbufs[thumbnail_img] ||= {}
684 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
686 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
689 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
690 #- init $modified_pixbufs correctly
691 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
693 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
695 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
697 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
698 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
699 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
703 _("You can fix the <b>white balance</b> of the image, if your image is too blue
704 or too yellow because the recorder didn't detect the light correctly. Drag the
705 slider below the image to the left for more blue, to the right for more yellow.
709 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
711 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
713 dialog.window_position = Gtk::Window::POS_MOUSE
717 timeout = Gtk.timeout_add(100) {
718 if hs.value != lastval
721 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
727 dialog.run { |response|
728 Gtk.timeout_remove(timeout)
729 if response == Gtk::Dialog::RESPONSE_OK
731 newval = hs.value.to_s
732 msg 3, "changing white balance to #{newval}"
734 return { :old => value, :new => newval }
737 $modified_pixbufs[thumbnail_img] ||= {}
738 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
739 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
747 def change_gammacorrect(xmlelem, attributes_prefix, value)
749 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
752 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
754 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
755 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
756 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
757 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
758 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
759 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
760 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
761 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
762 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
763 $modified_pixbufs[thumbnail_img] ||= {}
764 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
765 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
767 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
768 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
770 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
773 $modified_pixbufs[thumbnail_img] ||= {}
774 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
776 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
779 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
780 #- init $modified_pixbufs correctly
781 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
783 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
785 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
787 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
788 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
789 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
793 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
794 or too bright. Drag the slider below the image.
798 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
800 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
802 dialog.window_position = Gtk::Window::POS_MOUSE
806 timeout = Gtk.timeout_add(100) {
807 if hs.value != lastval
810 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
816 dialog.run { |response|
817 Gtk.timeout_remove(timeout)
818 if response == Gtk::Dialog::RESPONSE_OK
820 newval = hs.value.to_s
821 msg 3, "gamma correction to #{newval}"
823 return { :old => value, :new => newval }
826 $modified_pixbufs[thumbnail_img] ||= {}
827 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
828 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
836 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
837 if File.exists?(destfile)
838 File.delete(destfile)
840 #- type can be 'element' or 'subdir'
842 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
844 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
848 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
850 push_mousecursor_wait
851 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
854 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
860 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
861 distribute_multiple_call = Proc.new { |action, arg|
862 $selected_elements.each_key { |path|
863 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
865 if possible_actions[:can_multiple] && $selected_elements.length > 0
866 UndoHandler.begin_batch
867 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
868 UndoHandler.end_batch
870 closures[action].call(arg)
872 $selected_elements = {}
875 if optionals.include?('change_image')
876 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
877 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
878 changeimg.signal_connect('activate') { closures[:change].call }
879 menu.append(Gtk::SeparatorMenuItem.new)
881 if !possible_actions[:can_multiple] || $selected_elements.length == 0
884 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
885 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
886 view.signal_connect('activate') { closures[:view].call }
888 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
889 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
890 view.signal_connect('activate') { closures[:view].call }
891 menu.append(Gtk::SeparatorMenuItem.new)
894 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
895 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
896 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
897 exif.signal_connect('activate') { show_popup($main_window,
898 utf8(`exif -m '#{fullpath}'`),
899 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
900 menu.append(Gtk::SeparatorMenuItem.new)
903 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
904 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
905 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
906 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
907 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
908 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
909 if !possible_actions[:can_multiple] || $selected_elements.length == 0
910 menu.append(Gtk::SeparatorMenuItem.new)
911 if !possible_actions[:forbid_left]
912 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
913 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
914 moveleft.signal_connect('activate') { closures[:move].call('left') }
915 if !possible_actions[:can_left]
916 moveleft.sensitive = false
919 if !possible_actions[:forbid_right]
920 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
921 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
922 moveright.signal_connect('activate') { closures[:move].call('right') }
923 if !possible_actions[:can_right]
924 moveright.sensitive = false
927 if optionals.include?('move_top')
928 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
929 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
930 movetop.signal_connect('activate') { closures[:move].call('top') }
931 if !possible_actions[:can_top]
932 movetop.sensitive = false
935 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
936 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
937 moveup.signal_connect('activate') { closures[:move].call('up') }
938 if !possible_actions[:can_up]
939 moveup.sensitive = false
941 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
942 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
943 movedown.signal_connect('activate') { closures[:move].call('down') }
944 if !possible_actions[:can_down]
945 movedown.sensitive = false
947 if optionals.include?('move_bottom')
948 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
949 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
950 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
951 if !possible_actions[:can_bottom]
952 movebottom.sensitive = false
957 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
958 menu.append(Gtk::SeparatorMenuItem.new)
959 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
960 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
961 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
962 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
963 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
964 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
965 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
966 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
967 seektime.signal_connect('activate') {
968 if possible_actions[:can_multiple] && $selected_elements.length > 0
969 if values = ask_new_seektime(nil, '')
970 distribute_multiple_call.call(:seektime, values)
973 closures[:seektime].call
978 menu.append( Gtk::SeparatorMenuItem.new)
979 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
980 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
981 gammacorrect.signal_connect('activate') {
982 if possible_actions[:can_multiple] && $selected_elements.length > 0
983 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
984 distribute_multiple_call.call(:gammacorrect, values)
987 closures[:gammacorrect].call
990 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
991 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
992 whitebalance.signal_connect('activate') {
993 if possible_actions[:can_multiple] && $selected_elements.length > 0
994 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
995 distribute_multiple_call.call(:whitebalance, values)
998 closures[:whitebalance].call
1001 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1002 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
1003 _("Enhance constrast"))))
1005 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1007 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1008 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1009 if type == 'image' && possible_actions[:can_panorama]
1010 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1011 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1012 panorama.signal_connect('activate') {
1013 if possible_actions[:can_multiple] && $selected_elements.length > 0
1014 if values = ask_new_pano_amount(nil, '')
1015 distribute_multiple_call.call(:pano, values)
1018 distribute_multiple_call.call(:pano)
1022 menu.append( Gtk::SeparatorMenuItem.new)
1023 if optionals.include?('delete')
1024 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1025 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1026 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1027 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1028 paste_item.signal_connect('activate') { closures[:paste].call }
1029 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1030 clear_item.signal_connect('activate') { $cuts = [] }
1032 paste_item.sensitive = clear_item.sensitive = false
1035 menu.append( Gtk::SeparatorMenuItem.new)
1037 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1038 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1039 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1040 editexternally.signal_connect('activate') {
1041 if check_image_editor
1042 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1048 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1049 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1050 if optionals.include?('delete')
1051 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1052 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1055 menu.popup(nil, nil, event.button, event.time)
1058 def delete_current_subalbum
1060 sel = $albums_tv.selection.selected_rows
1061 $xmldir.elements.each { |e|
1062 if e.name == 'image' || e.name == 'video'
1063 e.add_attribute('deleted', 'true')
1066 #- branch if we have a non deleted subalbum
1067 if $xmldir.child_byname_notattr('dir', 'deleted')
1068 $xmldir.delete_attribute('thumbnails-caption')
1069 $xmldir.delete_attribute('thumbnails-captionfile')
1071 $xmldir.add_attribute('deleted', 'true')
1073 while moveup.parent.name == 'dir'
1074 moveup = moveup.parent
1075 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1076 moveup.add_attribute('deleted', 'true')
1083 save_changes('forced')
1084 populate_subalbums_treeview(false)
1085 $albums_tv.selection.select_path(sel[0])
1091 $current_path = nil #- prevent save_changes from being rerun again
1092 sel = $albums_tv.selection.selected_rows
1093 restore_one = proc { |xmldir|
1094 xmldir.elements.each { |e|
1095 if e.name == 'dir' && e.attributes['deleted']
1098 e.delete_attribute('deleted')
1101 restore_one.call($xmldir)
1102 populate_subalbums_treeview(false)
1103 $albums_tv.selection.select_path(sel[0])
1106 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1109 frame1 = Gtk::Frame.new
1110 fullpath = from_utf8("#{$current_path}/#{filename}")
1112 my_gen_real_thumbnail = proc {
1113 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1117 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1118 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1119 pack_start(img = Gtk::Image.new).
1120 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1121 px, mask = pxb.render_pixmap_and_mask(0)
1122 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1123 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1125 frame1.add(img = Gtk::Image.new)
1128 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1129 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1130 my_gen_real_thumbnail.call
1132 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1135 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1137 tooltips = Gtk::Tooltips.new
1138 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1139 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1141 frame2, textview = create_editzone($autotable_sw, 1, img)
1142 textview.buffer.text = caption
1143 textview.set_justification(Gtk::Justification::CENTER)
1145 vbox = Gtk::VBox.new(false, 5)
1146 vbox.pack_start(evtbox, false, false)
1147 vbox.pack_start(frame2, false, false)
1148 autotable.append(vbox, filename)
1150 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1151 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1153 #- to be able to find widgets by name
1154 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1156 cleanup_all_thumbnails = proc {
1157 #- remove out of sync images
1158 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1159 for sizeobj in $images_size
1160 #- cannot use sizeobj because panoramic images will have a larger width
1161 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1169 cleanup_all_thumbnails.call
1170 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1172 $xmldir.delete_attribute('already-generated')
1173 my_gen_real_thumbnail.call
1176 rotate_and_cleanup = proc { |angle|
1177 cleanup_all_thumbnails.call
1178 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1181 move = proc { |direction|
1182 do_method = "move_#{direction}"
1183 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1185 done = autotable.method(do_method).call(vbox)
1186 textview.grab_focus #- because if moving, focus is stolen
1190 save_undo(_("move %s") % direction,
1192 autotable.method(undo_method).call(vbox)
1193 textview.grab_focus #- because if moving, focus is stolen
1194 autoscroll_if_needed($autotable_sw, img, textview)
1195 $notebook.set_page(1)
1197 autotable.method(do_method).call(vbox)
1198 textview.grab_focus #- because if moving, focus is stolen
1199 autoscroll_if_needed($autotable_sw, img, textview)
1200 $notebook.set_page(1)
1206 color_swap_and_cleanup = proc {
1207 perform_color_swap_and_cleanup = proc {
1208 cleanup_all_thumbnails.call
1209 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1210 my_gen_real_thumbnail.call
1213 perform_color_swap_and_cleanup.call
1215 save_undo(_("color swap"),
1217 perform_color_swap_and_cleanup.call
1219 autoscroll_if_needed($autotable_sw, img, textview)
1220 $notebook.set_page(1)
1222 perform_color_swap_and_cleanup.call
1224 autoscroll_if_needed($autotable_sw, img, textview)
1225 $notebook.set_page(1)
1230 change_seektime_and_cleanup_real = proc { |values|
1231 perform_change_seektime_and_cleanup = proc { |val|
1232 cleanup_all_thumbnails.call
1233 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1234 my_gen_real_thumbnail.call
1236 perform_change_seektime_and_cleanup.call(values[:new])
1238 save_undo(_("specify seektime"),
1240 perform_change_seektime_and_cleanup.call(values[:old])
1242 autoscroll_if_needed($autotable_sw, img, textview)
1243 $notebook.set_page(1)
1245 perform_change_seektime_and_cleanup.call(values[:new])
1247 autoscroll_if_needed($autotable_sw, img, textview)
1248 $notebook.set_page(1)
1253 change_seektime_and_cleanup = proc {
1254 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1255 change_seektime_and_cleanup_real.call(values)
1259 change_pano_amount_and_cleanup_real = proc { |values|
1260 perform_change_pano_amount_and_cleanup = proc { |val|
1261 cleanup_all_thumbnails.call
1262 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1264 perform_change_pano_amount_and_cleanup.call(values[:new])
1266 save_undo(_("change panorama amount"),
1268 perform_change_pano_amount_and_cleanup.call(values[:old])
1270 autoscroll_if_needed($autotable_sw, img, textview)
1271 $notebook.set_page(1)
1273 perform_change_pano_amount_and_cleanup.call(values[:new])
1275 autoscroll_if_needed($autotable_sw, img, textview)
1276 $notebook.set_page(1)
1281 change_pano_amount_and_cleanup = proc {
1282 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1283 change_pano_amount_and_cleanup_real.call(values)
1287 whitebalance_and_cleanup_real = proc { |values|
1288 perform_change_whitebalance_and_cleanup = proc { |val|
1289 cleanup_all_thumbnails.call
1290 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1291 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1292 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1294 perform_change_whitebalance_and_cleanup.call(values[:new])
1296 save_undo(_("fix white balance"),
1298 perform_change_whitebalance_and_cleanup.call(values[:old])
1300 autoscroll_if_needed($autotable_sw, img, textview)
1301 $notebook.set_page(1)
1303 perform_change_whitebalance_and_cleanup.call(values[:new])
1305 autoscroll_if_needed($autotable_sw, img, textview)
1306 $notebook.set_page(1)
1311 whitebalance_and_cleanup = proc {
1312 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1313 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1314 whitebalance_and_cleanup_real.call(values)
1318 gammacorrect_and_cleanup_real = proc { |values|
1319 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1320 cleanup_all_thumbnails.call
1321 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1322 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1323 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1325 perform_change_gammacorrect_and_cleanup.call(values[:new])
1327 save_undo(_("gamma correction"),
1329 perform_change_gammacorrect_and_cleanup.call(values[:old])
1331 autoscroll_if_needed($autotable_sw, img, textview)
1332 $notebook.set_page(1)
1334 perform_change_gammacorrect_and_cleanup.call(values[:new])
1336 autoscroll_if_needed($autotable_sw, img, textview)
1337 $notebook.set_page(1)
1342 gammacorrect_and_cleanup = Proc.new {
1343 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1344 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1345 gammacorrect_and_cleanup_real.call(values)
1349 enhance_and_cleanup = proc {
1350 perform_enhance_and_cleanup = proc {
1351 cleanup_all_thumbnails.call
1352 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1353 my_gen_real_thumbnail.call
1356 cleanup_all_thumbnails.call
1357 perform_enhance_and_cleanup.call
1359 save_undo(_("enhance"),
1361 perform_enhance_and_cleanup.call
1363 autoscroll_if_needed($autotable_sw, img, textview)
1364 $notebook.set_page(1)
1366 perform_enhance_and_cleanup.call
1368 autoscroll_if_needed($autotable_sw, img, textview)
1369 $notebook.set_page(1)
1374 delete = proc { |isacut|
1375 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 })
1378 perform_delete = proc {
1379 after = autotable.get_next_widget(vbox)
1381 after = autotable.get_previous_widget(vbox)
1383 if $config['deleteondisk'] && !isacut
1384 msg 3, "scheduling for delete: #{fullpath}"
1385 $todelete << fullpath
1387 autotable.remove_widget(vbox)
1389 $vbox2widgets[after][:textview].grab_focus
1390 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1394 previous_pos = autotable.get_current_number(vbox)
1398 delete_current_subalbum
1400 save_undo(_("delete"),
1402 autotable.reinsert(pos, vbox, filename)
1403 $notebook.set_page(1)
1404 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1406 msg 3, "removing deletion schedule of: #{fullpath}"
1407 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1410 $notebook.set_page(1)
1419 $cuts << { :vbox => vbox, :filename => filename }
1420 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1425 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1428 autotable.queue_draws << proc {
1429 $vbox2widgets[last[:vbox]][:textview].grab_focus
1430 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1432 save_undo(_("paste"),
1434 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1435 $notebook.set_page(1)
1438 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1440 $notebook.set_page(1)
1443 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1448 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1449 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1450 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1451 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1453 textview.signal_connect('key-press-event') { |w, event|
1456 x, y = autotable.get_current_pos(vbox)
1457 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1458 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1459 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1460 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1462 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1463 $vbox2widgets[widget_up][:textview].grab_focus
1470 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1472 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1473 $vbox2widgets[widget_down][:textview].grab_focus
1480 if event.keyval == Gdk::Keyval::GDK_Left
1483 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1490 rotate_and_cleanup.call(-90)
1493 if event.keyval == Gdk::Keyval::GDK_Right
1494 next_ = autotable.get_next_widget(vbox)
1495 if next_ && autotable.get_current_pos(next_)[0] > x
1497 $vbox2widgets[next_][:textview].grab_focus
1504 rotate_and_cleanup.call(90)
1507 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1510 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1511 view_element(filename, { :delete => delete })
1514 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1517 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1521 !propagate #- propagate if needed
1524 $ignore_next_release = false
1525 evtbox.signal_connect('button-press-event') { |w, event|
1526 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1527 if event.state & Gdk::Window::BUTTON3_MASK != 0
1528 #- gesture redo: hold right mouse button then click left mouse button
1529 $config['nogestures'] or perform_redo
1530 $ignore_next_release = true
1532 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1534 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1536 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1537 elsif $enhance.active?
1538 enhance_and_cleanup.call
1539 elsif $delete.active?
1543 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1546 $button1_pressed_autotable = true
1547 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1548 if event.state & Gdk::Window::BUTTON1_MASK != 0
1549 #- gesture undo: hold left mouse button then click right mouse button
1550 $config['nogestures'] or perform_undo
1551 $ignore_next_release = true
1553 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1554 view_element(filename, { :delete => delete })
1559 evtbox.signal_connect('button-release-event') { |w, event|
1560 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1561 if !$ignore_next_release
1562 x, y = autotable.get_current_pos(vbox)
1563 next_ = autotable.get_next_widget(vbox)
1564 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1565 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1566 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1567 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1568 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1569 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1570 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1572 $ignore_next_release = false
1573 $gesture_press = nil
1578 #- handle reordering with drag and drop
1579 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1580 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1581 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1582 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1585 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1587 #- mouse gesture first (dnd disables button-release-event)
1588 if $gesture_press && $gesture_press[:filename] == filename
1589 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1590 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1591 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1592 rotate_and_cleanup.call(angle)
1593 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1595 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1596 msg 3, "gesture delete: click-drag right button to the bottom"
1598 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1603 ctxt.targets.each { |target|
1604 if target.name == 'reorder-elements'
1605 move_dnd = proc { |from,to|
1608 autotable.move(from, to)
1609 save_undo(_("reorder"),
1612 autotable.move(to - 1, from)
1614 autotable.move(to, from + 1)
1616 $notebook.set_page(1)
1618 autotable.move(from, to)
1619 $notebook.set_page(1)
1624 if $multiple_dnd.size == 0
1625 move_dnd.call(selection_data.data.to_i,
1626 autotable.get_current_number(vbox))
1628 UndoHandler.begin_batch
1629 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1631 #- need to update current position between each call
1632 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1633 autotable.get_current_number(vbox))
1635 UndoHandler.end_batch
1646 def create_auto_table
1648 $autotable = Gtk::AutoTable.new(5)
1650 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1651 thumbnails_vb = Gtk::VBox.new(false, 5)
1653 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1654 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1655 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1656 thumbnails_vb.add($autotable)
1658 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1659 $autotable_sw.add_with_viewport(thumbnails_vb)
1661 #- follows stuff for handling multiple elements selection
1662 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1664 update_selected = proc {
1665 $autotable.current_order.each { |path|
1666 w = $name2widgets[path][:evtbox].window
1667 xm = w.position[0] + w.size[0]/2
1668 ym = w.position[1] + w.size[1]/2
1669 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1670 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1671 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1672 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1675 if $selected_elements[path] && ! $selected_elements[path][:keep]
1676 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))
1677 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1678 $selected_elements.delete(path)
1683 $autotable.signal_connect('realize') { |w,e|
1684 gc = Gdk::GC.new($autotable.window)
1685 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1686 gc.function = Gdk::GC::INVERT
1687 #- autoscroll handling for DND and multiple selections
1688 Gtk.timeout_add(100) {
1689 if ! $autotable.window.nil?
1690 w, x, y, mask = $autotable.window.pointer
1691 if mask & Gdk::Window::BUTTON1_MASK != 0
1692 if y < $autotable_sw.vadjustment.value
1694 $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]])
1696 if $button1_pressed_autotable || press_x
1697 scroll_upper($autotable_sw, y)
1700 w, pos_x, pos_y = $autotable.window.pointer
1701 $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]])
1702 update_selected.call
1705 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1707 $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]])
1709 if $button1_pressed_autotable || press_x
1710 scroll_lower($autotable_sw, y)
1713 w, pos_x, pos_y = $autotable.window.pointer
1714 $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]])
1715 update_selected.call
1720 ! $autotable.window.nil?
1724 $autotable.signal_connect('button-press-event') { |w,e|
1726 if !$button1_pressed_autotable
1729 if e.state & Gdk::Window::SHIFT_MASK == 0
1730 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1731 $selected_elements = {}
1732 $statusbar.push(0, utf8(_("Nothing selected.")))
1734 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1736 set_mousecursor(Gdk::Cursor::TCROSS)
1740 $autotable.signal_connect('button-release-event') { |w,e|
1742 if $button1_pressed_autotable
1743 #- unselect all only now
1744 $multiple_dnd = $selected_elements.keys
1745 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1746 $selected_elements = {}
1747 $button1_pressed_autotable = false
1750 $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]])
1751 if $selected_elements.length > 0
1752 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1755 press_x = press_y = pos_x = pos_y = nil
1756 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1760 $autotable.signal_connect('motion-notify-event') { |w,e|
1763 $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]])
1767 $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]])
1768 update_selected.call
1774 def create_subalbums_page
1776 subalbums_hb = Gtk::HBox.new
1777 $subalbums_vb = Gtk::VBox.new(false, 5)
1778 subalbums_hb.pack_start($subalbums_vb, false, false)
1779 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1780 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1781 $subalbums_sw.add_with_viewport(subalbums_hb)
1784 def save_current_file
1790 ios = File.open($filename, "w")
1791 $xmldoc.write(ios, 0)
1793 rescue Iconv::IllegalSequence
1794 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1795 if ! ios.nil? && ! ios.closed?
1798 $xmldoc.xml_decl.encoding = 'UTF-8'
1799 ios = File.open($filename, "w")
1800 $xmldoc.write(ios, 0)
1811 def save_current_file_user
1812 save_tempfilename = $filename
1813 $filename = $orig_filename
1814 if ! save_current_file
1815 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1816 $filename = save_tempfilename
1820 $generated_outofline = false
1821 $filename = save_tempfilename
1823 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1824 $todelete.each { |f|
1829 def mark_document_as_dirty
1830 $xmldoc.elements.each('//dir') { |elem|
1831 elem.delete_attribute('already-generated')
1835 #- ret: true => ok false => cancel
1836 def ask_save_modifications(msg1, msg2, *options)
1838 options = options.size > 0 ? options[0] : {}
1840 if options[:disallow_cancel]
1841 dialog = Gtk::Dialog.new(msg1,
1843 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1844 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1845 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1847 dialog = Gtk::Dialog.new(msg1,
1849 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1850 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1851 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1852 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1854 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1855 dialog.vbox.add(Gtk::Label.new(msg2))
1856 dialog.window_position = Gtk::Window::POS_CENTER
1859 dialog.run { |response|
1861 if response == Gtk::Dialog::RESPONSE_YES
1862 if ! save_current_file_user
1863 return ask_save_modifications(msg1, msg2, options)
1866 #- if we have generated an album but won't save modifications, we must remove
1867 #- already-generated markers in original file
1868 if $generated_outofline
1870 $xmldoc = Synchronizator.new(REXML::Document.new(File.new($orig_filename)), $xmlaccesslock)
1871 mark_document_as_dirty
1872 ios = File.open($orig_filename, "w")
1873 $xmldoc.write(ios, 0)
1876 puts "exception: #{$!}"
1880 if response == Gtk::Dialog::RESPONSE_CANCEL
1883 $todelete = [] #- unconditionally clear the list of images/videos to delete
1889 def try_quit(*options)
1890 if ask_save_modifications(utf8(_("Save before quitting?")),
1891 utf8(_("Do you want to save your changes before quitting?")),
1897 def show_popup(parent, msg, *options)
1898 dialog = Gtk::Dialog.new
1899 if options[0] && options[0][:title]
1900 dialog.title = options[0][:title]
1902 dialog.title = utf8(_("Booh message"))
1904 lbl = Gtk::Label.new
1905 if options[0] && options[0][:nomarkup]
1910 if options[0] && options[0][:centered]
1911 lbl.set_justify(Gtk::Justification::CENTER)
1913 if options[0] && options[0][:selectable]
1914 lbl.selectable = true
1916 if options[0] && options[0][:topwidget]
1917 dialog.vbox.add(options[0][:topwidget])
1919 if options[0] && options[0][:scrolled]
1920 sw = Gtk::ScrolledWindow.new(nil, nil)
1921 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1922 sw.add_with_viewport(lbl)
1924 dialog.set_default_size(500, 600)
1926 dialog.vbox.add(lbl)
1927 dialog.set_default_size(200, 120)
1929 if options[0] && options[0][:okcancel]
1930 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1932 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1934 if options[0] && options[0][:pos_centered]
1935 dialog.window_position = Gtk::Window::POS_CENTER
1937 dialog.window_position = Gtk::Window::POS_MOUSE
1940 if options[0] && options[0][:linkurl]
1941 linkbut = Gtk::Button.new('')
1942 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1943 linkbut.signal_connect('clicked') {
1944 open_url(options[0][:linkurl])
1945 dialog.response(Gtk::Dialog::RESPONSE_OK)
1946 set_mousecursor_normal
1948 linkbut.relief = Gtk::RELIEF_NONE
1949 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1950 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1951 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1956 if !options[0] || !options[0][:not_transient]
1957 dialog.transient_for = parent
1958 dialog.run { |response|
1960 if options[0] && options[0][:okcancel]
1961 return response == Gtk::Dialog::RESPONSE_OK
1965 dialog.signal_connect('response') { dialog.destroy }
1969 def set_mainwindow_title(progress)
1970 filename = $orig_filename || $filename
1973 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
1975 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
1979 $main_window.title = 'booh - ' + File.basename(filename)
1981 $main_window.title = 'booh'
1986 def backend_wait_message(parent, msg, infopipe_path, mode)
1988 w.set_transient_for(parent)
1991 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1992 vb.pack_start(Gtk::Label.new(msg), false, false)
1994 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1995 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
1996 if mode != 'one dir scan'
1997 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1999 if mode == 'web-album'
2000 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2001 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2003 vb.pack_start(Gtk::HSeparator.new, false, false)
2005 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2006 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2007 vb.pack_end(bottom, false, false)
2010 update_progression_title_pb1 = proc {
2011 if mode == 'web-album'
2012 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2013 elsif mode != 'one dir scan'
2014 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2016 set_mainwindow_title(pb1_1.fraction)
2020 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2021 refresh_thread = Thread.new {
2022 directories_counter = 0
2023 while line = infopipe.gets
2024 if line =~ /^directories: (\d+), sizes: (\d+)/
2025 directories = $1.to_f + 1
2027 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2028 elements = $3.to_f + 1
2029 if mode == 'web-album'
2033 gtk_thread_protect { pb1_1.fraction = 0 }
2034 if mode != 'one dir scan'
2035 newtext = utf8(full_src_dir_to_rel($1, $2))
2036 newtext = '/' if newtext == ''
2037 gtk_thread_protect { pb1_2.text = newtext }
2038 directories_counter += 1
2039 gtk_thread_protect {
2040 pb1_2.fraction = directories_counter / directories
2041 update_progression_title_pb1.call
2044 elsif line =~ /^processing element$/
2045 element_counter += 1
2046 gtk_thread_protect {
2047 pb1_1.fraction = element_counter / elements
2048 update_progression_title_pb1.call
2050 elsif line =~ /^processing size$/
2051 element_counter += 1
2052 gtk_thread_protect {
2053 pb1_1.fraction = element_counter / elements
2054 update_progression_title_pb1.call
2056 elsif line =~ /^finished processing sizes$/
2057 gtk_thread_protect { pb1_1.fraction = 1 }
2058 elsif line =~ /^creating index.html$/
2059 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2060 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2061 directories_counter = 0
2062 elsif line =~ /^index.html: (.+)\|(.+)/
2063 newtext = utf8(full_src_dir_to_rel($1, $2))
2064 newtext = '/' if newtext == ''
2065 gtk_thread_protect { pb2.text = newtext }
2066 directories_counter += 1
2067 gtk_thread_protect {
2068 pb2.fraction = directories_counter / directories
2069 set_mainwindow_title(0.9 + pb2.fraction / 10)
2071 elsif line =~ /^die: (.*)$/
2078 w.signal_connect('delete-event') { w.destroy }
2079 w.signal_connect('destroy') {
2080 Thread.kill(refresh_thread)
2081 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2084 File.delete(infopipe_path)
2086 set_mainwindow_title(nil)
2088 w.window_position = Gtk::Window::POS_CENTER
2094 def call_backend(cmd, waitmsg, mode, params)
2095 pipe = Tempfile.new("boohpipe")
2096 Thread.critical = true
2099 system("mkfifo #{path}")
2100 Thread.critical = false
2101 cmd += " --info-pipe #{path}"
2102 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2107 id, exitstatus = Process.waitpid2(pid)
2108 gtk_thread_protect { w8.destroy }
2110 if params[:successmsg]
2111 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2113 if params[:closure_after]
2114 gtk_thread_protect(¶ms[:closure_after])
2116 elsif exitstatus == 15
2117 #- say nothing, user aborted
2119 gtk_thread_protect { show_popup($main_window,
2120 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2126 button.signal_connect('clicked') {
2127 Process.kill('SIGTERM', pid)
2131 def save_changes(*forced)
2132 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2136 $xmldir.delete_attribute('already-generated')
2138 propagate_children = proc { |xmldir|
2139 if xmldir.attributes['subdirs-caption']
2140 xmldir.delete_attribute('already-generated')
2142 xmldir.elements.each('dir') { |element|
2143 propagate_children.call(element)
2147 if $xmldir.child_byname_notattr('dir', 'deleted')
2148 new_title = $subalbums_title.buffer.text
2149 if new_title != $xmldir.attributes['subdirs-caption']
2150 parent = $xmldir.parent
2151 if parent.name == 'dir'
2152 parent.delete_attribute('already-generated')
2154 propagate_children.call($xmldir)
2156 $xmldir.add_attribute('subdirs-caption', new_title)
2157 $xmldir.elements.each('dir') { |element|
2158 if !element.attributes['deleted']
2159 path = element.attributes['path']
2160 newtext = $subalbums_edits[path][:editzone].buffer.text
2161 if element.attributes['subdirs-caption']
2162 if element.attributes['subdirs-caption'] != newtext
2163 propagate_children.call(element)
2165 element.add_attribute('subdirs-caption', newtext)
2166 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2168 if element.attributes['thumbnails-caption'] != newtext
2169 element.delete_attribute('already-generated')
2171 element.add_attribute('thumbnails-caption', newtext)
2172 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2178 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2179 if $xmldir.attributes['thumbnails-caption']
2180 path = $xmldir.attributes['path']
2181 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2183 elsif $xmldir.attributes['thumbnails-caption']
2184 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2187 if $xmldir.attributes['thumbnails-caption']
2188 if edit = $subalbums_edits[$xmldir.attributes['path']]
2189 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2193 #- remove and reinsert elements to reflect new ordering
2196 $xmldir.elements.each { |element|
2197 if element.name == 'image' || element.name == 'video'
2198 saves[element.attributes['filename']] = element.remove
2202 $autotable.current_order.each { |path|
2203 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2204 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2207 saves.each_key { |path|
2208 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2209 chld.add_attribute('deleted', 'true')
2213 def sort_by_exif_date
2217 $xmldir.elements.each { |element|
2218 if element.name == 'image' || element.name == 'video'
2219 current_order << element.attributes['filename']
2223 #- look for EXIF dates
2226 if current_order.size > 20
2228 w.set_transient_for($main_window)
2230 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2231 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2232 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2233 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2234 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2235 vb.pack_end(bottom, false, false)
2237 w.signal_connect('delete-event') { w.destroy }
2238 w.window_position = Gtk::Window::POS_CENTER
2242 b.signal_connect('clicked') { aborted = true }
2244 current_order.each { |f|
2246 if entry2type(f) == 'image'
2248 pb.fraction = i.to_f / current_order.size
2249 Gtk.main_iteration while Gtk.events_pending?
2250 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2252 dates[f] = date_time
2265 current_order.each { |f|
2266 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2268 dates[f] = date_time
2274 $xmldir.elements.each { |element|
2275 if element.name == 'image' || element.name == 'video'
2276 saves[element.attributes['filename']] = element.remove
2280 neworder = smartsort(current_order, dates)
2283 $xmldir.add_element(saves[f].name, saves[f].attributes)
2286 #- let the auto-table reflect new ordering
2290 def remove_all_captions
2293 $autotable.current_order.each { |path|
2294 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2295 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2297 save_undo(_("remove all captions"),
2299 texts.each_key { |key|
2300 $name2widgets[key][:textview].buffer.text = texts[key]
2302 $notebook.set_page(1)
2304 texts.each_key { |key|
2305 $name2widgets[key][:textview].buffer.text = ''
2307 $notebook.set_page(1)
2313 $selected_elements.each_key { |path|
2314 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2320 $selected_elements = {}
2324 $undo_tb.sensitive = $undo_mb.sensitive = false
2325 $redo_tb.sensitive = $redo_mb.sensitive = false
2331 $subalbums_vb.children.each { |chld|
2332 $subalbums_vb.remove(chld)
2334 $subalbums = Gtk::Table.new(0, 0, true)
2335 current_y_sub_albums = 0
2337 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"], $xmlaccesslock)
2338 $subalbums_edits = {}
2339 subalbums_counter = 0
2340 subalbums_edits_bypos = {}
2342 add_subalbum = proc { |xmldir, counter|
2343 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2344 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2345 if xmldir == $xmldir
2346 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2347 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2348 caption = xmldir.attributes['thumbnails-caption']
2349 infotype = 'thumbnails'
2351 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2352 captionfile, caption = find_subalbum_caption_info(xmldir)
2353 infotype = find_subalbum_info_type(xmldir)
2355 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2356 hbox = Gtk::HBox.new
2357 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2359 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2362 my_gen_real_thumbnail = proc {
2363 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2366 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2367 f.add(img = Gtk::Image.new)
2368 my_gen_real_thumbnail.call
2370 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2372 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2373 $subalbums.attach(hbox,
2374 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2376 frame, textview = create_editzone($subalbums_sw, 0, img)
2377 textview.buffer.text = caption
2378 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2379 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2381 change_image = proc {
2382 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2384 Gtk::FileChooser::ACTION_OPEN,
2386 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2387 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2388 fc.transient_for = $main_window
2389 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))
2390 f.add(preview_img = Gtk::Image.new)
2392 fc.signal_connect('update-preview') { |w|
2394 if fc.preview_filename
2395 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2396 fc.preview_widget_active = true
2398 rescue Gdk::PixbufError
2399 fc.preview_widget_active = false
2402 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2404 old_file = captionfile
2405 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2406 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2407 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2408 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2410 new_file = fc.filename
2411 msg 3, "new captionfile is: #{fc.filename}"
2412 perform_changefile = proc {
2413 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2414 $modified_pixbufs.delete(thumbnail_file)
2415 xmldir.delete_attribute("#{infotype}-rotate")
2416 xmldir.delete_attribute("#{infotype}-color-swap")
2417 xmldir.delete_attribute("#{infotype}-enhance")
2418 xmldir.delete_attribute("#{infotype}-seektime")
2419 my_gen_real_thumbnail.call
2421 perform_changefile.call
2423 save_undo(_("change caption file for sub-album"),
2425 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2426 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2427 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2428 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2429 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2430 my_gen_real_thumbnail.call
2431 $notebook.set_page(0)
2433 perform_changefile.call
2434 $notebook.set_page(0)
2442 if File.exists?(thumbnail_file)
2443 File.delete(thumbnail_file)
2445 my_gen_real_thumbnail.call
2448 rotate_and_cleanup = proc { |angle|
2449 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2450 if File.exists?(thumbnail_file)
2451 File.delete(thumbnail_file)
2455 move = proc { |direction|
2458 save_changes('forced')
2459 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2460 if direction == 'up'
2461 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2462 subalbums_edits_bypos[oldpos - 1][:position] += 1
2464 if direction == 'down'
2465 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2466 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2468 if direction == 'top'
2469 for i in 1 .. oldpos - 1
2470 subalbums_edits_bypos[i][:position] += 1
2472 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2474 if direction == 'bottom'
2475 for i in oldpos + 1 .. subalbums_counter
2476 subalbums_edits_bypos[i][:position] -= 1
2478 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2482 $xmldir.elements.each('dir') { |element|
2483 if (!element.attributes['deleted'])
2484 elems << [ element.attributes['path'], element.remove ]
2487 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2488 each { |e| $xmldir.add_element(e[1]) }
2489 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2490 $xmldir.elements.each('descendant::dir') { |elem|
2491 elem.delete_attribute('already-generated')
2494 sel = $albums_tv.selection.selected_rows
2496 populate_subalbums_treeview(false)
2497 $albums_tv.selection.select_path(sel[0])
2500 color_swap_and_cleanup = proc {
2501 perform_color_swap_and_cleanup = proc {
2502 color_swap(xmldir, "#{infotype}-")
2503 my_gen_real_thumbnail.call
2505 perform_color_swap_and_cleanup.call
2507 save_undo(_("color swap"),
2509 perform_color_swap_and_cleanup.call
2510 $notebook.set_page(0)
2512 perform_color_swap_and_cleanup.call
2513 $notebook.set_page(0)
2518 change_seektime_and_cleanup = proc {
2519 if values = ask_new_seektime(xmldir, "#{infotype}-")
2520 perform_change_seektime_and_cleanup = proc { |val|
2521 change_seektime(xmldir, "#{infotype}-", val)
2522 my_gen_real_thumbnail.call
2524 perform_change_seektime_and_cleanup.call(values[:new])
2526 save_undo(_("specify seektime"),
2528 perform_change_seektime_and_cleanup.call(values[:old])
2529 $notebook.set_page(0)
2531 perform_change_seektime_and_cleanup.call(values[:new])
2532 $notebook.set_page(0)
2538 whitebalance_and_cleanup = proc {
2539 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2540 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2541 perform_change_whitebalance_and_cleanup = proc { |val|
2542 change_whitebalance(xmldir, "#{infotype}-", val)
2543 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2544 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2545 if File.exists?(thumbnail_file)
2546 File.delete(thumbnail_file)
2549 perform_change_whitebalance_and_cleanup.call(values[:new])
2551 save_undo(_("fix white balance"),
2553 perform_change_whitebalance_and_cleanup.call(values[:old])
2554 $notebook.set_page(0)
2556 perform_change_whitebalance_and_cleanup.call(values[:new])
2557 $notebook.set_page(0)
2563 gammacorrect_and_cleanup = proc {
2564 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2565 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2566 perform_change_gammacorrect_and_cleanup = proc { |val|
2567 change_gammacorrect(xmldir, "#{infotype}-", val)
2568 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2569 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2570 if File.exists?(thumbnail_file)
2571 File.delete(thumbnail_file)
2574 perform_change_gammacorrect_and_cleanup.call(values[:new])
2576 save_undo(_("gamma correction"),
2578 perform_change_gammacorrect_and_cleanup.call(values[:old])
2579 $notebook.set_page(0)
2581 perform_change_gammacorrect_and_cleanup.call(values[:new])
2582 $notebook.set_page(0)
2588 enhance_and_cleanup = proc {
2589 perform_enhance_and_cleanup = proc {
2590 enhance(xmldir, "#{infotype}-")
2591 my_gen_real_thumbnail.call
2594 perform_enhance_and_cleanup.call
2596 save_undo(_("enhance"),
2598 perform_enhance_and_cleanup.call
2599 $notebook.set_page(0)
2601 perform_enhance_and_cleanup.call
2602 $notebook.set_page(0)
2607 evtbox.signal_connect('button-press-event') { |w, event|
2608 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2610 rotate_and_cleanup.call(90)
2612 rotate_and_cleanup.call(-90)
2613 elsif $enhance.active?
2614 enhance_and_cleanup.call
2617 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2618 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2619 { :forbid_left => true, :forbid_right => true,
2620 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2621 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2622 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2623 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2624 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2626 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2631 evtbox.signal_connect('button-press-event') { |w, event|
2632 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2636 evtbox.signal_connect('button-release-event') { |w, event|
2637 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2638 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2639 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2640 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2641 msg 3, "gesture rotate: #{angle}"
2642 rotate_and_cleanup.call(angle)
2645 $gesture_press = nil
2648 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2649 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2650 current_y_sub_albums += 1
2653 if $xmldir.child_byname_notattr('dir', 'deleted')
2655 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2656 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2657 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2658 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2659 #- this album image/caption
2660 if $xmldir.attributes['thumbnails-caption']
2661 add_subalbum.call($xmldir, 0)
2664 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2665 $xmldir.elements.each { |element|
2666 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2667 #- element (image or video) of this album
2668 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2669 msg 3, "dest_img: #{dest_img}"
2670 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2671 total[element.name] += 1
2673 if element.name == 'dir' && !element.attributes['deleted']
2674 #- sub-album image/caption
2675 add_subalbum.call(element, subalbums_counter += 1)
2676 total[element.name] += 1
2679 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2680 total['image'], total['video'], total['dir'] ]))
2681 $subalbums_vb.add($subalbums)
2682 $subalbums_vb.show_all
2684 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2685 $notebook.get_tab_label($autotable_sw).sensitive = false
2686 $notebook.set_page(0)
2687 $thumbnails_title.buffer.text = ''
2689 $notebook.get_tab_label($autotable_sw).sensitive = true
2690 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2693 if !$xmldir.child_byname_notattr('dir', 'deleted')
2694 $notebook.get_tab_label($subalbums_sw).sensitive = false
2695 $notebook.set_page(1)
2697 $notebook.get_tab_label($subalbums_sw).sensitive = true
2701 def pixbuf_or_nil(filename)
2703 return Gdk::Pixbuf.new(filename)
2709 def theme_choose(current)
2710 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2712 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2713 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2714 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2716 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2717 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2718 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2719 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2720 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2721 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2722 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2723 treeview.signal_connect('button-press-event') { |w, event|
2724 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2725 dialog.response(Gtk::Dialog::RESPONSE_OK)
2729 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2731 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2734 iter[0] = File.basename(dir)
2735 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2736 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2737 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2738 if File.basename(dir) == current
2739 treeview.selection.select_iter(iter)
2742 dialog.set_default_size(-1, 500)
2743 dialog.vbox.show_all
2745 dialog.run { |response|
2746 iter = treeview.selection.selected
2748 if response == Gtk::Dialog::RESPONSE_OK && iter
2749 return model.get_value(iter, 0)
2755 def show_password_protections
2756 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2757 child_iter = $albums_iters[xmldir.attributes['path']]
2758 if xmldir.attributes['password-protect']
2759 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2760 already_protected = true
2761 elsif already_protected
2762 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2764 pix = pix.saturate_and_pixelate(1, true)
2770 xmldir.elements.each('dir') { |elem|
2771 if !elem.attributes['deleted']
2772 examine_dir_elem.call(child_iter, elem, already_protected)
2776 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2779 def populate_subalbums_treeview(select_first)
2783 $subalbums_vb.children.each { |chld|
2784 $subalbums_vb.remove(chld)
2787 source = $xmldoc.root.attributes['source']
2788 msg 3, "source: #{source}"
2790 xmldir = $xmldoc.elements['//dir']
2791 if !xmldir || xmldir.attributes['path'] != source
2792 msg 1, _("Corrupted booh file...")
2796 append_dir_elem = proc { |parent_iter, xmldir|
2797 child_iter = $albums_ts.append(parent_iter)
2798 child_iter[0] = File.basename(xmldir.attributes['path'])
2799 child_iter[1] = xmldir.attributes['path']
2800 $albums_iters[xmldir.attributes['path']] = child_iter
2801 msg 3, "puttin location: #{xmldir.attributes['path']}"
2802 xmldir.elements.each('dir') { |elem|
2803 if !elem.attributes['deleted']
2804 append_dir_elem.call(child_iter, elem)
2808 append_dir_elem.call(nil, xmldir)
2809 show_password_protections
2811 $albums_tv.expand_all
2813 $albums_tv.selection.select_iter($albums_ts.iter_first)
2817 def select_current_theme
2818 select_theme($xmldoc.root.attributes['theme'],
2819 $xmldoc.root.attributes['limit-sizes'],
2820 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2821 $xmldoc.root.attributes['thumbnails-per-row'])
2824 def open_file(filename)
2828 $current_path = nil #- invalidate
2829 $modified_pixbufs = {}
2832 $subalbums_vb.children.each { |chld|
2833 $subalbums_vb.remove(chld)
2836 if !File.exists?(filename)
2837 return utf8(_("File not found."))
2841 $xmldoc = Synchronizator.new(REXML::Document.new(File.new(filename)), $xmlaccesslock)
2846 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2847 if entry2type(filename).nil?
2848 return utf8(_("Not a booh file!"))
2850 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."))
2854 if !source = $xmldoc.root.attributes['source']
2855 return utf8(_("Corrupted booh file..."))
2858 if !dest = $xmldoc.root.attributes['destination']
2859 return utf8(_("Corrupted booh file..."))
2862 if !theme = $xmldoc.root.attributes['theme']
2863 return utf8(_("Corrupted booh file..."))
2866 if $xmldoc.root.attributes['version'] < '0.9.0'
2867 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2868 mark_document_as_dirty
2869 if $xmldoc.root.attributes['version'] < '0.8.4'
2870 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2871 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2872 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2873 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2874 if old_dest_dir != new_dest_dir
2875 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2877 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2878 xmldir.elements.each { |element|
2879 if %w(image video).include?(element.name) && !element.attributes['deleted']
2880 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2881 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2882 Dir[old_name + '*'].each { |file|
2883 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2884 file != new_file and sys("mv '#{file}' '#{new_file}'")
2887 if element.name == 'dir' && !element.attributes['deleted']
2888 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2889 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2890 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2894 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2898 $xmldoc.root.add_attribute('version', $VERSION)
2901 select_current_theme
2903 $filename = filename
2904 set_mainwindow_title(nil)
2905 $default_size['thumbnails'] =~ /(.*)x(.*)/
2906 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2907 $albums_thumbnail_size =~ /(.*)x(.*)/
2908 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2910 populate_subalbums_treeview(true)
2912 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2916 def open_file_user(filename)
2917 result = open_file(filename)
2919 $config['last-opens'] ||= []
2920 if $config['last-opens'][-1] != utf8(filename)
2921 $config['last-opens'] << utf8(filename)
2923 $orig_filename = $filename
2924 $main_window.title = 'booh - ' + File.basename($orig_filename)
2925 tmp = Tempfile.new("boohtemp")
2926 Thread.critical = true
2927 $filename = tmp.path
2930 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
2931 Thread.critical = false
2933 $tempfiles << $filename << "#{$filename}.backup"
2935 $orig_filename = nil
2941 if !ask_save_modifications(utf8(_("Save this album?")),
2942 utf8(_("Do you want to save the changes to this album?")),
2943 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2946 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2948 Gtk::FileChooser::ACTION_OPEN,
2950 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2951 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2952 fc.set_current_folder(File.expand_path("~/.booh"))
2953 fc.transient_for = $main_window
2956 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2957 push_mousecursor_wait(fc)
2958 msg = open_file_user(fc.filename)
2973 def additional_booh_options
2976 options += "--mproc #{$config['mproc'].to_i} "
2978 options += "--comments-format '#{$config['comments-format']}' "
2979 if $config['transcode-videos']
2980 options += "--transcode-videos '#{$config['transcode-videos']}' "
2985 def ask_multi_languages(value)
2987 spl = value.split(',')
2988 value = [ spl[0..-2], spl[-1] ]
2991 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2994 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2995 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2997 lbl = Gtk::Label.new
2999 _("You can choose to activate <b>multi-languages</b> support for this web-album
3000 (it will work only if you publish your web-album on an Apache web-server). This will
3001 use the MultiViews feature of Apache; the pages will be served according to the
3002 value of the Accept-Language HTTP header sent by the web browsers, so that people
3003 with different languages preferences will be able to browse your web-album with
3004 navigation in their language (if language is available).
3007 dialog.vbox.add(lbl)
3008 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3009 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3010 add(languages = Gtk::Button.new))))
3012 pick_languages = proc {
3013 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3016 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3017 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3019 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3020 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3022 SUPPORTED_LANGUAGES.each { |lang|
3023 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3024 if ! value.nil? && value[0].include?(lang)
3030 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3031 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3032 fallback_language = nil
3033 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3034 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3035 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3036 fbl_rb.active = true
3037 fallback_language = SUPPORTED_LANGUAGES[0]
3039 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3040 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3041 rb.signal_connect('clicked') { fallback_language = lang }
3042 if ! value.nil? && value[1] == lang
3047 dialog2.window_position = Gtk::Window::POS_MOUSE
3051 dialog2.run { |response|
3053 if resp == Gtk::Dialog::RESPONSE_OK
3055 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3056 value[1] = fallback_language
3057 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3064 languages.signal_connect('clicked') {
3067 dialog.window_position = Gtk::Window::POS_MOUSE
3071 rb_yes.active = true
3072 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3074 rb_no.signal_connect('clicked') {
3078 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3091 dialog.run { |response|
3096 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3098 return [ true, nil ]
3100 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3109 if !ask_save_modifications(utf8(_("Save this album?")),
3110 utf8(_("Do you want to save the changes to this album?")),
3111 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3114 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3116 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3117 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3118 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3120 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3121 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3122 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3123 tbl.attach(src = Gtk::Entry.new,
3124 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3125 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3126 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3127 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3128 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3129 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3130 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3131 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3132 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3133 tbl.attach(dest = Gtk::Entry.new,
3134 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3135 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3136 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3137 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3138 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3139 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3140 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3141 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3142 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3144 tooltips = Gtk::Tooltips.new
3145 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3146 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3147 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3148 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3149 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3150 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3151 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)
3152 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3153 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3154 nperpage_model = Gtk::ListStore.new(String, String)
3155 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3156 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3157 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3158 nperpagecombo.set_attributes(crt, { :markup => 0 })
3159 iter = nperpage_model.append
3160 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3162 [ 12, 20, 30, 40, 50 ].each { |v|
3163 iter = nperpage_model.append
3164 iter[0] = iter[1] = v.to_s
3166 nperpagecombo.active = 0
3168 multilanguages_value = nil
3169 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3170 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3171 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)
3172 multilanguages.signal_connect('clicked') {
3173 retval = ask_multi_languages(multilanguages_value)
3175 multilanguages_value = retval[1]
3177 if multilanguages_value
3178 ml_label.text = utf8(_("Multi-languages: enabled."))
3180 ml_label.text = utf8(_("Multi-languages: disabled."))
3183 if $config['default-multi-languages']
3184 multilanguages_value = $config['default-multi-languages']
3185 ml_label.text = utf8(_("Multi-languages: enabled."))
3187 ml_label.text = utf8(_("Multi-languages: disabled."))
3190 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3191 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3192 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)
3193 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3194 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3195 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)
3197 src_nb_calculated_for = ''
3199 process_src_nb = proc {
3200 if src.text != src_nb_calculated_for
3201 src_nb_calculated_for = src.text
3203 Thread.kill(src_nb_thread)
3206 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3207 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3209 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3210 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3211 src_nb_thread = Thread.new {
3212 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3213 total = { 'image' => 0, 'video' => 0, nil => 0 }
3214 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3215 if File.basename(dir) =~ /^\./
3219 Dir.entries(dir.chomp).each { |file|
3220 total[entry2type(file)] += 1
3222 rescue Errno::EACCES, Errno::ENOENT
3226 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3230 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3233 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3239 timeout_src_nb = Gtk.timeout_add(100) {
3243 src_browse.signal_connect('clicked') {
3244 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3246 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3248 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3249 fc.transient_for = $main_window
3250 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3251 src.text = utf8(fc.filename)
3253 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3258 dest_browse.signal_connect('clicked') {
3259 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3261 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3263 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3264 fc.transient_for = $main_window
3265 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3266 dest.text = utf8(fc.filename)
3271 conf_browse.signal_connect('clicked') {
3272 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3274 Gtk::FileChooser::ACTION_SAVE,
3276 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3277 fc.transient_for = $main_window
3278 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3279 fc.set_current_folder(File.expand_path("~/.booh"))
3280 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3281 conf.text = utf8(fc.filename)
3288 recreate_theme_config = proc {
3289 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3291 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3292 $images_size.each { |s|
3293 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3297 tooltips.set_tip(cb, utf8(s['description']), nil)
3298 theme_sizes << { :widget => cb, :value => s['name'] }
3300 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3301 tooltips = Gtk::Tooltips.new
3302 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3303 theme_sizes << { :widget => cb, :value => 'original' }
3306 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3309 $allowed_N_values.each { |n|
3311 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3313 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3315 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3319 nperrows << { :widget => rb, :value => n }
3321 nperrowradios.show_all
3323 recreate_theme_config.call
3325 theme_button.signal_connect('clicked') {
3326 if newtheme = theme_choose(theme_button.label)
3327 theme_button.label = newtheme
3328 recreate_theme_config.call
3332 dialog.vbox.add(frame1)
3333 dialog.vbox.add(frame2)
3339 dialog.run { |response|
3340 if response == Gtk::Dialog::RESPONSE_OK
3341 srcdir = from_utf8_safe(src.text)
3342 destdir = from_utf8_safe(dest.text)
3343 confpath = from_utf8_safe(conf.text)
3344 if src.text != '' && srcdir == ''
3345 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3347 elsif !File.directory?(srcdir)
3348 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3350 elsif dest.text != '' && destdir == ''
3351 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3353 elsif destdir != make_dest_filename(destdir)
3354 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3356 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3357 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3358 inside it will be permanently removed before creating the web-album!
3359 Are you sure you want to continue?")), { :okcancel => true })
3361 elsif File.exists?(destdir) && !File.directory?(destdir)
3362 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3364 elsif conf.text == ''
3365 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3367 elsif conf.text != '' && confpath == ''
3368 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3370 elsif File.directory?(confpath)
3371 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3373 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3374 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3376 system("mkdir '#{destdir}'")
3377 if !File.directory?(destdir)
3378 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3390 srcdir = from_utf8(src.text)
3391 destdir = from_utf8(dest.text)
3392 configskel = File.expand_path(from_utf8(conf.text))
3393 theme = theme_button.label
3394 #- some sort of automatic theme preference
3395 $config['default-theme'] = theme
3396 $config['default-multi-languages'] = multilanguages_value
3397 $config['default-optimize32'] = optimize432.active?.to_s
3398 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3399 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3400 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3401 opt432 = optimize432.active?
3402 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3403 indexlink = indexlinkentry.text.gsub('\'', ''')
3406 Thread.kill(src_nb_thread)
3407 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3410 Gtk.timeout_remove(timeout_src_nb)
3413 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3414 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3415 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3416 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3417 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3418 utf8(_("Please wait while scanning source directory...")),
3420 { :closure_after => proc {
3421 open_file_user(configskel)
3422 $main_window.urgency_hint = true
3428 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3430 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3431 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3432 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3434 source = $xmldoc.root.attributes['source']
3435 dest = $xmldoc.root.attributes['destination']
3436 theme = $xmldoc.root.attributes['theme']
3437 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3438 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3439 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3440 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3442 limit_sizes = limit_sizes.split(/,/)
3444 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3445 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3446 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3448 tooltips = Gtk::Tooltips.new
3449 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3450 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3451 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3452 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3453 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3454 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3455 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3456 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3457 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3458 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3459 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3460 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3461 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3463 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3464 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3465 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3466 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3467 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3468 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3469 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)
3470 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3471 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3472 nperpage_model = Gtk::ListStore.new(String, String)
3473 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3474 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3475 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3476 nperpagecombo.set_attributes(crt, { :markup => 0 })
3477 iter = nperpage_model.append
3478 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3480 [ 12, 20, 30, 40, 50 ].each { |v|
3481 iter = nperpage_model.append
3482 iter[0] = iter[1] = v.to_s
3483 if nperpage && nperpage == v.to_s
3484 nperpagecombo.active_iter = iter
3487 if nperpagecombo.active_iter.nil?
3488 nperpagecombo.active = 0
3491 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3492 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3493 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)
3495 if save_multilanguages_value
3496 ml_label.text = utf8(_("Multi-languages: enabled."))
3498 ml_label.text = utf8(_("Multi-languages: disabled."))
3502 multilanguages.signal_connect('clicked') {
3503 retval = ask_multi_languages(save_multilanguages_value)
3505 save_multilanguages_value = retval[1]
3510 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3511 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3513 indexlinkentry.text = indexlink