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-2013 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
32 require 'booh/libadds'
33 require 'booh/GtkAutoTable'
37 bindtextdomain("booh")
39 require 'booh/rexml/document'
42 require 'booh/booh-lib'
44 require 'booh/UndoHandler'
49 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
50 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
51 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
54 #- default values for some globals
58 $ignore_videos = false
59 $button1_pressed_autotable = false
60 $generated_outofline = false
63 puts _("Usage: %s [OPTION]...") % File.basename($0)
65 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
70 parser = GetoptLong.new
71 parser.set_options(*$options.collect { |ary| ary[0..2] })
73 parser.each_option do |name, arg|
80 puts _("Booh version %s
82 Copyright (c) 2005-2013 Guillaume Cottenceau.
83 This is free software; see the source for copying conditions. There is NO
84 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
88 when '--verbose-level'
89 $verbose_level = arg.to_i
102 for line in IO.readlines('/proc/cpuinfo') do
103 line =~ /^processor/ and cpus += 1
110 $config_file = File.expand_path('~/.booh-gui-rc')
111 if File.readable?($config_file)
113 xmldoc = REXML::Document.new(File.new($config_file))
115 #- encoding unsupported anymore? file edited manually? ignore then
116 msg 1, "Ignoring #{$config_file}, failed to parse it: #{$!}"
119 xmldoc.root.elements.each { |element|
120 txt = element.get_text
122 if txt.value =~ /~~~/ || element.name == 'last-opens'
123 $config[element.name] = txt.value.split(/~~~/)
125 $config[element.name] = txt.value
127 elsif element.elements.size == 0
128 $config[element.name] = ''
130 $config[element.name] = {}
131 element.each { |chld|
133 $config[element.name][chld.name] = txt ? txt.value : nil
139 $config['video-viewer'] ||= '/usr/bin/mplayer %f || /usr/bin/vlc %f'
140 $config['image-editor'] ||= '/usr/bin/gimp-remote %f || /usr/bin/gimp %f'
141 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f || /usr/bin/firefox -remote 'openURL(%f,new-window)' || /usr/bin/firefox %f"
142 $config['use-mp4'] ||= "true"
143 $config['mp4-generator'] ||= "/usr/bin/ffmpeg -i %f -b 800k -ar 22050 -ab 32k %o"
144 $config['comments-format'] ||= '%t'
145 if !FileTest.directory?(File.expand_path('~/.booh'))
146 system("mkdir ~/.booh")
148 if $config['mproc'].nil?
151 $config['mproc'] = cpus
154 $config['rotate-set-exif'] ||= 'true'
159 def check_config_preferences_dep
160 viewer_binary = $config['video-viewer'].split.first
161 if viewer_binary && !File.executable?(viewer_binary)
162 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
163 You should fix this in Edit/Preferences so that you can view videos.
165 Problem was: '%s' is not an executable file.
166 Hint: don't forget to specify the full path to the executable,
167 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
170 mp4_generator_binary = $config['use-mp4'] == 'true' && $config['mp4-generator'].split.first
171 if mp4_generator_binary && !File.executable?(mp4_generator_binary)
172 show_popup($main_window, utf8(_("The configured .mp4 generator seems to be unavailable.
173 You should fix this in Edit/Preferences so that you can have working
174 embedded flash videos.
176 Problem was: '%s' is not an executable file.
177 Hint: don't forget to specify the full path to the executable,
178 e.g. '/usr/bin/ffmpeg' is correct but 'ffmpeg' only is not.") % mp4_generator_binary), { :pos_centered => true, :not_transient => true })
183 if !system("which convert >/dev/null 2>/dev/null")
184 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
185 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
188 if !system("which identify >/dev/null 2>/dev/null")
189 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
190 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
192 if !system("which exif >/dev/null 2>/dev/null")
193 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
195 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
197 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
200 check_config_preferences_dep
204 if $config['cpus'] && cpus > $config['cpus'].to_i
205 show_popup($main_window, utf8(_("It seems you now have more CPUs available than last time booh was run.
206 You should probably increase the amount of CPUs configured in Edit/Preferences,
207 so that web-albums are generated as fast as possible on this computer.")), { :pos_centered => true, :not_transient => true })
209 $config['cpus'] = cpus
212 def check_image_editor
213 if last_failed_binary = check_multi_binaries($config['image-editor'])
214 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
215 You should fix this in Edit/Preferences so that you can edit photos externally.
217 Problem was: '%s' is not an executable file.
218 Hint: don't forget to specify the full path to the executable,
219 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
227 if $config['last-opens'] && $config['last-opens'].size > 10
228 $config['last-opens'] = $config['last-opens'][-10, 10]
231 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
232 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
233 $config.each_pair { |key, value|
234 elem = xmldoc.root.add_element key
236 $config[key].each_pair { |subkey, subvalue|
237 subelem = elem.add_element subkey
238 subelem.add_text subvalue.to_s
240 elsif value.is_a? Array
241 elem.add_text value.join('~~~')
246 elem.add_text value.to_s
250 ios = File.open($config_file, "w")
254 $tempfiles.each { |f|
261 def set_mousecursor(what, *widget)
262 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
263 if widget[0] && widget[0].window
264 widget[0].window.cursor = cursor
266 if $main_window && $main_window.window
267 $main_window.window.cursor = cursor
269 $current_cursor = what
271 def set_mousecursor_wait(*widget)
272 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
273 if Thread.current == Thread.main
274 Gtk.main_iteration while Gtk.events_pending?
277 def set_mousecursor_normal(*widget)
278 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
280 def push_mousecursor_wait(*widget)
281 if $current_cursor != Gdk::Cursor::WATCH
282 $save_cursor = $current_cursor
283 gtk_thread_protect { set_mousecursor_wait(*widget) }
286 def pop_mousecursor(*widget)
287 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
291 source = $xmldoc.root.attributes['source']
292 dest = $xmldoc.root.attributes['destination']
293 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
296 def full_src_dir_to_rel(path, source)
297 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
300 def build_full_dest_filename(filename)
301 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
304 def save_undo(name, closure, *params)
305 UndoHandler.save_undo(name, closure, [ *params ])
306 $undo_tb.sensitive = $undo_mb.sensitive = true
307 $redo_tb.sensitive = $redo_mb.sensitive = false
310 def view_element(filename, closures)
311 if entry2type(filename) == 'video'
312 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
318 w = create_window.set_title(filename)
320 msg 3, "filename: #{filename}"
321 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
322 #- typically this file won't exist in case of videos; try with the largest thumbnail around
323 if !File.exists?(dest_img)
324 if entry2type(filename) == 'video'
325 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
326 if not alternatives.empty?
327 dest_img = alternatives[-1]
330 push_mousecursor_wait
331 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
333 if !File.exists?(dest_img)
334 msg 2, _("Could not generate fullscreen thumbnail!")
339 aspect = utf8(_("Aspect: unknown"))
340 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
342 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
344 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
345 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(vbox).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
346 evt.signal_connect('button-press-event') { |this, event|
347 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
348 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
350 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
352 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
353 delete_item.signal_connect('activate') {
355 closures[:delete].call(false)
358 menu.popup(nil, nil, event.button, event.time)
361 evt.signal_connect('button-release-event') { |this, event|
363 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
364 msg 3, "gesture delete: click-drag right button to the bottom"
366 closures[:delete].call(false)
367 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
371 tooltips = Gtk::Tooltips.new
372 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
374 w.signal_connect('key-press-event') { |w,event|
375 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
377 closures[:delete].call(false)
381 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
382 b.signal_connect('clicked') { w.destroy }
385 vb.pack_start(evt, false, false)
386 vb.pack_end(bottom, false, false)
389 w.signal_connect('delete-event') { w.destroy }
390 w.window_position = Gtk::Window::POS_CENTER
394 def scroll_upper(scrolledwindow, ypos_top)
395 newval = scrolledwindow.vadjustment.value -
396 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
397 if newval < scrolledwindow.vadjustment.lower
398 newval = scrolledwindow.vadjustment.lower
400 scrolledwindow.vadjustment.value = newval
403 def scroll_lower(scrolledwindow, ypos_bottom)
404 newval = scrolledwindow.vadjustment.value +
405 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
406 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
407 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
409 scrolledwindow.vadjustment.value = newval
412 def autoscroll_if_needed(scrolledwindow, image, textview)
413 #- autoscroll if cursor or image is not visible, if possible
414 if image && image.window || textview.window
415 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
416 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
417 current_miny_visible = scrolledwindow.vadjustment.value
418 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
419 if ypos_top < current_miny_visible
420 scroll_upper(scrolledwindow, ypos_top)
421 elsif ypos_bottom > current_maxy_visible
422 scroll_lower(scrolledwindow, ypos_bottom)
427 def create_editzone(scrolledwindow, pagenum, image)
428 frame = Gtk::Frame.new
429 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
430 frame.set_shadow_type(Gtk::SHADOW_IN)
431 textview.signal_connect('key-press-event') { |w, event|
432 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
433 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
434 scrolledwindow.signal_emit('key-press-event', event)
436 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
437 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
438 if event.keyval == Gdk::Keyval::GDK_Up
439 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
440 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
442 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
445 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
446 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
448 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
455 candidate_undo_text = nil
456 textview.signal_connect('focus-in-event') { |w, event|
457 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
458 candidate_undo_text = textview.buffer.text
462 textview.signal_connect('key-release-event') { |w, event|
463 if candidate_undo_text && candidate_undo_text != textview.buffer.text
465 save_undo(_("text edit"),
467 save_text = textview.buffer.text
468 textview.buffer.text = text
470 $notebook.set_page(pagenum)
472 textview.buffer.text = save_text
474 $notebook.set_page(pagenum)
476 }, candidate_undo_text)
477 candidate_undo_text = nil
480 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
481 autoscroll_if_needed(scrolledwindow, image, textview)
486 return [ frame, textview ]
489 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
491 if !$modified_pixbufs[thumbnail_img]
492 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
493 elsif !$modified_pixbufs[thumbnail_img][:orig]
494 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
497 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
500 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
501 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
502 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
503 if pixbuf.height > desired_y
504 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, :bilinear)
505 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
506 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), :bilinear)
511 if $modified_pixbufs[thumbnail_img][:whitebalance]
512 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
515 #- fix gamma correction
516 if $modified_pixbufs[thumbnail_img][:gammacorrect]
517 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
520 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
523 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
526 #- update rotate attribute
527 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
528 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
530 #- change exif orientation if configured so (but forget in case of thumbnails caption)
531 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
532 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
535 $modified_pixbufs[thumbnail_img] ||= {}
536 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
537 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
539 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
542 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
545 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
547 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
549 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
550 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
552 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
553 $notebook.set_page(0)
554 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
559 def color_swap(xmldir, attributes_prefix)
561 rexml_thread_protect {
562 if xmldir.attributes["#{attributes_prefix}color-swap"]
563 xmldir.delete_attribute("#{attributes_prefix}color-swap")
565 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
570 def enhance(xmldir, attributes_prefix)
572 rexml_thread_protect {
573 if xmldir.attributes["#{attributes_prefix}enhance"]
574 xmldir.delete_attribute("#{attributes_prefix}enhance")
576 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
581 def change_seektime(xmldir, attributes_prefix, value)
583 rexml_thread_protect {
584 xmldir.add_attribute("#{attributes_prefix}seektime", value)
588 def ask_new_seektime(xmldir, attributes_prefix)
589 value = rexml_thread_protect {
591 xmldir.attributes["#{attributes_prefix}seektime"]
597 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
599 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
600 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
601 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
605 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
609 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
610 entry.signal_connect('key-press-event') { |w, event|
611 if event.keyval == Gdk::Keyval::GDK_Return
612 dialog.response(Gtk::Dialog::RESPONSE_OK)
614 elsif event.keyval == Gdk::Keyval::GDK_Escape
615 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
618 false #- propagate if needed
622 dialog.window_position = Gtk::Window::POS_MOUSE
625 dialog.run { |response|
628 if response == Gtk::Dialog::RESPONSE_OK
630 msg 3, "changing seektime to #{newval}"
631 return { :old => value, :new => newval }
638 def change_pano_amount(xmldir, attributes_prefix, value)
640 rexml_thread_protect {
642 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
644 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
649 def ask_new_pano_amount(xmldir, attributes_prefix)
650 value = rexml_thread_protect {
652 xmldir.attributes["#{attributes_prefix}pano-amount"]
658 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
660 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
661 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
662 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
666 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
667 of this panorama image compared to other regular images. For example, if the panorama
668 was taken out of four photos on one row, counting the necessary overlap, the width of
669 this panorama image should probably be roughly three times the width of regular images.
671 With this information, booh will be able to generate panorama thumbnails looking
672 the right 'size', since the height of the thumbnail for this image will be similar
673 to the height of other thumbnails.
676 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
677 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
678 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
679 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
680 spin.signal_connect('value-changed') {
683 dialog.window_position = Gtk::Window::POS_MOUSE
686 spin.value = value.to_f
693 dialog.run { |response|
697 newval = spin.value.to_f
700 if response == Gtk::Dialog::RESPONSE_OK
702 msg 3, "changing panorama amount to #{newval}"
703 return { :old => value, :new => newval }
710 def change_whitebalance(xmlelem, attributes_prefix, value)
712 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
715 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
717 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
718 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
719 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
720 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
721 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
722 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
723 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
724 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
725 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
726 $modified_pixbufs[thumbnail_img] ||= {}
727 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
728 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
730 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
731 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
733 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
736 $modified_pixbufs[thumbnail_img] ||= {}
737 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
739 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
742 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
743 #- init $modified_pixbufs correctly
744 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
746 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
748 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
750 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
751 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
752 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
756 _("You can fix the <b>white balance</b> of the image, if your image is too blue
757 or too yellow because the recorder didn't detect the light correctly. Drag the
758 slider below the image to the left for more blue, to the right for more yellow.
762 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
764 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
766 dialog.window_position = Gtk::Window::POS_MOUSE
770 timeout = Gtk.timeout_add(100) {
771 if hs.value != lastval
774 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
780 dialog.run { |response|
781 Gtk.timeout_remove(timeout)
782 if response == Gtk::Dialog::RESPONSE_OK
784 newval = hs.value.to_s
785 msg 3, "changing white balance to #{newval}"
787 return { :old => value, :new => newval }
790 $modified_pixbufs[thumbnail_img] ||= {}
791 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
792 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
800 def change_gammacorrect(xmlelem, attributes_prefix, value)
802 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
805 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
807 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
808 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
809 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
810 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
811 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
812 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
813 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
814 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
815 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
816 $modified_pixbufs[thumbnail_img] ||= {}
817 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
818 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
820 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
821 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
823 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
826 $modified_pixbufs[thumbnail_img] ||= {}
827 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
829 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
832 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
833 #- init $modified_pixbufs correctly
834 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
836 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
838 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
840 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
841 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
842 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
846 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
847 or too bright. Drag the slider below the image.
851 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
853 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
855 dialog.window_position = Gtk::Window::POS_MOUSE
859 timeout = Gtk.timeout_add(100) {
860 if hs.value != lastval
863 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
869 dialog.run { |response|
870 Gtk.timeout_remove(timeout)
871 if response == Gtk::Dialog::RESPONSE_OK
873 newval = hs.value.to_s
874 msg 3, "gamma correction to #{newval}"
876 return { :old => value, :new => newval }
879 $modified_pixbufs[thumbnail_img] ||= {}
880 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
881 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
889 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
890 if File.exists?(destfile)
891 File.delete(destfile)
893 #- type can be 'element' or 'subdir'
895 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
897 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
901 $max_gen_thumbnail_threads = nil
902 $current_gen_thumbnail_threads = 0
903 $gen_thumbnail_monitor = Monitor.new
905 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
906 if $max_gen_thumbnail_threads.nil?
907 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
910 push_mousecursor_wait
911 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
914 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
919 $gen_thumbnail_monitor.synchronize {
920 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
921 $current_gen_thumbnail_threads += 1
926 msg 3, "generate thumbnail from new thread"
929 $gen_thumbnail_monitor.synchronize {
930 $current_gen_thumbnail_threads -= 1
934 msg 3, "generate thumbnail from current thread"
939 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
940 distribute_multiple_call = Proc.new { |action, arg|
941 $selected_elements.each_key { |path|
942 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
944 if possible_actions[:can_multiple] && $selected_elements.length > 0
945 UndoHandler.begin_batch
946 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
947 UndoHandler.end_batch
949 closures[action].call(arg)
951 $selected_elements = {}
954 if optionals.include?('change_image')
955 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
956 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
957 changeimg.signal_connect('activate') { closures[:change].call }
958 menu.append(Gtk::SeparatorMenuItem.new)
960 if !possible_actions[:can_multiple] || $selected_elements.length == 0
963 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
964 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
965 view.signal_connect('activate') { closures[:view].call }
967 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
968 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
969 view.signal_connect('activate') { closures[:view].call }
970 menu.append(Gtk::SeparatorMenuItem.new)
973 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
974 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
975 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
976 exif.signal_connect('activate') { show_popup($main_window,
977 utf8(`exif -m '#{fullpath}'`),
978 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
979 menu.append(Gtk::SeparatorMenuItem.new)
982 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
983 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
984 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
985 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
986 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
987 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
988 if !possible_actions[:can_multiple] || $selected_elements.length == 0
989 menu.append(Gtk::SeparatorMenuItem.new)
990 if !possible_actions[:forbid_left]
991 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
992 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
993 moveleft.signal_connect('activate') { closures[:move].call('left') }
994 if !possible_actions[:can_left]
995 moveleft.sensitive = false
998 if !possible_actions[:forbid_right]
999 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
1000 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
1001 moveright.signal_connect('activate') { closures[:move].call('right') }
1002 if !possible_actions[:can_right]
1003 moveright.sensitive = false
1006 if optionals.include?('move_top')
1007 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
1008 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
1009 movetop.signal_connect('activate') { closures[:move].call('top') }
1010 if !possible_actions[:can_top]
1011 movetop.sensitive = false
1014 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
1015 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
1016 moveup.signal_connect('activate') { closures[:move].call('up') }
1017 if !possible_actions[:can_up]
1018 moveup.sensitive = false
1020 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
1021 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
1022 movedown.signal_connect('activate') { closures[:move].call('down') }
1023 if !possible_actions[:can_down]
1024 movedown.sensitive = false
1026 if optionals.include?('move_bottom')
1027 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
1028 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
1029 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
1030 if !possible_actions[:can_bottom]
1031 movebottom.sensitive = false
1036 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
1037 menu.append(Gtk::SeparatorMenuItem.new)
1038 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
1039 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
1040 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
1041 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1042 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1043 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1044 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1045 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1046 seektime.signal_connect('activate') {
1047 if possible_actions[:can_multiple] && $selected_elements.length > 0
1048 if values = ask_new_seektime(nil, '')
1049 distribute_multiple_call.call(:seektime, values)
1052 closures[:seektime].call
1057 menu.append( Gtk::SeparatorMenuItem.new)
1058 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1059 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1060 gammacorrect.signal_connect('activate') {
1061 if possible_actions[:can_multiple] && $selected_elements.length > 0
1062 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1063 distribute_multiple_call.call(:gammacorrect, values)
1066 closures[:gammacorrect].call
1069 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1070 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1071 whitebalance.signal_connect('activate') {
1072 if possible_actions[:can_multiple] && $selected_elements.length > 0
1073 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1074 distribute_multiple_call.call(:whitebalance, values)
1077 closures[:whitebalance].call
1080 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1081 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1082 _("Enhance constrast"))))
1084 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1086 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1087 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1088 if type == 'image' && possible_actions[:can_panorama]
1089 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1090 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1091 panorama.signal_connect('activate') {
1092 if possible_actions[:can_multiple] && $selected_elements.length > 0
1093 if values = ask_new_pano_amount(nil, '')
1094 distribute_multiple_call.call(:pano, values)
1097 distribute_multiple_call.call(:pano)
1101 menu.append( Gtk::SeparatorMenuItem.new)
1102 if optionals.include?('delete')
1103 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1104 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1105 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1106 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1107 paste_item.signal_connect('activate') { closures[:paste].call }
1108 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1109 clear_item.signal_connect('activate') { $cuts = [] }
1111 paste_item.sensitive = clear_item.sensitive = false
1114 menu.append( Gtk::SeparatorMenuItem.new)
1116 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1117 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1118 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1119 editexternally.signal_connect('activate') {
1120 if check_image_editor
1121 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1127 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1128 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1129 if optionals.include?('delete')
1130 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1131 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1134 menu.popup(nil, nil, event.button, event.time)
1137 def delete_current_subalbum
1139 sel = $albums_tv.selection.selected_rows
1140 $xmldir.elements.each { |e|
1141 if e.name == 'image' || e.name == 'video'
1142 e.add_attribute('deleted', 'true')
1145 #- branch if we have a non deleted subalbum
1146 if $xmldir.child_byname_notattr('dir', 'deleted')
1147 $xmldir.delete_attribute('thumbnails-caption')
1148 $xmldir.delete_attribute('thumbnails-captionfile')
1150 $xmldir.add_attribute('deleted', 'true')
1152 while moveup.parent.name == 'dir'
1153 moveup = moveup.parent
1154 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1155 moveup.add_attribute('deleted', 'true')
1162 save_changes('forced')
1163 populate_subalbums_treeview(false)
1164 $albums_tv.selection.select_path(sel[0])
1170 $current_path = nil #- prevent save_changes from being rerun again
1171 sel = $albums_tv.selection.selected_rows
1172 restore_one = proc { |xmldir|
1173 xmldir.elements.each { |e|
1174 if e.name == 'dir' && e.attributes['deleted']
1177 e.delete_attribute('deleted')
1180 restore_one.call($xmldir)
1181 populate_subalbums_treeview(false)
1182 $albums_tv.selection.select_path(sel[0])
1185 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1188 frame1 = Gtk::Frame.new
1189 fullpath = from_utf8("#{$current_path}/#{filename}")
1191 my_gen_real_thumbnail = proc {
1192 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1196 pxb = GdkPixbuf::Pixbuf.new(:file => "#{$FPATH}/images/video_border.png")
1197 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1198 pack_start(img = Gtk::Image.new).
1199 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1200 px, mask = pxb.render_pixmap_and_mask(0)
1201 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1202 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1204 frame1.add(img = Gtk::Image.new)
1207 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1208 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1209 my_gen_real_thumbnail.call
1211 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1214 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1216 tooltips = Gtk::Tooltips.new
1217 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1218 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1220 frame2, textview = create_editzone($autotable_sw, 1, img)
1221 textview.buffer.text = caption
1222 textview.set_justification(Gtk::Justification::CENTER)
1224 vbox = Gtk::VBox.new(false, 5)
1225 vbox.pack_start(evtbox, false, false)
1226 vbox.pack_start(frame2, false, false)
1227 autotable.append(vbox, filename)
1229 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1230 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1232 #- to be able to find widgets by name
1233 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1235 cleanup_all_thumbnails = proc {
1236 #- remove out of sync images
1237 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1238 for sizeobj in $images_size
1239 #- cannot use sizeobj because panoramic images will have a larger width
1240 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1248 cleanup_all_thumbnails.call
1249 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1251 rexml_thread_protect {
1252 $xmldir.delete_attribute('already-generated')
1254 my_gen_real_thumbnail.call
1257 rotate_and_cleanup = proc { |angle|
1258 cleanup_all_thumbnails.call
1259 rexml_thread_protect {
1260 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1264 move = proc { |direction|
1265 do_method = "move_#{direction}"
1266 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1268 done = autotable.method(do_method).call(vbox)
1269 textview.grab_focus #- because if moving, focus is stolen
1273 save_undo(_("move %s") % direction,
1275 autotable.method(undo_method).call(vbox)
1276 textview.grab_focus #- because if moving, focus is stolen
1277 autoscroll_if_needed($autotable_sw, img, textview)
1278 $notebook.set_page(1)
1280 autotable.method(do_method).call(vbox)
1281 textview.grab_focus #- because if moving, focus is stolen
1282 autoscroll_if_needed($autotable_sw, img, textview)
1283 $notebook.set_page(1)
1289 color_swap_and_cleanup = proc {
1290 perform_color_swap_and_cleanup = proc {
1291 cleanup_all_thumbnails.call
1292 rexml_thread_protect {
1293 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1295 my_gen_real_thumbnail.call
1298 perform_color_swap_and_cleanup.call
1300 save_undo(_("color swap"),
1302 perform_color_swap_and_cleanup.call
1304 autoscroll_if_needed($autotable_sw, img, textview)
1305 $notebook.set_page(1)
1307 perform_color_swap_and_cleanup.call
1309 autoscroll_if_needed($autotable_sw, img, textview)
1310 $notebook.set_page(1)
1315 change_seektime_and_cleanup_real = proc { |values|
1316 perform_change_seektime_and_cleanup = proc { |val|
1317 cleanup_all_thumbnails.call
1318 rexml_thread_protect {
1319 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1321 my_gen_real_thumbnail.call
1323 perform_change_seektime_and_cleanup.call(values[:new])
1325 save_undo(_("specify seektime"),
1327 perform_change_seektime_and_cleanup.call(values[:old])
1329 autoscroll_if_needed($autotable_sw, img, textview)
1330 $notebook.set_page(1)
1332 perform_change_seektime_and_cleanup.call(values[:new])
1334 autoscroll_if_needed($autotable_sw, img, textview)
1335 $notebook.set_page(1)
1340 change_seektime_and_cleanup = proc {
1341 rexml_thread_protect {
1342 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1343 change_seektime_and_cleanup_real.call(values)
1348 change_pano_amount_and_cleanup_real = proc { |values|
1349 perform_change_pano_amount_and_cleanup = proc { |val|
1350 cleanup_all_thumbnails.call
1351 rexml_thread_protect {
1352 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1355 perform_change_pano_amount_and_cleanup.call(values[:new])
1357 save_undo(_("change panorama amount"),
1359 perform_change_pano_amount_and_cleanup.call(values[:old])
1361 autoscroll_if_needed($autotable_sw, img, textview)
1362 $notebook.set_page(1)
1364 perform_change_pano_amount_and_cleanup.call(values[:new])
1366 autoscroll_if_needed($autotable_sw, img, textview)
1367 $notebook.set_page(1)
1372 change_pano_amount_and_cleanup = proc {
1373 rexml_thread_protect {
1374 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1375 change_pano_amount_and_cleanup_real.call(values)
1380 whitebalance_and_cleanup_real = proc { |values|
1381 perform_change_whitebalance_and_cleanup = proc { |val|
1382 cleanup_all_thumbnails.call
1383 rexml_thread_protect {
1384 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1385 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1386 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1389 perform_change_whitebalance_and_cleanup.call(values[:new])
1391 save_undo(_("fix white balance"),
1393 perform_change_whitebalance_and_cleanup.call(values[:old])
1395 autoscroll_if_needed($autotable_sw, img, textview)
1396 $notebook.set_page(1)
1398 perform_change_whitebalance_and_cleanup.call(values[:new])
1400 autoscroll_if_needed($autotable_sw, img, textview)
1401 $notebook.set_page(1)
1406 whitebalance_and_cleanup = proc {
1407 rexml_thread_protect {
1408 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1409 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1410 whitebalance_and_cleanup_real.call(values)
1415 gammacorrect_and_cleanup_real = proc { |values|
1416 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1417 cleanup_all_thumbnails.call
1418 rexml_thread_protect {
1419 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1420 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1421 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1424 perform_change_gammacorrect_and_cleanup.call(values[:new])
1426 save_undo(_("gamma correction"),
1428 perform_change_gammacorrect_and_cleanup.call(values[:old])
1430 autoscroll_if_needed($autotable_sw, img, textview)
1431 $notebook.set_page(1)
1433 perform_change_gammacorrect_and_cleanup.call(values[:new])
1435 autoscroll_if_needed($autotable_sw, img, textview)
1436 $notebook.set_page(1)
1441 gammacorrect_and_cleanup = Proc.new {
1442 rexml_thread_protect {
1443 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1444 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1445 gammacorrect_and_cleanup_real.call(values)
1450 enhance_and_cleanup = proc {
1451 perform_enhance_and_cleanup = proc {
1452 cleanup_all_thumbnails.call
1453 rexml_thread_protect {
1454 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1456 my_gen_real_thumbnail.call
1459 cleanup_all_thumbnails.call
1460 perform_enhance_and_cleanup.call
1462 save_undo(_("enhance"),
1464 perform_enhance_and_cleanup.call
1466 autoscroll_if_needed($autotable_sw, img, textview)
1467 $notebook.set_page(1)
1469 perform_enhance_and_cleanup.call
1471 autoscroll_if_needed($autotable_sw, img, textview)
1472 $notebook.set_page(1)
1477 delete = proc { |isacut|
1478 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1481 perform_delete = proc {
1482 after = autotable.get_next_widget(vbox)
1484 after = autotable.get_previous_widget(vbox)
1486 if $config['deleteondisk'] && !isacut
1487 msg 3, "scheduling for delete: #{fullpath}"
1488 $todelete << fullpath
1490 autotable.remove_widget(vbox)
1492 $vbox2widgets[after][:textview].grab_focus
1493 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1497 previous_pos = autotable.get_current_number(vbox)
1501 delete_current_subalbum
1503 save_undo(_("delete"),
1505 autotable.reinsert(pos, vbox, filename)
1506 $notebook.set_page(1)
1507 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1509 msg 3, "removing deletion schedule of: #{fullpath}"
1510 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1513 $notebook.set_page(1)
1522 $cuts << { :vbox => vbox, :filename => filename }
1523 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1528 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1531 autotable.queue_draws << proc {
1532 $vbox2widgets[last[:vbox]][:textview].grab_focus
1533 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1535 save_undo(_("paste"),
1537 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1538 $notebook.set_page(1)
1541 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1543 $notebook.set_page(1)
1546 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1551 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1552 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1553 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1554 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1556 textview.signal_connect('key-press-event') { |w, event|
1559 x, y = autotable.get_current_pos(vbox)
1560 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1561 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1562 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1563 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1565 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1566 $vbox2widgets[widget_up][:textview].grab_focus
1573 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1575 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1576 $vbox2widgets[widget_down][:textview].grab_focus
1583 if event.keyval == Gdk::Keyval::GDK_Left
1586 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1593 rotate_and_cleanup.call(-90)
1596 if event.keyval == Gdk::Keyval::GDK_Right
1597 next_ = autotable.get_next_widget(vbox)
1598 if next_ && autotable.get_current_pos(next_)[0] > x
1600 $vbox2widgets[next_][:textview].grab_focus
1607 rotate_and_cleanup.call(90)
1610 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1613 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1614 view_element(filename, { :delete => delete })
1617 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1620 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1624 !propagate #- propagate if needed
1627 $ignore_next_release = false
1628 evtbox.signal_connect('button-press-event') { |w, event|
1629 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1630 if event.state & Gdk::Window::BUTTON3_MASK != 0
1631 #- gesture redo: hold right mouse button then click left mouse button
1632 $config['nogestures'] or perform_redo
1633 $ignore_next_release = true
1635 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1637 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1639 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1640 elsif $enhance.active?
1641 enhance_and_cleanup.call
1642 elsif $delete.active?
1646 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1649 $button1_pressed_autotable = true
1650 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1651 if event.state & Gdk::Window::BUTTON1_MASK != 0
1652 #- gesture undo: hold left mouse button then click right mouse button
1653 $config['nogestures'] or perform_undo
1654 $ignore_next_release = true
1656 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1657 view_element(filename, { :delete => delete })
1662 evtbox.signal_connect('button-release-event') { |w, event|
1663 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1664 if !$ignore_next_release
1665 x, y = autotable.get_current_pos(vbox)
1666 next_ = autotable.get_next_widget(vbox)
1667 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1668 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1669 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1670 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1671 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1672 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1673 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1675 $ignore_next_release = false
1676 $gesture_press = nil
1681 #- handle reordering with drag and drop
1682 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1683 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1684 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1685 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1688 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1690 #- mouse gesture first (dnd disables button-release-event)
1691 if $gesture_press && $gesture_press[:filename] == filename
1692 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1693 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1694 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1695 rotate_and_cleanup.call(angle)
1696 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1698 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1699 msg 3, "gesture delete: click-drag right button to the bottom"
1701 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1706 ctxt.targets.each { |target|
1707 if target.name == 'reorder-elements'
1708 move_dnd = proc { |from,to|
1711 autotable.move(from, to)
1712 save_undo(_("reorder"),
1715 autotable.move(to - 1, from)
1717 autotable.move(to, from + 1)
1719 $notebook.set_page(1)
1721 autotable.move(from, to)
1722 $notebook.set_page(1)
1727 if $multiple_dnd.size == 0
1728 move_dnd.call(selection_data.data.to_i,
1729 autotable.get_current_number(vbox))
1731 UndoHandler.begin_batch
1732 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1734 #- need to update current position between each call
1735 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1736 autotable.get_current_number(vbox))
1738 UndoHandler.end_batch
1749 def create_auto_table
1751 $autotable = Gtk::AutoTable.new(5)
1753 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1754 thumbnails_vb = Gtk::VBox.new(false, 5)
1756 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1757 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1758 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1759 thumbnails_vb.add($autotable)
1761 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1762 $autotable_sw.add_with_viewport(thumbnails_vb)
1764 #- follows stuff for handling multiple elements selection
1765 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1767 update_selected = proc {
1768 $autotable.current_order.each { |path|
1769 w = $name2widgets[path][:evtbox].window
1770 xm = w.position[0] + w.size[0]/2
1771 ym = w.position[1] + w.size[1]/2
1772 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1773 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1774 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1775 if $name2widgets[path][:img].pixbuf
1776 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1780 if $selected_elements[path] && ! $selected_elements[path][:keep]
1781 if ((xm < press_x && xm < pos_x || xm > pos_x && xm > press_x) || (ym < press_y && ym < pos_y || ym > pos_y && ym > press_y))
1782 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1783 $selected_elements.delete(path)
1788 $autotable.signal_connect('realize') { |w,e|
1789 gc = Gdk::GC.new($autotable.window)
1790 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1791 gc.function = Gdk::GC::INVERT
1792 #- autoscroll handling for DND and multiple selections
1793 Gtk.timeout_add(100) {
1794 if ! $autotable.window.nil?
1795 w, x, y, mask = $autotable.window.pointer
1796 if mask & Gdk::Window::BUTTON1_MASK != 0
1797 if y < $autotable_sw.vadjustment.value
1799 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1801 if $button1_pressed_autotable || press_x
1802 scroll_upper($autotable_sw, y)
1805 w, pos_x, pos_y = $autotable.window.pointer
1806 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1807 update_selected.call
1810 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1812 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1814 if $button1_pressed_autotable || press_x
1815 scroll_lower($autotable_sw, y)
1818 w, pos_x, pos_y = $autotable.window.pointer
1819 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1820 update_selected.call
1825 ! $autotable.window.nil?
1829 $autotable.signal_connect('button-press-event') { |w,e|
1831 if !$button1_pressed_autotable
1834 if e.state & Gdk::Window::SHIFT_MASK == 0
1835 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1836 $selected_elements = {}
1837 $statusbar.push(0, utf8(_("Nothing selected.")))
1839 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1841 set_mousecursor(Gdk::Cursor::TCROSS)
1845 $autotable.signal_connect('button-release-event') { |w,e|
1847 if $button1_pressed_autotable
1848 #- unselect all only now
1849 $multiple_dnd = $selected_elements.keys
1850 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1851 $selected_elements = {}
1852 $button1_pressed_autotable = false
1855 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1856 if $selected_elements.length > 0
1857 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1860 press_x = press_y = pos_x = pos_y = nil
1861 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1865 $autotable.signal_connect('motion-notify-event') { |w,e|
1868 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1872 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1873 update_selected.call
1879 def create_subalbums_page
1881 subalbums_hb = Gtk::HBox.new
1882 $subalbums_vb = Gtk::VBox.new(false, 5)
1883 subalbums_hb.pack_start($subalbums_vb, false, false)
1884 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1885 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1886 $subalbums_sw.add_with_viewport(subalbums_hb)
1889 def save_current_file
1895 ios = File.open($filename, "w")
1898 rescue Iconv::IllegalSequence
1899 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1900 if ! ios.nil? && ! ios.closed?
1903 $xmldoc.xml_decl.encoding = 'UTF-8'
1904 ios = File.open($filename, "w")
1916 def save_current_file_user
1917 save_tempfilename = $filename
1918 $filename = $orig_filename
1919 if ! save_current_file
1920 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1921 $filename = save_tempfilename
1925 $generated_outofline = false
1926 $filename = save_tempfilename
1928 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1929 $todelete.each { |f|
1933 puts "Failed to delete #{f}: #{$!}"
1939 def mark_document_as_dirty
1940 $xmldoc.elements.each('//dir') { |elem|
1941 elem.delete_attribute('already-generated')
1945 #- ret: true => ok false => cancel
1946 def ask_save_modifications(msg1, msg2, *options)
1948 options = options.size > 0 ? options[0] : {}
1950 if options[:disallow_cancel]
1951 dialog = Gtk::Dialog.new(msg1,
1953 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1954 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1955 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1957 dialog = Gtk::Dialog.new(msg1,
1959 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1960 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1961 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1962 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1964 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1965 dialog.vbox.add(Gtk::Label.new(msg2))
1966 dialog.window_position = Gtk::Window::POS_CENTER
1969 dialog.run { |response|
1971 if response == Gtk::Dialog::RESPONSE_YES
1972 if ! save_current_file_user
1973 return ask_save_modifications(msg1, msg2, options)
1976 #- if we have generated an album but won't save modifications, we must remove
1977 #- already-generated markers in original file
1978 if $generated_outofline
1980 $xmldoc = REXML::Document.new(File.new($orig_filename))
1981 mark_document_as_dirty
1982 ios = File.open($orig_filename, "w")
1986 puts "exception: #{$!}"
1990 if response == Gtk::Dialog::RESPONSE_CANCEL
1998 def try_quit(*options)
1999 if ask_save_modifications(utf8(_("Save before quitting?")),
2000 utf8(_("Do you want to save your changes before quitting?")),
2006 def show_popup(parent, msg, *options)
2007 dialog = Gtk::Dialog.new
2008 if options[0] && options[0][:title]
2009 dialog.title = options[0][:title]
2011 dialog.title = utf8(_("Booh message"))
2013 lbl = Gtk::Label.new
2014 if options[0] && options[0][:nomarkup]
2019 if options[0] && options[0][:centered]
2020 lbl.set_justify(Gtk::Justification::CENTER)
2022 if options[0] && options[0][:selectable]
2023 lbl.selectable = true
2025 if options[0] && options[0][:scrolled]
2026 sw = Gtk::ScrolledWindow.new(nil, nil)
2027 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
2028 sw.add_with_viewport(lbl)
2030 dialog.set_default_size(500, 600)
2032 dialog.vbox.add(lbl)
2033 dialog.set_default_size(200, 120)
2035 if options[0] && options[0][:okcancel]
2036 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2038 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2040 if options[0] && options[0][:pos_centered]
2041 dialog.window_position = Gtk::Window::POS_CENTER
2043 dialog.window_position = Gtk::Window::POS_MOUSE
2046 if options[0] && options[0][:linkurl]
2047 linkbut = Gtk::Button.new('')
2048 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2049 linkbut.signal_connect('clicked') {
2050 open_url(options[0][:linkurl])
2051 dialog.response(Gtk::Dialog::RESPONSE_OK)
2052 set_mousecursor_normal
2054 linkbut.relief = Gtk::RELIEF_NONE
2055 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2056 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2057 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2062 if !options[0] || !options[0][:not_transient]
2063 dialog.transient_for = parent
2064 dialog.run { |response|
2066 if options[0] && options[0][:okcancel]
2067 return response == Gtk::Dialog::RESPONSE_OK
2071 dialog.signal_connect('response') { dialog.destroy }
2075 def set_mainwindow_title(progress)
2076 filename = $orig_filename || $filename
2079 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2081 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2085 $main_window.title = 'booh - ' + File.basename(filename)
2087 $main_window.title = 'booh'
2092 def backend_wait_message(parent, msg, infopipe_path, mode)
2094 w.set_transient_for(parent)
2097 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2098 vb.pack_start(Gtk::Label.new(msg), false, false)
2100 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2101 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2102 if mode != 'one dir scan'
2103 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2105 if mode == 'web-album'
2106 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2107 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2109 vb.pack_start(Gtk::HSeparator.new, false, false)
2111 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2112 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2113 vb.pack_end(bottom, false, false)
2116 update_progression_title_pb1 = proc {
2117 if mode == 'web-album'
2118 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2119 elsif mode != 'one dir scan'
2120 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2122 set_mainwindow_title(pb1_1.fraction)
2126 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2127 refresh_thread = Thread.new {
2128 directories_counter = 0
2129 while line = infopipe.gets
2130 msg 3, "infopipe got data: #{line}"
2131 if line =~ /^directories: (\d+), sizes: (\d+)/
2132 directories = $1.to_f + 1
2134 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2135 elements = $3.to_f + 1
2136 if mode == 'web-album'
2140 gtk_thread_protect { pb1_1.fraction = 0 }
2141 if mode != 'one dir scan'
2142 newtext = utf8(full_src_dir_to_rel($1, $2))
2143 newtext = '/' if newtext == ''
2144 gtk_thread_protect { pb1_2.text = newtext }
2145 directories_counter += 1
2146 gtk_thread_protect {
2147 pb1_2.fraction = directories_counter / directories
2148 update_progression_title_pb1.call
2151 elsif line =~ /^processing element$/
2152 element_counter += 1
2153 gtk_thread_protect {
2154 pb1_1.fraction = element_counter / elements
2155 update_progression_title_pb1.call
2157 elsif line =~ /^processing size$/
2158 element_counter += 1
2159 gtk_thread_protect {
2160 pb1_1.fraction = element_counter / elements
2161 update_progression_title_pb1.call
2163 elsif line =~ /^finished processing sizes$/
2164 gtk_thread_protect { pb1_1.fraction = 1 }
2165 elsif line =~ /^creating index.html$/
2166 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2167 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2168 directories_counter = 0
2169 elsif line =~ /^index.html: (.+)\|(.+)/
2170 newtext = utf8(full_src_dir_to_rel($1, $2))
2171 newtext = '/' if newtext == ''
2172 gtk_thread_protect { pb2.text = newtext }
2173 directories_counter += 1
2174 gtk_thread_protect {
2175 pb2.fraction = directories_counter / directories
2176 set_mainwindow_title(0.9 + pb2.fraction / 10)
2178 elsif line =~ /^die: (.*)$/
2185 w.signal_connect('delete-event') { w.destroy }
2186 w.signal_connect('destroy') {
2187 Thread.kill(refresh_thread)
2188 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2191 File.delete(infopipe_path)
2193 set_mainwindow_title(nil)
2195 w.window_position = Gtk::Window::POS_CENTER
2201 def call_backend(cmd, waitmsg, mode, params)
2202 pipe = Tempfile.new("boohpipe")
2205 system("mkfifo #{path}")
2206 cmd += " --info-pipe #{path}"
2207 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2212 id, exitstatus = Process.waitpid2(pid)
2213 gtk_thread_protect { w8.destroy }
2215 if params[:successmsg]
2216 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2218 if params[:closure_after]
2219 gtk_thread_protect(¶ms[:closure_after])
2221 elsif exitstatus == 15
2222 #- say nothing, user aborted
2224 gtk_thread_protect { show_popup($main_window,
2225 utf8($diemsg ? _("Unexpected internal error, sorry:\n\n%s") % $diemsg :
2226 _("Unexpected internal error, sorry.\nCheck console for error message."))) }
2233 button.signal_connect('clicked') {
2234 Process.kill('SIGTERM', pid)
2238 def save_changes(*forced)
2239 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2243 $xmldir.delete_attribute('already-generated')
2245 propagate_children = proc { |xmldir|
2246 if xmldir.attributes['subdirs-caption']
2247 xmldir.delete_attribute('already-generated')
2249 xmldir.elements.each('dir') { |element|
2250 propagate_children.call(element)
2254 if $xmldir.child_byname_notattr('dir', 'deleted')
2255 new_title = $subalbums_title.buffer.text
2256 if new_title != $xmldir.attributes['subdirs-caption']
2257 parent = $xmldir.parent
2258 if parent.name == 'dir'
2259 parent.delete_attribute('already-generated')
2261 propagate_children.call($xmldir)
2263 $xmldir.add_attribute('subdirs-caption', new_title)
2264 $xmldir.elements.each('dir') { |element|
2265 if !element.attributes['deleted']
2266 path = element.attributes['path']
2267 newtext = $subalbums_edits[path][:editzone].buffer.text
2268 if element.attributes['subdirs-caption']
2269 if element.attributes['subdirs-caption'] != newtext
2270 propagate_children.call(element)
2272 element.add_attribute('subdirs-caption', newtext)
2273 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2275 if element.attributes['thumbnails-caption'] != newtext
2276 element.delete_attribute('already-generated')
2278 element.add_attribute('thumbnails-caption', newtext)
2279 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2285 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2286 if $xmldir.attributes['thumbnails-caption']
2287 path = $xmldir.attributes['path']
2288 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2290 elsif $xmldir.attributes['thumbnails-caption']
2291 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2294 if $xmldir.attributes['thumbnails-caption']
2295 if edit = $subalbums_edits[$xmldir.attributes['path']]
2296 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2300 #- remove and reinsert elements to reflect new ordering
2303 $xmldir.elements.each { |element|
2304 if element.name == 'image' || element.name == 'video'
2305 saves[element.attributes['filename']] = element.remove
2309 $autotable.current_order.each { |path|
2310 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2311 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2314 saves.each_key { |path|
2315 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2316 chld.add_attribute('deleted', 'true')
2320 def sort_by_exif_date
2324 rexml_thread_protect {
2325 $xmldir.elements.each { |element|
2326 if element.name == 'image' || element.name == 'video'
2327 current_order << element.attributes['filename']
2332 #- look for EXIF dates
2335 if current_order.size > 20
2337 w.set_transient_for($main_window)
2339 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2340 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2341 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2342 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2343 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2344 vb.pack_end(bottom, false, false)
2346 w.signal_connect('delete-event') { w.destroy }
2347 w.window_position = Gtk::Window::POS_CENTER
2351 b.signal_connect('clicked') { aborted = true }
2353 current_order.each { |f|
2355 if entry2type(f) == 'image'
2357 pb.fraction = i.to_f / current_order.size
2358 Gtk.main_iteration while Gtk.events_pending?
2359 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2361 dates[f] = date_time
2362 elsif f =~ /(20\d{2}).?(\d{2}).?(\d{2}).(\d{2}).?(\d{2}).?(\d{2})/
2363 dates[f] = "#$1:#$2:#$3 #$4:#$5:#$6"
2376 current_order.each { |f|
2377 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2379 dates[f] = date_time
2380 elsif f =~ /(20\d{2}).?(\d{2}).?(\d{2}).(\d{2}).?(\d{2}).?(\d{2})/
2381 dates[f] = "#$1:#$2:#$3 #$4:#$5:#$6"
2387 rexml_thread_protect {
2388 $xmldir.elements.each { |element|
2389 if element.name == 'image' || element.name == 'video'
2390 saves[element.attributes['filename']] = element.remove
2395 neworder = smartsort(current_order, dates)
2397 rexml_thread_protect {
2399 $xmldir.add_element(saves[f].name, saves[f].attributes)
2403 #- let the auto-table reflect new ordering
2407 def remove_all_captions
2410 $autotable.current_order.each { |path|
2411 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2412 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2414 save_undo(_("remove all captions"),
2416 texts.each_key { |key|
2417 $name2widgets[key][:textview].buffer.text = texts[key]
2419 $notebook.set_page(1)
2421 texts.each_key { |key|
2422 $name2widgets[key][:textview].buffer.text = ''
2424 $notebook.set_page(1)
2430 $selected_elements.each_key { |path|
2431 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2437 $selected_elements = {}
2441 $undo_tb.sensitive = $undo_mb.sensitive = false
2442 $redo_tb.sensitive = $redo_mb.sensitive = false
2448 $subalbums_vb.children.each { |chld|
2449 $subalbums_vb.remove(chld)
2451 $subalbums = Gtk::Table.new(0, 0, true)
2452 current_y_sub_albums = 0
2454 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2455 $subalbums_edits = {}
2456 subalbums_counter = 0
2457 subalbums_edits_bypos = {}
2459 add_subalbum = proc { |xmldir, counter|
2460 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2461 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2462 if xmldir == $xmldir
2463 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2464 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2465 caption = xmldir.attributes['thumbnails-caption']
2466 infotype = 'thumbnails'
2468 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2469 captionfile, caption = find_subalbum_caption_info(xmldir)
2470 infotype = find_subalbum_info_type(xmldir)
2472 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2473 hbox = Gtk::HBox.new
2474 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2476 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2479 my_gen_real_thumbnail = proc {
2480 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2483 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2484 f.add(img = Gtk::Image.new)
2485 my_gen_real_thumbnail.call
2487 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2489 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2490 $subalbums.attach(hbox,
2491 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2493 frame, textview = create_editzone($subalbums_sw, 0, img)
2494 textview.buffer.text = caption
2495 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2496 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2498 change_image = proc {
2499 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2501 Gtk::FileChooser::ACTION_OPEN,
2503 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2504 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2505 fc.transient_for = $main_window
2506 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))
2507 f.add(preview_img = Gtk::Image.new)
2509 fc.signal_connect('update-preview') { |w|
2510 if fc.preview_filename
2511 if entry2type(fc.preview_filename) == 'video'
2515 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2517 fc.preview_widget_active = false
2519 tmpimage = "#{tmpdir}/00000001.jpg"
2521 preview_img.pixbuf = GdkPixbuf::Pixbuf.new(:file => tmpimage, :width => 240,
2523 fc.preview_widget_active = true
2524 rescue Gdk::PixbufError
2525 fc.preview_widget_active = false
2527 File.delete(tmpimage)
2534 preview_img.pixbuf = rotate_pixbuf(GdkPixbuf::Pixbuf.new(:file => fc.preview_filename, :width => 240, :height => 180), guess_rotate(fc.preview_filename))
2535 fc.preview_widget_active = true
2536 rescue Gdk::PixbufError
2537 fc.preview_widget_active = false
2542 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2544 old_file = captionfile
2545 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2546 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2547 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2548 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2550 new_file = fc.filename
2551 msg 3, "new captionfile is: #{fc.filename}"
2552 perform_changefile = proc {
2553 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2554 $modified_pixbufs.delete(thumbnail_file)
2555 xmldir.delete_attribute("#{infotype}-rotate")
2556 xmldir.delete_attribute("#{infotype}-color-swap")
2557 xmldir.delete_attribute("#{infotype}-enhance")
2558 xmldir.delete_attribute("#{infotype}-seektime")
2559 my_gen_real_thumbnail.call
2561 perform_changefile.call
2563 save_undo(_("change caption file for sub-album"),
2565 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2566 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2567 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2568 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2569 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2570 my_gen_real_thumbnail.call
2571 $notebook.set_page(0)
2573 perform_changefile.call
2574 $notebook.set_page(0)
2582 if File.exists?(thumbnail_file)
2583 File.delete(thumbnail_file)
2585 my_gen_real_thumbnail.call
2588 rotate_and_cleanup = proc { |angle|
2589 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2590 if File.exists?(thumbnail_file)
2591 File.delete(thumbnail_file)
2595 move = proc { |direction|
2598 save_changes('forced')
2599 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2600 if direction == 'up'
2601 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2602 subalbums_edits_bypos[oldpos - 1][:position] += 1
2604 if direction == 'down'
2605 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2606 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2608 if direction == 'top'
2609 for i in 1 .. oldpos - 1
2610 subalbums_edits_bypos[i][:position] += 1
2612 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2614 if direction == 'bottom'
2615 for i in oldpos + 1 .. subalbums_counter
2616 subalbums_edits_bypos[i][:position] -= 1
2618 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2622 $xmldir.elements.each('dir') { |element|
2623 if (!element.attributes['deleted'])
2624 elems << [ element.attributes['path'], element.remove ]
2627 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2628 each { |e| $xmldir.add_element(e[1]) }
2629 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2630 $xmldir.elements.each('descendant::dir') { |elem|
2631 elem.delete_attribute('already-generated')
2634 sel = $albums_tv.selection.selected_rows
2636 populate_subalbums_treeview(false)
2637 $albums_tv.selection.select_path(sel[0])
2640 color_swap_and_cleanup = proc {
2641 perform_color_swap_and_cleanup = proc {
2642 color_swap(xmldir, "#{infotype}-")
2643 my_gen_real_thumbnail.call
2645 perform_color_swap_and_cleanup.call
2647 save_undo(_("color swap"),
2649 perform_color_swap_and_cleanup.call
2650 $notebook.set_page(0)
2652 perform_color_swap_and_cleanup.call
2653 $notebook.set_page(0)
2658 change_seektime_and_cleanup = proc {
2659 if values = ask_new_seektime(xmldir, "#{infotype}-")
2660 perform_change_seektime_and_cleanup = proc { |val|
2661 change_seektime(xmldir, "#{infotype}-", val)
2662 my_gen_real_thumbnail.call
2664 perform_change_seektime_and_cleanup.call(values[:new])
2666 save_undo(_("specify seektime"),
2668 perform_change_seektime_and_cleanup.call(values[:old])
2669 $notebook.set_page(0)
2671 perform_change_seektime_and_cleanup.call(values[:new])
2672 $notebook.set_page(0)
2678 whitebalance_and_cleanup = proc {
2679 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2680 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2681 perform_change_whitebalance_and_cleanup = proc { |val|
2682 change_whitebalance(xmldir, "#{infotype}-", val)
2683 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2684 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2685 if File.exists?(thumbnail_file)
2686 File.delete(thumbnail_file)
2689 perform_change_whitebalance_and_cleanup.call(values[:new])
2691 save_undo(_("fix white balance"),
2693 perform_change_whitebalance_and_cleanup.call(values[:old])
2694 $notebook.set_page(0)
2696 perform_change_whitebalance_and_cleanup.call(values[:new])
2697 $notebook.set_page(0)
2703 gammacorrect_and_cleanup = proc {
2704 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2705 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2706 perform_change_gammacorrect_and_cleanup = proc { |val|
2707 change_gammacorrect(xmldir, "#{infotype}-", val)
2708 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2709 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2710 if File.exists?(thumbnail_file)
2711 File.delete(thumbnail_file)
2714 perform_change_gammacorrect_and_cleanup.call(values[:new])
2716 save_undo(_("gamma correction"),
2718 perform_change_gammacorrect_and_cleanup.call(values[:old])
2719 $notebook.set_page(0)
2721 perform_change_gammacorrect_and_cleanup.call(values[:new])
2722 $notebook.set_page(0)
2728 enhance_and_cleanup = proc {
2729 perform_enhance_and_cleanup = proc {
2730 enhance(xmldir, "#{infotype}-")
2731 my_gen_real_thumbnail.call
2734 perform_enhance_and_cleanup.call
2736 save_undo(_("enhance"),
2738 perform_enhance_and_cleanup.call
2739 $notebook.set_page(0)
2741 perform_enhance_and_cleanup.call
2742 $notebook.set_page(0)
2747 evtbox.signal_connect('button-press-event') { |w, event|
2748 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2750 rotate_and_cleanup.call(90)
2752 rotate_and_cleanup.call(-90)
2753 elsif $enhance.active?
2754 enhance_and_cleanup.call
2757 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2758 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2759 { :forbid_left => true, :forbid_right => true,
2760 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2761 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2762 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2763 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2764 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2766 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2771 evtbox.signal_connect('button-press-event') { |w, event|
2772 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2776 evtbox.signal_connect('button-release-event') { |w, event|
2777 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2778 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2779 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2780 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2781 msg 3, "gesture rotate: #{angle}"
2782 rotate_and_cleanup.call(angle)
2785 $gesture_press = nil
2788 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2789 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2790 current_y_sub_albums += 1
2793 if $xmldir.child_byname_notattr('dir', 'deleted')
2795 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2796 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2797 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2798 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2799 #- this album image/caption
2800 if $xmldir.attributes['thumbnails-caption']
2801 add_subalbum.call($xmldir, 0)
2804 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2805 $xmldir.elements.each { |element|
2806 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2807 #- element (image or video) of this album
2808 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2809 msg 3, "dest_img: #{dest_img}"
2810 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2811 total[element.name] += 1
2813 if element.name == 'dir' && !element.attributes['deleted']
2814 #- sub-album image/caption
2815 add_subalbum.call(element, subalbums_counter += 1)
2816 total[element.name] += 1
2819 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2820 total['image'], total['video'], total['dir'] ]))
2821 $subalbums_vb.add($subalbums)
2822 $subalbums_vb.show_all
2824 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2825 $notebook.get_tab_label($autotable_sw).sensitive = false
2826 $notebook.set_page(0)
2827 $thumbnails_title.buffer.text = ''
2829 $notebook.get_tab_label($autotable_sw).sensitive = true
2830 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2833 if !$xmldir.child_byname_notattr('dir', 'deleted')
2834 $notebook.get_tab_label($subalbums_sw).sensitive = false
2835 $notebook.set_page(1)
2837 $notebook.get_tab_label($subalbums_sw).sensitive = true
2841 def pixbuf_or_nil(filename)
2843 return GdkPixbuf::Pixbuf.new(:file => filename)
2849 def theme_choose(current)
2850 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2852 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2853 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2854 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2856 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2857 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2858 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2859 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2860 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2861 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2862 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2863 treeview.signal_connect('button-press-event') { |w, event|
2864 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2865 dialog.response(Gtk::Dialog::RESPONSE_OK)
2869 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2871 ([ $FPATH + '/themes/gradient' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.split("\n").find_all { |e| e !~ /\bgradient\b/ }.sort)).each { |dir|
2874 iter[0] = File.basename(dir)
2875 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2876 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2877 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2878 if File.basename(dir) == current
2879 treeview.selection.select_iter(iter)
2882 dialog.set_default_size(-1, 500)
2883 dialog.vbox.show_all
2885 dialog.run { |response|
2886 iter = treeview.selection.selected
2888 if response == Gtk::Dialog::RESPONSE_OK && iter
2889 return model.get_value(iter, 0)
2895 def show_password_protections
2896 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2897 child_iter = $albums_iters[xmldir.attributes['path']]
2898 if xmldir.attributes['password-protect']
2899 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2900 already_protected = true
2901 elsif already_protected
2902 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2904 pix = pix.saturate_and_pixelate(1, true)
2910 xmldir.elements.each('dir') { |elem|
2911 if !elem.attributes['deleted']
2912 examine_dir_elem.call(child_iter, elem, already_protected)
2916 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2919 def populate_subalbums_treeview(select_first)
2923 $subalbums_vb.children.each { |chld|
2924 $subalbums_vb.remove(chld)
2927 source = $xmldoc.root.attributes['source']
2928 msg 3, "source: #{source}"
2930 xmldir = $xmldoc.elements['//dir']
2931 if !xmldir || xmldir.attributes['path'] != source
2932 msg 1, _("Corrupted booh file...")
2936 append_dir_elem = proc { |parent_iter, xmldir|
2937 child_iter = $albums_ts.append(parent_iter)
2938 child_iter[0] = File.basename(xmldir.attributes['path'])
2939 child_iter[1] = xmldir.attributes['path']
2940 $albums_iters[xmldir.attributes['path']] = child_iter
2941 msg 3, "puttin location: #{xmldir.attributes['path']}"
2942 xmldir.elements.each('dir') { |elem|
2943 if !elem.attributes['deleted']
2944 append_dir_elem.call(child_iter, elem)
2948 append_dir_elem.call(nil, xmldir)
2949 show_password_protections
2951 $albums_tv.expand_all
2953 $albums_tv.selection.select_iter($albums_ts.iter_first)
2957 def select_current_theme
2958 select_theme($xmldoc.root.attributes['theme'],
2959 $xmldoc.root.attributes['limit-sizes'],
2960 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2961 $xmldoc.root.attributes['thumbnails-per-row'])
2964 def open_file(filename)
2968 $current_path = nil #- invalidate
2969 $modified_pixbufs = {}
2972 $subalbums_vb.children.each { |chld|
2973 $subalbums_vb.remove(chld)
2976 if !File.exists?(filename)
2977 return utf8(_("File not found."))
2981 $xmldoc = REXML::Document.new(File.new(filename))
2986 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2987 if entry2type(filename).nil?
2988 return utf8(_("Not a booh file!"))
2990 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."))
2994 if !source = $xmldoc.root.attributes['source']
2995 return utf8(_("Corrupted booh file..."))
2998 if !dest = $xmldoc.root.attributes['destination']
2999 return utf8(_("Corrupted booh file..."))
3002 if !theme = $xmldoc.root.attributes['theme']
3003 return utf8(_("Corrupted booh file..."))
3006 if $xmldoc.root.attributes['version'] < $VERSION
3007 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
3008 mark_document_as_dirty
3009 if $xmldoc.root.attributes['version'] < '0.8.4'
3010 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
3011 `find '#{source}' -type d -follow`.split("\n").sort.collect { |v| v.chomp }.each { |dir|
3012 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
3013 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
3014 if old_dest_dir != new_dest_dir
3015 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
3017 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
3018 xmldir.elements.each { |element|
3019 if %w(image video).include?(element.name) && !element.attributes['deleted']
3020 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3021 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3022 Dir[old_name + '*'].each { |file|
3023 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
3024 file != new_file and sys("mv '#{file}' '#{new_file}'")
3027 if element.name == 'dir' && !element.attributes['deleted']
3028 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3029 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3030 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
3034 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3038 $xmldoc.root.add_attribute('version', $VERSION)
3041 select_current_theme
3043 $filename = filename
3044 set_mainwindow_title(nil)
3045 $default_size['thumbnails'] =~ /(.*)x(.*)/
3046 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3047 $albums_thumbnail_size =~ /(.*)x(.*)/
3048 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3050 populate_subalbums_treeview(true)
3052 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $upload.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
3056 def open_file_user(filename)
3057 result = open_file(filename)
3059 $config['last-opens'] ||= []
3060 if $config['last-opens'][-1] != utf8(filename)
3061 $config['last-opens'] << utf8(filename)
3063 $orig_filename = $filename
3064 $main_window.title = 'booh - ' + File.basename($orig_filename)
3065 tmp = Tempfile.new("boohtemp")
3066 $filename = tmp.path
3069 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3071 $tempfiles << $filename << "#{$filename}.backup"
3073 $orig_filename = nil
3079 if !ask_save_modifications(utf8(_("Save this album?")),
3080 utf8(_("Do you want to save the changes to this album?")),
3081 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3084 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3086 Gtk::FileChooser::ACTION_OPEN,
3088 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3089 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3090 fc.set_current_folder(File.expand_path("~/.booh"))
3091 fc.transient_for = $main_window
3092 fc.preview_widget = previewlabel = Gtk::Label.new.show
3093 fc.signal_connect('update-preview') { |w|
3094 if fc.preview_filename
3096 push_mousecursor_wait(fc)
3097 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3101 xmldoc.elements.each('//*') { |elem|
3102 if elem.name == 'dir'
3104 elsif elem.name == 'image'
3106 elsif elem.name == 'video'
3114 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3115 fc.preview_widget_active = false
3117 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") %
3118 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3119 fc.preview_widget_active = true
3125 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3126 push_mousecursor_wait(fc)
3127 msg = open_file_user(fc.filename)
3142 def additional_booh_options
3145 options += "--mproc #{$config['mproc'].to_i} "
3147 options += "--comments-format '#{$config['comments-format']}' "
3148 if $config['transcode-videos']
3149 options += "--transcode-videos '#{$config['transcode-videos']}' "
3151 if $config['use-mp4'] == 'true'
3152 options += "--mp4-generator '#{$config['mp4-generator']}' "
3157 def ask_multi_languages(value)
3159 spl = value.split(',')
3160 value = [ spl[0..-2], spl[-1] ]
3163 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3166 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3167 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3169 lbl = Gtk::Label.new
3171 _("You can choose to activate <b>multi-languages</b> support for this web-album
3172 (it will work only if you publish your web-album on an Apache web-server). This will
3173 use the MultiViews feature of Apache; the pages will be served according to the
3174 value of the Accept-Language HTTP header sent by the web browsers, so that people
3175 with different languages preferences will be able to browse your web-album with
3176 navigation in their language (if language is available).
3179 dialog.vbox.add(lbl)
3180 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3181 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3182 add(languages = Gtk::Button.new))))
3184 pick_languages = proc {
3185 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3188 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3189 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3191 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3192 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3194 SUPPORTED_LANGUAGES.each { |lang|
3195 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3196 if ! value.nil? && value[0].include?(lang)
3202 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3203 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3204 fallback_language = nil
3205 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3206 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3207 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3208 fbl_rb.active = true
3209 fallback_language = SUPPORTED_LANGUAGES[0]
3211 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3212 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3213 rb.signal_connect('clicked') { fallback_language = lang }
3214 if ! value.nil? && value[1] == lang
3219 dialog2.window_position = Gtk::Window::POS_MOUSE
3223 dialog2.run { |response|
3225 if resp == Gtk::Dialog::RESPONSE_OK
3227 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3228 value[1] = fallback_language
3229 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3236 languages.signal_connect('clicked') {
3239 dialog.window_position = Gtk::Window::POS_MOUSE
3243 rb_yes.active = true
3244 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3246 rb_no.signal_connect('clicked') {
3250 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3263 dialog.run { |response|
3268 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3270 return [ true, nil ]
3272 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3281 if !ask_save_modifications(utf8(_("Save this album?")),
3282 utf8(_("Do you want to save the changes to this album?")),
3283 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3286 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3288 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3289 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3290 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3292 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3293 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3294 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3295 tbl.attach(src = Gtk::Entry.new,
3296 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3297 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3298 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3299 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3300 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3301 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3302 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3303 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3304 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3305 tbl.attach(dest = Gtk::Entry.new,
3306 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3307 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3308 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3309 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3310 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3311 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3312 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3313 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3314 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3316 tooltips = Gtk::Tooltips.new
3317 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3318 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3319 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'gradient'), false, false, 0))
3320 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3321 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3322 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3323 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)
3324 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3325 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3326 nperpage_model = Gtk::ListStore.new(String, String)
3327 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3328 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3329 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3330 nperpagecombo.set_attributes(crt, { :markup => 0 })
3331 iter = nperpage_model.append
3332 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3334 [ 12, 20, 30, 40, 50 ].each { |v|
3335 iter = nperpage_model.append
3336 iter[0] = iter[1] = v.to_s
3338 nperpagecombo.active = 0
3340 multilanguages_value = nil
3341 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3342 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3343 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)
3344 multilanguages.signal_connect('clicked') {
3345 retval = ask_multi_languages(multilanguages_value)
3347 multilanguages_value = retval[1]
3349 if multilanguages_value
3350 ml_label.text = utf8(_("Multi-languages: enabled."))
3352 ml_label.text = utf8(_("Multi-languages: disabled."))
3355 if $config['default-multi-languages']
3356 multilanguages_value = $config['default-multi-languages']
3357 ml_label.text = utf8(_("Multi-languages: enabled."))
3359 ml_label.text = utf8(_("Multi-languages: disabled."))
3362 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3363 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3364 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)
3365 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3366 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3367 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)
3368 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3369 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3370 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)
3372 src_nb_calculated_for = ''
3373 src_nb_process = nil
3374 process_src_nb = proc {
3375 if src.text != src_nb_calculated_for
3376 src_nb_calculated_for = src.text
3379 Process.kill(9, src_nb_process)
3381 #- process doesn't exist anymore - race condition
3384 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3385 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3387 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3388 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3391 while src_nb_process
3392 msg 3, "sleeping for completion of previous process"
3395 gtk_thread_flush #- flush to avoid race condition in src_nb markup update
3397 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3398 total = { 'image' => 0, 'video' => 0, nil => 0 }
3399 if src_nb_process = fork
3400 msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3404 rd.readlines.each { |dir|
3405 if File.basename(dir) =~ /^\./
3409 Dir.entries(dir.chomp).each { |file|
3410 total[entry2type(file)] += 1
3412 rescue Errno::EACCES, Errno::ENOENT
3417 msg 3, "ripping #{src_nb_process}"
3418 dummy, exitstatus = Process.waitpid2(src_nb_process)
3420 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3422 src_nb_process = nil
3428 wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3429 Process.exit!(0) #- _exit
3432 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3435 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3441 timeout_src_nb = Gtk.timeout_add(100) {
3445 src_browse.signal_connect('clicked') {
3446 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3448 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3450 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3451 fc.transient_for = $main_window
3452 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3453 src.text = utf8(fc.filename)
3455 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3460 dest_browse.signal_connect('clicked') {
3461 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3463 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3465 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3466 fc.transient_for = $main_window
3467 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3468 dest.text = utf8(fc.filename)
3473 conf_browse.signal_connect('clicked') {
3474 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3476 Gtk::FileChooser::ACTION_SAVE,
3478 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3479 fc.transient_for = $main_window
3480 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3481 fc.set_current_folder(File.expand_path("~/.booh"))
3482 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3483 conf.text = utf8(fc.filename)
3490 recreate_theme_config = proc {
3491 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3493 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3494 $images_size.each { |s|
3495 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3499 tooltips.set_tip(cb, utf8(s['description']), nil)
3500 theme_sizes << { :widget => cb, :value => s['name'] }
3502 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3503 tooltips = Gtk::Tooltips.new
3504 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3505 theme_sizes << { :widget => cb, :value => 'original' }
3508 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3511 $allowed_N_values.each { |n|