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-2009 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'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
45 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
46 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
75 puts _("Booh version %s
77 Copyright (c) 2005-2009 Guillaume Cottenceau.
78 This is free software; see the source for copying conditions. There is NO
79 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
83 when '--verbose-level'
84 $verbose_level = arg.to_i
97 $config_file = File.expand_path('~/.booh-gui-rc')
98 if File.readable?($config_file)
100 xmldoc = REXML::Document.new(File.new($config_file))
102 #- encoding unsupported anymore? file edited manually? ignore then
103 msg 1, "Ignoring #{$config_file}, failed to parse it: #{$!}"
106 xmldoc.root.elements.each { |element|
107 txt = element.get_text
109 if txt.value =~ /~~~/ || element.name == 'last-opens'
110 $config[element.name] = txt.value.split(/~~~/)
112 $config[element.name] = txt.value
114 elsif element.elements.size == 0
115 $config[element.name] = ''
117 $config[element.name] = {}
118 element.each { |chld|
120 $config[element.name][chld.name] = txt ? txt.value : nil
126 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
127 $config['image-editor'] ||= '/usr/bin/gimp-remote %f || /usr/bin/gimp %f'
128 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f || /usr/bin/firefox %f"
129 $config['comments-format'] ||= '%t'
130 if !FileTest.directory?(File.expand_path('~/.booh'))
131 system("mkdir ~/.booh")
133 if $config['mproc'].nil?
135 for line in IO.readlines('/proc/cpuinfo') do
136 line =~ /^processor/ and cpus += 1
139 $config['mproc'] = cpus
142 $config['rotate-set-exif'] ||= 'true'
148 if !system("which convert >/dev/null 2>/dev/null")
149 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
150 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
153 if !system("which identify >/dev/null 2>/dev/null")
154 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
155 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
157 if !system("which exif >/dev/null 2>/dev/null")
158 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
160 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
162 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
165 viewer_binary = $config['video-viewer'].split.first
166 if viewer_binary && !File.executable?(viewer_binary)
167 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
168 You should fix this in Edit/Preferences so that you can view videos.
170 Problem was: '%s' is not an executable file.
171 Hint: don't forget to specify the full path to the executable,
172 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
176 def check_image_editor
177 if last_failed_binary = check_multi_binaries($config['image-editor'])
178 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
179 You should fix this in Edit/Preferences so that you can edit photos externally.
181 Problem was: '%s' is not an executable file.
182 Hint: don't forget to specify the full path to the executable,
183 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
191 if $config['last-opens'] && $config['last-opens'].size > 10
192 $config['last-opens'] = $config['last-opens'][-10, 10]
195 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
196 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
197 $config.each_pair { |key, value|
198 elem = xmldoc.root.add_element key
200 $config[key].each_pair { |subkey, subvalue|
201 subelem = elem.add_element subkey
202 subelem.add_text subvalue.to_s
204 elsif value.is_a? Array
205 elem.add_text value.join('~~~')
210 elem.add_text value.to_s
214 ios = File.open($config_file, "w")
218 $tempfiles.each { |f|
225 def set_mousecursor(what, *widget)
226 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
227 if widget[0] && widget[0].window
228 widget[0].window.cursor = cursor
230 if $main_window && $main_window.window
231 $main_window.window.cursor = cursor
233 $current_cursor = what
235 def set_mousecursor_wait(*widget)
236 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
237 if Thread.current == Thread.main
238 Gtk.main_iteration while Gtk.events_pending?
241 def set_mousecursor_normal(*widget)
242 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
244 def push_mousecursor_wait(*widget)
245 if $current_cursor != Gdk::Cursor::WATCH
246 $save_cursor = $current_cursor
247 gtk_thread_protect { set_mousecursor_wait(*widget) }
250 def pop_mousecursor(*widget)
251 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
255 source = $xmldoc.root.attributes['source']
256 dest = $xmldoc.root.attributes['destination']
257 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
260 def full_src_dir_to_rel(path, source)
261 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
264 def build_full_dest_filename(filename)
265 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
268 def save_undo(name, closure, *params)
269 UndoHandler.save_undo(name, closure, [ *params ])
270 $undo_tb.sensitive = $undo_mb.sensitive = true
271 $redo_tb.sensitive = $redo_mb.sensitive = false
274 def view_element(filename, closures)
275 if entry2type(filename) == 'video'
276 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
282 w = create_window.set_title(filename)
284 msg 3, "filename: #{filename}"
285 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
286 #- typically this file won't exist in case of videos; try with the largest thumbnail around
287 if !File.exists?(dest_img)
288 if entry2type(filename) == 'video'
289 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
290 if not alternatives.empty?
291 dest_img = alternatives[-1]
294 push_mousecursor_wait
295 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
297 if !File.exists?(dest_img)
298 msg 2, _("Could not generate fullscreen thumbnail!")
303 aspect = utf8(_("Aspect: unknown"))
304 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
306 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
308 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
309 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)))
310 evt.signal_connect('button-press-event') { |this, event|
311 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
312 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
314 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
316 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
317 delete_item.signal_connect('activate') {
319 closures[:delete].call(false)
322 menu.popup(nil, nil, event.button, event.time)
325 evt.signal_connect('button-release-event') { |this, event|
327 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
328 msg 3, "gesture delete: click-drag right button to the bottom"
330 closures[:delete].call(false)
331 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
335 tooltips = Gtk::Tooltips.new
336 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
338 w.signal_connect('key-press-event') { |w,event|
339 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
341 closures[:delete].call(false)
345 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
346 b.signal_connect('clicked') { w.destroy }
349 vb.pack_start(evt, false, false)
350 vb.pack_end(bottom, false, false)
353 w.signal_connect('delete-event') { w.destroy }
354 w.window_position = Gtk::Window::POS_CENTER
358 def scroll_upper(scrolledwindow, ypos_top)
359 newval = scrolledwindow.vadjustment.value -
360 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
361 if newval < scrolledwindow.vadjustment.lower
362 newval = scrolledwindow.vadjustment.lower
364 scrolledwindow.vadjustment.value = newval
367 def scroll_lower(scrolledwindow, ypos_bottom)
368 newval = scrolledwindow.vadjustment.value +
369 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
370 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
371 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
373 scrolledwindow.vadjustment.value = newval
376 def autoscroll_if_needed(scrolledwindow, image, textview)
377 #- autoscroll if cursor or image is not visible, if possible
378 if image && image.window || textview.window
379 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
380 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
381 current_miny_visible = scrolledwindow.vadjustment.value
382 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
383 if ypos_top < current_miny_visible
384 scroll_upper(scrolledwindow, ypos_top)
385 elsif ypos_bottom > current_maxy_visible
386 scroll_lower(scrolledwindow, ypos_bottom)
391 def create_editzone(scrolledwindow, pagenum, image)
392 frame = Gtk::Frame.new
393 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
394 frame.set_shadow_type(Gtk::SHADOW_IN)
395 textview.signal_connect('key-press-event') { |w, event|
396 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
397 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
398 scrolledwindow.signal_emit('key-press-event', event)
400 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
401 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
402 if event.keyval == Gdk::Keyval::GDK_Up
403 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
404 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
406 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
409 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
410 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
412 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
419 candidate_undo_text = nil
420 textview.signal_connect('focus-in-event') { |w, event|
421 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
422 candidate_undo_text = textview.buffer.text
426 textview.signal_connect('key-release-event') { |w, event|
427 if candidate_undo_text && candidate_undo_text != textview.buffer.text
429 save_undo(_("text edit"),
431 save_text = textview.buffer.text
432 textview.buffer.text = text
434 $notebook.set_page(pagenum)
436 textview.buffer.text = save_text
438 $notebook.set_page(pagenum)
440 }, candidate_undo_text)
441 candidate_undo_text = nil
444 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)
445 autoscroll_if_needed(scrolledwindow, image, textview)
450 return [ frame, textview ]
453 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
455 if !$modified_pixbufs[thumbnail_img]
456 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
457 elsif !$modified_pixbufs[thumbnail_img][:orig]
458 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
461 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
464 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
465 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
466 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
467 if pixbuf.height > desired_y
468 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
469 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
470 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
475 if $modified_pixbufs[thumbnail_img][:whitebalance]
476 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
479 #- fix gamma correction
480 if $modified_pixbufs[thumbnail_img][:gammacorrect]
481 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
484 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
487 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
490 #- update rotate attribute
491 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
492 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
494 #- change exif orientation if configured so (but forget in case of thumbnails caption)
495 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
496 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
499 $modified_pixbufs[thumbnail_img] ||= {}
500 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
501 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
503 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
506 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
509 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
511 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
513 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
514 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
516 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
517 $notebook.set_page(0)
518 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
523 def color_swap(xmldir, attributes_prefix)
525 rexml_thread_protect {
526 if xmldir.attributes["#{attributes_prefix}color-swap"]
527 xmldir.delete_attribute("#{attributes_prefix}color-swap")
529 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
534 def enhance(xmldir, attributes_prefix)
536 rexml_thread_protect {
537 if xmldir.attributes["#{attributes_prefix}enhance"]
538 xmldir.delete_attribute("#{attributes_prefix}enhance")
540 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
545 def change_seektime(xmldir, attributes_prefix, value)
547 rexml_thread_protect {
548 xmldir.add_attribute("#{attributes_prefix}seektime", value)
552 def ask_new_seektime(xmldir, attributes_prefix)
553 value = rexml_thread_protect {
555 xmldir.attributes["#{attributes_prefix}seektime"]
561 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
563 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
564 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
565 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
569 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
573 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
574 entry.signal_connect('key-press-event') { |w, event|
575 if event.keyval == Gdk::Keyval::GDK_Return
576 dialog.response(Gtk::Dialog::RESPONSE_OK)
578 elsif event.keyval == Gdk::Keyval::GDK_Escape
579 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
582 false #- propagate if needed
586 dialog.window_position = Gtk::Window::POS_MOUSE
589 dialog.run { |response|
592 if response == Gtk::Dialog::RESPONSE_OK
594 msg 3, "changing seektime to #{newval}"
595 return { :old => value, :new => newval }
602 def change_pano_amount(xmldir, attributes_prefix, value)
604 rexml_thread_protect {
606 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
608 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
613 def ask_new_pano_amount(xmldir, attributes_prefix)
614 value = rexml_thread_protect {
616 xmldir.attributes["#{attributes_prefix}pano-amount"]
622 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
624 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
625 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
626 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
630 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
631 of this panorama image compared to other regular images. For example, if the panorama
632 was taken out of four photos on one row, counting the necessary overlap, the width of
633 this panorama image should probably be roughly three times the width of regular images.
635 With this information, booh will be able to generate panorama thumbnails looking
636 the right 'size', since the height of the thumbnail for this image will be similar
637 to the height of other thumbnails.
640 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)")))).
641 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
642 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
643 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
644 spin.signal_connect('value-changed') {
647 dialog.window_position = Gtk::Window::POS_MOUSE
650 spin.value = value.to_f
657 dialog.run { |response|
661 newval = spin.value.to_f
664 if response == Gtk::Dialog::RESPONSE_OK
666 msg 3, "changing panorama amount to #{newval}"
667 return { :old => value, :new => newval }
674 def change_whitebalance(xmlelem, attributes_prefix, value)
676 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
679 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
681 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
682 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
683 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
684 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
685 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
686 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
687 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
688 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
689 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
690 $modified_pixbufs[thumbnail_img] ||= {}
691 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
692 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
694 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
695 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
697 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
700 $modified_pixbufs[thumbnail_img] ||= {}
701 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
703 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
706 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
707 #- init $modified_pixbufs correctly
708 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
710 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
712 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
714 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
715 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
716 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
720 _("You can fix the <b>white balance</b> of the image, if your image is too blue
721 or too yellow because the recorder didn't detect the light correctly. Drag the
722 slider below the image to the left for more blue, to the right for more yellow.
726 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
728 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
730 dialog.window_position = Gtk::Window::POS_MOUSE
734 timeout = Gtk.timeout_add(100) {
735 if hs.value != lastval
738 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
744 dialog.run { |response|
745 Gtk.timeout_remove(timeout)
746 if response == Gtk::Dialog::RESPONSE_OK
748 newval = hs.value.to_s
749 msg 3, "changing white balance to #{newval}"
751 return { :old => value, :new => newval }
754 $modified_pixbufs[thumbnail_img] ||= {}
755 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
756 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
764 def change_gammacorrect(xmlelem, attributes_prefix, value)
766 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
769 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
771 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
772 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
773 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
774 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
775 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
776 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
777 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
778 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
779 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
780 $modified_pixbufs[thumbnail_img] ||= {}
781 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
782 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
784 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
785 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
787 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
790 $modified_pixbufs[thumbnail_img] ||= {}
791 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
793 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
796 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
797 #- init $modified_pixbufs correctly
798 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
800 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
802 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
804 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
805 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
806 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
810 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
811 or too bright. Drag the slider below the image.
815 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
817 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
819 dialog.window_position = Gtk::Window::POS_MOUSE
823 timeout = Gtk.timeout_add(100) {
824 if hs.value != lastval
827 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
833 dialog.run { |response|
834 Gtk.timeout_remove(timeout)
835 if response == Gtk::Dialog::RESPONSE_OK
837 newval = hs.value.to_s
838 msg 3, "gamma correction to #{newval}"
840 return { :old => value, :new => newval }
843 $modified_pixbufs[thumbnail_img] ||= {}
844 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
845 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
853 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
854 if File.exists?(destfile)
855 File.delete(destfile)
857 #- type can be 'element' or 'subdir'
859 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
861 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
865 $max_gen_thumbnail_threads = nil
866 $current_gen_thumbnail_threads = 0
867 $gen_thumbnail_monitor = Monitor.new
869 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
870 if $max_gen_thumbnail_threads.nil?
871 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
874 push_mousecursor_wait
875 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
878 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
883 $gen_thumbnail_monitor.synchronize {
884 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
885 $current_gen_thumbnail_threads += 1
890 msg 3, "generate thumbnail from new thread"
893 $gen_thumbnail_monitor.synchronize {
894 $current_gen_thumbnail_threads -= 1
898 msg 3, "generate thumbnail from current thread"
903 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
904 distribute_multiple_call = Proc.new { |action, arg|
905 $selected_elements.each_key { |path|
906 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
908 if possible_actions[:can_multiple] && $selected_elements.length > 0
909 UndoHandler.begin_batch
910 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
911 UndoHandler.end_batch
913 closures[action].call(arg)
915 $selected_elements = {}
918 if optionals.include?('change_image')
919 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
920 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
921 changeimg.signal_connect('activate') { closures[:change].call }
922 menu.append(Gtk::SeparatorMenuItem.new)
924 if !possible_actions[:can_multiple] || $selected_elements.length == 0
927 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
928 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
929 view.signal_connect('activate') { closures[:view].call }
931 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
932 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
933 view.signal_connect('activate') { closures[:view].call }
934 menu.append(Gtk::SeparatorMenuItem.new)
937 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
938 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
939 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
940 exif.signal_connect('activate') { show_popup($main_window,
941 utf8(`exif -m '#{fullpath}'`),
942 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
943 menu.append(Gtk::SeparatorMenuItem.new)
946 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
947 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
948 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
949 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
950 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
951 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
952 if !possible_actions[:can_multiple] || $selected_elements.length == 0
953 menu.append(Gtk::SeparatorMenuItem.new)
954 if !possible_actions[:forbid_left]
955 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
956 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
957 moveleft.signal_connect('activate') { closures[:move].call('left') }
958 if !possible_actions[:can_left]
959 moveleft.sensitive = false
962 if !possible_actions[:forbid_right]
963 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
964 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
965 moveright.signal_connect('activate') { closures[:move].call('right') }
966 if !possible_actions[:can_right]
967 moveright.sensitive = false
970 if optionals.include?('move_top')
971 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
972 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
973 movetop.signal_connect('activate') { closures[:move].call('top') }
974 if !possible_actions[:can_top]
975 movetop.sensitive = false
978 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
979 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
980 moveup.signal_connect('activate') { closures[:move].call('up') }
981 if !possible_actions[:can_up]
982 moveup.sensitive = false
984 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
985 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
986 movedown.signal_connect('activate') { closures[:move].call('down') }
987 if !possible_actions[:can_down]
988 movedown.sensitive = false
990 if optionals.include?('move_bottom')
991 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
992 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
993 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
994 if !possible_actions[:can_bottom]
995 movebottom.sensitive = false
1000 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
1001 menu.append(Gtk::SeparatorMenuItem.new)
1002 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
1003 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
1004 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
1005 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1006 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1007 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1008 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1009 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1010 seektime.signal_connect('activate') {
1011 if possible_actions[:can_multiple] && $selected_elements.length > 0
1012 if values = ask_new_seektime(nil, '')
1013 distribute_multiple_call.call(:seektime, values)
1016 closures[:seektime].call
1021 menu.append( Gtk::SeparatorMenuItem.new)
1022 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1023 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1024 gammacorrect.signal_connect('activate') {
1025 if possible_actions[:can_multiple] && $selected_elements.length > 0
1026 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1027 distribute_multiple_call.call(:gammacorrect, values)
1030 closures[:gammacorrect].call
1033 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1034 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1035 whitebalance.signal_connect('activate') {
1036 if possible_actions[:can_multiple] && $selected_elements.length > 0
1037 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1038 distribute_multiple_call.call(:whitebalance, values)
1041 closures[:whitebalance].call
1044 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1045 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1046 _("Enhance constrast"))))
1048 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1050 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1051 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1052 if type == 'image' && possible_actions[:can_panorama]
1053 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1054 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1055 panorama.signal_connect('activate') {
1056 if possible_actions[:can_multiple] && $selected_elements.length > 0
1057 if values = ask_new_pano_amount(nil, '')
1058 distribute_multiple_call.call(:pano, values)
1061 distribute_multiple_call.call(:pano)
1065 menu.append( Gtk::SeparatorMenuItem.new)
1066 if optionals.include?('delete')
1067 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1068 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1069 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1070 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1071 paste_item.signal_connect('activate') { closures[:paste].call }
1072 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1073 clear_item.signal_connect('activate') { $cuts = [] }
1075 paste_item.sensitive = clear_item.sensitive = false
1078 menu.append( Gtk::SeparatorMenuItem.new)
1080 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1081 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1082 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1083 editexternally.signal_connect('activate') {
1084 if check_image_editor
1085 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1091 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1092 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1093 if optionals.include?('delete')
1094 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1095 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1098 menu.popup(nil, nil, event.button, event.time)
1101 def delete_current_subalbum
1103 sel = $albums_tv.selection.selected_rows
1104 $xmldir.elements.each { |e|
1105 if e.name == 'image' || e.name == 'video'
1106 e.add_attribute('deleted', 'true')
1109 #- branch if we have a non deleted subalbum
1110 if $xmldir.child_byname_notattr('dir', 'deleted')
1111 $xmldir.delete_attribute('thumbnails-caption')
1112 $xmldir.delete_attribute('thumbnails-captionfile')
1114 $xmldir.add_attribute('deleted', 'true')
1116 while moveup.parent.name == 'dir'
1117 moveup = moveup.parent
1118 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1119 moveup.add_attribute('deleted', 'true')
1126 save_changes('forced')
1127 populate_subalbums_treeview(false)
1128 $albums_tv.selection.select_path(sel[0])
1134 $current_path = nil #- prevent save_changes from being rerun again
1135 sel = $albums_tv.selection.selected_rows
1136 restore_one = proc { |xmldir|
1137 xmldir.elements.each { |e|
1138 if e.name == 'dir' && e.attributes['deleted']
1141 e.delete_attribute('deleted')
1144 restore_one.call($xmldir)
1145 populate_subalbums_treeview(false)
1146 $albums_tv.selection.select_path(sel[0])
1149 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1152 frame1 = Gtk::Frame.new
1153 fullpath = from_utf8("#{$current_path}/#{filename}")
1155 my_gen_real_thumbnail = proc {
1156 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1160 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1161 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1162 pack_start(img = Gtk::Image.new).
1163 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1164 px, mask = pxb.render_pixmap_and_mask(0)
1165 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1166 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1168 frame1.add(img = Gtk::Image.new)
1171 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1172 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1173 my_gen_real_thumbnail.call
1175 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1178 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1180 tooltips = Gtk::Tooltips.new
1181 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1182 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1184 frame2, textview = create_editzone($autotable_sw, 1, img)
1185 textview.buffer.text = caption
1186 textview.set_justification(Gtk::Justification::CENTER)
1188 vbox = Gtk::VBox.new(false, 5)
1189 vbox.pack_start(evtbox, false, false)
1190 vbox.pack_start(frame2, false, false)
1191 autotable.append(vbox, filename)
1193 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1194 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1196 #- to be able to find widgets by name
1197 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1199 cleanup_all_thumbnails = proc {
1200 #- remove out of sync images
1201 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1202 for sizeobj in $images_size
1203 #- cannot use sizeobj because panoramic images will have a larger width
1204 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1212 cleanup_all_thumbnails.call
1213 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1215 rexml_thread_protect {
1216 $xmldir.delete_attribute('already-generated')
1218 my_gen_real_thumbnail.call
1221 rotate_and_cleanup = proc { |angle|
1222 cleanup_all_thumbnails.call
1223 rexml_thread_protect {
1224 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1228 move = proc { |direction|
1229 do_method = "move_#{direction}"
1230 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1232 done = autotable.method(do_method).call(vbox)
1233 textview.grab_focus #- because if moving, focus is stolen
1237 save_undo(_("move %s") % direction,
1239 autotable.method(undo_method).call(vbox)
1240 textview.grab_focus #- because if moving, focus is stolen
1241 autoscroll_if_needed($autotable_sw, img, textview)
1242 $notebook.set_page(1)
1244 autotable.method(do_method).call(vbox)
1245 textview.grab_focus #- because if moving, focus is stolen
1246 autoscroll_if_needed($autotable_sw, img, textview)
1247 $notebook.set_page(1)
1253 color_swap_and_cleanup = proc {
1254 perform_color_swap_and_cleanup = proc {
1255 cleanup_all_thumbnails.call
1256 rexml_thread_protect {
1257 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1259 my_gen_real_thumbnail.call
1262 perform_color_swap_and_cleanup.call
1264 save_undo(_("color swap"),
1266 perform_color_swap_and_cleanup.call
1268 autoscroll_if_needed($autotable_sw, img, textview)
1269 $notebook.set_page(1)
1271 perform_color_swap_and_cleanup.call
1273 autoscroll_if_needed($autotable_sw, img, textview)
1274 $notebook.set_page(1)
1279 change_seektime_and_cleanup_real = proc { |values|
1280 perform_change_seektime_and_cleanup = proc { |val|
1281 cleanup_all_thumbnails.call
1282 rexml_thread_protect {
1283 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1285 my_gen_real_thumbnail.call
1287 perform_change_seektime_and_cleanup.call(values[:new])
1289 save_undo(_("specify seektime"),
1291 perform_change_seektime_and_cleanup.call(values[:old])
1293 autoscroll_if_needed($autotable_sw, img, textview)
1294 $notebook.set_page(1)
1296 perform_change_seektime_and_cleanup.call(values[:new])
1298 autoscroll_if_needed($autotable_sw, img, textview)
1299 $notebook.set_page(1)
1304 change_seektime_and_cleanup = proc {
1305 rexml_thread_protect {
1306 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1307 change_seektime_and_cleanup_real.call(values)
1312 change_pano_amount_and_cleanup_real = proc { |values|
1313 perform_change_pano_amount_and_cleanup = proc { |val|
1314 cleanup_all_thumbnails.call
1315 rexml_thread_protect {
1316 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1319 perform_change_pano_amount_and_cleanup.call(values[:new])
1321 save_undo(_("change panorama amount"),
1323 perform_change_pano_amount_and_cleanup.call(values[:old])
1325 autoscroll_if_needed($autotable_sw, img, textview)
1326 $notebook.set_page(1)
1328 perform_change_pano_amount_and_cleanup.call(values[:new])
1330 autoscroll_if_needed($autotable_sw, img, textview)
1331 $notebook.set_page(1)
1336 change_pano_amount_and_cleanup = proc {
1337 rexml_thread_protect {
1338 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1339 change_pano_amount_and_cleanup_real.call(values)
1344 whitebalance_and_cleanup_real = proc { |values|
1345 perform_change_whitebalance_and_cleanup = proc { |val|
1346 cleanup_all_thumbnails.call
1347 rexml_thread_protect {
1348 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1349 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1350 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1353 perform_change_whitebalance_and_cleanup.call(values[:new])
1355 save_undo(_("fix white balance"),
1357 perform_change_whitebalance_and_cleanup.call(values[:old])
1359 autoscroll_if_needed($autotable_sw, img, textview)
1360 $notebook.set_page(1)
1362 perform_change_whitebalance_and_cleanup.call(values[:new])
1364 autoscroll_if_needed($autotable_sw, img, textview)
1365 $notebook.set_page(1)
1370 whitebalance_and_cleanup = proc {
1371 rexml_thread_protect {
1372 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1373 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1374 whitebalance_and_cleanup_real.call(values)
1379 gammacorrect_and_cleanup_real = proc { |values|
1380 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1381 cleanup_all_thumbnails.call
1382 rexml_thread_protect {
1383 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1384 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1385 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1388 perform_change_gammacorrect_and_cleanup.call(values[:new])
1390 save_undo(_("gamma correction"),
1392 perform_change_gammacorrect_and_cleanup.call(values[:old])
1394 autoscroll_if_needed($autotable_sw, img, textview)
1395 $notebook.set_page(1)
1397 perform_change_gammacorrect_and_cleanup.call(values[:new])
1399 autoscroll_if_needed($autotable_sw, img, textview)
1400 $notebook.set_page(1)
1405 gammacorrect_and_cleanup = Proc.new {
1406 rexml_thread_protect {
1407 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1408 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1409 gammacorrect_and_cleanup_real.call(values)
1414 enhance_and_cleanup = proc {
1415 perform_enhance_and_cleanup = proc {
1416 cleanup_all_thumbnails.call
1417 rexml_thread_protect {
1418 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1420 my_gen_real_thumbnail.call
1423 cleanup_all_thumbnails.call
1424 perform_enhance_and_cleanup.call
1426 save_undo(_("enhance"),
1428 perform_enhance_and_cleanup.call
1430 autoscroll_if_needed($autotable_sw, img, textview)
1431 $notebook.set_page(1)
1433 perform_enhance_and_cleanup.call
1435 autoscroll_if_needed($autotable_sw, img, textview)
1436 $notebook.set_page(1)
1441 delete = proc { |isacut|
1442 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 })
1445 perform_delete = proc {
1446 after = autotable.get_next_widget(vbox)
1448 after = autotable.get_previous_widget(vbox)
1450 if $config['deleteondisk'] && !isacut
1451 msg 3, "scheduling for delete: #{fullpath}"
1452 $todelete << fullpath
1454 autotable.remove_widget(vbox)
1456 $vbox2widgets[after][:textview].grab_focus
1457 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1461 previous_pos = autotable.get_current_number(vbox)
1465 delete_current_subalbum
1467 save_undo(_("delete"),
1469 autotable.reinsert(pos, vbox, filename)
1470 $notebook.set_page(1)
1471 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1473 msg 3, "removing deletion schedule of: #{fullpath}"
1474 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1477 $notebook.set_page(1)
1486 $cuts << { :vbox => vbox, :filename => filename }
1487 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1492 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1495 autotable.queue_draws << proc {
1496 $vbox2widgets[last[:vbox]][:textview].grab_focus
1497 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1499 save_undo(_("paste"),
1501 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1502 $notebook.set_page(1)
1505 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1507 $notebook.set_page(1)
1510 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1515 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1516 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1517 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1518 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1520 textview.signal_connect('key-press-event') { |w, event|
1523 x, y = autotable.get_current_pos(vbox)
1524 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1525 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1526 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1527 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1529 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1530 $vbox2widgets[widget_up][:textview].grab_focus
1537 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1539 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1540 $vbox2widgets[widget_down][:textview].grab_focus
1547 if event.keyval == Gdk::Keyval::GDK_Left
1550 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1557 rotate_and_cleanup.call(-90)
1560 if event.keyval == Gdk::Keyval::GDK_Right
1561 next_ = autotable.get_next_widget(vbox)
1562 if next_ && autotable.get_current_pos(next_)[0] > x
1564 $vbox2widgets[next_][:textview].grab_focus
1571 rotate_and_cleanup.call(90)
1574 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1577 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1578 view_element(filename, { :delete => delete })
1581 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1584 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1588 !propagate #- propagate if needed
1591 $ignore_next_release = false
1592 evtbox.signal_connect('button-press-event') { |w, event|
1593 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1594 if event.state & Gdk::Window::BUTTON3_MASK != 0
1595 #- gesture redo: hold right mouse button then click left mouse button
1596 $config['nogestures'] or perform_redo
1597 $ignore_next_release = true
1599 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1601 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1603 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1604 elsif $enhance.active?
1605 enhance_and_cleanup.call
1606 elsif $delete.active?
1610 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1613 $button1_pressed_autotable = true
1614 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1615 if event.state & Gdk::Window::BUTTON1_MASK != 0
1616 #- gesture undo: hold left mouse button then click right mouse button
1617 $config['nogestures'] or perform_undo
1618 $ignore_next_release = true
1620 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1621 view_element(filename, { :delete => delete })
1626 evtbox.signal_connect('button-release-event') { |w, event|
1627 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1628 if !$ignore_next_release
1629 x, y = autotable.get_current_pos(vbox)
1630 next_ = autotable.get_next_widget(vbox)
1631 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1632 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1633 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1634 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1635 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1636 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1637 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1639 $ignore_next_release = false
1640 $gesture_press = nil
1645 #- handle reordering with drag and drop
1646 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1647 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1648 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1649 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1652 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1654 #- mouse gesture first (dnd disables button-release-event)
1655 if $gesture_press && $gesture_press[:filename] == filename
1656 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1657 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1658 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1659 rotate_and_cleanup.call(angle)
1660 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1662 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1663 msg 3, "gesture delete: click-drag right button to the bottom"
1665 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1670 ctxt.targets.each { |target|
1671 if target.name == 'reorder-elements'
1672 move_dnd = proc { |from,to|
1675 autotable.move(from, to)
1676 save_undo(_("reorder"),
1679 autotable.move(to - 1, from)
1681 autotable.move(to, from + 1)
1683 $notebook.set_page(1)
1685 autotable.move(from, to)
1686 $notebook.set_page(1)
1691 if $multiple_dnd.size == 0
1692 move_dnd.call(selection_data.data.to_i,
1693 autotable.get_current_number(vbox))
1695 UndoHandler.begin_batch
1696 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1698 #- need to update current position between each call
1699 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1700 autotable.get_current_number(vbox))
1702 UndoHandler.end_batch
1713 def create_auto_table
1715 $autotable = Gtk::AutoTable.new(5)
1717 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1718 thumbnails_vb = Gtk::VBox.new(false, 5)
1720 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1721 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1722 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1723 thumbnails_vb.add($autotable)
1725 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1726 $autotable_sw.add_with_viewport(thumbnails_vb)
1728 #- follows stuff for handling multiple elements selection
1729 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1731 update_selected = proc {
1732 $autotable.current_order.each { |path|
1733 w = $name2widgets[path][:evtbox].window
1734 xm = w.position[0] + w.size[0]/2
1735 ym = w.position[1] + w.size[1]/2
1736 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1737 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1738 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1739 if $name2widgets[path][:img].pixbuf
1740 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1744 if $selected_elements[path] && ! $selected_elements[path][:keep]
1745 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))
1746 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1747 $selected_elements.delete(path)
1752 $autotable.signal_connect('realize') { |w,e|
1753 gc = Gdk::GC.new($autotable.window)
1754 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1755 gc.function = Gdk::GC::INVERT
1756 #- autoscroll handling for DND and multiple selections
1757 Gtk.timeout_add(100) {
1758 if ! $autotable.window.nil?
1759 w, x, y, mask = $autotable.window.pointer
1760 if mask & Gdk::Window::BUTTON1_MASK != 0
1761 if y < $autotable_sw.vadjustment.value
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]])
1765 if $button1_pressed_autotable || press_x
1766 scroll_upper($autotable_sw, y)
1769 w, pos_x, pos_y = $autotable.window.pointer
1770 $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]])
1771 update_selected.call
1774 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1776 $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]])
1778 if $button1_pressed_autotable || press_x
1779 scroll_lower($autotable_sw, y)
1782 w, pos_x, pos_y = $autotable.window.pointer
1783 $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]])
1784 update_selected.call
1789 ! $autotable.window.nil?
1793 $autotable.signal_connect('button-press-event') { |w,e|
1795 if !$button1_pressed_autotable
1798 if e.state & Gdk::Window::SHIFT_MASK == 0
1799 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1800 $selected_elements = {}
1801 $statusbar.push(0, utf8(_("Nothing selected.")))
1803 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1805 set_mousecursor(Gdk::Cursor::TCROSS)
1809 $autotable.signal_connect('button-release-event') { |w,e|
1811 if $button1_pressed_autotable
1812 #- unselect all only now
1813 $multiple_dnd = $selected_elements.keys
1814 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1815 $selected_elements = {}
1816 $button1_pressed_autotable = false
1819 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1820 if $selected_elements.length > 0
1821 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1824 press_x = press_y = pos_x = pos_y = nil
1825 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1829 $autotable.signal_connect('motion-notify-event') { |w,e|
1832 $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]])
1836 $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]])
1837 update_selected.call
1843 def create_subalbums_page
1845 subalbums_hb = Gtk::HBox.new
1846 $subalbums_vb = Gtk::VBox.new(false, 5)
1847 subalbums_hb.pack_start($subalbums_vb, false, false)
1848 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1849 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1850 $subalbums_sw.add_with_viewport(subalbums_hb)
1853 def save_current_file
1859 ios = File.open($filename, "w")
1862 rescue Iconv::IllegalSequence
1863 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1864 if ! ios.nil? && ! ios.closed?
1867 $xmldoc.xml_decl.encoding = 'UTF-8'
1868 ios = File.open($filename, "w")
1880 def save_current_file_user
1881 save_tempfilename = $filename
1882 $filename = $orig_filename
1883 if ! save_current_file
1884 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1885 $filename = save_tempfilename
1889 $generated_outofline = false
1890 $filename = save_tempfilename
1892 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1893 $todelete.each { |f|
1898 def mark_document_as_dirty
1899 $xmldoc.elements.each('//dir') { |elem|
1900 elem.delete_attribute('already-generated')
1904 #- ret: true => ok false => cancel
1905 def ask_save_modifications(msg1, msg2, *options)
1907 options = options.size > 0 ? options[0] : {}
1909 if options[:disallow_cancel]
1910 dialog = Gtk::Dialog.new(msg1,
1912 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1913 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1914 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1916 dialog = Gtk::Dialog.new(msg1,
1918 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1919 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1920 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1921 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1923 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1924 dialog.vbox.add(Gtk::Label.new(msg2))
1925 dialog.window_position = Gtk::Window::POS_CENTER
1928 dialog.run { |response|
1930 if response == Gtk::Dialog::RESPONSE_YES
1931 if ! save_current_file_user
1932 return ask_save_modifications(msg1, msg2, options)
1935 #- if we have generated an album but won't save modifications, we must remove
1936 #- already-generated markers in original file
1937 if $generated_outofline
1939 $xmldoc = REXML::Document.new(File.new($orig_filename))
1940 mark_document_as_dirty
1941 ios = File.open($orig_filename, "w")
1945 puts "exception: #{$!}"
1949 if response == Gtk::Dialog::RESPONSE_CANCEL
1952 $todelete = [] #- unconditionally clear the list of images/videos to delete
1958 def try_quit(*options)
1959 if ask_save_modifications(utf8(_("Save before quitting?")),
1960 utf8(_("Do you want to save your changes before quitting?")),
1966 def show_popup(parent, msg, *options)
1967 dialog = Gtk::Dialog.new
1968 if options[0] && options[0][:title]
1969 dialog.title = options[0][:title]
1971 dialog.title = utf8(_("Booh message"))
1973 lbl = Gtk::Label.new
1974 if options[0] && options[0][:nomarkup]
1979 if options[0] && options[0][:centered]
1980 lbl.set_justify(Gtk::Justification::CENTER)
1982 if options[0] && options[0][:selectable]
1983 lbl.selectable = true
1985 if options[0] && options[0][:topwidget]
1986 dialog.vbox.add(options[0][:topwidget])
1988 if options[0] && options[0][:scrolled]
1989 sw = Gtk::ScrolledWindow.new(nil, nil)
1990 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1991 sw.add_with_viewport(lbl)
1993 dialog.set_default_size(500, 600)
1995 dialog.vbox.add(lbl)
1996 dialog.set_default_size(200, 120)
1998 if options[0] && options[0][:okcancel]
1999 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2001 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2003 if options[0] && options[0][:pos_centered]
2004 dialog.window_position = Gtk::Window::POS_CENTER
2006 dialog.window_position = Gtk::Window::POS_MOUSE
2009 if options[0] && options[0][:linkurl]
2010 linkbut = Gtk::Button.new('')
2011 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2012 linkbut.signal_connect('clicked') {
2013 open_url(options[0][:linkurl])
2014 dialog.response(Gtk::Dialog::RESPONSE_OK)
2015 set_mousecursor_normal
2017 linkbut.relief = Gtk::RELIEF_NONE
2018 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2019 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2020 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2025 if !options[0] || !options[0][:not_transient]
2026 dialog.transient_for = parent
2027 dialog.run { |response|
2029 if options[0] && options[0][:okcancel]
2030 return response == Gtk::Dialog::RESPONSE_OK
2034 dialog.signal_connect('response') { dialog.destroy }
2038 def set_mainwindow_title(progress)
2039 filename = $orig_filename || $filename
2042 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2044 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2048 $main_window.title = 'booh - ' + File.basename(filename)
2050 $main_window.title = 'booh'
2055 def backend_wait_message(parent, msg, infopipe_path, mode)
2057 w.set_transient_for(parent)
2060 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2061 vb.pack_start(Gtk::Label.new(msg), false, false)
2063 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2064 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2065 if mode != 'one dir scan'
2066 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2068 if mode == 'web-album'
2069 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2070 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2072 vb.pack_start(Gtk::HSeparator.new, false, false)
2074 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2075 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2076 vb.pack_end(bottom, false, false)
2079 update_progression_title_pb1 = proc {
2080 if mode == 'web-album'
2081 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2082 elsif mode != 'one dir scan'
2083 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2085 set_mainwindow_title(pb1_1.fraction)
2089 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2090 refresh_thread = Thread.new {
2091 directories_counter = 0
2092 while line = infopipe.gets
2093 msg 3, "infopipe got data: #{line}"
2094 if line =~ /^directories: (\d+), sizes: (\d+)/
2095 directories = $1.to_f + 1
2097 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2098 elements = $3.to_f + 1
2099 if mode == 'web-album'
2103 gtk_thread_protect { pb1_1.fraction = 0 }
2104 if mode != 'one dir scan'
2105 newtext = utf8(full_src_dir_to_rel($1, $2))
2106 newtext = '/' if newtext == ''
2107 gtk_thread_protect { pb1_2.text = newtext }
2108 directories_counter += 1
2109 gtk_thread_protect {
2110 pb1_2.fraction = directories_counter / directories
2111 update_progression_title_pb1.call
2114 elsif line =~ /^processing element$/
2115 element_counter += 1
2116 gtk_thread_protect {
2117 pb1_1.fraction = element_counter / elements
2118 update_progression_title_pb1.call
2120 elsif line =~ /^processing size$/
2121 element_counter += 1
2122 gtk_thread_protect {
2123 pb1_1.fraction = element_counter / elements
2124 update_progression_title_pb1.call
2126 elsif line =~ /^finished processing sizes$/
2127 gtk_thread_protect { pb1_1.fraction = 1 }
2128 elsif line =~ /^creating index.html$/
2129 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2130 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2131 directories_counter = 0
2132 elsif line =~ /^index.html: (.+)\|(.+)/
2133 newtext = utf8(full_src_dir_to_rel($1, $2))
2134 newtext = '/' if newtext == ''
2135 gtk_thread_protect { pb2.text = newtext }
2136 directories_counter += 1
2137 gtk_thread_protect {
2138 pb2.fraction = directories_counter / directories
2139 set_mainwindow_title(0.9 + pb2.fraction / 10)
2141 elsif line =~ /^die: (.*)$/
2148 w.signal_connect('delete-event') { w.destroy }
2149 w.signal_connect('destroy') {
2150 Thread.kill(refresh_thread)
2151 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2154 File.delete(infopipe_path)
2156 set_mainwindow_title(nil)
2158 w.window_position = Gtk::Window::POS_CENTER
2164 def call_backend(cmd, waitmsg, mode, params)
2165 pipe = Tempfile.new("boohpipe")
2166 Thread.critical = true
2169 system("mkfifo #{path}")
2170 Thread.critical = false
2171 cmd += " --info-pipe #{path}"
2172 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2177 id, exitstatus = Process.waitpid2(pid)
2178 gtk_thread_protect { w8.destroy }
2180 if params[:successmsg]
2181 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2183 if params[:closure_after]
2184 gtk_thread_protect(¶ms[:closure_after])
2186 elsif exitstatus == 15
2187 #- say nothing, user aborted
2189 gtk_thread_protect { show_popup($main_window,
2190 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2196 button.signal_connect('clicked') {
2197 Process.kill('SIGTERM', pid)
2201 def save_changes(*forced)
2202 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2206 $xmldir.delete_attribute('already-generated')
2208 propagate_children = proc { |xmldir|
2209 if xmldir.attributes['subdirs-caption']
2210 xmldir.delete_attribute('already-generated')
2212 xmldir.elements.each('dir') { |element|
2213 propagate_children.call(element)
2217 if $xmldir.child_byname_notattr('dir', 'deleted')
2218 new_title = $subalbums_title.buffer.text
2219 if new_title != $xmldir.attributes['subdirs-caption']
2220 parent = $xmldir.parent
2221 if parent.name == 'dir'
2222 parent.delete_attribute('already-generated')
2224 propagate_children.call($xmldir)
2226 $xmldir.add_attribute('subdirs-caption', new_title)
2227 $xmldir.elements.each('dir') { |element|
2228 if !element.attributes['deleted']
2229 path = element.attributes['path']
2230 newtext = $subalbums_edits[path][:editzone].buffer.text
2231 if element.attributes['subdirs-caption']
2232 if element.attributes['subdirs-caption'] != newtext
2233 propagate_children.call(element)
2235 element.add_attribute('subdirs-caption', newtext)
2236 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2238 if element.attributes['thumbnails-caption'] != newtext
2239 element.delete_attribute('already-generated')
2241 element.add_attribute('thumbnails-caption', newtext)
2242 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2248 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2249 if $xmldir.attributes['thumbnails-caption']
2250 path = $xmldir.attributes['path']
2251 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2253 elsif $xmldir.attributes['thumbnails-caption']
2254 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2257 if $xmldir.attributes['thumbnails-caption']
2258 if edit = $subalbums_edits[$xmldir.attributes['path']]
2259 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2263 #- remove and reinsert elements to reflect new ordering
2266 $xmldir.elements.each { |element|
2267 if element.name == 'image' || element.name == 'video'
2268 saves[element.attributes['filename']] = element.remove
2272 $autotable.current_order.each { |path|
2273 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2274 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2277 saves.each_key { |path|
2278 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2279 chld.add_attribute('deleted', 'true')
2283 def sort_by_exif_date
2287 rexml_thread_protect {
2288 $xmldir.elements.each { |element|
2289 if element.name == 'image' || element.name == 'video'
2290 current_order << element.attributes['filename']
2295 #- look for EXIF dates
2298 if current_order.size > 20
2300 w.set_transient_for($main_window)
2302 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2303 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2304 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2305 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2306 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2307 vb.pack_end(bottom, false, false)
2309 w.signal_connect('delete-event') { w.destroy }
2310 w.window_position = Gtk::Window::POS_CENTER
2314 b.signal_connect('clicked') { aborted = true }
2316 current_order.each { |f|
2318 if entry2type(f) == 'image'
2320 pb.fraction = i.to_f / current_order.size
2321 Gtk.main_iteration while Gtk.events_pending?
2322 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2324 dates[f] = date_time
2337 current_order.each { |f|
2338 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2340 dates[f] = date_time
2346 rexml_thread_protect {
2347 $xmldir.elements.each { |element|
2348 if element.name == 'image' || element.name == 'video'
2349 saves[element.attributes['filename']] = element.remove
2354 neworder = smartsort(current_order, dates)
2356 rexml_thread_protect {
2358 $xmldir.add_element(saves[f].name, saves[f].attributes)
2362 #- let the auto-table reflect new ordering
2366 def remove_all_captions
2369 $autotable.current_order.each { |path|
2370 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2371 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2373 save_undo(_("remove all captions"),
2375 texts.each_key { |key|
2376 $name2widgets[key][:textview].buffer.text = texts[key]
2378 $notebook.set_page(1)
2380 texts.each_key { |key|
2381 $name2widgets[key][:textview].buffer.text = ''
2383 $notebook.set_page(1)
2389 $selected_elements.each_key { |path|
2390 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2396 $selected_elements = {}
2400 $undo_tb.sensitive = $undo_mb.sensitive = false
2401 $redo_tb.sensitive = $redo_mb.sensitive = false
2407 $subalbums_vb.children.each { |chld|
2408 $subalbums_vb.remove(chld)
2410 $subalbums = Gtk::Table.new(0, 0, true)
2411 current_y_sub_albums = 0
2413 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2414 $subalbums_edits = {}
2415 subalbums_counter = 0
2416 subalbums_edits_bypos = {}
2418 add_subalbum = proc { |xmldir, counter|
2419 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2420 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2421 if xmldir == $xmldir
2422 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2423 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2424 caption = xmldir.attributes['thumbnails-caption']
2425 infotype = 'thumbnails'
2427 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2428 captionfile, caption = find_subalbum_caption_info(xmldir)
2429 infotype = find_subalbum_info_type(xmldir)
2431 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2432 hbox = Gtk::HBox.new
2433 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2435 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2438 my_gen_real_thumbnail = proc {
2439 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2442 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2443 f.add(img = Gtk::Image.new)
2444 my_gen_real_thumbnail.call
2446 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2448 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2449 $subalbums.attach(hbox,
2450 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2452 frame, textview = create_editzone($subalbums_sw, 0, img)
2453 textview.buffer.text = caption
2454 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2455 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2457 change_image = proc {
2458 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2460 Gtk::FileChooser::ACTION_OPEN,
2462 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2463 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2464 fc.transient_for = $main_window
2465 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))
2466 f.add(preview_img = Gtk::Image.new)
2468 fc.signal_connect('update-preview') { |w|
2469 if fc.preview_filename
2470 if entry2type(fc.preview_filename) == 'video'
2474 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2476 fc.preview_widget_active = false
2478 tmpimage = "#{tmpdir}/00000001.jpg"
2480 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2481 fc.preview_widget_active = true
2482 rescue Gdk::PixbufError
2483 fc.preview_widget_active = false
2485 File.delete(tmpimage)
2492 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2493 fc.preview_widget_active = true
2494 rescue Gdk::PixbufError
2495 fc.preview_widget_active = false
2500 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2502 old_file = captionfile
2503 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2504 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2505 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2506 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2508 new_file = fc.filename
2509 msg 3, "new captionfile is: #{fc.filename}"
2510 perform_changefile = proc {
2511 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2512 $modified_pixbufs.delete(thumbnail_file)
2513 xmldir.delete_attribute("#{infotype}-rotate")
2514 xmldir.delete_attribute("#{infotype}-color-swap")
2515 xmldir.delete_attribute("#{infotype}-enhance")
2516 xmldir.delete_attribute("#{infotype}-seektime")
2517 my_gen_real_thumbnail.call
2519 perform_changefile.call
2521 save_undo(_("change caption file for sub-album"),
2523 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2524 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2525 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2526 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2527 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2528 my_gen_real_thumbnail.call
2529 $notebook.set_page(0)
2531 perform_changefile.call
2532 $notebook.set_page(0)
2540 if File.exists?(thumbnail_file)
2541 File.delete(thumbnail_file)
2543 my_gen_real_thumbnail.call
2546 rotate_and_cleanup = proc { |angle|
2547 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2548 if File.exists?(thumbnail_file)
2549 File.delete(thumbnail_file)
2553 move = proc { |direction|
2556 save_changes('forced')
2557 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2558 if direction == 'up'
2559 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2560 subalbums_edits_bypos[oldpos - 1][:position] += 1
2562 if direction == 'down'
2563 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2564 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2566 if direction == 'top'
2567 for i in 1 .. oldpos - 1
2568 subalbums_edits_bypos[i][:position] += 1
2570 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2572 if direction == 'bottom'
2573 for i in oldpos + 1 .. subalbums_counter
2574 subalbums_edits_bypos[i][:position] -= 1
2576 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2580 $xmldir.elements.each('dir') { |element|
2581 if (!element.attributes['deleted'])
2582 elems << [ element.attributes['path'], element.remove ]
2585 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2586 each { |e| $xmldir.add_element(e[1]) }
2587 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2588 $xmldir.elements.each('descendant::dir') { |elem|
2589 elem.delete_attribute('already-generated')
2592 sel = $albums_tv.selection.selected_rows
2594 populate_subalbums_treeview(false)
2595 $albums_tv.selection.select_path(sel[0])
2598 color_swap_and_cleanup = proc {
2599 perform_color_swap_and_cleanup = proc {
2600 color_swap(xmldir, "#{infotype}-")
2601 my_gen_real_thumbnail.call
2603 perform_color_swap_and_cleanup.call
2605 save_undo(_("color swap"),
2607 perform_color_swap_and_cleanup.call
2608 $notebook.set_page(0)
2610 perform_color_swap_and_cleanup.call
2611 $notebook.set_page(0)
2616 change_seektime_and_cleanup = proc {
2617 if values = ask_new_seektime(xmldir, "#{infotype}-")
2618 perform_change_seektime_and_cleanup = proc { |val|
2619 change_seektime(xmldir, "#{infotype}-", val)
2620 my_gen_real_thumbnail.call
2622 perform_change_seektime_and_cleanup.call(values[:new])
2624 save_undo(_("specify seektime"),
2626 perform_change_seektime_and_cleanup.call(values[:old])
2627 $notebook.set_page(0)
2629 perform_change_seektime_and_cleanup.call(values[:new])
2630 $notebook.set_page(0)
2636 whitebalance_and_cleanup = proc {
2637 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2638 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2639 perform_change_whitebalance_and_cleanup = proc { |val|
2640 change_whitebalance(xmldir, "#{infotype}-", val)
2641 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2642 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2643 if File.exists?(thumbnail_file)
2644 File.delete(thumbnail_file)
2647 perform_change_whitebalance_and_cleanup.call(values[:new])
2649 save_undo(_("fix white balance"),
2651 perform_change_whitebalance_and_cleanup.call(values[:old])
2652 $notebook.set_page(0)
2654 perform_change_whitebalance_and_cleanup.call(values[:new])
2655 $notebook.set_page(0)
2661 gammacorrect_and_cleanup = proc {
2662 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2663 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2664 perform_change_gammacorrect_and_cleanup = proc { |val|
2665 change_gammacorrect(xmldir, "#{infotype}-", val)
2666 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2667 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2668 if File.exists?(thumbnail_file)
2669 File.delete(thumbnail_file)
2672 perform_change_gammacorrect_and_cleanup.call(values[:new])
2674 save_undo(_("gamma correction"),
2676 perform_change_gammacorrect_and_cleanup.call(values[:old])
2677 $notebook.set_page(0)
2679 perform_change_gammacorrect_and_cleanup.call(values[:new])
2680 $notebook.set_page(0)
2686 enhance_and_cleanup = proc {
2687 perform_enhance_and_cleanup = proc {
2688 enhance(xmldir, "#{infotype}-")
2689 my_gen_real_thumbnail.call
2692 perform_enhance_and_cleanup.call
2694 save_undo(_("enhance"),
2696 perform_enhance_and_cleanup.call
2697 $notebook.set_page(0)
2699 perform_enhance_and_cleanup.call
2700 $notebook.set_page(0)
2705 evtbox.signal_connect('button-press-event') { |w, event|
2706 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2708 rotate_and_cleanup.call(90)
2710 rotate_and_cleanup.call(-90)
2711 elsif $enhance.active?
2712 enhance_and_cleanup.call
2715 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2716 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2717 { :forbid_left => true, :forbid_right => true,
2718 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2719 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2720 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2721 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2722 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2724 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2729 evtbox.signal_connect('button-press-event') { |w, event|
2730 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2734 evtbox.signal_connect('button-release-event') { |w, event|
2735 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2736 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2737 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2738 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2739 msg 3, "gesture rotate: #{angle}"
2740 rotate_and_cleanup.call(angle)
2743 $gesture_press = nil
2746 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2747 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2748 current_y_sub_albums += 1
2751 if $xmldir.child_byname_notattr('dir', 'deleted')
2753 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2754 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2755 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2756 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2757 #- this album image/caption
2758 if $xmldir.attributes['thumbnails-caption']
2759 add_subalbum.call($xmldir, 0)
2762 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2763 $xmldir.elements.each { |element|
2764 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2765 #- element (image or video) of this album
2766 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2767 msg 3, "dest_img: #{dest_img}"
2768 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2769 total[element.name] += 1
2771 if element.name == 'dir' && !element.attributes['deleted']
2772 #- sub-album image/caption
2773 add_subalbum.call(element, subalbums_counter += 1)
2774 total[element.name] += 1
2777 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2778 total['image'], total['video'], total['dir'] ]))
2779 $subalbums_vb.add($subalbums)
2780 $subalbums_vb.show_all
2782 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2783 $notebook.get_tab_label($autotable_sw).sensitive = false
2784 $notebook.set_page(0)
2785 $thumbnails_title.buffer.text = ''
2787 $notebook.get_tab_label($autotable_sw).sensitive = true
2788 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2791 if !$xmldir.child_byname_notattr('dir', 'deleted')
2792 $notebook.get_tab_label($subalbums_sw).sensitive = false
2793 $notebook.set_page(1)
2795 $notebook.get_tab_label($subalbums_sw).sensitive = true
2799 def pixbuf_or_nil(filename)
2801 return Gdk::Pixbuf.new(filename)
2807 def theme_choose(current)
2808 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2810 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2811 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2812 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2814 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2815 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2816 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2817 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2818 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2819 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2820 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2821 treeview.signal_connect('button-press-event') { |w, event|
2822 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2823 dialog.response(Gtk::Dialog::RESPONSE_OK)
2827 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2829 ([ $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|
2832 iter[0] = File.basename(dir)
2833 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2834 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2835 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2836 if File.basename(dir) == current
2837 treeview.selection.select_iter(iter)
2840 dialog.set_default_size(-1, 500)
2841 dialog.vbox.show_all
2843 dialog.run { |response|
2844 iter = treeview.selection.selected
2846 if response == Gtk::Dialog::RESPONSE_OK && iter
2847 return model.get_value(iter, 0)
2853 def show_password_protections
2854 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2855 child_iter = $albums_iters[xmldir.attributes['path']]
2856 if xmldir.attributes['password-protect']
2857 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2858 already_protected = true
2859 elsif already_protected
2860 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2862 pix = pix.saturate_and_pixelate(1, true)
2868 xmldir.elements.each('dir') { |elem|
2869 if !elem.attributes['deleted']
2870 examine_dir_elem.call(child_iter, elem, already_protected)
2874 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2877 def populate_subalbums_treeview(select_first)
2881 $subalbums_vb.children.each { |chld|
2882 $subalbums_vb.remove(chld)
2885 source = $xmldoc.root.attributes['source']
2886 msg 3, "source: #{source}"
2888 xmldir = $xmldoc.elements['//dir']
2889 if !xmldir || xmldir.attributes['path'] != source
2890 msg 1, _("Corrupted booh file...")
2894 append_dir_elem = proc { |parent_iter, xmldir|
2895 child_iter = $albums_ts.append(parent_iter)
2896 child_iter[0] = File.basename(xmldir.attributes['path'])
2897 child_iter[1] = xmldir.attributes['path']
2898 $albums_iters[xmldir.attributes['path']] = child_iter
2899 msg 3, "puttin location: #{xmldir.attributes['path']}"
2900 xmldir.elements.each('dir') { |elem|
2901 if !elem.attributes['deleted']
2902 append_dir_elem.call(child_iter, elem)
2906 append_dir_elem.call(nil, xmldir)
2907 show_password_protections
2909 $albums_tv.expand_all
2911 $albums_tv.selection.select_iter($albums_ts.iter_first)
2915 def select_current_theme
2916 select_theme($xmldoc.root.attributes['theme'],
2917 $xmldoc.root.attributes['limit-sizes'],
2918 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2919 $xmldoc.root.attributes['thumbnails-per-row'])
2922 def open_file(filename)
2926 $current_path = nil #- invalidate
2927 $modified_pixbufs = {}
2930 $subalbums_vb.children.each { |chld|
2931 $subalbums_vb.remove(chld)
2934 if !File.exists?(filename)
2935 return utf8(_("File not found."))
2939 $xmldoc = REXML::Document.new(File.new(filename))
2944 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2945 if entry2type(filename).nil?
2946 return utf8(_("Not a booh file!"))
2948 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."))
2952 if !source = $xmldoc.root.attributes['source']
2953 return utf8(_("Corrupted booh file..."))
2956 if !dest = $xmldoc.root.attributes['destination']
2957 return utf8(_("Corrupted booh file..."))
2960 if !theme = $xmldoc.root.attributes['theme']
2961 return utf8(_("Corrupted booh file..."))
2964 if $xmldoc.root.attributes['version'] < '0.9.0'
2965 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2966 mark_document_as_dirty
2967 if $xmldoc.root.attributes['version'] < '0.8.4'
2968 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2969 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2970 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2971 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2972 if old_dest_dir != new_dest_dir
2973 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2975 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2976 xmldir.elements.each { |element|
2977 if %w(image video).include?(element.name) && !element.attributes['deleted']
2978 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2979 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2980 Dir[old_name + '*'].each { |file|
2981 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2982 file != new_file and sys("mv '#{file}' '#{new_file}'")
2985 if element.name == 'dir' && !element.attributes['deleted']
2986 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2987 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2988 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2992 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2996 $xmldoc.root.add_attribute('version', $VERSION)
2999 select_current_theme
3001 $filename = filename
3002 set_mainwindow_title(nil)
3003 $default_size['thumbnails'] =~ /(.*)x(.*)/
3004 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3005 $albums_thumbnail_size =~ /(.*)x(.*)/
3006 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3008 populate_subalbums_treeview(true)
3010 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
3014 def open_file_user(filename)
3015 result = open_file(filename)
3017 $config['last-opens'] ||= []
3018 if $config['last-opens'][-1] != utf8(filename)
3019 $config['last-opens'] << utf8(filename)
3021 $orig_filename = $filename
3022 $main_window.title = 'booh - ' + File.basename($orig_filename)
3023 tmp = Tempfile.new("boohtemp")
3024 Thread.critical = true
3025 $filename = tmp.path
3028 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3029 Thread.critical = false
3031 $tempfiles << $filename << "#{$filename}.backup"
3033 $orig_filename = nil
3039 if !ask_save_modifications(utf8(_("Save this album?")),
3040 utf8(_("Do you want to save the changes to this album?")),
3041 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3044 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3046 Gtk::FileChooser::ACTION_OPEN,
3048 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3049 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3050 fc.set_current_folder(File.expand_path("~/.booh"))
3051 fc.transient_for = $main_window
3052 fc.preview_widget = previewlabel = Gtk::Label.new.show
3053 fc.signal_connect('update-preview') { |w|
3054 if fc.preview_filename
3056 push_mousecursor_wait(fc)
3057 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3061 xmldoc.elements.each('//*') { |elem|
3062 if elem.name == 'dir'
3064 elsif elem.name == 'image'
3066 elsif elem.name == 'video'
3074 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3075 fc.preview_widget_active = false
3077 previewlabel.markup = utf8(_("<i>Source:</i> %s\n<i>Destination:</i> %s\n<i>Subalbums:</i> %s\n<i>Images:</i> %s\n<i>Videos:</i> %s") %
3078 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3079 fc.preview_widget_active = true
3085 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3086 push_mousecursor_wait(fc)
3087 msg = open_file_user(fc.filename)
3102 def additional_booh_options
3105 options += "--mproc #{$config['mproc'].to_i} "
3107 options += "--comments-format '#{$config['comments-format']}' "
3108 if $config['transcode-videos']
3109 options += "--transcode-videos '#{$config['transcode-videos']}' "
3114 def ask_multi_languages(value)
3116 spl = value.split(',')
3117 value = [ spl[0..-2], spl[-1] ]
3120 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3123 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3124 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3126 lbl = Gtk::Label.new
3128 _("You can choose to activate <b>multi-languages</b> support for this web-album
3129 (it will work only if you publish your web-album on an Apache web-server). This will
3130 use the MultiViews feature of Apache; the pages will be served according to the
3131 value of the Accept-Language HTTP header sent by the web browsers, so that people
3132 with different languages preferences will be able to browse your web-album with
3133 navigation in their language (if language is available).
3136 dialog.vbox.add(lbl)
3137 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3138 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3139 add(languages = Gtk::Button.new))))
3141 pick_languages = proc {
3142 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3145 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3146 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3148 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3149 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3151 SUPPORTED_LANGUAGES.each { |lang|
3152 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3153 if ! value.nil? && value[0].include?(lang)
3159 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3160 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3161 fallback_language = nil
3162 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3163 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3164 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3165 fbl_rb.active = true
3166 fallback_language = SUPPORTED_LANGUAGES[0]
3168 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3169 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3170 rb.signal_connect('clicked') { fallback_language = lang }
3171 if ! value.nil? && value[1] == lang
3176 dialog2.window_position = Gtk::Window::POS_MOUSE
3180 dialog2.run { |response|
3182 if resp == Gtk::Dialog::RESPONSE_OK
3184 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3185 value[1] = fallback_language
3186 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3193 languages.signal_connect('clicked') {
3196 dialog.window_position = Gtk::Window::POS_MOUSE
3200 rb_yes.active = true
3201 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3203 rb_no.signal_connect('clicked') {
3207 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3220 dialog.run { |response|
3225 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3227 return [ true, nil ]
3229 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3238 if !ask_save_modifications(utf8(_("Save this album?")),
3239 utf8(_("Do you want to save the changes to this album?")),
3240 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3243 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3245 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3246 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3247 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3249 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3250 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3251 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3252 tbl.attach(src = Gtk::Entry.new,
3253 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3254 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3255 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3256 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3257 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3258 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3259 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3260 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3261 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3262 tbl.attach(dest = Gtk::Entry.new,
3263 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3264 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3265 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3266 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3267 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3268 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3269 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3270 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3271 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3273 tooltips = Gtk::Tooltips.new
3274 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3275 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3276 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3277 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3278 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3279 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3280 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)
3281 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3282 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3283 nperpage_model = Gtk::ListStore.new(String, String)
3284 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3285 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3286 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3287 nperpagecombo.set_attributes(crt, { :markup => 0 })
3288 iter = nperpage_model.append
3289 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3291 [ 12, 20, 30, 40, 50 ].each { |v|
3292 iter = nperpage_model.append
3293 iter[0] = iter[1] = v.to_s
3295 nperpagecombo.active = 0
3297 multilanguages_value = nil
3298 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3299 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3300 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)
3301 multilanguages.signal_connect('clicked') {
3302 retval = ask_multi_languages(multilanguages_value)
3304 multilanguages_value = retval[1]
3306 if multilanguages_value
3307 ml_label.text = utf8(_("Multi-languages: enabled."))
3309 ml_label.text = utf8(_("Multi-languages: disabled."))
3312 if $config['default-multi-languages']
3313 multilanguages_value = $config['default-multi-languages']
3314 ml_label.text = utf8(_("Multi-languages: enabled."))
3316 ml_label.text = utf8(_("Multi-languages: disabled."))
3319 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3320 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3321 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)
3322 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3323 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3324 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)
3325 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3326 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3327 tooltips.set_tip(quotehtml, utf8(_("If checked, text using markup special characters such as '<grin>' will be shown properly; if unchecked, markup such as '<a href..' links will be interpreted by the browser properly")), nil)
3329 src_nb_calculated_for = ''
3330 src_nb_process = nil
3331 process_src_nb = proc {
3332 if src.text != src_nb_calculated_for
3333 src_nb_calculated_for = src.text
3336 Process.kill(9, src_nb_process)
3338 #- process doesn't exist anymore - race condition
3341 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3342 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3344 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3345 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3348 while src_nb_process
3349 msg 3, "sleeping for completion of previous process"
3352 gtk_thread_flush #- flush to avoid race condition in src_nb markup update
3354 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3355 total = { 'image' => 0, 'video' => 0, nil => 0 }
3356 if src_nb_process = fork
3357 msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3361 rd.readlines.each { |dir|
3362 if File.basename(dir) =~ /^\./
3366 Dir.entries(dir.chomp).each { |file|
3367 total[entry2type(file)] += 1
3369 rescue Errno::EACCES, Errno::ENOENT
3374 msg 3, "ripping #{src_nb_process}"
3375 dummy, exitstatus = Process.waitpid2(src_nb_process)
3377 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3379 src_nb_process = nil
3385 wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3386 Process.exit!(0) #- _exit
3389 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3392 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3398 timeout_src_nb = Gtk.timeout_add(100) {
3402 src_browse.signal_connect('clicked') {
3403 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3405 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3407 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3408 fc.transient_for = $main_window
3409 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3410 src.text = utf8(fc.filename)
3412 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3417 dest_browse.signal_connect('clicked') {
3418 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3420 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3422 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3423 fc.transient_for = $main_window
3424 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3425 dest.text = utf8(fc.filename)
3430 conf_browse.signal_connect('clicked') {
3431 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3433 Gtk::FileChooser::ACTION_SAVE,
3435 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3436 fc.transient_for = $main_window
3437 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3438 fc.set_current_folder(File.expand_path("~/.booh"))
3439 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3440 conf.text = utf8(fc.filename)
3447 recreate_theme_config = proc {
3448 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3450 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3451 $images_size.each { |s|
3452 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3456 tooltips.set_tip(cb, utf8(s['description']), nil)
3457 theme_sizes << { :widget => cb, :value => s['name'] }
3459 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3460 tooltips = Gtk::Tooltips.new
3461 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3462 theme_sizes << { :widget => cb, :value => 'original' }
3465 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3468 $allowed_N_values.each { |n|
3470 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3472 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3474 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3478 nperrows << { :widget => rb, :value => n }
3480 nperrowradios.show_all
3482 recreate_theme_config.call
3484 theme_button.signal_connect('clicked') {
3485 if newtheme = theme_choose(theme_button.label)
3486 theme_button.label = newtheme
3487 recreate_theme_config.call
3491 dialog.vbox.add(frame1)
3492 dialog.vbox.add(frame2)
3498 dialog.run { |response|
3499 if response == Gtk::Dialog::RESPONSE_OK
3500 srcdir = from_utf8_safe(src.text)
3501 destdir = from_utf8_safe(dest.text)
3502 confpath = from_utf8_safe(conf.text)
3503 if src.text != '' && srcdir == ''
3504 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3506 elsif !File.directory?(srcdir)
3507 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3509 elsif dest.text != '' && destdir == ''
3510 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))