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|
1897 puts "Failed to delete #{f}: #{$!}"
1903 def mark_document_as_dirty
1904 $xmldoc.elements.each('//dir') { |elem|
1905 elem.delete_attribute('already-generated')
1909 #- ret: true => ok false => cancel
1910 def ask_save_modifications(msg1, msg2, *options)
1912 options = options.size > 0 ? options[0] : {}
1914 if options[:disallow_cancel]
1915 dialog = Gtk::Dialog.new(msg1,
1917 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1918 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1919 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1921 dialog = Gtk::Dialog.new(msg1,
1923 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1924 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1925 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1926 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1928 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1929 dialog.vbox.add(Gtk::Label.new(msg2))
1930 dialog.window_position = Gtk::Window::POS_CENTER
1933 dialog.run { |response|
1935 if response == Gtk::Dialog::RESPONSE_YES
1936 if ! save_current_file_user
1937 return ask_save_modifications(msg1, msg2, options)
1940 #- if we have generated an album but won't save modifications, we must remove
1941 #- already-generated markers in original file
1942 if $generated_outofline
1944 $xmldoc = REXML::Document.new(File.new($orig_filename))
1945 mark_document_as_dirty
1946 ios = File.open($orig_filename, "w")
1950 puts "exception: #{$!}"
1954 if response == Gtk::Dialog::RESPONSE_CANCEL
1962 def try_quit(*options)
1963 if ask_save_modifications(utf8(_("Save before quitting?")),
1964 utf8(_("Do you want to save your changes before quitting?")),
1970 def show_popup(parent, msg, *options)
1971 dialog = Gtk::Dialog.new
1972 if options[0] && options[0][:title]
1973 dialog.title = options[0][:title]
1975 dialog.title = utf8(_("Booh message"))
1977 lbl = Gtk::Label.new
1978 if options[0] && options[0][:nomarkup]
1983 if options[0] && options[0][:centered]
1984 lbl.set_justify(Gtk::Justification::CENTER)
1986 if options[0] && options[0][:selectable]
1987 lbl.selectable = true
1989 if options[0] && options[0][:topwidget]
1990 dialog.vbox.add(options[0][:topwidget])
1992 if options[0] && options[0][:scrolled]
1993 sw = Gtk::ScrolledWindow.new(nil, nil)
1994 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1995 sw.add_with_viewport(lbl)
1997 dialog.set_default_size(500, 600)
1999 dialog.vbox.add(lbl)
2000 dialog.set_default_size(200, 120)
2002 if options[0] && options[0][:okcancel]
2003 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2005 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2007 if options[0] && options[0][:pos_centered]
2008 dialog.window_position = Gtk::Window::POS_CENTER
2010 dialog.window_position = Gtk::Window::POS_MOUSE
2013 if options[0] && options[0][:linkurl]
2014 linkbut = Gtk::Button.new('')
2015 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2016 linkbut.signal_connect('clicked') {
2017 open_url(options[0][:linkurl])
2018 dialog.response(Gtk::Dialog::RESPONSE_OK)
2019 set_mousecursor_normal
2021 linkbut.relief = Gtk::RELIEF_NONE
2022 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2023 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2024 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2029 if !options[0] || !options[0][:not_transient]
2030 dialog.transient_for = parent
2031 dialog.run { |response|
2033 if options[0] && options[0][:okcancel]
2034 return response == Gtk::Dialog::RESPONSE_OK
2038 dialog.signal_connect('response') { dialog.destroy }
2042 def set_mainwindow_title(progress)
2043 filename = $orig_filename || $filename
2046 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2048 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2052 $main_window.title = 'booh - ' + File.basename(filename)
2054 $main_window.title = 'booh'
2059 def backend_wait_message(parent, msg, infopipe_path, mode)
2061 w.set_transient_for(parent)
2064 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2065 vb.pack_start(Gtk::Label.new(msg), false, false)
2067 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2068 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2069 if mode != 'one dir scan'
2070 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2072 if mode == 'web-album'
2073 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2074 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2076 vb.pack_start(Gtk::HSeparator.new, false, false)
2078 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2079 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2080 vb.pack_end(bottom, false, false)
2083 update_progression_title_pb1 = proc {
2084 if mode == 'web-album'
2085 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2086 elsif mode != 'one dir scan'
2087 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2089 set_mainwindow_title(pb1_1.fraction)
2093 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2094 refresh_thread = Thread.new {
2095 directories_counter = 0
2096 while line = infopipe.gets
2097 msg 3, "infopipe got data: #{line}"
2098 if line =~ /^directories: (\d+), sizes: (\d+)/
2099 directories = $1.to_f + 1
2101 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2102 elements = $3.to_f + 1
2103 if mode == 'web-album'
2107 gtk_thread_protect { pb1_1.fraction = 0 }
2108 if mode != 'one dir scan'
2109 newtext = utf8(full_src_dir_to_rel($1, $2))
2110 newtext = '/' if newtext == ''
2111 gtk_thread_protect { pb1_2.text = newtext }
2112 directories_counter += 1
2113 gtk_thread_protect {
2114 pb1_2.fraction = directories_counter / directories
2115 update_progression_title_pb1.call
2118 elsif line =~ /^processing element$/
2119 element_counter += 1
2120 gtk_thread_protect {
2121 pb1_1.fraction = element_counter / elements
2122 update_progression_title_pb1.call
2124 elsif line =~ /^processing size$/
2125 element_counter += 1
2126 gtk_thread_protect {
2127 pb1_1.fraction = element_counter / elements
2128 update_progression_title_pb1.call
2130 elsif line =~ /^finished processing sizes$/
2131 gtk_thread_protect { pb1_1.fraction = 1 }
2132 elsif line =~ /^creating index.html$/
2133 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2134 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2135 directories_counter = 0
2136 elsif line =~ /^index.html: (.+)\|(.+)/
2137 newtext = utf8(full_src_dir_to_rel($1, $2))
2138 newtext = '/' if newtext == ''
2139 gtk_thread_protect { pb2.text = newtext }
2140 directories_counter += 1
2141 gtk_thread_protect {
2142 pb2.fraction = directories_counter / directories
2143 set_mainwindow_title(0.9 + pb2.fraction / 10)
2145 elsif line =~ /^die: (.*)$/
2152 w.signal_connect('delete-event') { w.destroy }
2153 w.signal_connect('destroy') {
2154 Thread.kill(refresh_thread)
2155 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2158 File.delete(infopipe_path)
2160 set_mainwindow_title(nil)
2162 w.window_position = Gtk::Window::POS_CENTER
2168 def call_backend(cmd, waitmsg, mode, params)
2169 pipe = Tempfile.new("boohpipe")
2170 Thread.critical = true
2173 system("mkfifo #{path}")
2174 Thread.critical = false
2175 cmd += " --info-pipe #{path}"
2176 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2181 id, exitstatus = Process.waitpid2(pid)
2182 gtk_thread_protect { w8.destroy }
2184 if params[:successmsg]
2185 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2187 if params[:closure_after]
2188 gtk_thread_protect(¶ms[:closure_after])
2190 elsif exitstatus == 15
2191 #- say nothing, user aborted
2193 gtk_thread_protect { show_popup($main_window,
2194 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2200 button.signal_connect('clicked') {
2201 Process.kill('SIGTERM', pid)
2205 def save_changes(*forced)
2206 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2210 $xmldir.delete_attribute('already-generated')
2212 propagate_children = proc { |xmldir|
2213 if xmldir.attributes['subdirs-caption']
2214 xmldir.delete_attribute('already-generated')
2216 xmldir.elements.each('dir') { |element|
2217 propagate_children.call(element)
2221 if $xmldir.child_byname_notattr('dir', 'deleted')
2222 new_title = $subalbums_title.buffer.text
2223 if new_title != $xmldir.attributes['subdirs-caption']
2224 parent = $xmldir.parent
2225 if parent.name == 'dir'
2226 parent.delete_attribute('already-generated')
2228 propagate_children.call($xmldir)
2230 $xmldir.add_attribute('subdirs-caption', new_title)
2231 $xmldir.elements.each('dir') { |element|
2232 if !element.attributes['deleted']
2233 path = element.attributes['path']
2234 newtext = $subalbums_edits[path][:editzone].buffer.text
2235 if element.attributes['subdirs-caption']
2236 if element.attributes['subdirs-caption'] != newtext
2237 propagate_children.call(element)
2239 element.add_attribute('subdirs-caption', newtext)
2240 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2242 if element.attributes['thumbnails-caption'] != newtext
2243 element.delete_attribute('already-generated')
2245 element.add_attribute('thumbnails-caption', newtext)
2246 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2252 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2253 if $xmldir.attributes['thumbnails-caption']
2254 path = $xmldir.attributes['path']
2255 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2257 elsif $xmldir.attributes['thumbnails-caption']
2258 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2261 if $xmldir.attributes['thumbnails-caption']
2262 if edit = $subalbums_edits[$xmldir.attributes['path']]
2263 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2267 #- remove and reinsert elements to reflect new ordering
2270 $xmldir.elements.each { |element|
2271 if element.name == 'image' || element.name == 'video'
2272 saves[element.attributes['filename']] = element.remove
2276 $autotable.current_order.each { |path|
2277 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2278 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2281 saves.each_key { |path|
2282 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2283 chld.add_attribute('deleted', 'true')
2287 def sort_by_exif_date
2291 rexml_thread_protect {
2292 $xmldir.elements.each { |element|
2293 if element.name == 'image' || element.name == 'video'
2294 current_order << element.attributes['filename']
2299 #- look for EXIF dates
2302 if current_order.size > 20
2304 w.set_transient_for($main_window)
2306 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2307 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2308 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2309 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2310 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2311 vb.pack_end(bottom, false, false)
2313 w.signal_connect('delete-event') { w.destroy }
2314 w.window_position = Gtk::Window::POS_CENTER
2318 b.signal_connect('clicked') { aborted = true }
2320 current_order.each { |f|
2322 if entry2type(f) == 'image'
2324 pb.fraction = i.to_f / current_order.size
2325 Gtk.main_iteration while Gtk.events_pending?
2326 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2328 dates[f] = date_time
2341 current_order.each { |f|
2342 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2344 dates[f] = date_time
2350 rexml_thread_protect {
2351 $xmldir.elements.each { |element|
2352 if element.name == 'image' || element.name == 'video'
2353 saves[element.attributes['filename']] = element.remove
2358 neworder = smartsort(current_order, dates)
2360 rexml_thread_protect {
2362 $xmldir.add_element(saves[f].name, saves[f].attributes)
2366 #- let the auto-table reflect new ordering
2370 def remove_all_captions
2373 $autotable.current_order.each { |path|
2374 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2375 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2377 save_undo(_("remove all captions"),
2379 texts.each_key { |key|
2380 $name2widgets[key][:textview].buffer.text = texts[key]
2382 $notebook.set_page(1)
2384 texts.each_key { |key|
2385 $name2widgets[key][:textview].buffer.text = ''
2387 $notebook.set_page(1)
2393 $selected_elements.each_key { |path|
2394 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2400 $selected_elements = {}
2404 $undo_tb.sensitive = $undo_mb.sensitive = false
2405 $redo_tb.sensitive = $redo_mb.sensitive = false
2411 $subalbums_vb.children.each { |chld|
2412 $subalbums_vb.remove(chld)
2414 $subalbums = Gtk::Table.new(0, 0, true)
2415 current_y_sub_albums = 0
2417 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2418 $subalbums_edits = {}
2419 subalbums_counter = 0
2420 subalbums_edits_bypos = {}
2422 add_subalbum = proc { |xmldir, counter|
2423 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2424 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2425 if xmldir == $xmldir
2426 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2427 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2428 caption = xmldir.attributes['thumbnails-caption']
2429 infotype = 'thumbnails'
2431 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2432 captionfile, caption = find_subalbum_caption_info(xmldir)
2433 infotype = find_subalbum_info_type(xmldir)
2435 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2436 hbox = Gtk::HBox.new
2437 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2439 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2442 my_gen_real_thumbnail = proc {
2443 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2446 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2447 f.add(img = Gtk::Image.new)
2448 my_gen_real_thumbnail.call
2450 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2452 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2453 $subalbums.attach(hbox,
2454 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2456 frame, textview = create_editzone($subalbums_sw, 0, img)
2457 textview.buffer.text = caption
2458 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2459 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2461 change_image = proc {
2462 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2464 Gtk::FileChooser::ACTION_OPEN,
2466 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2467 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2468 fc.transient_for = $main_window
2469 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))
2470 f.add(preview_img = Gtk::Image.new)
2472 fc.signal_connect('update-preview') { |w|
2473 if fc.preview_filename
2474 if entry2type(fc.preview_filename) == 'video'
2478 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2480 fc.preview_widget_active = false
2482 tmpimage = "#{tmpdir}/00000001.jpg"
2484 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2485 fc.preview_widget_active = true
2486 rescue Gdk::PixbufError
2487 fc.preview_widget_active = false
2489 File.delete(tmpimage)
2496 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2497 fc.preview_widget_active = true
2498 rescue Gdk::PixbufError
2499 fc.preview_widget_active = false
2504 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2506 old_file = captionfile
2507 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2508 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2509 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2510 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2512 new_file = fc.filename
2513 msg 3, "new captionfile is: #{fc.filename}"
2514 perform_changefile = proc {
2515 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2516 $modified_pixbufs.delete(thumbnail_file)
2517 xmldir.delete_attribute("#{infotype}-rotate")
2518 xmldir.delete_attribute("#{infotype}-color-swap")
2519 xmldir.delete_attribute("#{infotype}-enhance")
2520 xmldir.delete_attribute("#{infotype}-seektime")
2521 my_gen_real_thumbnail.call
2523 perform_changefile.call
2525 save_undo(_("change caption file for sub-album"),
2527 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2528 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2529 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2530 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2531 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2532 my_gen_real_thumbnail.call
2533 $notebook.set_page(0)
2535 perform_changefile.call
2536 $notebook.set_page(0)
2544 if File.exists?(thumbnail_file)
2545 File.delete(thumbnail_file)
2547 my_gen_real_thumbnail.call
2550 rotate_and_cleanup = proc { |angle|
2551 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2552 if File.exists?(thumbnail_file)
2553 File.delete(thumbnail_file)
2557 move = proc { |direction|
2560 save_changes('forced')
2561 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2562 if direction == 'up'
2563 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2564 subalbums_edits_bypos[oldpos - 1][:position] += 1
2566 if direction == 'down'
2567 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2568 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2570 if direction == 'top'
2571 for i in 1 .. oldpos - 1
2572 subalbums_edits_bypos[i][:position] += 1
2574 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2576 if direction == 'bottom'
2577 for i in oldpos + 1 .. subalbums_counter
2578 subalbums_edits_bypos[i][:position] -= 1
2580 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2584 $xmldir.elements.each('dir') { |element|
2585 if (!element.attributes['deleted'])
2586 elems << [ element.attributes['path'], element.remove ]
2589 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2590 each { |e| $xmldir.add_element(e[1]) }
2591 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2592 $xmldir.elements.each('descendant::dir') { |elem|
2593 elem.delete_attribute('already-generated')
2596 sel = $albums_tv.selection.selected_rows
2598 populate_subalbums_treeview(false)
2599 $albums_tv.selection.select_path(sel[0])
2602 color_swap_and_cleanup = proc {
2603 perform_color_swap_and_cleanup = proc {
2604 color_swap(xmldir, "#{infotype}-")
2605 my_gen_real_thumbnail.call
2607 perform_color_swap_and_cleanup.call
2609 save_undo(_("color swap"),
2611 perform_color_swap_and_cleanup.call
2612 $notebook.set_page(0)
2614 perform_color_swap_and_cleanup.call
2615 $notebook.set_page(0)
2620 change_seektime_and_cleanup = proc {
2621 if values = ask_new_seektime(xmldir, "#{infotype}-")
2622 perform_change_seektime_and_cleanup = proc { |val|
2623 change_seektime(xmldir, "#{infotype}-", val)
2624 my_gen_real_thumbnail.call
2626 perform_change_seektime_and_cleanup.call(values[:new])
2628 save_undo(_("specify seektime"),
2630 perform_change_seektime_and_cleanup.call(values[:old])
2631 $notebook.set_page(0)
2633 perform_change_seektime_and_cleanup.call(values[:new])
2634 $notebook.set_page(0)
2640 whitebalance_and_cleanup = proc {
2641 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2642 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2643 perform_change_whitebalance_and_cleanup = proc { |val|
2644 change_whitebalance(xmldir, "#{infotype}-", val)
2645 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2646 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2647 if File.exists?(thumbnail_file)
2648 File.delete(thumbnail_file)
2651 perform_change_whitebalance_and_cleanup.call(values[:new])
2653 save_undo(_("fix white balance"),
2655 perform_change_whitebalance_and_cleanup.call(values[:old])
2656 $notebook.set_page(0)
2658 perform_change_whitebalance_and_cleanup.call(values[:new])
2659 $notebook.set_page(0)
2665 gammacorrect_and_cleanup = proc {
2666 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2667 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2668 perform_change_gammacorrect_and_cleanup = proc { |val|
2669 change_gammacorrect(xmldir, "#{infotype}-", val)
2670 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2671 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2672 if File.exists?(thumbnail_file)
2673 File.delete(thumbnail_file)
2676 perform_change_gammacorrect_and_cleanup.call(values[:new])
2678 save_undo(_("gamma correction"),
2680 perform_change_gammacorrect_and_cleanup.call(values[:old])
2681 $notebook.set_page(0)
2683 perform_change_gammacorrect_and_cleanup.call(values[:new])
2684 $notebook.set_page(0)
2690 enhance_and_cleanup = proc {
2691 perform_enhance_and_cleanup = proc {
2692 enhance(xmldir, "#{infotype}-")
2693 my_gen_real_thumbnail.call
2696 perform_enhance_and_cleanup.call
2698 save_undo(_("enhance"),
2700 perform_enhance_and_cleanup.call
2701 $notebook.set_page(0)
2703 perform_enhance_and_cleanup.call
2704 $notebook.set_page(0)
2709 evtbox.signal_connect('button-press-event') { |w, event|
2710 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2712 rotate_and_cleanup.call(90)
2714 rotate_and_cleanup.call(-90)
2715 elsif $enhance.active?
2716 enhance_and_cleanup.call
2719 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2720 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2721 { :forbid_left => true, :forbid_right => true,
2722 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2723 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2724 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2725 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2726 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2728 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2733 evtbox.signal_connect('button-press-event') { |w, event|
2734 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2738 evtbox.signal_connect('button-release-event') { |w, event|
2739 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2740 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2741 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2742 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2743 msg 3, "gesture rotate: #{angle}"
2744 rotate_and_cleanup.call(angle)
2747 $gesture_press = nil
2750 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2751 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2752 current_y_sub_albums += 1
2755 if $xmldir.child_byname_notattr('dir', 'deleted')
2757 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2758 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2759 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2760 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2761 #- this album image/caption
2762 if $xmldir.attributes['thumbnails-caption']
2763 add_subalbum.call($xmldir, 0)
2766 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2767 $xmldir.elements.each { |element|
2768 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2769 #- element (image or video) of this album
2770 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2771 msg 3, "dest_img: #{dest_img}"
2772 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2773 total[element.name] += 1
2775 if element.name == 'dir' && !element.attributes['deleted']
2776 #- sub-album image/caption
2777 add_subalbum.call(element, subalbums_counter += 1)
2778 total[element.name] += 1
2781 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2782 total['image'], total['video'], total['dir'] ]))
2783 $subalbums_vb.add($subalbums)
2784 $subalbums_vb.show_all
2786 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2787 $notebook.get_tab_label($autotable_sw).sensitive = false
2788 $notebook.set_page(0)
2789 $thumbnails_title.buffer.text = ''
2791 $notebook.get_tab_label($autotable_sw).sensitive = true
2792 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2795 if !$xmldir.child_byname_notattr('dir', 'deleted')
2796 $notebook.get_tab_label($subalbums_sw).sensitive = false
2797 $notebook.set_page(1)
2799 $notebook.get_tab_label($subalbums_sw).sensitive = true
2803 def pixbuf_or_nil(filename)
2805 return Gdk::Pixbuf.new(filename)
2811 def theme_choose(current)
2812 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2814 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2815 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2816 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2818 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2819 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2820 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2821 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2822 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2823 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2824 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2825 treeview.signal_connect('button-press-event') { |w, event|
2826 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2827 dialog.response(Gtk::Dialog::RESPONSE_OK)
2831 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2833 ([ $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|
2836 iter[0] = File.basename(dir)
2837 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2838 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2839 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2840 if File.basename(dir) == current
2841 treeview.selection.select_iter(iter)
2844 dialog.set_default_size(-1, 500)
2845 dialog.vbox.show_all
2847 dialog.run { |response|
2848 iter = treeview.selection.selected
2850 if response == Gtk::Dialog::RESPONSE_OK && iter
2851 return model.get_value(iter, 0)
2857 def show_password_protections
2858 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2859 child_iter = $albums_iters[xmldir.attributes['path']]
2860 if xmldir.attributes['password-protect']
2861 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2862 already_protected = true
2863 elsif already_protected
2864 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2866 pix = pix.saturate_and_pixelate(1, true)
2872 xmldir.elements.each('dir') { |elem|
2873 if !elem.attributes['deleted']
2874 examine_dir_elem.call(child_iter, elem, already_protected)
2878 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2881 def populate_subalbums_treeview(select_first)
2885 $subalbums_vb.children.each { |chld|
2886 $subalbums_vb.remove(chld)
2889 source = $xmldoc.root.attributes['source']
2890 msg 3, "source: #{source}"
2892 xmldir = $xmldoc.elements['//dir']
2893 if !xmldir || xmldir.attributes['path'] != source
2894 msg 1, _("Corrupted booh file...")
2898 append_dir_elem = proc { |parent_iter, xmldir|
2899 child_iter = $albums_ts.append(parent_iter)
2900 child_iter[0] = File.basename(xmldir.attributes['path'])
2901 child_iter[1] = xmldir.attributes['path']
2902 $albums_iters[xmldir.attributes['path']] = child_iter
2903 msg 3, "puttin location: #{xmldir.attributes['path']}"
2904 xmldir.elements.each('dir') { |elem|
2905 if !elem.attributes['deleted']
2906 append_dir_elem.call(child_iter, elem)
2910 append_dir_elem.call(nil, xmldir)
2911 show_password_protections
2913 $albums_tv.expand_all
2915 $albums_tv.selection.select_iter($albums_ts.iter_first)
2919 def select_current_theme
2920 select_theme($xmldoc.root.attributes['theme'],
2921 $xmldoc.root.attributes['limit-sizes'],
2922 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2923 $xmldoc.root.attributes['thumbnails-per-row'])
2926 def open_file(filename)
2930 $current_path = nil #- invalidate
2931 $modified_pixbufs = {}
2934 $subalbums_vb.children.each { |chld|
2935 $subalbums_vb.remove(chld)
2938 if !File.exists?(filename)
2939 return utf8(_("File not found."))
2943 $xmldoc = REXML::Document.new(File.new(filename))
2948 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2949 if entry2type(filename).nil?
2950 return utf8(_("Not a booh file!"))
2952 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."))
2956 if !source = $xmldoc.root.attributes['source']
2957 return utf8(_("Corrupted booh file..."))
2960 if !dest = $xmldoc.root.attributes['destination']
2961 return utf8(_("Corrupted booh file..."))
2964 if !theme = $xmldoc.root.attributes['theme']
2965 return utf8(_("Corrupted booh file..."))
2968 if $xmldoc.root.attributes['version'] < $VERSION
2969 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2970 mark_document_as_dirty
2971 if $xmldoc.root.attributes['version'] < '0.8.4'
2972 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2973 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2974 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2975 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2976 if old_dest_dir != new_dest_dir
2977 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2979 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2980 xmldir.elements.each { |element|
2981 if %w(image video).include?(element.name) && !element.attributes['deleted']
2982 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2983 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2984 Dir[old_name + '*'].each { |file|
2985 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2986 file != new_file and sys("mv '#{file}' '#{new_file}'")
2989 if element.name == 'dir' && !element.attributes['deleted']
2990 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2991 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2992 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2996 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3000 $xmldoc.root.add_attribute('version', $VERSION)
3003 select_current_theme
3005 $filename = filename
3006 set_mainwindow_title(nil)
3007 $default_size['thumbnails'] =~ /(.*)x(.*)/
3008 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3009 $albums_thumbnail_size =~ /(.*)x(.*)/
3010 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3012 populate_subalbums_treeview(true)
3014 $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
3018 def open_file_user(filename)
3019 result = open_file(filename)
3021 $config['last-opens'] ||= []
3022 if $config['last-opens'][-1] != utf8(filename)
3023 $config['last-opens'] << utf8(filename)
3025 $orig_filename = $filename
3026 $main_window.title = 'booh - ' + File.basename($orig_filename)
3027 tmp = Tempfile.new("boohtemp")
3028 Thread.critical = true
3029 $filename = tmp.path
3032 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3033 Thread.critical = false
3035 $tempfiles << $filename << "#{$filename}.backup"
3037 $orig_filename = nil
3043 if !ask_save_modifications(utf8(_("Save this album?")),
3044 utf8(_("Do you want to save the changes to this album?")),
3045 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3048 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3050 Gtk::FileChooser::ACTION_OPEN,
3052 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3053 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3054 fc.set_current_folder(File.expand_path("~/.booh"))
3055 fc.transient_for = $main_window
3056 fc.preview_widget = previewlabel = Gtk::Label.new.show
3057 fc.signal_connect('update-preview') { |w|
3058 if fc.preview_filename
3060 push_mousecursor_wait(fc)
3061 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3065 xmldoc.elements.each('//*') { |elem|
3066 if elem.name == 'dir'
3068 elsif elem.name == 'image'
3070 elsif elem.name == 'video'
3078 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3079 fc.preview_widget_active = false
3081 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") %
3082 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3083 fc.preview_widget_active = true
3089 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3090 push_mousecursor_wait(fc)
3091 msg = open_file_user(fc.filename)
3106 def additional_booh_options
3109 options += "--mproc #{$config['mproc'].to_i} "
3111 options += "--comments-format '#{$config['comments-format']}' "
3112 if $config['transcode-videos']
3113 options += "--transcode-videos '#{$config['transcode-videos']}' "
3118 def ask_multi_languages(value)
3120 spl = value.split(',')
3121 value = [ spl[0..-2], spl[-1] ]
3124 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3127 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3128 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3130 lbl = Gtk::Label.new
3132 _("You can choose to activate <b>multi-languages</b> support for this web-album
3133 (it will work only if you publish your web-album on an Apache web-server). This will
3134 use the MultiViews feature of Apache; the pages will be served according to the
3135 value of the Accept-Language HTTP header sent by the web browsers, so that people
3136 with different languages preferences will be able to browse your web-album with
3137 navigation in their language (if language is available).
3140 dialog.vbox.add(lbl)
3141 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3142 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3143 add(languages = Gtk::Button.new))))
3145 pick_languages = proc {
3146 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3149 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3150 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3152 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3153 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3155 SUPPORTED_LANGUAGES.each { |lang|
3156 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3157 if ! value.nil? && value[0].include?(lang)
3163 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3164 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3165 fallback_language = nil
3166 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3167 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3168 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3169 fbl_rb.active = true
3170 fallback_language = SUPPORTED_LANGUAGES[0]
3172 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3173 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3174 rb.signal_connect('clicked') { fallback_language = lang }
3175 if ! value.nil? && value[1] == lang
3180 dialog2.window_position = Gtk::Window::POS_MOUSE
3184 dialog2.run { |response|
3186 if resp == Gtk::Dialog::RESPONSE_OK
3188 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3189 value[1] = fallback_language
3190 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3197 languages.signal_connect('clicked') {
3200 dialog.window_position = Gtk::Window::POS_MOUSE
3204 rb_yes.active = true
3205 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3207 rb_no.signal_connect('clicked') {
3211 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3224 dialog.run { |response|
3229 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3231 return [ true, nil ]
3233 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3242 if !ask_save_modifications(utf8(_("Save this album?")),
3243 utf8(_("Do you want to save the changes to this album?")),
3244 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3247 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3249 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3250 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3251 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3253 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3254 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3255 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3256 tbl.attach(src = Gtk::Entry.new,
3257 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3258 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3259 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3260 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3261 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3262 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3263 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3264 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3265 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3266 tbl.attach(dest = Gtk::Entry.new,
3267 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3268 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3269 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3270 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3271 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3272 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3273 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3274 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3275 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3277 tooltips = Gtk::Tooltips.new
3278 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3279 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3280 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3281 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3282 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3283 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3284 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)
3285 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3286 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3287 nperpage_model = Gtk::ListStore.new(String, String)
3288 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3289 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3290 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3291 nperpagecombo.set_attributes(crt, { :markup => 0 })
3292 iter = nperpage_model.append
3293 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3295 [ 12, 20, 30, 40, 50 ].each { |v|
3296 iter = nperpage_model.append
3297 iter[0] = iter[1] = v.to_s
3299 nperpagecombo.active = 0
3301 multilanguages_value = nil
3302 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3303 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3304 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)
3305 multilanguages.signal_connect('clicked') {
3306 retval = ask_multi_languages(multilanguages_value)
3308 multilanguages_value = retval[1]
3310 if multilanguages_value
3311 ml_label.text = utf8(_("Multi-languages: enabled."))
3313 ml_label.text = utf8(_("Multi-languages: disabled."))
3316 if $config['default-multi-languages']
3317 multilanguages_value = $config['default-multi-languages']
3318 ml_label.text = utf8(_("Multi-languages: enabled."))
3320 ml_label.text = utf8(_("Multi-languages: disabled."))
3323 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3324 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3325 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)
3326 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3327 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3328 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)
3329 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3330 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3331 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)
3333 src_nb_calculated_for = ''
3334 src_nb_process = nil
3335 process_src_nb = proc {
3336 if src.text != src_nb_calculated_for
3337 src_nb_calculated_for = src.text
3340 Process.kill(9, src_nb_process)
3342 #- process doesn't exist anymore - race condition
3345 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3346 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3348 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3349 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3352 while src_nb_process
3353 msg 3, "sleeping for completion of previous process"
3356 gtk_thread_flush #- flush to avoid race condition in src_nb markup update
3358 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3359 total = { 'image' => 0, 'video' => 0, nil => 0 }
3360 if src_nb_process = fork
3361 msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3365 rd.readlines.each { |dir|
3366 if File.basename(dir) =~ /^\./
3370 Dir.entries(dir.chomp).each { |file|
3371 total[entry2type(file)] += 1
3373 rescue Errno::EACCES, Errno::ENOENT
3378 msg 3, "ripping #{src_nb_process}"
3379 dummy, exitstatus = Process.waitpid2(src_nb_process)
3381 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3383 src_nb_process = nil
3389 wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3390 Process.exit!(0) #- _exit
3393 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3396 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3402 timeout_src_nb = Gtk.timeout_add(100) {
3406 src_browse.signal_connect('clicked') {
3407 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3409 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3411 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3412 fc.transient_for = $main_window
3413 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3414 src.text = utf8(fc.filename)
3416 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3421 dest_browse.signal_connect('clicked') {
3422 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3424 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3426 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3427 fc.transient_for = $main_window
3428 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3429 dest.text = utf8(fc.filename)
3434 conf_browse.signal_connect('clicked') {
3435 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3437 Gtk::FileChooser::ACTION_SAVE,
3439 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3440 fc.transient_for = $main_window
3441 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3442 fc.set_current_folder(File.expand_path("~/.booh"))
3443 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3444 conf.text = utf8(fc.filename)
3451 recreate_theme_config = proc {
3452 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3454 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3455 $images_size.each { |s|
3456 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3460 tooltips.set_tip(cb, utf8(s['description']), nil)
3461 theme_sizes << { :widget => cb, :value => s['name'] }
3463 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3464 tooltips = Gtk::Tooltips.new
3465 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3466 theme_sizes << { :widget => cb, :value => 'original' }
3469 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3472 $allowed_N_values.each { |n|
3474 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3476 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3478 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3482 nperrows << { :widget => rb, :value => n }
3484 nperrowradios.show_all
3486 recreate_theme_config.call
3488 theme_button.signal_connect('clicked') {
3489 if newtheme = theme_choose(theme_button.label)
3490 theme_button.label = newtheme
3491 recreate_theme_config.call
3495 dialog.vbox.add(frame1)
3496 dialog.vbox.add(frame2)
3502 dialog.run { |response|
3503 if response == Gtk::Dialog::RESPONSE_OK
3504 srcdir = from_utf8_safe(src.text)
3505 destdir = from_utf8_safe(dest.text)
3506 confpath = from_utf8_safe(conf.text)
3507 if src.text != '' && srcdir == ''
3508 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3510 elsif !File.directory?(srcdir)
3511 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3513 elsif dest.text != '' && destdir == ''