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-2011 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-2011 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?
150 $config['mproc'] = cpus
153 $config['rotate-set-exif'] ||= 'true'
158 def check_config_preferences_dep
159 viewer_binary = $config['video-viewer'].split.first
160 if viewer_binary && !File.executable?(viewer_binary)
161 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
162 You should fix this in Edit/Preferences so that you can view videos.
164 Problem was: '%s' is not an executable file.
165 Hint: don't forget to specify the full path to the executable,
166 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
169 flv_generator_binary = $config['use-flv'] == 'true' && $config['flv-generator'].split.first
170 if flv_generator_binary && !File.executable?(flv_generator_binary)
171 show_popup($main_window, utf8(_("The configured .flv generator seems to be unavailable.
172 You should fix this in Edit/Preferences so that you can have working
173 embedded flash videos.
175 Problem was: '%s' is not an executable file.
176 Hint: don't forget to specify the full path to the executable,
177 e.g. '/usr/bin/ffmpeg' is correct but 'ffmpeg' only is not.") % flv_generator_binary), { :pos_centered => true, :not_transient => true })
182 if !system("which convert >/dev/null 2>/dev/null")
183 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
184 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
187 if !system("which identify >/dev/null 2>/dev/null")
188 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
189 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
191 if !system("which exif >/dev/null 2>/dev/null")
192 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
194 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
196 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
199 check_config_preferences_dep
202 for line in IO.readlines('/proc/cpuinfo') do
203 line =~ /^processor/ and cpus += 1
206 if $config['cpus'] && count_cpus > $config['cpus'].to_i
207 show_popup($main_window, utf8(_("It seems you now have more CPUs available than last time booh was run.
208 You should probably increase the amount of CPUs configured in Edit/Preferences,
209 so that web-albums are generated as fast as possible on this computer.")), { :pos_centered => true, :not_transient => true })
211 $config['cpus'] = cpus
214 def check_image_editor
215 if last_failed_binary = check_multi_binaries($config['image-editor'])
216 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
217 You should fix this in Edit/Preferences so that you can edit photos externally.
219 Problem was: '%s' is not an executable file.
220 Hint: don't forget to specify the full path to the executable,
221 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
229 if $config['last-opens'] && $config['last-opens'].size > 10
230 $config['last-opens'] = $config['last-opens'][-10, 10]
233 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
234 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
235 $config.each_pair { |key, value|
236 elem = xmldoc.root.add_element key
238 $config[key].each_pair { |subkey, subvalue|
239 subelem = elem.add_element subkey
240 subelem.add_text subvalue.to_s
242 elsif value.is_a? Array
243 elem.add_text value.join('~~~')
248 elem.add_text value.to_s
252 ios = File.open($config_file, "w")
256 $tempfiles.each { |f|
263 def set_mousecursor(what, *widget)
264 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
265 if widget[0] && widget[0].window
266 widget[0].window.cursor = cursor
268 if $main_window && $main_window.window
269 $main_window.window.cursor = cursor
271 $current_cursor = what
273 def set_mousecursor_wait(*widget)
274 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
275 if Thread.current == Thread.main
276 Gtk.main_iteration while Gtk.events_pending?
279 def set_mousecursor_normal(*widget)
280 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
282 def push_mousecursor_wait(*widget)
283 if $current_cursor != Gdk::Cursor::WATCH
284 $save_cursor = $current_cursor
285 gtk_thread_protect { set_mousecursor_wait(*widget) }
288 def pop_mousecursor(*widget)
289 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
293 source = $xmldoc.root.attributes['source']
294 dest = $xmldoc.root.attributes['destination']
295 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
298 def full_src_dir_to_rel(path, source)
299 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
302 def build_full_dest_filename(filename)
303 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
306 def save_undo(name, closure, *params)
307 UndoHandler.save_undo(name, closure, [ *params ])
308 $undo_tb.sensitive = $undo_mb.sensitive = true
309 $redo_tb.sensitive = $redo_mb.sensitive = false
312 def view_element(filename, closures)
313 if entry2type(filename) == 'video'
314 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
320 w = create_window.set_title(filename)
322 msg 3, "filename: #{filename}"
323 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
324 #- typically this file won't exist in case of videos; try with the largest thumbnail around
325 if !File.exists?(dest_img)
326 if entry2type(filename) == 'video'
327 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
328 if not alternatives.empty?
329 dest_img = alternatives[-1]
332 push_mousecursor_wait
333 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
335 if !File.exists?(dest_img)
336 msg 2, _("Could not generate fullscreen thumbnail!")
341 aspect = utf8(_("Aspect: unknown"))
342 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
344 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
346 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
347 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)))
348 evt.signal_connect('button-press-event') { |this, event|
349 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
350 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
352 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
354 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
355 delete_item.signal_connect('activate') {
357 closures[:delete].call(false)
360 menu.popup(nil, nil, event.button, event.time)
363 evt.signal_connect('button-release-event') { |this, event|
365 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
366 msg 3, "gesture delete: click-drag right button to the bottom"
368 closures[:delete].call(false)
369 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
373 tooltips = Gtk::Tooltips.new
374 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
376 w.signal_connect('key-press-event') { |w,event|
377 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
379 closures[:delete].call(false)
383 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
384 b.signal_connect('clicked') { w.destroy }
387 vb.pack_start(evt, false, false)
388 vb.pack_end(bottom, false, false)
391 w.signal_connect('delete-event') { w.destroy }
392 w.window_position = Gtk::Window::POS_CENTER
396 def scroll_upper(scrolledwindow, ypos_top)
397 newval = scrolledwindow.vadjustment.value -
398 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
399 if newval < scrolledwindow.vadjustment.lower
400 newval = scrolledwindow.vadjustment.lower
402 scrolledwindow.vadjustment.value = newval
405 def scroll_lower(scrolledwindow, ypos_bottom)
406 newval = scrolledwindow.vadjustment.value +
407 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
408 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
409 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
411 scrolledwindow.vadjustment.value = newval
414 def autoscroll_if_needed(scrolledwindow, image, textview)
415 #- autoscroll if cursor or image is not visible, if possible
416 if image && image.window || textview.window
417 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
418 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
419 current_miny_visible = scrolledwindow.vadjustment.value
420 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
421 if ypos_top < current_miny_visible
422 scroll_upper(scrolledwindow, ypos_top)
423 elsif ypos_bottom > current_maxy_visible
424 scroll_lower(scrolledwindow, ypos_bottom)
429 def create_editzone(scrolledwindow, pagenum, image)
430 frame = Gtk::Frame.new
431 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
432 frame.set_shadow_type(Gtk::SHADOW_IN)
433 textview.signal_connect('key-press-event') { |w, event|
434 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
435 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
436 scrolledwindow.signal_emit('key-press-event', event)
438 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
439 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
440 if event.keyval == Gdk::Keyval::GDK_Up
441 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
442 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
444 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
447 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
448 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
450 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
457 candidate_undo_text = nil
458 textview.signal_connect('focus-in-event') { |w, event|
459 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
460 candidate_undo_text = textview.buffer.text
464 textview.signal_connect('key-release-event') { |w, event|
465 if candidate_undo_text && candidate_undo_text != textview.buffer.text
467 save_undo(_("text edit"),
469 save_text = textview.buffer.text
470 textview.buffer.text = text
472 $notebook.set_page(pagenum)
474 textview.buffer.text = save_text
476 $notebook.set_page(pagenum)
478 }, candidate_undo_text)
479 candidate_undo_text = nil
482 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)
483 autoscroll_if_needed(scrolledwindow, image, textview)
488 return [ frame, textview ]
491 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
493 if !$modified_pixbufs[thumbnail_img]
494 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
495 elsif !$modified_pixbufs[thumbnail_img][:orig]
496 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
499 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
502 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
503 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
504 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
505 if pixbuf.height > desired_y
506 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
507 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
508 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
513 if $modified_pixbufs[thumbnail_img][:whitebalance]
514 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
517 #- fix gamma correction
518 if $modified_pixbufs[thumbnail_img][:gammacorrect]
519 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
522 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
525 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
528 #- update rotate attribute
529 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
530 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
532 #- change exif orientation if configured so (but forget in case of thumbnails caption)
533 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
534 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
537 $modified_pixbufs[thumbnail_img] ||= {}
538 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
539 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
541 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
544 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
547 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
549 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
551 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
552 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
554 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
555 $notebook.set_page(0)
556 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
561 def color_swap(xmldir, attributes_prefix)
563 rexml_thread_protect {
564 if xmldir.attributes["#{attributes_prefix}color-swap"]
565 xmldir.delete_attribute("#{attributes_prefix}color-swap")
567 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
572 def enhance(xmldir, attributes_prefix)
574 rexml_thread_protect {
575 if xmldir.attributes["#{attributes_prefix}enhance"]
576 xmldir.delete_attribute("#{attributes_prefix}enhance")
578 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
583 def change_seektime(xmldir, attributes_prefix, value)
585 rexml_thread_protect {
586 xmldir.add_attribute("#{attributes_prefix}seektime", value)
590 def ask_new_seektime(xmldir, attributes_prefix)
591 value = rexml_thread_protect {
593 xmldir.attributes["#{attributes_prefix}seektime"]
599 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
601 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
602 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
603 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
607 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
611 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
612 entry.signal_connect('key-press-event') { |w, event|
613 if event.keyval == Gdk::Keyval::GDK_Return
614 dialog.response(Gtk::Dialog::RESPONSE_OK)
616 elsif event.keyval == Gdk::Keyval::GDK_Escape
617 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
620 false #- propagate if needed
624 dialog.window_position = Gtk::Window::POS_MOUSE
627 dialog.run { |response|
630 if response == Gtk::Dialog::RESPONSE_OK
632 msg 3, "changing seektime to #{newval}"
633 return { :old => value, :new => newval }
640 def change_pano_amount(xmldir, attributes_prefix, value)
642 rexml_thread_protect {
644 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
646 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
651 def ask_new_pano_amount(xmldir, attributes_prefix)
652 value = rexml_thread_protect {
654 xmldir.attributes["#{attributes_prefix}pano-amount"]
660 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
662 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
663 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
664 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
668 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
669 of this panorama image compared to other regular images. For example, if the panorama
670 was taken out of four photos on one row, counting the necessary overlap, the width of
671 this panorama image should probably be roughly three times the width of regular images.
673 With this information, booh will be able to generate panorama thumbnails looking
674 the right 'size', since the height of the thumbnail for this image will be similar
675 to the height of other thumbnails.
678 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)")))).
679 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
680 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
681 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
682 spin.signal_connect('value-changed') {
685 dialog.window_position = Gtk::Window::POS_MOUSE
688 spin.value = value.to_f
695 dialog.run { |response|
699 newval = spin.value.to_f
702 if response == Gtk::Dialog::RESPONSE_OK
704 msg 3, "changing panorama amount to #{newval}"
705 return { :old => value, :new => newval }
712 def change_whitebalance(xmlelem, attributes_prefix, value)
714 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
717 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
719 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
720 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
721 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
722 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
723 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
724 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
725 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
726 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
727 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
728 $modified_pixbufs[thumbnail_img] ||= {}
729 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
730 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
732 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
733 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
735 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
738 $modified_pixbufs[thumbnail_img] ||= {}
739 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
741 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
744 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
745 #- init $modified_pixbufs correctly
746 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
748 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
750 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
752 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
753 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
754 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
758 _("You can fix the <b>white balance</b> of the image, if your image is too blue
759 or too yellow because the recorder didn't detect the light correctly. Drag the
760 slider below the image to the left for more blue, to the right for more yellow.
764 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
766 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
768 dialog.window_position = Gtk::Window::POS_MOUSE
772 timeout = Gtk.timeout_add(100) {
773 if hs.value != lastval
776 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
782 dialog.run { |response|
783 Gtk.timeout_remove(timeout)
784 if response == Gtk::Dialog::RESPONSE_OK
786 newval = hs.value.to_s
787 msg 3, "changing white balance to #{newval}"
789 return { :old => value, :new => newval }
792 $modified_pixbufs[thumbnail_img] ||= {}
793 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
794 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
802 def change_gammacorrect(xmlelem, attributes_prefix, value)
804 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
807 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
809 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
810 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
811 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
812 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
813 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
814 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
815 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
816 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
817 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
818 $modified_pixbufs[thumbnail_img] ||= {}
819 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
820 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
822 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
823 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
825 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
828 $modified_pixbufs[thumbnail_img] ||= {}
829 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
831 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
834 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
835 #- init $modified_pixbufs correctly
836 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
838 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
840 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
842 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
843 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
844 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
848 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
849 or too bright. Drag the slider below the image.
853 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
855 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
857 dialog.window_position = Gtk::Window::POS_MOUSE
861 timeout = Gtk.timeout_add(100) {
862 if hs.value != lastval
865 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
871 dialog.run { |response|
872 Gtk.timeout_remove(timeout)
873 if response == Gtk::Dialog::RESPONSE_OK
875 newval = hs.value.to_s
876 msg 3, "gamma correction to #{newval}"
878 return { :old => value, :new => newval }
881 $modified_pixbufs[thumbnail_img] ||= {}
882 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
883 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
891 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
892 if File.exists?(destfile)
893 File.delete(destfile)
895 #- type can be 'element' or 'subdir'
897 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
899 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
903 $max_gen_thumbnail_threads = nil
904 $current_gen_thumbnail_threads = 0
905 $gen_thumbnail_monitor = Monitor.new
907 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
908 if $max_gen_thumbnail_threads.nil?
909 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
912 push_mousecursor_wait
913 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
916 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
921 $gen_thumbnail_monitor.synchronize {
922 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
923 $current_gen_thumbnail_threads += 1
928 msg 3, "generate thumbnail from new thread"
931 $gen_thumbnail_monitor.synchronize {
932 $current_gen_thumbnail_threads -= 1
936 msg 3, "generate thumbnail from current thread"
941 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
942 distribute_multiple_call = Proc.new { |action, arg|
943 $selected_elements.each_key { |path|
944 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
946 if possible_actions[:can_multiple] && $selected_elements.length > 0
947 UndoHandler.begin_batch
948 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
949 UndoHandler.end_batch
951 closures[action].call(arg)
953 $selected_elements = {}
956 if optionals.include?('change_image')
957 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
958 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
959 changeimg.signal_connect('activate') { closures[:change].call }
960 menu.append(Gtk::SeparatorMenuItem.new)
962 if !possible_actions[:can_multiple] || $selected_elements.length == 0
965 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
966 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
967 view.signal_connect('activate') { closures[:view].call }
969 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
970 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
971 view.signal_connect('activate') { closures[:view].call }
972 menu.append(Gtk::SeparatorMenuItem.new)
975 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
976 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
977 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
978 exif.signal_connect('activate') { show_popup($main_window,
979 utf8(`exif -m '#{fullpath}'`),
980 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
981 menu.append(Gtk::SeparatorMenuItem.new)
984 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
985 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
986 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
987 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
988 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
989 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
990 if !possible_actions[:can_multiple] || $selected_elements.length == 0
991 menu.append(Gtk::SeparatorMenuItem.new)
992 if !possible_actions[:forbid_left]
993 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
994 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
995 moveleft.signal_connect('activate') { closures[:move].call('left') }
996 if !possible_actions[:can_left]
997 moveleft.sensitive = false
1000 if !possible_actions[:forbid_right]
1001 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
1002 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
1003 moveright.signal_connect('activate') { closures[:move].call('right') }
1004 if !possible_actions[:can_right]
1005 moveright.sensitive = false
1008 if optionals.include?('move_top')
1009 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
1010 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
1011 movetop.signal_connect('activate') { closures[:move].call('top') }
1012 if !possible_actions[:can_top]
1013 movetop.sensitive = false
1016 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
1017 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
1018 moveup.signal_connect('activate') { closures[:move].call('up') }
1019 if !possible_actions[:can_up]
1020 moveup.sensitive = false
1022 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
1023 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
1024 movedown.signal_connect('activate') { closures[:move].call('down') }
1025 if !possible_actions[:can_down]
1026 movedown.sensitive = false
1028 if optionals.include?('move_bottom')
1029 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
1030 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
1031 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
1032 if !possible_actions[:can_bottom]
1033 movebottom.sensitive = false
1038 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
1039 menu.append(Gtk::SeparatorMenuItem.new)
1040 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
1041 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
1042 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
1043 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1044 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1045 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1046 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1047 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1048 seektime.signal_connect('activate') {
1049 if possible_actions[:can_multiple] && $selected_elements.length > 0
1050 if values = ask_new_seektime(nil, '')
1051 distribute_multiple_call.call(:seektime, values)
1054 closures[:seektime].call
1059 menu.append( Gtk::SeparatorMenuItem.new)
1060 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1061 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1062 gammacorrect.signal_connect('activate') {
1063 if possible_actions[:can_multiple] && $selected_elements.length > 0
1064 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1065 distribute_multiple_call.call(:gammacorrect, values)
1068 closures[:gammacorrect].call
1071 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1072 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1073 whitebalance.signal_connect('activate') {
1074 if possible_actions[:can_multiple] && $selected_elements.length > 0
1075 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1076 distribute_multiple_call.call(:whitebalance, values)
1079 closures[:whitebalance].call
1082 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1083 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1084 _("Enhance constrast"))))
1086 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1088 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1089 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1090 if type == 'image' && possible_actions[:can_panorama]
1091 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1092 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1093 panorama.signal_connect('activate') {
1094 if possible_actions[:can_multiple] && $selected_elements.length > 0
1095 if values = ask_new_pano_amount(nil, '')
1096 distribute_multiple_call.call(:pano, values)
1099 distribute_multiple_call.call(:pano)
1103 menu.append( Gtk::SeparatorMenuItem.new)
1104 if optionals.include?('delete')
1105 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1106 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1107 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1108 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1109 paste_item.signal_connect('activate') { closures[:paste].call }
1110 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1111 clear_item.signal_connect('activate') { $cuts = [] }
1113 paste_item.sensitive = clear_item.sensitive = false
1116 menu.append( Gtk::SeparatorMenuItem.new)
1118 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1119 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1120 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1121 editexternally.signal_connect('activate') {
1122 if check_image_editor
1123 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1129 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1130 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1131 if optionals.include?('delete')
1132 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1133 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1136 menu.popup(nil, nil, event.button, event.time)
1139 def delete_current_subalbum
1141 sel = $albums_tv.selection.selected_rows
1142 $xmldir.elements.each { |e|
1143 if e.name == 'image' || e.name == 'video'
1144 e.add_attribute('deleted', 'true')
1147 #- branch if we have a non deleted subalbum
1148 if $xmldir.child_byname_notattr('dir', 'deleted')
1149 $xmldir.delete_attribute('thumbnails-caption')
1150 $xmldir.delete_attribute('thumbnails-captionfile')
1152 $xmldir.add_attribute('deleted', 'true')
1154 while moveup.parent.name == 'dir'
1155 moveup = moveup.parent
1156 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1157 moveup.add_attribute('deleted', 'true')
1164 save_changes('forced')
1165 populate_subalbums_treeview(false)
1166 $albums_tv.selection.select_path(sel[0])
1172 $current_path = nil #- prevent save_changes from being rerun again
1173 sel = $albums_tv.selection.selected_rows
1174 restore_one = proc { |xmldir|
1175 xmldir.elements.each { |e|
1176 if e.name == 'dir' && e.attributes['deleted']
1179 e.delete_attribute('deleted')
1182 restore_one.call($xmldir)
1183 populate_subalbums_treeview(false)
1184 $albums_tv.selection.select_path(sel[0])
1187 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1190 frame1 = Gtk::Frame.new
1191 fullpath = from_utf8("#{$current_path}/#{filename}")
1193 my_gen_real_thumbnail = proc {
1194 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1198 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1199 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1200 pack_start(img = Gtk::Image.new).
1201 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1202 px, mask = pxb.render_pixmap_and_mask(0)
1203 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1204 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1206 frame1.add(img = Gtk::Image.new)
1209 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1210 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1211 my_gen_real_thumbnail.call
1213 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1216 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1218 tooltips = Gtk::Tooltips.new
1219 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1220 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1222 frame2, textview = create_editzone($autotable_sw, 1, img)
1223 textview.buffer.text = caption
1224 textview.set_justification(Gtk::Justification::CENTER)
1226 vbox = Gtk::VBox.new(false, 5)
1227 vbox.pack_start(evtbox, false, false)
1228 vbox.pack_start(frame2, false, false)
1229 autotable.append(vbox, filename)
1231 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1232 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1234 #- to be able to find widgets by name
1235 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1237 cleanup_all_thumbnails = proc {
1238 #- remove out of sync images
1239 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1240 for sizeobj in $images_size
1241 #- cannot use sizeobj because panoramic images will have a larger width
1242 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1250 cleanup_all_thumbnails.call
1251 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1253 rexml_thread_protect {
1254 $xmldir.delete_attribute('already-generated')
1256 my_gen_real_thumbnail.call
1259 rotate_and_cleanup = proc { |angle|
1260 cleanup_all_thumbnails.call
1261 rexml_thread_protect {
1262 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1266 move = proc { |direction|
1267 do_method = "move_#{direction}"
1268 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1270 done = autotable.method(do_method).call(vbox)
1271 textview.grab_focus #- because if moving, focus is stolen
1275 save_undo(_("move %s") % direction,
1277 autotable.method(undo_method).call(vbox)
1278 textview.grab_focus #- because if moving, focus is stolen
1279 autoscroll_if_needed($autotable_sw, img, textview)
1280 $notebook.set_page(1)
1282 autotable.method(do_method).call(vbox)
1283 textview.grab_focus #- because if moving, focus is stolen
1284 autoscroll_if_needed($autotable_sw, img, textview)
1285 $notebook.set_page(1)
1291 color_swap_and_cleanup = proc {
1292 perform_color_swap_and_cleanup = proc {
1293 cleanup_all_thumbnails.call
1294 rexml_thread_protect {
1295 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1297 my_gen_real_thumbnail.call
1300 perform_color_swap_and_cleanup.call
1302 save_undo(_("color swap"),
1304 perform_color_swap_and_cleanup.call
1306 autoscroll_if_needed($autotable_sw, img, textview)
1307 $notebook.set_page(1)
1309 perform_color_swap_and_cleanup.call
1311 autoscroll_if_needed($autotable_sw, img, textview)
1312 $notebook.set_page(1)
1317 change_seektime_and_cleanup_real = proc { |values|
1318 perform_change_seektime_and_cleanup = proc { |val|
1319 cleanup_all_thumbnails.call
1320 rexml_thread_protect {
1321 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1323 my_gen_real_thumbnail.call
1325 perform_change_seektime_and_cleanup.call(values[:new])
1327 save_undo(_("specify seektime"),
1329 perform_change_seektime_and_cleanup.call(values[:old])
1331 autoscroll_if_needed($autotable_sw, img, textview)
1332 $notebook.set_page(1)
1334 perform_change_seektime_and_cleanup.call(values[:new])
1336 autoscroll_if_needed($autotable_sw, img, textview)
1337 $notebook.set_page(1)
1342 change_seektime_and_cleanup = proc {
1343 rexml_thread_protect {
1344 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1345 change_seektime_and_cleanup_real.call(values)
1350 change_pano_amount_and_cleanup_real = proc { |values|
1351 perform_change_pano_amount_and_cleanup = proc { |val|
1352 cleanup_all_thumbnails.call
1353 rexml_thread_protect {
1354 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1357 perform_change_pano_amount_and_cleanup.call(values[:new])
1359 save_undo(_("change panorama amount"),
1361 perform_change_pano_amount_and_cleanup.call(values[:old])
1363 autoscroll_if_needed($autotable_sw, img, textview)
1364 $notebook.set_page(1)
1366 perform_change_pano_amount_and_cleanup.call(values[:new])
1368 autoscroll_if_needed($autotable_sw, img, textview)
1369 $notebook.set_page(1)
1374 change_pano_amount_and_cleanup = proc {
1375 rexml_thread_protect {
1376 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1377 change_pano_amount_and_cleanup_real.call(values)
1382 whitebalance_and_cleanup_real = proc { |values|
1383 perform_change_whitebalance_and_cleanup = proc { |val|
1384 cleanup_all_thumbnails.call
1385 rexml_thread_protect {
1386 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1387 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1388 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1391 perform_change_whitebalance_and_cleanup.call(values[:new])
1393 save_undo(_("fix white balance"),
1395 perform_change_whitebalance_and_cleanup.call(values[:old])
1397 autoscroll_if_needed($autotable_sw, img, textview)
1398 $notebook.set_page(1)
1400 perform_change_whitebalance_and_cleanup.call(values[:new])
1402 autoscroll_if_needed($autotable_sw, img, textview)
1403 $notebook.set_page(1)
1408 whitebalance_and_cleanup = proc {
1409 rexml_thread_protect {
1410 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1411 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1412 whitebalance_and_cleanup_real.call(values)
1417 gammacorrect_and_cleanup_real = proc { |values|
1418 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1419 cleanup_all_thumbnails.call
1420 rexml_thread_protect {
1421 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1422 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1423 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1426 perform_change_gammacorrect_and_cleanup.call(values[:new])
1428 save_undo(_("gamma correction"),
1430 perform_change_gammacorrect_and_cleanup.call(values[:old])
1432 autoscroll_if_needed($autotable_sw, img, textview)
1433 $notebook.set_page(1)
1435 perform_change_gammacorrect_and_cleanup.call(values[:new])
1437 autoscroll_if_needed($autotable_sw, img, textview)
1438 $notebook.set_page(1)
1443 gammacorrect_and_cleanup = Proc.new {
1444 rexml_thread_protect {
1445 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1446 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1447 gammacorrect_and_cleanup_real.call(values)
1452 enhance_and_cleanup = proc {
1453 perform_enhance_and_cleanup = proc {
1454 cleanup_all_thumbnails.call
1455 rexml_thread_protect {
1456 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1458 my_gen_real_thumbnail.call
1461 cleanup_all_thumbnails.call
1462 perform_enhance_and_cleanup.call
1464 save_undo(_("enhance"),
1466 perform_enhance_and_cleanup.call
1468 autoscroll_if_needed($autotable_sw, img, textview)
1469 $notebook.set_page(1)
1471 perform_enhance_and_cleanup.call
1473 autoscroll_if_needed($autotable_sw, img, textview)
1474 $notebook.set_page(1)
1479 delete = proc { |isacut|
1480 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 })
1483 perform_delete = proc {
1484 after = autotable.get_next_widget(vbox)
1486 after = autotable.get_previous_widget(vbox)
1488 if $config['deleteondisk'] && !isacut
1489 msg 3, "scheduling for delete: #{fullpath}"
1490 $todelete << fullpath
1492 autotable.remove_widget(vbox)
1494 $vbox2widgets[after][:textview].grab_focus
1495 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1499 previous_pos = autotable.get_current_number(vbox)
1503 delete_current_subalbum
1505 save_undo(_("delete"),
1507 autotable.reinsert(pos, vbox, filename)
1508 $notebook.set_page(1)
1509 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1511 msg 3, "removing deletion schedule of: #{fullpath}"
1512 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1515 $notebook.set_page(1)
1524 $cuts << { :vbox => vbox, :filename => filename }
1525 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1530 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1533 autotable.queue_draws << proc {
1534 $vbox2widgets[last[:vbox]][:textview].grab_focus
1535 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1537 save_undo(_("paste"),
1539 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1540 $notebook.set_page(1)
1543 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1545 $notebook.set_page(1)
1548 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1553 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1554 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1555 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1556 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1558 textview.signal_connect('key-press-event') { |w, event|
1561 x, y = autotable.get_current_pos(vbox)
1562 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1563 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1564 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1565 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1567 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1568 $vbox2widgets[widget_up][:textview].grab_focus
1575 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1577 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1578 $vbox2widgets[widget_down][:textview].grab_focus
1585 if event.keyval == Gdk::Keyval::GDK_Left
1588 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1595 rotate_and_cleanup.call(-90)
1598 if event.keyval == Gdk::Keyval::GDK_Right
1599 next_ = autotable.get_next_widget(vbox)
1600 if next_ && autotable.get_current_pos(next_)[0] > x
1602 $vbox2widgets[next_][:textview].grab_focus
1609 rotate_and_cleanup.call(90)
1612 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1615 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1616 view_element(filename, { :delete => delete })
1619 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1622 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1626 !propagate #- propagate if needed
1629 $ignore_next_release = false
1630 evtbox.signal_connect('button-press-event') { |w, event|
1631 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1632 if event.state & Gdk::Window::BUTTON3_MASK != 0
1633 #- gesture redo: hold right mouse button then click left mouse button
1634 $config['nogestures'] or perform_redo
1635 $ignore_next_release = true
1637 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1639 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1641 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1642 elsif $enhance.active?
1643 enhance_and_cleanup.call
1644 elsif $delete.active?
1648 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1651 $button1_pressed_autotable = true
1652 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1653 if event.state & Gdk::Window::BUTTON1_MASK != 0
1654 #- gesture undo: hold left mouse button then click right mouse button
1655 $config['nogestures'] or perform_undo
1656 $ignore_next_release = true
1658 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1659 view_element(filename, { :delete => delete })
1664 evtbox.signal_connect('button-release-event') { |w, event|
1665 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1666 if !$ignore_next_release
1667 x, y = autotable.get_current_pos(vbox)
1668 next_ = autotable.get_next_widget(vbox)
1669 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1670 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1671 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1672 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1673 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1674 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1675 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1677 $ignore_next_release = false
1678 $gesture_press = nil
1683 #- handle reordering with drag and drop
1684 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1685 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1686 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1687 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1690 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1692 #- mouse gesture first (dnd disables button-release-event)
1693 if $gesture_press && $gesture_press[:filename] == filename
1694 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1695 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1696 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1697 rotate_and_cleanup.call(angle)
1698 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1700 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1701 msg 3, "gesture delete: click-drag right button to the bottom"
1703 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1708 ctxt.targets.each { |target|
1709 if target.name == 'reorder-elements'
1710 move_dnd = proc { |from,to|
1713 autotable.move(from, to)
1714 save_undo(_("reorder"),
1717 autotable.move(to - 1, from)
1719 autotable.move(to, from + 1)
1721 $notebook.set_page(1)
1723 autotable.move(from, to)
1724 $notebook.set_page(1)
1729 if $multiple_dnd.size == 0
1730 move_dnd.call(selection_data.data.to_i,
1731 autotable.get_current_number(vbox))
1733 UndoHandler.begin_batch
1734 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1736 #- need to update current position between each call
1737 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1738 autotable.get_current_number(vbox))
1740 UndoHandler.end_batch
1751 def create_auto_table
1753 $autotable = Gtk::AutoTable.new(5)
1755 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1756 thumbnails_vb = Gtk::VBox.new(false, 5)
1758 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1759 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1760 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1761 thumbnails_vb.add($autotable)
1763 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1764 $autotable_sw.add_with_viewport(thumbnails_vb)
1766 #- follows stuff for handling multiple elements selection
1767 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1769 update_selected = proc {
1770 $autotable.current_order.each { |path|
1771 w = $name2widgets[path][:evtbox].window
1772 xm = w.position[0] + w.size[0]/2
1773 ym = w.position[1] + w.size[1]/2
1774 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1775 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1776 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1777 if $name2widgets[path][:img].pixbuf
1778 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1782 if $selected_elements[path] && ! $selected_elements[path][:keep]
1783 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))
1784 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1785 $selected_elements.delete(path)
1790 $autotable.signal_connect('realize') { |w,e|
1791 gc = Gdk::GC.new($autotable.window)
1792 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1793 gc.function = Gdk::GC::INVERT
1794 #- autoscroll handling for DND and multiple selections
1795 Gtk.timeout_add(100) {
1796 if ! $autotable.window.nil?
1797 w, x, y, mask = $autotable.window.pointer
1798 if mask & Gdk::Window::BUTTON1_MASK != 0
1799 if y < $autotable_sw.vadjustment.value
1801 $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]])
1803 if $button1_pressed_autotable || press_x
1804 scroll_upper($autotable_sw, y)
1807 w, pos_x, pos_y = $autotable.window.pointer
1808 $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]])
1809 update_selected.call
1812 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1814 $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]])
1816 if $button1_pressed_autotable || press_x
1817 scroll_lower($autotable_sw, y)
1820 w, pos_x, pos_y = $autotable.window.pointer
1821 $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]])
1822 update_selected.call
1827 ! $autotable.window.nil?
1831 $autotable.signal_connect('button-press-event') { |w,e|
1833 if !$button1_pressed_autotable
1836 if e.state & Gdk::Window::SHIFT_MASK == 0
1837 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1838 $selected_elements = {}
1839 $statusbar.push(0, utf8(_("Nothing selected.")))
1841 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1843 set_mousecursor(Gdk::Cursor::TCROSS)
1847 $autotable.signal_connect('button-release-event') { |w,e|
1849 if $button1_pressed_autotable
1850 #- unselect all only now
1851 $multiple_dnd = $selected_elements.keys
1852 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1853 $selected_elements = {}
1854 $button1_pressed_autotable = false
1857 $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]])
1858 if $selected_elements.length > 0
1859 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1862 press_x = press_y = pos_x = pos_y = nil
1863 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1867 $autotable.signal_connect('motion-notify-event') { |w,e|
1870 $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]])
1874 $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]])
1875 update_selected.call
1881 def create_subalbums_page
1883 subalbums_hb = Gtk::HBox.new
1884 $subalbums_vb = Gtk::VBox.new(false, 5)
1885 subalbums_hb.pack_start($subalbums_vb, false, false)
1886 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1887 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1888 $subalbums_sw.add_with_viewport(subalbums_hb)
1891 def save_current_file
1897 ios = File.open($filename, "w")
1900 rescue Iconv::IllegalSequence
1901 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1902 if ! ios.nil? && ! ios.closed?
1905 $xmldoc.xml_decl.encoding = 'UTF-8'
1906 ios = File.open($filename, "w")
1918 def save_current_file_user
1919 save_tempfilename = $filename
1920 $filename = $orig_filename
1921 if ! save_current_file
1922 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1923 $filename = save_tempfilename
1927 $generated_outofline = false
1928 $filename = save_tempfilename
1930 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1931 $todelete.each { |f|
1935 puts "Failed to delete #{f}: #{$!}"
1941 def mark_document_as_dirty
1942 $xmldoc.elements.each('//dir') { |elem|
1943 elem.delete_attribute('already-generated')
1947 #- ret: true => ok false => cancel
1948 def ask_save_modifications(msg1, msg2, *options)
1950 options = options.size > 0 ? options[0] : {}
1952 if options[:disallow_cancel]
1953 dialog = Gtk::Dialog.new(msg1,
1955 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1956 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1957 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1959 dialog = Gtk::Dialog.new(msg1,
1961 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1962 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1963 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1964 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1966 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1967 dialog.vbox.add(Gtk::Label.new(msg2))
1968 dialog.window_position = Gtk::Window::POS_CENTER
1971 dialog.run { |response|
1973 if response == Gtk::Dialog::RESPONSE_YES
1974 if ! save_current_file_user
1975 return ask_save_modifications(msg1, msg2, options)
1978 #- if we have generated an album but won't save modifications, we must remove
1979 #- already-generated markers in original file
1980 if $generated_outofline
1982 $xmldoc = REXML::Document.new(File.new($orig_filename))
1983 mark_document_as_dirty
1984 ios = File.open($orig_filename, "w")
1988 puts "exception: #{$!}"
1992 if response == Gtk::Dialog::RESPONSE_CANCEL
2000 def try_quit(*options)
2001 if ask_save_modifications(utf8(_("Save before quitting?")),
2002 utf8(_("Do you want to save your changes before quitting?")),
2008 def show_popup(parent, msg, *options)
2009 dialog = Gtk::Dialog.new
2010 if options[0] && options[0][:title]
2011 dialog.title = options[0][:title]
2013 dialog.title = utf8(_("Booh message"))
2015 lbl = Gtk::Label.new
2016 if options[0] && options[0][:nomarkup]
2021 if options[0] && options[0][:centered]
2022 lbl.set_justify(Gtk::Justification::CENTER)
2024 if options[0] && options[0][:selectable]
2025 lbl.selectable = true
2027 if options[0] && options[0][:scrolled]
2028 sw = Gtk::ScrolledWindow.new(nil, nil)
2029 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
2030 sw.add_with_viewport(lbl)
2032 dialog.set_default_size(500, 600)
2034 dialog.vbox.add(lbl)
2035 dialog.set_default_size(200, 120)
2037 if options[0] && options[0][:okcancel]
2038 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2040 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2042 if options[0] && options[0][:pos_centered]
2043 dialog.window_position = Gtk::Window::POS_CENTER
2045 dialog.window_position = Gtk::Window::POS_MOUSE
2048 if options[0] && options[0][:linkurl]
2049 linkbut = Gtk::Button.new('')
2050 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2051 linkbut.signal_connect('clicked') {
2052 open_url(options[0][:linkurl])
2053 dialog.response(Gtk::Dialog::RESPONSE_OK)
2054 set_mousecursor_normal
2056 linkbut.relief = Gtk::RELIEF_NONE
2057 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2058 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2059 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2064 if !options[0] || !options[0][:not_transient]
2065 dialog.transient_for = parent
2066 dialog.run { |response|
2068 if options[0] && options[0][:okcancel]
2069 return response == Gtk::Dialog::RESPONSE_OK
2073 dialog.signal_connect('response') { dialog.destroy }
2077 def set_mainwindow_title(progress)
2078 filename = $orig_filename || $filename
2081 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2083 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2087 $main_window.title = 'booh - ' + File.basename(filename)
2089 $main_window.title = 'booh'
2094 def backend_wait_message(parent, msg, infopipe_path, mode)
2096 w.set_transient_for(parent)
2099 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2100 vb.pack_start(Gtk::Label.new(msg), false, false)
2102 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2103 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2104 if mode != 'one dir scan'
2105 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2107 if mode == 'web-album'
2108 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2109 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2111 vb.pack_start(Gtk::HSeparator.new, false, false)
2113 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2114 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2115 vb.pack_end(bottom, false, false)
2118 update_progression_title_pb1 = proc {
2119 if mode == 'web-album'
2120 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2121 elsif mode != 'one dir scan'
2122 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2124 set_mainwindow_title(pb1_1.fraction)
2128 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2129 refresh_thread = Thread.new {
2130 directories_counter = 0
2131 while line = infopipe.gets
2132 msg 3, "infopipe got data: #{line}"
2133 if line =~ /^directories: (\d+), sizes: (\d+)/
2134 directories = $1.to_f + 1
2136 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2137 elements = $3.to_f + 1
2138 if mode == 'web-album'
2142 gtk_thread_protect { pb1_1.fraction = 0 }
2143 if mode != 'one dir scan'
2144 newtext = utf8(full_src_dir_to_rel($1, $2))
2145 newtext = '/' if newtext == ''
2146 gtk_thread_protect { pb1_2.text = newtext }
2147 directories_counter += 1
2148 gtk_thread_protect {
2149 pb1_2.fraction = directories_counter / directories
2150 update_progression_title_pb1.call
2153 elsif line =~ /^processing element$/
2154 element_counter += 1
2155 gtk_thread_protect {
2156 pb1_1.fraction = element_counter / elements
2157 update_progression_title_pb1.call
2159 elsif line =~ /^processing size$/
2160 element_counter += 1
2161 gtk_thread_protect {
2162 pb1_1.fraction = element_counter / elements
2163 update_progression_title_pb1.call
2165 elsif line =~ /^finished processing sizes$/
2166 gtk_thread_protect { pb1_1.fraction = 1 }
2167 elsif line =~ /^creating index.html$/
2168 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2169 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2170 directories_counter = 0
2171 elsif line =~ /^index.html: (.+)\|(.+)/
2172 newtext = utf8(full_src_dir_to_rel($1, $2))
2173 newtext = '/' if newtext == ''
2174 gtk_thread_protect { pb2.text = newtext }
2175 directories_counter += 1
2176 gtk_thread_protect {
2177 pb2.fraction = directories_counter / directories
2178 set_mainwindow_title(0.9 + pb2.fraction / 10)
2180 elsif line =~ /^die: (.*)$/
2187 w.signal_connect('delete-event') { w.destroy }
2188 w.signal_connect('destroy') {
2189 Thread.kill(refresh_thread)
2190 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2193 File.delete(infopipe_path)
2195 set_mainwindow_title(nil)
2197 w.window_position = Gtk::Window::POS_CENTER
2203 def call_backend(cmd, waitmsg, mode, params)
2204 pipe = Tempfile.new("boohpipe")
2207 system("mkfifo #{path}")
2208 cmd += " --info-pipe #{path}"
2209 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2214 id, exitstatus = Process.waitpid2(pid)
2215 gtk_thread_protect { w8.destroy }
2217 if params[:successmsg]
2218 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2220 if params[:closure_after]
2221 gtk_thread_protect(¶ms[:closure_after])
2223 elsif exitstatus == 15
2224 #- say nothing, user aborted
2226 gtk_thread_protect { show_popup($main_window,
2227 utf8($diemsg ? _("Unexpected internal error, sorry:\n\n%s") % $diemsg :
2228 _("Unexpected internal error, sorry.\nCheck console for error message."))) }
2235 button.signal_connect('clicked') {
2236 Process.kill('SIGTERM', pid)
2240 def save_changes(*forced)
2241 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2245 $xmldir.delete_attribute('already-generated')
2247 propagate_children = proc { |xmldir|
2248 if xmldir.attributes['subdirs-caption']
2249 xmldir.delete_attribute('already-generated')
2251 xmldir.elements.each('dir') { |element|
2252 propagate_children.call(element)
2256 if $xmldir.child_byname_notattr('dir', 'deleted')
2257 new_title = $subalbums_title.buffer.text
2258 if new_title != $xmldir.attributes['subdirs-caption']
2259 parent = $xmldir.parent
2260 if parent.name == 'dir'
2261 parent.delete_attribute('already-generated')
2263 propagate_children.call($xmldir)
2265 $xmldir.add_attribute('subdirs-caption', new_title)
2266 $xmldir.elements.each('dir') { |element|
2267 if !element.attributes['deleted']
2268 path = element.attributes['path']
2269 newtext = $subalbums_edits[path][:editzone].buffer.text
2270 if element.attributes['subdirs-caption']
2271 if element.attributes['subdirs-caption'] != newtext
2272 propagate_children.call(element)
2274 element.add_attribute('subdirs-caption', newtext)
2275 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2277 if element.attributes['thumbnails-caption'] != newtext
2278 element.delete_attribute('already-generated')
2280 element.add_attribute('thumbnails-caption', newtext)
2281 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2287 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2288 if $xmldir.attributes['thumbnails-caption']
2289 path = $xmldir.attributes['path']
2290 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2292 elsif $xmldir.attributes['thumbnails-caption']
2293 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2296 if $xmldir.attributes['thumbnails-caption']
2297 if edit = $subalbums_edits[$xmldir.attributes['path']]
2298 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2302 #- remove and reinsert elements to reflect new ordering
2305 $xmldir.elements.each { |element|
2306 if element.name == 'image' || element.name == 'video'
2307 saves[element.attributes['filename']] = element.remove
2311 $autotable.current_order.each { |path|
2312 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2313 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2316 saves.each_key { |path|
2317 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2318 chld.add_attribute('deleted', 'true')
2322 def sort_by_exif_date
2326 rexml_thread_protect {
2327 $xmldir.elements.each { |element|
2328 if element.name == 'image' || element.name == 'video'
2329 current_order << element.attributes['filename']
2334 #- look for EXIF dates
2337 if current_order.size > 20
2339 w.set_transient_for($main_window)
2341 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2342 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2343 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2344 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2345 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2346 vb.pack_end(bottom, false, false)
2348 w.signal_connect('delete-event') { w.destroy }
2349 w.window_position = Gtk::Window::POS_CENTER
2353 b.signal_connect('clicked') { aborted = true }
2355 current_order.each { |f|
2357 if entry2type(f) == 'image'
2359 pb.fraction = i.to_f / current_order.size
2360 Gtk.main_iteration while Gtk.events_pending?
2361 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2363 dates[f] = date_time
2376 current_order.each { |f|
2377 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2379 dates[f] = date_time
2385 rexml_thread_protect {
2386 $xmldir.elements.each { |element|
2387 if element.name == 'image' || element.name == 'video'
2388 saves[element.attributes['filename']] = element.remove
2393 neworder = smartsort(current_order, dates)
2395 rexml_thread_protect {
2397 $xmldir.add_element(saves[f].name, saves[f].attributes)
2401 #- let the auto-table reflect new ordering
2405 def remove_all_captions
2408 $autotable.current_order.each { |path|
2409 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2410 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2412 save_undo(_("remove all captions"),
2414 texts.each_key { |key|
2415 $name2widgets[key][:textview].buffer.text = texts[key]
2417 $notebook.set_page(1)
2419 texts.each_key { |key|
2420 $name2widgets[key][:textview].buffer.text = ''
2422 $notebook.set_page(1)
2428 $selected_elements.each_key { |path|
2429 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2435 $selected_elements = {}
2439 $undo_tb.sensitive = $undo_mb.sensitive = false
2440 $redo_tb.sensitive = $redo_mb.sensitive = false
2446 $subalbums_vb.children.each { |chld|
2447 $subalbums_vb.remove(chld)
2449 $subalbums = Gtk::Table.new(0, 0, true)
2450 current_y_sub_albums = 0
2452 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2453 $subalbums_edits = {}
2454 subalbums_counter = 0
2455 subalbums_edits_bypos = {}
2457 add_subalbum = proc { |xmldir, counter|
2458 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2459 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2460 if xmldir == $xmldir
2461 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2462 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2463 caption = xmldir.attributes['thumbnails-caption']
2464 infotype = 'thumbnails'
2466 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2467 captionfile, caption = find_subalbum_caption_info(xmldir)
2468 infotype = find_subalbum_info_type(xmldir)
2470 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2471 hbox = Gtk::HBox.new
2472 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2474 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2477 my_gen_real_thumbnail = proc {
2478 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2481 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2482 f.add(img = Gtk::Image.new)
2483 my_gen_real_thumbnail.call
2485 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2487 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2488 $subalbums.attach(hbox,
2489 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2491 frame, textview = create_editzone($subalbums_sw, 0, img)
2492 textview.buffer.text = caption
2493 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2494 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2496 change_image = proc {
2497 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2499 Gtk::FileChooser::ACTION_OPEN,
2501 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2502 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2503 fc.transient_for = $main_window
2504 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))
2505 f.add(preview_img = Gtk::Image.new)
2507 fc.signal_connect('update-preview') { |w|
2508 if fc.preview_filename
2509 if entry2type(fc.preview_filename) == 'video'
2513 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2515 fc.preview_widget_active = false
2517 tmpimage = "#{tmpdir}/00000001.jpg"
2519 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2520 fc.preview_widget_active = true
2521 rescue Gdk::PixbufError
2522 fc.preview_widget_active = false
2524 File.delete(tmpimage)
2531 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2532 fc.preview_widget_active = true
2533 rescue Gdk::PixbufError
2534 fc.preview_widget_active = false
2539 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2541 old_file = captionfile
2542 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2543 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2544 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2545 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2547 new_file = fc.filename
2548 msg 3, "new captionfile is: #{fc.filename}"
2549 perform_changefile = proc {
2550 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2551 $modified_pixbufs.delete(thumbnail_file)
2552 xmldir.delete_attribute("#{infotype}-rotate")
2553 xmldir.delete_attribute("#{infotype}-color-swap")
2554 xmldir.delete_attribute("#{infotype}-enhance")
2555 xmldir.delete_attribute("#{infotype}-seektime")
2556 my_gen_real_thumbnail.call
2558 perform_changefile.call
2560 save_undo(_("change caption file for sub-album"),
2562 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2563 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2564 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2565 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2566 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2567 my_gen_real_thumbnail.call
2568 $notebook.set_page(0)
2570 perform_changefile.call
2571 $notebook.set_page(0)
2579 if File.exists?(thumbnail_file)
2580 File.delete(thumbnail_file)
2582 my_gen_real_thumbnail.call
2585 rotate_and_cleanup = proc { |angle|
2586 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2587 if File.exists?(thumbnail_file)
2588 File.delete(thumbnail_file)
2592 move = proc { |direction|
2595 save_changes('forced')
2596 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2597 if direction == 'up'
2598 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2599 subalbums_edits_bypos[oldpos - 1][:position] += 1
2601 if direction == 'down'
2602 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2603 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2605 if direction == 'top'
2606 for i in 1 .. oldpos - 1
2607 subalbums_edits_bypos[i][:position] += 1
2609 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2611 if direction == 'bottom'
2612 for i in oldpos + 1 .. subalbums_counter
2613 subalbums_edits_bypos[i][:position] -= 1
2615 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2619 $xmldir.elements.each('dir') { |element|
2620 if (!element.attributes['deleted'])
2621 elems << [ element.attributes['path'], element.remove ]
2624 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2625 each { |e| $xmldir.add_element(e[1]) }
2626 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2627 $xmldir.elements.each('descendant::dir') { |elem|
2628 elem.delete_attribute('already-generated')
2631 sel = $albums_tv.selection.selected_rows
2633 populate_subalbums_treeview(false)
2634 $albums_tv.selection.select_path(sel[0])
2637 color_swap_and_cleanup = proc {
2638 perform_color_swap_and_cleanup = proc {
2639 color_swap(xmldir, "#{infotype}-")
2640 my_gen_real_thumbnail.call
2642 perform_color_swap_and_cleanup.call
2644 save_undo(_("color swap"),
2646 perform_color_swap_and_cleanup.call
2647 $notebook.set_page(0)
2649 perform_color_swap_and_cleanup.call
2650 $notebook.set_page(0)
2655 change_seektime_and_cleanup = proc {
2656 if values = ask_new_seektime(xmldir, "#{infotype}-")
2657 perform_change_seektime_and_cleanup = proc { |val|
2658 change_seektime(xmldir, "#{infotype}-", val)
2659 my_gen_real_thumbnail.call
2661 perform_change_seektime_and_cleanup.call(values[:new])
2663 save_undo(_("specify seektime"),
2665 perform_change_seektime_and_cleanup.call(values[:old])
2666 $notebook.set_page(0)
2668 perform_change_seektime_and_cleanup.call(values[:new])
2669 $notebook.set_page(0)
2675 whitebalance_and_cleanup = proc {
2676 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2677 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2678 perform_change_whitebalance_and_cleanup = proc { |val|
2679 change_whitebalance(xmldir, "#{infotype}-", val)
2680 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2681 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2682 if File.exists?(thumbnail_file)
2683 File.delete(thumbnail_file)
2686 perform_change_whitebalance_and_cleanup.call(values[:new])
2688 save_undo(_("fix white balance"),
2690 perform_change_whitebalance_and_cleanup.call(values[:old])
2691 $notebook.set_page(0)
2693 perform_change_whitebalance_and_cleanup.call(values[:new])
2694 $notebook.set_page(0)
2700 gammacorrect_and_cleanup = proc {
2701 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2702 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2703 perform_change_gammacorrect_and_cleanup = proc { |val|
2704 change_gammacorrect(xmldir, "#{infotype}-", val)
2705 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2706 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2707 if File.exists?(thumbnail_file)
2708 File.delete(thumbnail_file)
2711 perform_change_gammacorrect_and_cleanup.call(values[:new])
2713 save_undo(_("gamma correction"),
2715 perform_change_gammacorrect_and_cleanup.call(values[:old])
2716 $notebook.set_page(0)
2718 perform_change_gammacorrect_and_cleanup.call(values[:new])
2719 $notebook.set_page(0)
2725 enhance_and_cleanup = proc {
2726 perform_enhance_and_cleanup = proc {
2727 enhance(xmldir, "#{infotype}-")
2728 my_gen_real_thumbnail.call
2731 perform_enhance_and_cleanup.call
2733 save_undo(_("enhance"),
2735 perform_enhance_and_cleanup.call
2736 $notebook.set_page(0)
2738 perform_enhance_and_cleanup.call
2739 $notebook.set_page(0)
2744 evtbox.signal_connect('button-press-event') { |w, event|
2745 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2747 rotate_and_cleanup.call(90)
2749 rotate_and_cleanup.call(-90)
2750 elsif $enhance.active?
2751 enhance_and_cleanup.call
2754 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2755 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2756 { :forbid_left => true, :forbid_right => true,
2757 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2758 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2759 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2760 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2761 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2763 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2768 evtbox.signal_connect('button-press-event') { |w, event|
2769 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2773 evtbox.signal_connect('button-release-event') { |w, event|
2774 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2775 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2776 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2777 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2778 msg 3, "gesture rotate: #{angle}"
2779 rotate_and_cleanup.call(angle)
2782 $gesture_press = nil
2785 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2786 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2787 current_y_sub_albums += 1
2790 if $xmldir.child_byname_notattr('dir', 'deleted')
2792 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2793 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2794 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2795 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2796 #- this album image/caption
2797 if $xmldir.attributes['thumbnails-caption']
2798 add_subalbum.call($xmldir, 0)
2801 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2802 $xmldir.elements.each { |element|
2803 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2804 #- element (image or video) of this album
2805 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2806 msg 3, "dest_img: #{dest_img}"
2807 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2808 total[element.name] += 1
2810 if element.name == 'dir' && !element.attributes['deleted']
2811 #- sub-album image/caption
2812 add_subalbum.call(element, subalbums_counter += 1)
2813 total[element.name] += 1
2816 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2817 total['image'], total['video'], total['dir'] ]))
2818 $subalbums_vb.add($subalbums)
2819 $subalbums_vb.show_all
2821 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2822 $notebook.get_tab_label($autotable_sw).sensitive = false
2823 $notebook.set_page(0)
2824 $thumbnails_title.buffer.text = ''
2826 $notebook.get_tab_label($autotable_sw).sensitive = true
2827 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2830 if !$xmldir.child_byname_notattr('dir', 'deleted')
2831 $notebook.get_tab_label($subalbums_sw).sensitive = false
2832 $notebook.set_page(1)
2834 $notebook.get_tab_label($subalbums_sw).sensitive = true
2838 def pixbuf_or_nil(filename)
2840 return Gdk::Pixbuf.new(filename)
2846 def theme_choose(current)
2847 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2849 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2850 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2851 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2853 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2854 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2855 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2856 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2857 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2858 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2859 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2860 treeview.signal_connect('button-press-event') { |w, event|
2861 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2862 dialog.response(Gtk::Dialog::RESPONSE_OK)
2866 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2868 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.split("\n").find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2871 iter[0] = File.basename(dir)
2872 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2873 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2874 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2875 if File.basename(dir) == current
2876 treeview.selection.select_iter(iter)
2879 dialog.set_default_size(-1, 500)
2880 dialog.vbox.show_all
2882 dialog.run { |response|
2883 iter = treeview.selection.selected
2885 if response == Gtk::Dialog::RESPONSE_OK && iter
2886 return model.get_value(iter, 0)
2892 def show_password_protections
2893 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2894 child_iter = $albums_iters[xmldir.attributes['path']]
2895 if xmldir.attributes['password-protect']
2896 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2897 already_protected = true
2898 elsif already_protected
2899 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2901 pix = pix.saturate_and_pixelate(1, true)
2907 xmldir.elements.each('dir') { |elem|
2908 if !elem.attributes['deleted']
2909 examine_dir_elem.call(child_iter, elem, already_protected)
2913 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2916 def populate_subalbums_treeview(select_first)
2920 $subalbums_vb.children.each { |chld|
2921 $subalbums_vb.remove(chld)
2924 source = $xmldoc.root.attributes['source']
2925 msg 3, "source: #{source}"
2927 xmldir = $xmldoc.elements['//dir']
2928 if !xmldir || xmldir.attributes['path'] != source
2929 msg 1, _("Corrupted booh file...")
2933 append_dir_elem = proc { |parent_iter, xmldir|
2934 child_iter = $albums_ts.append(parent_iter)
2935 child_iter[0] = File.basename(xmldir.attributes['path'])
2936 child_iter[1] = xmldir.attributes['path']
2937 $albums_iters[xmldir.attributes['path']] = child_iter
2938 msg 3, "puttin location: #{xmldir.attributes['path']}"
2939 xmldir.elements.each('dir') { |elem|
2940 if !elem.attributes['deleted']
2941 append_dir_elem.call(child_iter, elem)
2945 append_dir_elem.call(nil, xmldir)
2946 show_password_protections
2948 $albums_tv.expand_all
2950 $albums_tv.selection.select_iter($albums_ts.iter_first)
2954 def select_current_theme
2955 select_theme($xmldoc.root.attributes['theme'],
2956 $xmldoc.root.attributes['limit-sizes'],
2957 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2958 $xmldoc.root.attributes['thumbnails-per-row'])
2961 def open_file(filename)
2965 $current_path = nil #- invalidate
2966 $modified_pixbufs = {}
2969 $subalbums_vb.children.each { |chld|
2970 $subalbums_vb.remove(chld)
2973 if !File.exists?(filename)
2974 return utf8(_("File not found."))
2978 $xmldoc = REXML::Document.new(File.new(filename))
2983 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2984 if entry2type(filename).nil?
2985 return utf8(_("Not a booh file!"))
2987 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."))
2991 if !source = $xmldoc.root.attributes['source']
2992 return utf8(_("Corrupted booh file..."))
2995 if !dest = $xmldoc.root.attributes['destination']
2996 return utf8(_("Corrupted booh file..."))
2999 if !theme = $xmldoc.root.attributes['theme']
3000 return utf8(_("Corrupted booh file..."))
3003 if $xmldoc.root.attributes['version'] < $VERSION
3004 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
3005 mark_document_as_dirty
3006 if $xmldoc.root.attributes['version'] < '0.8.4'
3007 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
3008 `find '#{source}' -type d -follow`.split("\n").sort.collect { |v| v.chomp }.each { |dir|
3009 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
3010 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
3011 if old_dest_dir != new_dest_dir
3012 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
3014 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
3015 xmldir.elements.each { |element|
3016 if %w(image video).include?(element.name) && !element.attributes['deleted']
3017 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3018 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3019 Dir[old_name + '*'].each { |file|
3020 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
3021 file != new_file and sys("mv '#{file}' '#{new_file}'")
3024 if element.name == 'dir' && !element.attributes['deleted']
3025 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3026 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3027 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
3031 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3035 $xmldoc.root.add_attribute('version', $VERSION)
3038 select_current_theme
3040 $filename = filename
3041 set_mainwindow_title(nil)
3042 $default_size['thumbnails'] =~ /(.*)x(.*)/
3043 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3044 $albums_thumbnail_size =~ /(.*)x(.*)/
3045 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3047 populate_subalbums_treeview(true)
3049 $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
3053 def open_file_user(filename)
3054 result = open_file(filename)
3056 $config['last-opens'] ||= []
3057 if $config['last-opens'][-1] != utf8(filename)
3058 $config['last-opens'] << utf8(filename)
3060 $orig_filename = $filename
3061 $main_window.title = 'booh - ' + File.basename($orig_filename)
3062 tmp = Tempfile.new("boohtemp")
3063 $filename = tmp.path
3066 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3068 $tempfiles << $filename << "#{$filename}.backup"
3070 $orig_filename = nil
3076 if !ask_save_modifications(utf8(_("Save this album?")),
3077 utf8(_("Do you want to save the changes to this album?")),
3078 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3081 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3083 Gtk::FileChooser::ACTION_OPEN,
3085 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3086 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3087 fc.set_current_folder(File.expand_path("~/.booh"))
3088 fc.transient_for = $main_window
3089 fc.preview_widget = previewlabel = Gtk::Label.new.show
3090 fc.signal_connect('update-preview') { |w|
3091 if fc.preview_filename
3093 push_mousecursor_wait(fc)
3094 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3098 xmldoc.elements.each('//*') { |elem|
3099 if elem.name == 'dir'
3101 elsif elem.name == 'image'
3103 elsif elem.name == 'video'
3111 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3112 fc.preview_widget_active = false
3114 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") %
3115 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3116 fc.preview_widget_active = true
3122 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3123 push_mousecursor_wait(fc)
3124 msg = open_file_user(fc.filename)
3139 def additional_booh_options
3142 options += "--mproc #{$config['mproc'].to_i} "
3144 options += "--comments-format '#{$config['comments-format']}' "
3145 if $config['transcode-videos']
3146 options += "--transcode-videos '#{$config['transcode-videos']}' "
3148 if $config['use-flv'] == 'true'
3149 options += "--flv-generator '#{$config['flv-generator']}' "
3154 def ask_multi_languages(value)
3156 spl = value.split(',')
3157 value = [ spl[0..-2], spl[-1] ]
3160 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3163 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3164 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3166 lbl = Gtk::Label.new
3168 _("You can choose to activate <b>multi-languages</b> support for this web-album
3169 (it will work only if you publish your web-album on an Apache web-server). This will
3170 use the MultiViews feature of Apache; the pages will be served according to the
3171 value of the Accept-Language HTTP header sent by the web browsers, so that people
3172 with different languages preferences will be able to browse your web-album with
3173 navigation in their language (if language is available).
3176 dialog.vbox.add(lbl)
3177 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3178 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3179 add(languages = Gtk::Button.new))))
3181 pick_languages = proc {
3182 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3185 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3186 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3188 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3189 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3191 SUPPORTED_LANGUAGES.each { |lang|
3192 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3193 if ! value.nil? && value[0].include?(lang)
3199 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3200 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3201 fallback_language = nil
3202 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3203 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3204 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3205 fbl_rb.active = true
3206 fallback_language = SUPPORTED_LANGUAGES[0]
3208 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3209 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3210 rb.signal_connect('clicked') { fallback_language = lang }
3211 if ! value.nil? && value[1] == lang
3216 dialog2.window_position = Gtk::Window::POS_MOUSE
3220 dialog2.run { |response|
3222 if resp == Gtk::Dialog::RESPONSE_OK
3224 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3225 value[1] = fallback_language
3226 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3233 languages.signal_connect('clicked') {
3236 dialog.window_position = Gtk::Window::POS_MOUSE
3240 rb_yes.active = true
3241 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3243 rb_no.signal_connect('clicked') {
3247 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3260 dialog.run { |response|
3265 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3267 return [ true, nil ]
3269 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3278 if !ask_save_modifications(utf8(_("Save this album?")),
3279 utf8(_("Do you want to save the changes to this album?")),
3280 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3283 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3285 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3286 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3287 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3289 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3290 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3291 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3292 tbl.attach(src = Gtk::Entry.new,
3293 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3294 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3295 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3296 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3297 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3298 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3299 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3300 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3301 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3302 tbl.attach(dest = Gtk::Entry.new,
3303 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3304 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3305 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3306 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3307 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3308 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3309 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3310 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3311 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3313 tooltips = Gtk::Tooltips.new
3314 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3315 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3316 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3317 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3318 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3319 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3320 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)
3321 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3322 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3323 nperpage_model = Gtk::ListStore.new(String, String)
3324 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3325 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3326 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3327 nperpagecombo.set_attributes(crt, { :markup => 0 })
3328 iter = nperpage_model.append
3329 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3331 [ 12, 20, 30, 40, 50 ].each { |v|
3332 iter = nperpage_model.append
3333 iter[0] = iter[1] = v.to_s
3335 nperpagecombo.active = 0
3337 multilanguages_value = nil
3338 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3339 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3340 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)
3341 multilanguages.signal_connect('clicked') {
3342 retval = ask_multi_languages(multilanguages_value)
3344 multilanguages_value = retval[1]
3346 if multilanguages_value
3347 ml_label.text = utf8(_("Multi-languages: enabled."))
3349 ml_label.text = utf8(_("Multi-languages: disabled."))
3352 if $config['default-multi-languages']
3353 multilanguages_value = $config['default-multi-languages']
3354 ml_label.text = utf8(_("Multi-languages: enabled."))
3356 ml_label.text = utf8(_("Multi-languages: disabled."))
3359 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3360 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3361 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)
3362 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3363 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3364 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)
3365 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3366 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3367 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)
3369 src_nb_calculated_for = ''
3370 src_nb_process = nil
3371 process_src_nb = proc {
3372 if src.text != src_nb_calculated_for
3373 src_nb_calculated_for = src.text
3376 Process.kill(9, src_nb_process)
3378 #- process doesn't exist anymore - race condition
3381 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3382 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3384 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3385 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3388 while src_nb_process
3389 msg 3, "sleeping for completion of previous process"
3392 gtk_thread_flush #- flush to avoid race condition in src_nb markup update
3394 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3395 total = { 'image' => 0, 'video' => 0, nil => 0 }
3396 if src_nb_process = fork
3397 msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3401 rd.readlines.each { |dir|
3402 if File.basename(dir) =~ /^\./
3406 Dir.entries(dir.chomp).each { |file|
3407 total[entry2type(file)] += 1
3409 rescue Errno::EACCES, Errno::ENOENT
3414 msg 3, "ripping #{src_nb_process}"
3415 dummy, exitstatus = Process.waitpid2(src_nb_process)
3417 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3419 src_nb_process = nil
3425 wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3426 Process.exit!(0) #- _exit
3429 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3432 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3438 timeout_src_nb = Gtk.timeout_add(100) {
3442 src_browse.signal_connect('clicked') {
3443 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3445 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3447 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3448 fc.transient_for = $main_window
3449 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3450 src.text = utf8(fc.filename)
3452 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3457 dest_browse.signal_connect('clicked') {
3458 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3460 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3462 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3463 fc.transient_for = $main_window
3464 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3465 dest.text = utf8(fc.filename)
3470 conf_browse.signal_connect('clicked') {
3471 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3473 Gtk::FileChooser::ACTION_SAVE,
3475 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3476 fc.transient_for = $main_window
3477 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3478 fc.set_current_folder(File.expand_path("~/.booh"))
3479 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3480 conf.text = utf8(fc.filename)
3487 recreate_theme_config = proc {
3488 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3490 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3491 $images_size.each { |s|
3492 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3496 tooltips.set_tip(cb, utf8(s['description']), nil)
3497 theme_sizes << { :widget => cb, :value => s['name'] }
3499 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3500 tooltips = Gtk::Tooltips.new
3501 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3502 theme_sizes << { :widget => cb, :value => 'original' }
3505 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3508 $allowed_N_values.each { |n|
3510 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3512 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3514 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3518 nperrows << { :widget => rb, :value => n }
3520 nperrowradios.show_all
3522 recreate_theme_config.call
3524 theme_button.signal_connect('clicked') {
3525 if newtheme = theme_choose(theme_button.label)
3526 theme_button.label = newtheme
3527 recreate_theme_config.call
3531 dialog.vbox.add(frame1)
3532 dialog.vbox.add(frame2)
3538 dialog.run { |response|
3539 if response == Gtk::Dialog::RESPONSE_OK
3540 srcdir = from_utf8_safe(src.text)
3541 destdir = from_utf8_safe(dest.text)
3542 confpath = from_utf8_safe(conf.text)
3543 if src.text != '' && srcdir == ''
3544 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3546 elsif !File.directory?(srcdir)
3547 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3549 elsif dest.text != '' && destdir == ''
3550 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3552 elsif destdir != make_dest_filename(destdir)
3553 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3555 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3556 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3557 inside it will be permanently removed before creating the web-album!
3558 Are you sure you want to continue?")), { :okcancel => true })
3560 elsif File.exists?(destdir) && !File.directory?(destdir)
3561 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3563 elsif conf.text == ''
3564 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3566 elsif conf.text != '' && confpath == ''
3567 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3569 elsif File.directory?(confpath)
3570 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3572 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3573 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3575 system("mkdir '#{destdir}'")
3576 if !File.directory?(destdir)
3577 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3589 srcdir = from_utf8(src.text)
3590 destdir = from_utf8(dest.text)
3591 configskel = File.expand_path(from_utf8(conf.text))
3592 theme = theme_button.label
3593 #- some sort of automatic theme preference
3594 $config['default-theme'] = theme
3595 $config['default-multi-languages'] = multilanguages_value
3596 $config['default-optimize32'] = optimize432.active?.to_s
3597 $config['default-addthis'] = addthis.active?.to_s
3598 $config['default-quotehtml'] = quotehtml.active?.to_s
3599 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3600 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3601 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3602 opt432 = optimize432.active?
3603 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3604 indexlink = indexlinkentry.text.gsub('\'', ''')
3605 athis = addthis.active?
3606 qhtml = quotehtml.active?
3610 Process.kill(9, src_nb_process)
3611 while src_nb_process
3612 msg 3, "sleeping for completion of previous process"
3616 #- process doesn't exist
3618 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3621 Gtk.timeout_remove(timeout_src_nb)
3624 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3625 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3626 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3627 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3628 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' " +
3629 "#{athis ? '--addthis' : ''} #{qhtml ? '--quote-html' : ''} #{additional_booh_options}",
3630 utf8(_("Please wait while scanning source directory...")),
3632 { :closure_after => proc {
3633 open_file_user(configskel)
3634 $main_window.urgency_hint = true
3640 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3642 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3643 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3644 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3646 source = $xmldoc.root.attributes['source']
3647 dest = $xmldoc.root.attributes['destination']
3648 theme = $xmldoc.root.attributes['theme']
3649 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3650 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3651 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3652 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3654 limit_sizes = limit_sizes.split(/,/)
3656 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3657 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3658 athis = !$xmldoc.root.attributes['addthis'].nil?
3659 qhtml = !$xmldoc.root.attributes['quote-html'].nil?
3660 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3662 tooltips = Gtk::Tooltips.new
3663 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3664 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3665 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3666 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3667 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3668 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3669 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3670 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3671 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3672 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3673 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3674 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3675 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3677 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3678 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3679 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3680 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3681 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3682 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3683 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)
3684 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3685 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3686 nperpage_model = Gtk::ListStore.new(String, String)
3687 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3688 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3689 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3690 nperpagecombo.set_attributes(crt, { :markup => 0 })
3691 iter = nperpage_model.append
3692 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3694 [ 12, 20, 30, 40, 50 ].each { |v|
3695 iter = nperpage_model.append
3696 iter[0] = iter[1] = v.to_s
3697 if nperpage && nperpage == v.to_s
3698 nperpagecombo.active_iter = iter
3701 if nperpagecombo.active_iter.nil?
3702 nperpagecombo.active = 0
3705 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3706 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3707 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)
3709 if save_multilanguages_value
3710 ml_label.text = utf8(_("Multi-languages: enabled."))
3712 ml_label.text = utf8(_("Multi-languages: disabled."))
3716 multilanguages.signal_connect('clicked') {
3717 retval = ask_multi_languages(save_multilanguages_value)
3719 save_multilanguages_value = retval[1]
3724 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3725 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3727 indexlinkentry.text = indexlink
3729 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)
3730 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3731 pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3733 madewithentry.text = madewith
3735 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)
3736 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active(athis))
3737 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active(qhtml))
3738 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)
3742 recreate_theme_config = proc {
3743 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3745 select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3747 $images_size.each { |s|
3748 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3750 if limit_sizes.include?(s['name'])
3758 tooltips.set_tip(cb, utf8(s['description']), nil)
3759 theme_sizes << { :widget => cb, :value => s['name'] }
3761 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3762 tooltips = Gtk::Tooltips.new
3763 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3764 if limit_sizes && limit_sizes.include?('original')
3767 theme_sizes << { :widget => cb, :value => 'original' }
3770 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3773 $allowed_N_values.each { |n|
3775 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3777 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3779 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3780 nperrowradios.add(Gtk::Label.new(' '))
3781 if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
3784 nperrows << { :widget => rb, :value => n.to_s }
3786 nperrowradios.show_all
3788 recreate_theme_config.call
3790 theme_button.signal_connect('clicked') {
3791 if newtheme = theme_choose(theme_button.label)
3794 theme_button.label = newtheme
3795 recreate_theme_config.call
3799 dialog.vbox.add(frame1)
3800 dialog.vbox.add(frame2)
3806 dialog.run { |response|
3807 if response == Gtk::Dialog::RESPONSE_OK
3808 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3809 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3818 save_theme = theme_button.label
3819 save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
3820 save_opt432 = optimize432.active?
3821 save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3822 save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3823 save_madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3824 save_indexlink = indexlinkentry.text.gsub('\'', ''')
3825 save_addthis = addthis.active?
3826 save_quotehtml = quotehtml.active?
3829 if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlink || save_multilanguages_value != multilanguages_value || save_quotehtml != qhtml || save_addthis != athis)
3830 #- some sort of automatic preferences
3831 if save_theme != theme
3832 $config['default-theme'] = save_theme
3834 if save_multilanguages_value != multilanguages_value
3835 $config['default-multi-languages'] = save_multilanguages_value
3837 if save_opt432 != opt432
3838 $config['default-optimize32'] = save_opt432.to_s
3840 if save_addthis != athis
3841 $config['default-addthis'] = save_addthis.to_s
3843 if save_quotehtml != qhtml
3844 $config['default-quotehtml'] = save_quotehtml.to_s
3846 mark_document_as_dirty
3848 call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
3849 "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
3850 (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
3851 (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
3852 "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' " +
3853 "#{save_addthis ? '--addthis' : ''} #{save_quotehtml ? '--quote-html' : ''} #{additional_booh_options}",
3854 utf8(_("Please wait while scanning source directory...")),
3856 { :closure_after => proc {
3857 open_file($filename)
3859 $main_window.urgency_hint = true
3862 #- select_theme merges global variables, need to return to current choices
3863 select_current_theme
3870 sel = $albums_tv.selection.selected_rows
3872 call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3873 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3874 utf8(_("Please wait while scanning source directory...")),
3876 { :closure_after => proc {
3877 open_file($filename)
3878 $albums_tv.selection.select_path(sel[0])
3880 $main_window.urgency_hint = true
3887 sel = $albums_tv.selection.selected_rows
3889 call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
3890 "--verbose-level #{$verbose_level} #{additional_booh_options}",
3891 utf8(_("Please wait while scanning source directory...")),
3893 { :closure_after => proc {
3894 open_file($filename)
3895 $albums_tv.selection.select_path(sel[0])
3897 $main_window.urgency_hint = true
3904 theme = $xmldoc.root.attributes['theme']
3905 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3907 limit_sizes = "--sizes #{limit_sizes}"
3909 call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
3910 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
3911 utf8(_("Please wait while scanning source directory...")),
3913 { :closure_after => proc {
3914 open_file($filename)
3916 $main_window.urgency_hint = true
3921 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
3923 Gtk::FileChooser::ACTION_SAVE,
3925 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3926 fc.transient_for = $main_window
3927 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3928 fc.set_current_folder(File.expand_path("~/.booh"))
3929 fc.filename = $orig_filename
3930 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3931 $orig_filename = fc.filename
3932 if ! save_current_file_user
3936 $config['last-opens'] ||= []
3937 $config['last-opens'] << $orig_filename
3943 dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
3945 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3946 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3947 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3950 dialog.vbox.add(notebook = Gtk::Notebook.new)
3951 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
3952 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
3953 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3954 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
3955 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3956 tooltips = Gtk::Tooltips.new
3957 tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
3958 for example: /usr/bin/mplayer %f")), nil)
3961 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
3962 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3963 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
3964 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3965 tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
3966 for example: /usr/bin/gimp-remote %f")), nil)
3969 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
3970 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3971 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
3972 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3973 tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
3974 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
3977 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(flv_check = Gtk::CheckButton.new(utf8(_("Use embedded flash player for videos,\nand use this .flv generator:")))),
3978 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3979 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(flv_generator_entry = Gtk::Entry.new.set_text($config['flv-generator']).set_sensitive(false)),
3980 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3981 tooltips.set_tip(flv_check, utf8(_("Flowplayer will be used for embedded video playback")), nil)
3982 tooltips.set_tip(flv_generator_entry, utf8(_("Use %f to specify the input filename, %o the output filename;
3983 for example: /usr/bin/ffmpeg -i %f -b ${i}k -ar 22050 -ab 32k %o")), nil)
3986 tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
3987 0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3988 tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
3989 1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3990 tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
3993 tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
3994 0, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3995 tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
3998 tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
3999 0, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
4000 tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting a photo or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
4002 flv_check.signal_connect('toggled') {
4003 flv_generator_entry.sensitive = flv_check.active?
4005 if $config['use-flv'] == 'true'
4006 flv_check.active = true
4008 smp_check.signal_connect('toggled') {
4009 smp_hbox.sensitive = smp_check.active?
4012 smp_check.active = true
4013 smp_spin.value = $config['mproc'].to_i
4015 nogestures_check.active = $config['nogestures']
4016 deleteondisk_check.active = $config['deleteondisk']
4018 notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
4019 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
4020 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
4021 tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
4022 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
4023 tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nphotos in new albums:"))),
4024 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
4025 tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
4026 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
4027 tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
4028 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
4029 tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for photos and videos in new albums. Use this entry to use something else.")), nil)
4030 commentsformat_help.signal_connect('clicked') {
4031 show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
4032 hence you should look at ImageMagick/identify documentation for the most
4033 accurate and up-to-date documentation. Last time I checked, documentation
4036 Print information about the image in a format of your choosing. You can
4037 include the image filename, type, width, height, Exif data, or other image
4038 attributes by embedding special format characters:
4041 %P page width and height
4045 %e filename extension
4050 %k number of unique colors
4057 %r image class and colorspace
4060 %u unique temporary filename