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