5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2009 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'booh/rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
45 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
46 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
75 puts _("Booh version %s
77 Copyright (c) 2005-2009 Guillaume Cottenceau.
78 This is free software; see the source for copying conditions. There is NO
79 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
83 when '--verbose-level'
84 $verbose_level = arg.to_i
97 $config_file = File.expand_path('~/.booh-gui-rc')
98 if File.readable?($config_file)
100 xmldoc = REXML::Document.new(File.new($config_file))
102 #- encoding unsupported anymore? file edited manually? ignore then
103 msg 1, "Ignoring #{$config_file}, failed to parse it: #{$!}"
106 xmldoc.root.elements.each { |element|
107 txt = element.get_text
109 if txt.value =~ /~~~/ || element.name == 'last-opens'
110 $config[element.name] = txt.value.split(/~~~/)
112 $config[element.name] = txt.value
114 elsif element.elements.size == 0
115 $config[element.name] = ''
117 $config[element.name] = {}
118 element.each { |chld|
120 $config[element.name][chld.name] = txt ? txt.value : nil
126 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
127 $config['image-editor'] ||= '/usr/bin/gimp-remote %f || /usr/bin/gimp %f'
128 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f || /usr/bin/firefox %f"
129 $config['use-flv'] ||= "true"
130 $config['flv-generator'] ||= "/usr/bin/ffmpeg -i %f -b 800k -ar 22050 -ab 32k %o"
131 $config['comments-format'] ||= '%t'
132 if !FileTest.directory?(File.expand_path('~/.booh'))
133 system("mkdir ~/.booh")
135 if $config['mproc'].nil?
137 for line in IO.readlines('/proc/cpuinfo') do
138 line =~ /^processor/ and cpus += 1
141 $config['mproc'] = cpus
144 $config['rotate-set-exif'] ||= 'true'
149 def check_config_preferences_dep
150 viewer_binary = $config['video-viewer'].split.first
151 if viewer_binary && !File.executable?(viewer_binary)
152 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
153 You should fix this in Edit/Preferences so that you can view videos.
155 Problem was: '%s' is not an executable file.
156 Hint: don't forget to specify the full path to the executable,
157 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
160 flv_generator_binary = $config['use-flv'] == 'true' && $config['flv-generator'].split.first
161 if flv_generator_binary && !File.executable?(flv_generator_binary)
162 show_popup($main_window, utf8(_("The configured .flv generator seems to be unavailable.
163 You should fix this in Edit/Preferences so that you can have working
164 embedded flash videos.
166 Problem was: '%s' is not an executable file.
167 Hint: don't forget to specify the full path to the executable,
168 e.g. '/usr/bin/ffmpeg' is correct but 'ffmpeg' only is not.") % flv_generator_binary), { :pos_centered => true, :not_transient => true })
173 if !system("which convert >/dev/null 2>/dev/null")
174 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
175 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
178 if !system("which identify >/dev/null 2>/dev/null")
179 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
180 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
182 if !system("which exif >/dev/null 2>/dev/null")
183 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
185 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
187 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
190 check_config_preferences_dep
193 def check_image_editor
194 if last_failed_binary = check_multi_binaries($config['image-editor'])
195 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
196 You should fix this in Edit/Preferences so that you can edit photos externally.
198 Problem was: '%s' is not an executable file.
199 Hint: don't forget to specify the full path to the executable,
200 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
208 if $config['last-opens'] && $config['last-opens'].size > 10
209 $config['last-opens'] = $config['last-opens'][-10, 10]
212 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
213 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
214 $config.each_pair { |key, value|
215 elem = xmldoc.root.add_element key
217 $config[key].each_pair { |subkey, subvalue|
218 subelem = elem.add_element subkey
219 subelem.add_text subvalue.to_s
221 elsif value.is_a? Array
222 elem.add_text value.join('~~~')
227 elem.add_text value.to_s
231 ios = File.open($config_file, "w")
235 $tempfiles.each { |f|
242 def set_mousecursor(what, *widget)
243 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
244 if widget[0] && widget[0].window
245 widget[0].window.cursor = cursor
247 if $main_window && $main_window.window
248 $main_window.window.cursor = cursor
250 $current_cursor = what
252 def set_mousecursor_wait(*widget)
253 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
254 if Thread.current == Thread.main
255 Gtk.main_iteration while Gtk.events_pending?
258 def set_mousecursor_normal(*widget)
259 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
261 def push_mousecursor_wait(*widget)
262 if $current_cursor != Gdk::Cursor::WATCH
263 $save_cursor = $current_cursor
264 gtk_thread_protect { set_mousecursor_wait(*widget) }
267 def pop_mousecursor(*widget)
268 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
272 source = $xmldoc.root.attributes['source']
273 dest = $xmldoc.root.attributes['destination']
274 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
277 def full_src_dir_to_rel(path, source)
278 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
281 def build_full_dest_filename(filename)
282 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
285 def save_undo(name, closure, *params)
286 UndoHandler.save_undo(name, closure, [ *params ])
287 $undo_tb.sensitive = $undo_mb.sensitive = true
288 $redo_tb.sensitive = $redo_mb.sensitive = false
291 def view_element(filename, closures)
292 if entry2type(filename) == 'video'
293 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
299 w = create_window.set_title(filename)
301 msg 3, "filename: #{filename}"
302 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
303 #- typically this file won't exist in case of videos; try with the largest thumbnail around
304 if !File.exists?(dest_img)
305 if entry2type(filename) == 'video'
306 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
307 if not alternatives.empty?
308 dest_img = alternatives[-1]
311 push_mousecursor_wait
312 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
314 if !File.exists?(dest_img)
315 msg 2, _("Could not generate fullscreen thumbnail!")
320 aspect = utf8(_("Aspect: unknown"))
321 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
323 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
325 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
326 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)))
327 evt.signal_connect('button-press-event') { |this, event|
328 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
329 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
331 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
333 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
334 delete_item.signal_connect('activate') {
336 closures[:delete].call(false)
339 menu.popup(nil, nil, event.button, event.time)
342 evt.signal_connect('button-release-event') { |this, event|
344 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
345 msg 3, "gesture delete: click-drag right button to the bottom"
347 closures[:delete].call(false)
348 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
352 tooltips = Gtk::Tooltips.new
353 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
355 w.signal_connect('key-press-event') { |w,event|
356 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
358 closures[:delete].call(false)
362 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
363 b.signal_connect('clicked') { w.destroy }
366 vb.pack_start(evt, false, false)
367 vb.pack_end(bottom, false, false)
370 w.signal_connect('delete-event') { w.destroy }
371 w.window_position = Gtk::Window::POS_CENTER
375 def scroll_upper(scrolledwindow, ypos_top)
376 newval = scrolledwindow.vadjustment.value -
377 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
378 if newval < scrolledwindow.vadjustment.lower
379 newval = scrolledwindow.vadjustment.lower
381 scrolledwindow.vadjustment.value = newval
384 def scroll_lower(scrolledwindow, ypos_bottom)
385 newval = scrolledwindow.vadjustment.value +
386 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
387 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
388 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
390 scrolledwindow.vadjustment.value = newval
393 def autoscroll_if_needed(scrolledwindow, image, textview)
394 #- autoscroll if cursor or image is not visible, if possible
395 if image && image.window || textview.window
396 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
397 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
398 current_miny_visible = scrolledwindow.vadjustment.value
399 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
400 if ypos_top < current_miny_visible
401 scroll_upper(scrolledwindow, ypos_top)
402 elsif ypos_bottom > current_maxy_visible
403 scroll_lower(scrolledwindow, ypos_bottom)
408 def create_editzone(scrolledwindow, pagenum, image)
409 frame = Gtk::Frame.new
410 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
411 frame.set_shadow_type(Gtk::SHADOW_IN)
412 textview.signal_connect('key-press-event') { |w, event|
413 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
414 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
415 scrolledwindow.signal_emit('key-press-event', event)
417 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
418 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
419 if event.keyval == Gdk::Keyval::GDK_Up
420 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
421 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
423 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
426 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
427 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
429 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
436 candidate_undo_text = nil
437 textview.signal_connect('focus-in-event') { |w, event|
438 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
439 candidate_undo_text = textview.buffer.text
443 textview.signal_connect('key-release-event') { |w, event|
444 if candidate_undo_text && candidate_undo_text != textview.buffer.text
446 save_undo(_("text edit"),
448 save_text = textview.buffer.text
449 textview.buffer.text = text
451 $notebook.set_page(pagenum)
453 textview.buffer.text = save_text
455 $notebook.set_page(pagenum)
457 }, candidate_undo_text)
458 candidate_undo_text = nil
461 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)
462 autoscroll_if_needed(scrolledwindow, image, textview)
467 return [ frame, textview ]
470 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
472 if !$modified_pixbufs[thumbnail_img]
473 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
474 elsif !$modified_pixbufs[thumbnail_img][:orig]
475 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
478 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
481 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
482 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
483 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
484 if pixbuf.height > desired_y
485 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
486 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
487 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
492 if $modified_pixbufs[thumbnail_img][:whitebalance]
493 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
496 #- fix gamma correction
497 if $modified_pixbufs[thumbnail_img][:gammacorrect]
498 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
501 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
504 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
507 #- update rotate attribute
508 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
509 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
511 #- change exif orientation if configured so (but forget in case of thumbnails caption)
512 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
513 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
516 $modified_pixbufs[thumbnail_img] ||= {}
517 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
518 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
520 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
523 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
526 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
528 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
530 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
531 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
533 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
534 $notebook.set_page(0)
535 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
540 def color_swap(xmldir, attributes_prefix)
542 rexml_thread_protect {
543 if xmldir.attributes["#{attributes_prefix}color-swap"]
544 xmldir.delete_attribute("#{attributes_prefix}color-swap")
546 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
551 def enhance(xmldir, attributes_prefix)
553 rexml_thread_protect {
554 if xmldir.attributes["#{attributes_prefix}enhance"]
555 xmldir.delete_attribute("#{attributes_prefix}enhance")
557 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
562 def change_seektime(xmldir, attributes_prefix, value)
564 rexml_thread_protect {
565 xmldir.add_attribute("#{attributes_prefix}seektime", value)
569 def ask_new_seektime(xmldir, attributes_prefix)
570 value = rexml_thread_protect {
572 xmldir.attributes["#{attributes_prefix}seektime"]
578 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
580 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
581 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
582 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
586 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
590 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
591 entry.signal_connect('key-press-event') { |w, event|
592 if event.keyval == Gdk::Keyval::GDK_Return
593 dialog.response(Gtk::Dialog::RESPONSE_OK)
595 elsif event.keyval == Gdk::Keyval::GDK_Escape
596 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
599 false #- propagate if needed
603 dialog.window_position = Gtk::Window::POS_MOUSE
606 dialog.run { |response|
609 if response == Gtk::Dialog::RESPONSE_OK
611 msg 3, "changing seektime to #{newval}"
612 return { :old => value, :new => newval }
619 def change_pano_amount(xmldir, attributes_prefix, value)
621 rexml_thread_protect {
623 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
625 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
630 def ask_new_pano_amount(xmldir, attributes_prefix)
631 value = rexml_thread_protect {
633 xmldir.attributes["#{attributes_prefix}pano-amount"]
639 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
641 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
642 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
643 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
647 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
648 of this panorama image compared to other regular images. For example, if the panorama
649 was taken out of four photos on one row, counting the necessary overlap, the width of
650 this panorama image should probably be roughly three times the width of regular images.
652 With this information, booh will be able to generate panorama thumbnails looking
653 the right 'size', since the height of the thumbnail for this image will be similar
654 to the height of other thumbnails.
657 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)")))).
658 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
659 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
660 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
661 spin.signal_connect('value-changed') {
664 dialog.window_position = Gtk::Window::POS_MOUSE
667 spin.value = value.to_f
674 dialog.run { |response|
678 newval = spin.value.to_f
681 if response == Gtk::Dialog::RESPONSE_OK
683 msg 3, "changing panorama amount to #{newval}"
684 return { :old => value, :new => newval }
691 def change_whitebalance(xmlelem, attributes_prefix, value)
693 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
696 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
698 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
699 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
700 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
701 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
702 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
703 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
704 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
705 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
706 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
707 $modified_pixbufs[thumbnail_img] ||= {}
708 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
709 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
711 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
712 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
714 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
717 $modified_pixbufs[thumbnail_img] ||= {}
718 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
720 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
723 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
724 #- init $modified_pixbufs correctly
725 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
727 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
729 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
731 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
732 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
733 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
737 _("You can fix the <b>white balance</b> of the image, if your image is too blue
738 or too yellow because the recorder didn't detect the light correctly. Drag the
739 slider below the image to the left for more blue, to the right for more yellow.
743 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
745 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
747 dialog.window_position = Gtk::Window::POS_MOUSE
751 timeout = Gtk.timeout_add(100) {
752 if hs.value != lastval
755 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
761 dialog.run { |response|
762 Gtk.timeout_remove(timeout)
763 if response == Gtk::Dialog::RESPONSE_OK
765 newval = hs.value.to_s
766 msg 3, "changing white balance to #{newval}"
768 return { :old => value, :new => newval }
771 $modified_pixbufs[thumbnail_img] ||= {}
772 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
773 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
781 def change_gammacorrect(xmlelem, attributes_prefix, value)
783 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
786 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
788 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
789 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
790 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
791 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
792 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
793 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
794 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
795 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
796 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
797 $modified_pixbufs[thumbnail_img] ||= {}
798 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
799 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
801 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
802 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
804 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
807 $modified_pixbufs[thumbnail_img] ||= {}
808 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
810 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
813 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
814 #- init $modified_pixbufs correctly
815 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
817 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
819 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
821 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
822 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
823 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
827 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
828 or too bright. Drag the slider below the image.
832 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
834 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
836 dialog.window_position = Gtk::Window::POS_MOUSE
840 timeout = Gtk.timeout_add(100) {
841 if hs.value != lastval
844 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
850 dialog.run { |response|
851 Gtk.timeout_remove(timeout)
852 if response == Gtk::Dialog::RESPONSE_OK
854 newval = hs.value.to_s
855 msg 3, "gamma correction to #{newval}"
857 return { :old => value, :new => newval }
860 $modified_pixbufs[thumbnail_img] ||= {}
861 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
862 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
870 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
871 if File.exists?(destfile)
872 File.delete(destfile)
874 #- type can be 'element' or 'subdir'
876 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
878 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
882 $max_gen_thumbnail_threads = nil
883 $current_gen_thumbnail_threads = 0
884 $gen_thumbnail_monitor = Monitor.new
886 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
887 if $max_gen_thumbnail_threads.nil?
888 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
891 push_mousecursor_wait
892 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
895 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
900 $gen_thumbnail_monitor.synchronize {
901 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
902 $current_gen_thumbnail_threads += 1
907 msg 3, "generate thumbnail from new thread"
910 $gen_thumbnail_monitor.synchronize {
911 $current_gen_thumbnail_threads -= 1
915 msg 3, "generate thumbnail from current thread"
920 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
921 distribute_multiple_call = Proc.new { |action, arg|
922 $selected_elements.each_key { |path|
923 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
925 if possible_actions[:can_multiple] && $selected_elements.length > 0
926 UndoHandler.begin_batch
927 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
928 UndoHandler.end_batch
930 closures[action].call(arg)
932 $selected_elements = {}
935 if optionals.include?('change_image')
936 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
937 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
938 changeimg.signal_connect('activate') { closures[:change].call }
939 menu.append(Gtk::SeparatorMenuItem.new)
941 if !possible_actions[:can_multiple] || $selected_elements.length == 0
944 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
945 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
946 view.signal_connect('activate') { closures[:view].call }
948 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
949 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
950 view.signal_connect('activate') { closures[:view].call }
951 menu.append(Gtk::SeparatorMenuItem.new)
954 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
955 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
956 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
957 exif.signal_connect('activate') { show_popup($main_window,
958 utf8(`exif -m '#{fullpath}'`),
959 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
960 menu.append(Gtk::SeparatorMenuItem.new)
963 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
964 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
965 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
966 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
967 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
968 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
969 if !possible_actions[:can_multiple] || $selected_elements.length == 0
970 menu.append(Gtk::SeparatorMenuItem.new)
971 if !possible_actions[:forbid_left]
972 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
973 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
974 moveleft.signal_connect('activate') { closures[:move].call('left') }
975 if !possible_actions[:can_left]
976 moveleft.sensitive = false
979 if !possible_actions[:forbid_right]
980 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
981 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
982 moveright.signal_connect('activate') { closures[:move].call('right') }
983 if !possible_actions[:can_right]
984 moveright.sensitive = false
987 if optionals.include?('move_top')
988 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
989 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
990 movetop.signal_connect('activate') { closures[:move].call('top') }
991 if !possible_actions[:can_top]
992 movetop.sensitive = false
995 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
996 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
997 moveup.signal_connect('activate') { closures[:move].call('up') }
998 if !possible_actions[:can_up]
999 moveup.sensitive = false
1001 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
1002 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
1003 movedown.signal_connect('activate') { closures[:move].call('down') }
1004 if !possible_actions[:can_down]
1005 movedown.sensitive = false
1007 if optionals.include?('move_bottom')
1008 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
1009 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
1010 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
1011 if !possible_actions[:can_bottom]
1012 movebottom.sensitive = false
1017 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
1018 menu.append(Gtk::SeparatorMenuItem.new)
1019 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
1020 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
1021 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
1022 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1023 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1024 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1025 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1026 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1027 seektime.signal_connect('activate') {
1028 if possible_actions[:can_multiple] && $selected_elements.length > 0
1029 if values = ask_new_seektime(nil, '')
1030 distribute_multiple_call.call(:seektime, values)
1033 closures[:seektime].call
1038 menu.append( Gtk::SeparatorMenuItem.new)
1039 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1040 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1041 gammacorrect.signal_connect('activate') {
1042 if possible_actions[:can_multiple] && $selected_elements.length > 0
1043 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1044 distribute_multiple_call.call(:gammacorrect, values)
1047 closures[:gammacorrect].call
1050 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1051 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1052 whitebalance.signal_connect('activate') {
1053 if possible_actions[:can_multiple] && $selected_elements.length > 0
1054 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1055 distribute_multiple_call.call(:whitebalance, values)
1058 closures[:whitebalance].call
1061 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1062 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1063 _("Enhance constrast"))))
1065 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1067 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1068 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1069 if type == 'image' && possible_actions[:can_panorama]
1070 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1071 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1072 panorama.signal_connect('activate') {
1073 if possible_actions[:can_multiple] && $selected_elements.length > 0
1074 if values = ask_new_pano_amount(nil, '')
1075 distribute_multiple_call.call(:pano, values)
1078 distribute_multiple_call.call(:pano)
1082 menu.append( Gtk::SeparatorMenuItem.new)
1083 if optionals.include?('delete')
1084 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1085 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1086 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1087 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1088 paste_item.signal_connect('activate') { closures[:paste].call }
1089 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1090 clear_item.signal_connect('activate') { $cuts = [] }
1092 paste_item.sensitive = clear_item.sensitive = false
1095 menu.append( Gtk::SeparatorMenuItem.new)
1097 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1098 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1099 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1100 editexternally.signal_connect('activate') {
1101 if check_image_editor
1102 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1108 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1109 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1110 if optionals.include?('delete')
1111 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1112 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1115 menu.popup(nil, nil, event.button, event.time)
1118 def delete_current_subalbum
1120 sel = $albums_tv.selection.selected_rows
1121 $xmldir.elements.each { |e|
1122 if e.name == 'image' || e.name == 'video'
1123 e.add_attribute('deleted', 'true')
1126 #- branch if we have a non deleted subalbum
1127 if $xmldir.child_byname_notattr('dir', 'deleted')
1128 $xmldir.delete_attribute('thumbnails-caption')
1129 $xmldir.delete_attribute('thumbnails-captionfile')
1131 $xmldir.add_attribute('deleted', 'true')
1133 while moveup.parent.name == 'dir'
1134 moveup = moveup.parent
1135 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1136 moveup.add_attribute('deleted', 'true')
1143 save_changes('forced')
1144 populate_subalbums_treeview(false)
1145 $albums_tv.selection.select_path(sel[0])
1151 $current_path = nil #- prevent save_changes from being rerun again
1152 sel = $albums_tv.selection.selected_rows
1153 restore_one = proc { |xmldir|
1154 xmldir.elements.each { |e|
1155 if e.name == 'dir' && e.attributes['deleted']
1158 e.delete_attribute('deleted')
1161 restore_one.call($xmldir)
1162 populate_subalbums_treeview(false)
1163 $albums_tv.selection.select_path(sel[0])
1166 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1169 frame1 = Gtk::Frame.new
1170 fullpath = from_utf8("#{$current_path}/#{filename}")
1172 my_gen_real_thumbnail = proc {
1173 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1177 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1178 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1179 pack_start(img = Gtk::Image.new).
1180 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1181 px, mask = pxb.render_pixmap_and_mask(0)
1182 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1183 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1185 frame1.add(img = Gtk::Image.new)
1188 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1189 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1190 my_gen_real_thumbnail.call
1192 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1195 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1197 tooltips = Gtk::Tooltips.new
1198 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1199 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1201 frame2, textview = create_editzone($autotable_sw, 1, img)
1202 textview.buffer.text = caption
1203 textview.set_justification(Gtk::Justification::CENTER)
1205 vbox = Gtk::VBox.new(false, 5)
1206 vbox.pack_start(evtbox, false, false)
1207 vbox.pack_start(frame2, false, false)
1208 autotable.append(vbox, filename)
1210 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1211 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1213 #- to be able to find widgets by name
1214 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1216 cleanup_all_thumbnails = proc {
1217 #- remove out of sync images
1218 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1219 for sizeobj in $images_size
1220 #- cannot use sizeobj because panoramic images will have a larger width
1221 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1229 cleanup_all_thumbnails.call
1230 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1232 rexml_thread_protect {
1233 $xmldir.delete_attribute('already-generated')
1235 my_gen_real_thumbnail.call
1238 rotate_and_cleanup = proc { |angle|
1239 cleanup_all_thumbnails.call
1240 rexml_thread_protect {
1241 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1245 move = proc { |direction|
1246 do_method = "move_#{direction}"
1247 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1249 done = autotable.method(do_method).call(vbox)
1250 textview.grab_focus #- because if moving, focus is stolen
1254 save_undo(_("move %s") % direction,
1256 autotable.method(undo_method).call(vbox)
1257 textview.grab_focus #- because if moving, focus is stolen
1258 autoscroll_if_needed($autotable_sw, img, textview)
1259 $notebook.set_page(1)
1261 autotable.method(do_method).call(vbox)
1262 textview.grab_focus #- because if moving, focus is stolen
1263 autoscroll_if_needed($autotable_sw, img, textview)
1264 $notebook.set_page(1)
1270 color_swap_and_cleanup = proc {
1271 perform_color_swap_and_cleanup = proc {
1272 cleanup_all_thumbnails.call
1273 rexml_thread_protect {
1274 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1276 my_gen_real_thumbnail.call
1279 perform_color_swap_and_cleanup.call
1281 save_undo(_("color swap"),
1283 perform_color_swap_and_cleanup.call
1285 autoscroll_if_needed($autotable_sw, img, textview)
1286 $notebook.set_page(1)
1288 perform_color_swap_and_cleanup.call
1290 autoscroll_if_needed($autotable_sw, img, textview)
1291 $notebook.set_page(1)
1296 change_seektime_and_cleanup_real = proc { |values|
1297 perform_change_seektime_and_cleanup = proc { |val|
1298 cleanup_all_thumbnails.call
1299 rexml_thread_protect {
1300 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1302 my_gen_real_thumbnail.call
1304 perform_change_seektime_and_cleanup.call(values[:new])
1306 save_undo(_("specify seektime"),
1308 perform_change_seektime_and_cleanup.call(values[:old])
1310 autoscroll_if_needed($autotable_sw, img, textview)
1311 $notebook.set_page(1)
1313 perform_change_seektime_and_cleanup.call(values[:new])
1315 autoscroll_if_needed($autotable_sw, img, textview)
1316 $notebook.set_page(1)
1321 change_seektime_and_cleanup = proc {
1322 rexml_thread_protect {
1323 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1324 change_seektime_and_cleanup_real.call(values)
1329 change_pano_amount_and_cleanup_real = proc { |values|
1330 perform_change_pano_amount_and_cleanup = proc { |val|
1331 cleanup_all_thumbnails.call
1332 rexml_thread_protect {
1333 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1336 perform_change_pano_amount_and_cleanup.call(values[:new])
1338 save_undo(_("change panorama amount"),
1340 perform_change_pano_amount_and_cleanup.call(values[:old])
1342 autoscroll_if_needed($autotable_sw, img, textview)
1343 $notebook.set_page(1)
1345 perform_change_pano_amount_and_cleanup.call(values[:new])
1347 autoscroll_if_needed($autotable_sw, img, textview)
1348 $notebook.set_page(1)
1353 change_pano_amount_and_cleanup = proc {
1354 rexml_thread_protect {
1355 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1356 change_pano_amount_and_cleanup_real.call(values)
1361 whitebalance_and_cleanup_real = proc { |values|
1362 perform_change_whitebalance_and_cleanup = proc { |val|
1363 cleanup_all_thumbnails.call
1364 rexml_thread_protect {
1365 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1366 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1367 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1370 perform_change_whitebalance_and_cleanup.call(values[:new])
1372 save_undo(_("fix white balance"),
1374 perform_change_whitebalance_and_cleanup.call(values[:old])
1376 autoscroll_if_needed($autotable_sw, img, textview)
1377 $notebook.set_page(1)
1379 perform_change_whitebalance_and_cleanup.call(values[:new])
1381 autoscroll_if_needed($autotable_sw, img, textview)
1382 $notebook.set_page(1)
1387 whitebalance_and_cleanup = proc {
1388 rexml_thread_protect {
1389 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1390 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1391 whitebalance_and_cleanup_real.call(values)
1396 gammacorrect_and_cleanup_real = proc { |values|
1397 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1398 cleanup_all_thumbnails.call
1399 rexml_thread_protect {
1400 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1401 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1402 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1405 perform_change_gammacorrect_and_cleanup.call(values[:new])
1407 save_undo(_("gamma correction"),
1409 perform_change_gammacorrect_and_cleanup.call(values[:old])
1411 autoscroll_if_needed($autotable_sw, img, textview)
1412 $notebook.set_page(1)
1414 perform_change_gammacorrect_and_cleanup.call(values[:new])
1416 autoscroll_if_needed($autotable_sw, img, textview)
1417 $notebook.set_page(1)
1422 gammacorrect_and_cleanup = Proc.new {
1423 rexml_thread_protect {
1424 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1425 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1426 gammacorrect_and_cleanup_real.call(values)
1431 enhance_and_cleanup = proc {
1432 perform_enhance_and_cleanup = proc {
1433 cleanup_all_thumbnails.call
1434 rexml_thread_protect {
1435 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1437 my_gen_real_thumbnail.call
1440 cleanup_all_thumbnails.call
1441 perform_enhance_and_cleanup.call
1443 save_undo(_("enhance"),
1445 perform_enhance_and_cleanup.call
1447 autoscroll_if_needed($autotable_sw, img, textview)
1448 $notebook.set_page(1)
1450 perform_enhance_and_cleanup.call
1452 autoscroll_if_needed($autotable_sw, img, textview)
1453 $notebook.set_page(1)
1458 delete = proc { |isacut|
1459 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 })
1462 perform_delete = proc {
1463 after = autotable.get_next_widget(vbox)
1465 after = autotable.get_previous_widget(vbox)
1467 if $config['deleteondisk'] && !isacut
1468 msg 3, "scheduling for delete: #{fullpath}"
1469 $todelete << fullpath
1471 autotable.remove_widget(vbox)
1473 $vbox2widgets[after][:textview].grab_focus
1474 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1478 previous_pos = autotable.get_current_number(vbox)
1482 delete_current_subalbum
1484 save_undo(_("delete"),
1486 autotable.reinsert(pos, vbox, filename)
1487 $notebook.set_page(1)
1488 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1490 msg 3, "removing deletion schedule of: #{fullpath}"
1491 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1494 $notebook.set_page(1)
1503 $cuts << { :vbox => vbox, :filename => filename }
1504 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1509 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1512 autotable.queue_draws << proc {
1513 $vbox2widgets[last[:vbox]][:textview].grab_focus
1514 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1516 save_undo(_("paste"),
1518 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1519 $notebook.set_page(1)
1522 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1524 $notebook.set_page(1)
1527 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1532 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1533 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1534 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1535 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1537 textview.signal_connect('key-press-event') { |w, event|
1540 x, y = autotable.get_current_pos(vbox)
1541 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1542 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1543 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1544 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1546 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1547 $vbox2widgets[widget_up][:textview].grab_focus
1554 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1556 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1557 $vbox2widgets[widget_down][:textview].grab_focus
1564 if event.keyval == Gdk::Keyval::GDK_Left
1567 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1574 rotate_and_cleanup.call(-90)
1577 if event.keyval == Gdk::Keyval::GDK_Right
1578 next_ = autotable.get_next_widget(vbox)
1579 if next_ && autotable.get_current_pos(next_)[0] > x
1581 $vbox2widgets[next_][:textview].grab_focus
1588 rotate_and_cleanup.call(90)
1591 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1594 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1595 view_element(filename, { :delete => delete })
1598 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1601 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1605 !propagate #- propagate if needed
1608 $ignore_next_release = false
1609 evtbox.signal_connect('button-press-event') { |w, event|
1610 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1611 if event.state & Gdk::Window::BUTTON3_MASK != 0
1612 #- gesture redo: hold right mouse button then click left mouse button
1613 $config['nogestures'] or perform_redo
1614 $ignore_next_release = true
1616 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1618 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1620 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1621 elsif $enhance.active?
1622 enhance_and_cleanup.call
1623 elsif $delete.active?
1627 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1630 $button1_pressed_autotable = true
1631 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1632 if event.state & Gdk::Window::BUTTON1_MASK != 0
1633 #- gesture undo: hold left mouse button then click right mouse button
1634 $config['nogestures'] or perform_undo
1635 $ignore_next_release = true
1637 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1638 view_element(filename, { :delete => delete })
1643 evtbox.signal_connect('button-release-event') { |w, event|
1644 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1645 if !$ignore_next_release
1646 x, y = autotable.get_current_pos(vbox)
1647 next_ = autotable.get_next_widget(vbox)
1648 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1649 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1650 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1651 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1652 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1653 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1654 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1656 $ignore_next_release = false
1657 $gesture_press = nil
1662 #- handle reordering with drag and drop
1663 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1664 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1665 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1666 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1669 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1671 #- mouse gesture first (dnd disables button-release-event)
1672 if $gesture_press && $gesture_press[:filename] == filename
1673 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1674 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1675 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1676 rotate_and_cleanup.call(angle)
1677 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1679 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1680 msg 3, "gesture delete: click-drag right button to the bottom"
1682 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1687 ctxt.targets.each { |target|
1688 if target.name == 'reorder-elements'
1689 move_dnd = proc { |from,to|
1692 autotable.move(from, to)
1693 save_undo(_("reorder"),
1696 autotable.move(to - 1, from)
1698 autotable.move(to, from + 1)
1700 $notebook.set_page(1)
1702 autotable.move(from, to)
1703 $notebook.set_page(1)
1708 if $multiple_dnd.size == 0
1709 move_dnd.call(selection_data.data.to_i,
1710 autotable.get_current_number(vbox))
1712 UndoHandler.begin_batch
1713 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1715 #- need to update current position between each call
1716 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1717 autotable.get_current_number(vbox))
1719 UndoHandler.end_batch
1730 def create_auto_table
1732 $autotable = Gtk::AutoTable.new(5)
1734 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1735 thumbnails_vb = Gtk::VBox.new(false, 5)
1737 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1738 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1739 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1740 thumbnails_vb.add($autotable)
1742 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1743 $autotable_sw.add_with_viewport(thumbnails_vb)
1745 #- follows stuff for handling multiple elements selection
1746 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1748 update_selected = proc {
1749 $autotable.current_order.each { |path|
1750 w = $name2widgets[path][:evtbox].window
1751 xm = w.position[0] + w.size[0]/2
1752 ym = w.position[1] + w.size[1]/2
1753 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1754 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1755 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1756 if $name2widgets[path][:img].pixbuf
1757 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1761 if $selected_elements[path] && ! $selected_elements[path][:keep]
1762 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))
1763 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1764 $selected_elements.delete(path)
1769 $autotable.signal_connect('realize') { |w,e|
1770 gc = Gdk::GC.new($autotable.window)
1771 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1772 gc.function = Gdk::GC::INVERT
1773 #- autoscroll handling for DND and multiple selections
1774 Gtk.timeout_add(100) {
1775 if ! $autotable.window.nil?
1776 w, x, y, mask = $autotable.window.pointer
1777 if mask & Gdk::Window::BUTTON1_MASK != 0
1778 if y < $autotable_sw.vadjustment.value
1780 $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]])
1782 if $button1_pressed_autotable || press_x
1783 scroll_upper($autotable_sw, y)
1786 w, pos_x, pos_y = $autotable.window.pointer
1787 $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]])
1788 update_selected.call
1791 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1793 $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]])
1795 if $button1_pressed_autotable || press_x
1796 scroll_lower($autotable_sw, y)
1799 w, pos_x, pos_y = $autotable.window.pointer
1800 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1801 update_selected.call
1806 ! $autotable.window.nil?
1810 $autotable.signal_connect('button-press-event') { |w,e|
1812 if !$button1_pressed_autotable
1815 if e.state & Gdk::Window::SHIFT_MASK == 0
1816 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1817 $selected_elements = {}
1818 $statusbar.push(0, utf8(_("Nothing selected.")))
1820 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1822 set_mousecursor(Gdk::Cursor::TCROSS)
1826 $autotable.signal_connect('button-release-event') { |w,e|
1828 if $button1_pressed_autotable
1829 #- unselect all only now
1830 $multiple_dnd = $selected_elements.keys
1831 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1832 $selected_elements = {}
1833 $button1_pressed_autotable = false
1836 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1837 if $selected_elements.length > 0
1838 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1841 press_x = press_y = pos_x = pos_y = nil
1842 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1846 $autotable.signal_connect('motion-notify-event') { |w,e|
1849 $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]])
1853 $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]])
1854 update_selected.call
1860 def create_subalbums_page
1862 subalbums_hb = Gtk::HBox.new
1863 $subalbums_vb = Gtk::VBox.new(false, 5)
1864 subalbums_hb.pack_start($subalbums_vb, false, false)
1865 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1866 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1867 $subalbums_sw.add_with_viewport(subalbums_hb)
1870 def save_current_file
1876 ios = File.open($filename, "w")
1879 rescue Iconv::IllegalSequence
1880 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1881 if ! ios.nil? && ! ios.closed?
1884 $xmldoc.xml_decl.encoding = 'UTF-8'
1885 ios = File.open($filename, "w")
1897 def save_current_file_user
1898 save_tempfilename = $filename
1899 $filename = $orig_filename
1900 if ! save_current_file
1901 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1902 $filename = save_tempfilename
1906 $generated_outofline = false
1907 $filename = save_tempfilename
1909 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1910 $todelete.each { |f|
1914 puts "Failed to delete #{f}: #{$!}"
1920 def mark_document_as_dirty
1921 $xmldoc.elements.each('//dir') { |elem|
1922 elem.delete_attribute('already-generated')
1926 #- ret: true => ok false => cancel
1927 def ask_save_modifications(msg1, msg2, *options)
1929 options = options.size > 0 ? options[0] : {}
1931 if options[:disallow_cancel]
1932 dialog = Gtk::Dialog.new(msg1,
1934 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1935 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1936 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1938 dialog = Gtk::Dialog.new(msg1,
1940 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1941 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1942 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1943 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1945 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1946 dialog.vbox.add(Gtk::Label.new(msg2))
1947 dialog.window_position = Gtk::Window::POS_CENTER
1950 dialog.run { |response|
1952 if response == Gtk::Dialog::RESPONSE_YES
1953 if ! save_current_file_user
1954 return ask_save_modifications(msg1, msg2, options)
1957 #- if we have generated an album but won't save modifications, we must remove
1958 #- already-generated markers in original file
1959 if $generated_outofline
1961 $xmldoc = REXML::Document.new(File.new($orig_filename))
1962 mark_document_as_dirty
1963 ios = File.open($orig_filename, "w")
1967 puts "exception: #{$!}"
1971 if response == Gtk::Dialog::RESPONSE_CANCEL
1979 def try_quit(*options)
1980 if ask_save_modifications(utf8(_("Save before quitting?")),
1981 utf8(_("Do you want to save your changes before quitting?")),
1987 def show_popup(parent, msg, *options)
1988 dialog = Gtk::Dialog.new
1989 if options[0] && options[0][:title]
1990 dialog.title = options[0][:title]
1992 dialog.title = utf8(_("Booh message"))
1994 lbl = Gtk::Label.new
1995 if options[0] && options[0][:nomarkup]
2000 if options[0] && options[0][:centered]
2001 lbl.set_justify(Gtk::Justification::CENTER)
2003 if options[0] && options[0][:selectable]
2004 lbl.selectable = true
2006 if options[0] && options[0][:scrolled]
2007 sw = Gtk::ScrolledWindow.new(nil, nil)
2008 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
2009 sw.add_with_viewport(lbl)
2011 dialog.set_default_size(500, 600)
2013 dialog.vbox.add(lbl)
2014 dialog.set_default_size(200, 120)
2016 if options[0] && options[0][:okcancel]
2017 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2019 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2021 if options[0] && options[0][:pos_centered]
2022 dialog.window_position = Gtk::Window::POS_CENTER
2024 dialog.window_position = Gtk::Window::POS_MOUSE
2027 if options[0] && options[0][:linkurl]
2028 linkbut = Gtk::Button.new('')
2029 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2030 linkbut.signal_connect('clicked') {
2031 open_url(options[0][:linkurl])
2032 dialog.response(Gtk::Dialog::RESPONSE_OK)
2033 set_mousecursor_normal
2035 linkbut.relief = Gtk::RELIEF_NONE
2036 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2037 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2038 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2043 if !options[0] || !options[0][:not_transient]
2044 dialog.transient_for = parent
2045 dialog.run { |response|
2047 if options[0] && options[0][:okcancel]
2048 return response == Gtk::Dialog::RESPONSE_OK
2052 dialog.signal_connect('response') { dialog.destroy }
2056 def set_mainwindow_title(progress)
2057 filename = $orig_filename || $filename
2060 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2062 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2066 $main_window.title = 'booh - ' + File.basename(filename)
2068 $main_window.title = 'booh'
2073 def backend_wait_message(parent, msg, infopipe_path, mode)
2075 w.set_transient_for(parent)
2078 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2079 vb.pack_start(Gtk::Label.new(msg), false, false)
2081 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2082 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2083 if mode != 'one dir scan'
2084 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2086 if mode == 'web-album'
2087 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2088 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2090 vb.pack_start(Gtk::HSeparator.new, false, false)
2092 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2093 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2094 vb.pack_end(bottom, false, false)
2097 update_progression_title_pb1 = proc {
2098 if mode == 'web-album'
2099 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2100 elsif mode != 'one dir scan'
2101 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2103 set_mainwindow_title(pb1_1.fraction)
2107 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2108 refresh_thread = Thread.new {
2109 directories_counter = 0
2110 while line = infopipe.gets
2111 msg 3, "infopipe got data: #{line}"
2112 if line =~ /^directories: (\d+), sizes: (\d+)/
2113 directories = $1.to_f + 1
2115 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2116 elements = $3.to_f + 1
2117 if mode == 'web-album'
2121 gtk_thread_protect { pb1_1.fraction = 0 }
2122 if mode != 'one dir scan'
2123 newtext = utf8(full_src_dir_to_rel($1, $2))
2124 newtext = '/' if newtext == ''
2125 gtk_thread_protect { pb1_2.text = newtext }
2126 directories_counter += 1
2127 gtk_thread_protect {
2128 pb1_2.fraction = directories_counter / directories
2129 update_progression_title_pb1.call
2132 elsif line =~ /^processing element$/
2133 element_counter += 1
2134 gtk_thread_protect {
2135 pb1_1.fraction = element_counter / elements
2136 update_progression_title_pb1.call
2138 elsif line =~ /^processing size$/
2139 element_counter += 1
2140 gtk_thread_protect {
2141 pb1_1.fraction = element_counter / elements
2142 update_progression_title_pb1.call
2144 elsif line =~ /^finished processing sizes$/
2145 gtk_thread_protect { pb1_1.fraction = 1 }
2146 elsif line =~ /^creating index.html$/
2147 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2148 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2149 directories_counter = 0
2150 elsif line =~ /^index.html: (.+)\|(.+)/
2151 newtext = utf8(full_src_dir_to_rel($1, $2))
2152 newtext = '/' if newtext == ''
2153 gtk_thread_protect { pb2.text = newtext }
2154 directories_counter += 1
2155 gtk_thread_protect {
2156 pb2.fraction = directories_counter / directories
2157 set_mainwindow_title(0.9 + pb2.fraction / 10)
2159 elsif line =~ /^die: (.*)$/
2166 w.signal_connect('delete-event') { w.destroy }
2167 w.signal_connect('destroy') {
2168 Thread.kill(refresh_thread)
2169 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2172 File.delete(infopipe_path)
2174 set_mainwindow_title(nil)
2176 w.window_position = Gtk::Window::POS_CENTER
2182 def call_backend(cmd, waitmsg, mode, params)
2183 pipe = Tempfile.new("boohpipe")
2184 Thread.critical = true
2187 system("mkfifo #{path}")
2188 Thread.critical = false
2189 cmd += " --info-pipe #{path}"
2190 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2195 id, exitstatus = Process.waitpid2(pid)
2196 gtk_thread_protect { w8.destroy }
2198 if params[:successmsg]
2199 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2201 if params[:closure_after]
2202 gtk_thread_protect(¶ms[:closure_after])
2204 elsif exitstatus == 15
2205 #- say nothing, user aborted
2207 gtk_thread_protect { show_popup($main_window,
2208 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2214 button.signal_connect('clicked') {
2215 Process.kill('SIGTERM', pid)
2219 def save_changes(*forced)
2220 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2224 $xmldir.delete_attribute('already-generated')
2226 propagate_children = proc { |xmldir|
2227 if xmldir.attributes['subdirs-caption']
2228 xmldir.delete_attribute('already-generated')
2230 xmldir.elements.each('dir') { |element|
2231 propagate_children.call(element)
2235 if $xmldir.child_byname_notattr('dir', 'deleted')
2236 new_title = $subalbums_title.buffer.text
2237 if new_title != $xmldir.attributes['subdirs-caption']
2238 parent = $xmldir.parent
2239 if parent.name == 'dir'
2240 parent.delete_attribute('already-generated')
2242 propagate_children.call($xmldir)
2244 $xmldir.add_attribute('subdirs-caption', new_title)
2245 $xmldir.elements.each('dir') { |element|
2246 if !element.attributes['deleted']
2247 path = element.attributes['path']
2248 newtext = $subalbums_edits[path][:editzone].buffer.text
2249 if element.attributes['subdirs-caption']
2250 if element.attributes['subdirs-caption'] != newtext
2251 propagate_children.call(element)
2253 element.add_attribute('subdirs-caption', newtext)
2254 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2256 if element.attributes['thumbnails-caption'] != newtext
2257 element.delete_attribute('already-generated')
2259 element.add_attribute('thumbnails-caption', newtext)
2260 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2266 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2267 if $xmldir.attributes['thumbnails-caption']
2268 path = $xmldir.attributes['path']
2269 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2271 elsif $xmldir.attributes['thumbnails-caption']
2272 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2275 if $xmldir.attributes['thumbnails-caption']
2276 if edit = $subalbums_edits[$xmldir.attributes['path']]
2277 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2281 #- remove and reinsert elements to reflect new ordering
2284 $xmldir.elements.each { |element|
2285 if element.name == 'image' || element.name == 'video'
2286 saves[element.attributes['filename']] = element.remove
2290 $autotable.current_order.each { |path|
2291 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2292 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2295 saves.each_key { |path|
2296 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2297 chld.add_attribute('deleted', 'true')
2301 def sort_by_exif_date
2305 rexml_thread_protect {
2306 $xmldir.elements.each { |element|
2307 if element.name == 'image' || element.name == 'video'
2308 current_order << element.attributes['filename']
2313 #- look for EXIF dates
2316 if current_order.size > 20
2318 w.set_transient_for($main_window)
2320 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2321 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2322 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2323 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2324 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2325 vb.pack_end(bottom, false, false)
2327 w.signal_connect('delete-event') { w.destroy }
2328 w.window_position = Gtk::Window::POS_CENTER
2332 b.signal_connect('clicked') { aborted = true }
2334 current_order.each { |f|
2336 if entry2type(f) == 'image'
2338 pb.fraction = i.to_f / current_order.size
2339 Gtk.main_iteration while Gtk.events_pending?
2340 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2342 dates[f] = date_time
2355 current_order.each { |f|
2356 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2358 dates[f] = date_time
2364 rexml_thread_protect {
2365 $xmldir.elements.each { |element|
2366 if element.name == 'image' || element.name == 'video'
2367 saves[element.attributes['filename']] = element.remove
2372 neworder = smartsort(current_order, dates)
2374 rexml_thread_protect {
2376 $xmldir.add_element(saves[f].name, saves[f].attributes)
2380 #- let the auto-table reflect new ordering
2384 def remove_all_captions
2387 $autotable.current_order.each { |path|
2388 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2389 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2391 save_undo(_("remove all captions"),
2393 texts.each_key { |key|
2394 $name2widgets[key][:textview].buffer.text = texts[key]
2396 $notebook.set_page(1)
2398 texts.each_key { |key|
2399 $name2widgets[key][:textview].buffer.text = ''
2401 $notebook.set_page(1)
2407 $selected_elements.each_key { |path|
2408 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2414 $selected_elements = {}
2418 $undo_tb.sensitive = $undo_mb.sensitive = false
2419 $redo_tb.sensitive = $redo_mb.sensitive = false
2425 $subalbums_vb.children.each { |chld|
2426 $subalbums_vb.remove(chld)
2428 $subalbums = Gtk::Table.new(0, 0, true)
2429 current_y_sub_albums = 0
2431 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2432 $subalbums_edits = {}
2433 subalbums_counter = 0
2434 subalbums_edits_bypos = {}
2436 add_subalbum = proc { |xmldir, counter|
2437 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2438 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2439 if xmldir == $xmldir
2440 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2441 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2442 caption = xmldir.attributes['thumbnails-caption']
2443 infotype = 'thumbnails'
2445 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2446 captionfile, caption = find_subalbum_caption_info(xmldir)
2447 infotype = find_subalbum_info_type(xmldir)
2449 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2450 hbox = Gtk::HBox.new
2451 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2453 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2456 my_gen_real_thumbnail = proc {
2457 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2460 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2461 f.add(img = Gtk::Image.new)
2462 my_gen_real_thumbnail.call
2464 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2466 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2467 $subalbums.attach(hbox,
2468 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2470 frame, textview = create_editzone($subalbums_sw, 0, img)
2471 textview.buffer.text = caption
2472 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2473 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2475 change_image = proc {
2476 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2478 Gtk::FileChooser::ACTION_OPEN,
2480 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2481 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2482 fc.transient_for = $main_window
2483 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))
2484 f.add(preview_img = Gtk::Image.new)
2486 fc.signal_connect('update-preview') { |w|
2487 if fc.preview_filename
2488 if entry2type(fc.preview_filename) == 'video'
2492 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2494 fc.preview_widget_active = false
2496 tmpimage = "#{tmpdir}/00000001.jpg"
2498 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2499 fc.preview_widget_active = true
2500 rescue Gdk::PixbufError
2501 fc.preview_widget_active = false
2503 File.delete(tmpimage)
2510 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2511 fc.preview_widget_active = true
2512 rescue Gdk::PixbufError
2513 fc.preview_widget_active = false
2518 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2520 old_file = captionfile
2521 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2522 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2523 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2524 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2526 new_file = fc.filename
2527 msg 3, "new captionfile is: #{fc.filename}"
2528 perform_changefile = proc {
2529 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2530 $modified_pixbufs.delete(thumbnail_file)
2531 xmldir.delete_attribute("#{infotype}-rotate")
2532 xmldir.delete_attribute("#{infotype}-color-swap")
2533 xmldir.delete_attribute("#{infotype}-enhance")
2534 xmldir.delete_attribute("#{infotype}-seektime")
2535 my_gen_real_thumbnail.call
2537 perform_changefile.call
2539 save_undo(_("change caption file for sub-album"),
2541 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2542 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2543 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2544 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2545 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2546 my_gen_real_thumbnail.call
2547 $notebook.set_page(0)
2549 perform_changefile.call
2550 $notebook.set_page(0)
2558 if File.exists?(thumbnail_file)
2559 File.delete(thumbnail_file)
2561 my_gen_real_thumbnail.call
2564 rotate_and_cleanup = proc { |angle|
2565 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2566 if File.exists?(thumbnail_file)
2567 File.delete(thumbnail_file)
2571 move = proc { |direction|
2574 save_changes('forced')
2575 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2576 if direction == 'up'
2577 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2578 subalbums_edits_bypos[oldpos - 1][:position] += 1
2580 if direction == 'down'
2581 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2582 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2584 if direction == 'top'
2585 for i in 1 .. oldpos - 1
2586 subalbums_edits_bypos[i][:position] += 1
2588 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2590 if direction == 'bottom'
2591 for i in oldpos + 1 .. subalbums_counter
2592 subalbums_edits_bypos[i][:position] -= 1
2594 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2598 $xmldir.elements.each('dir') { |element|
2599 if (!element.attributes['deleted'])
2600 elems << [ element.attributes['path'], element.remove ]
2603 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2604 each { |e| $xmldir.add_element(e[1]) }
2605 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2606 $xmldir.elements.each('descendant::dir') { |elem|
2607 elem.delete_attribute('already-generated')
2610 sel = $albums_tv.selection.selected_rows
2612 populate_subalbums_treeview(false)
2613 $albums_tv.selection.select_path(sel[0])
2616 color_swap_and_cleanup = proc {
2617 perform_color_swap_and_cleanup = proc {
2618 color_swap(xmldir, "#{infotype}-")
2619 my_gen_real_thumbnail.call
2621 perform_color_swap_and_cleanup.call
2623 save_undo(_("color swap"),
2625 perform_color_swap_and_cleanup.call
2626 $notebook.set_page(0)
2628 perform_color_swap_and_cleanup.call
2629 $notebook.set_page(0)
2634 change_seektime_and_cleanup = proc {
2635 if values = ask_new_seektime(xmldir, "#{infotype}-")
2636 perform_change_seektime_and_cleanup = proc { |val|
2637 change_seektime(xmldir, "#{infotype}-", val)
2638 my_gen_real_thumbnail.call
2640 perform_change_seektime_and_cleanup.call(values[:new])
2642 save_undo(_("specify seektime"),
2644 perform_change_seektime_and_cleanup.call(values[:old])
2645 $notebook.set_page(0)
2647 perform_change_seektime_and_cleanup.call(values[:new])
2648 $notebook.set_page(0)
2654 whitebalance_and_cleanup = proc {
2655 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2656 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2657 perform_change_whitebalance_and_cleanup = proc { |val|
2658 change_whitebalance(xmldir, "#{infotype}-", val)
2659 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2660 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2661 if File.exists?(thumbnail_file)
2662 File.delete(thumbnail_file)
2665 perform_change_whitebalance_and_cleanup.call(values[:new])
2667 save_undo(_("fix white balance"),
2669 perform_change_whitebalance_and_cleanup.call(values[:old])
2670 $notebook.set_page(0)
2672 perform_change_whitebalance_and_cleanup.call(values[:new])
2673 $notebook.set_page(0)
2679 gammacorrect_and_cleanup = proc {
2680 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2681 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2682 perform_change_gammacorrect_and_cleanup = proc { |val|
2683 change_gammacorrect(xmldir, "#{infotype}-", val)
2684 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2685 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2686 if File.exists?(thumbnail_file)
2687 File.delete(thumbnail_file)
2690 perform_change_gammacorrect_and_cleanup.call(values[:new])
2692 save_undo(_("gamma correction"),
2694 perform_change_gammacorrect_and_cleanup.call(values[:old])
2695 $notebook.set_page(0)
2697 perform_change_gammacorrect_and_cleanup.call(values[:new])
2698 $notebook.set_page(0)
2704 enhance_and_cleanup = proc {
2705 perform_enhance_and_cleanup = proc {
2706 enhance(xmldir, "#{infotype}-")
2707 my_gen_real_thumbnail.call
2710 perform_enhance_and_cleanup.call
2712 save_undo(_("enhance"),
2714 perform_enhance_and_cleanup.call
2715 $notebook.set_page(0)
2717 perform_enhance_and_cleanup.call
2718 $notebook.set_page(0)
2723 evtbox.signal_connect('button-press-event') { |w, event|
2724 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2726 rotate_and_cleanup.call(90)
2728 rotate_and_cleanup.call(-90)
2729 elsif $enhance.active?
2730 enhance_and_cleanup.call
2733 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2734 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2735 { :forbid_left => true, :forbid_right => true,
2736 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2737 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2738 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2739 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2740 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2742 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2747 evtbox.signal_connect('button-press-event') { |w, event|
2748 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2752 evtbox.signal_connect('button-release-event') { |w, event|
2753 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2754 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2755 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2756 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2757 msg 3, "gesture rotate: #{angle}"
2758 rotate_and_cleanup.call(angle)
2761 $gesture_press = nil
2764 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2765 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2766 current_y_sub_albums += 1
2769 if $xmldir.child_byname_notattr('dir', 'deleted')
2771 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2772 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2773 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2774 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2775 #- this album image/caption
2776 if $xmldir.attributes['thumbnails-caption']
2777 add_subalbum.call($xmldir, 0)
2780 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2781 $xmldir.elements.each { |element|
2782 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2783 #- element (image or video) of this album
2784 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2785 msg 3, "dest_img: #{dest_img}"
2786 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2787 total[element.name] += 1
2789 if element.name == 'dir' && !element.attributes['deleted']
2790 #- sub-album image/caption
2791 add_subalbum.call(element, subalbums_counter += 1)
2792 total[element.name] += 1
2795 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2796 total['image'], total['video'], total['dir'] ]))
2797 $subalbums_vb.add($subalbums)
2798 $subalbums_vb.show_all
2800 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2801 $notebook.get_tab_label($autotable_sw).sensitive = false
2802 $notebook.set_page(0)
2803 $thumbnails_title.buffer.text = ''
2805 $notebook.get_tab_label($autotable_sw).sensitive = true
2806 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2809 if !$xmldir.child_byname_notattr('dir', 'deleted')
2810 $notebook.get_tab_label($subalbums_sw).sensitive = false
2811 $notebook.set_page(1)
2813 $notebook.get_tab_label($subalbums_sw).sensitive = true
2817 def pixbuf_or_nil(filename)
2819 return Gdk::Pixbuf.new(filename)
2825 def theme_choose(current)
2826 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2828 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2829 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2830 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2832 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2833 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2834 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2835 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2836 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2837 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2838 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2839 treeview.signal_connect('button-press-event') { |w, event|
2840 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2841 dialog.response(Gtk::Dialog::RESPONSE_OK)
2845 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2847 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2850 iter[0] = File.basename(dir)
2851 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2852 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2853 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2854 if File.basename(dir) == current
2855 treeview.selection.select_iter(iter)
2858 dialog.set_default_size(-1, 500)
2859 dialog.vbox.show_all
2861 dialog.run { |response|
2862 iter = treeview.selection.selected
2864 if response == Gtk::Dialog::RESPONSE_OK && iter
2865 return model.get_value(iter, 0)
2871 def show_password_protections
2872 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2873 child_iter = $albums_iters[xmldir.attributes['path']]
2874 if xmldir.attributes['password-protect']
2875 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2876 already_protected = true
2877 elsif already_protected
2878 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2880 pix = pix.saturate_and_pixelate(1, true)
2886 xmldir.elements.each('dir') { |elem|
2887 if !elem.attributes['deleted']
2888 examine_dir_elem.call(child_iter, elem, already_protected)
2892 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2895 def populate_subalbums_treeview(select_first)
2899 $subalbums_vb.children.each { |chld|
2900 $subalbums_vb.remove(chld)
2903 source = $xmldoc.root.attributes['source']
2904 msg 3, "source: #{source}"
2906 xmldir = $xmldoc.elements['//dir']
2907 if !xmldir || xmldir.attributes['path'] != source
2908 msg 1, _("Corrupted booh file...")
2912 append_dir_elem = proc { |parent_iter, xmldir|
2913 child_iter = $albums_ts.append(parent_iter)
2914 child_iter[0] = File.basename(xmldir.attributes['path'])
2915 child_iter[1] = xmldir.attributes['path']
2916 $albums_iters[xmldir.attributes['path']] = child_iter
2917 msg 3, "puttin location: #{xmldir.attributes['path']}"
2918 xmldir.elements.each('dir') { |elem|
2919 if !elem.attributes['deleted']
2920 append_dir_elem.call(child_iter, elem)
2924 append_dir_elem.call(nil, xmldir)
2925 show_password_protections
2927 $albums_tv.expand_all
2929 $albums_tv.selection.select_iter($albums_ts.iter_first)
2933 def select_current_theme
2934 select_theme($xmldoc.root.attributes['theme'],
2935 $xmldoc.root.attributes['limit-sizes'],
2936 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2937 $xmldoc.root.attributes['thumbnails-per-row'])
2940 def open_file(filename)
2944 $current_path = nil #- invalidate
2945 $modified_pixbufs = {}
2948 $subalbums_vb.children.each { |chld|
2949 $subalbums_vb.remove(chld)
2952 if !File.exists?(filename)
2953 return utf8(_("File not found."))
2957 $xmldoc = REXML::Document.new(File.new(filename))
2962 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2963 if entry2type(filename).nil?
2964 return utf8(_("Not a booh file!"))
2966 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."))
2970 if !source = $xmldoc.root.attributes['source']
2971 return utf8(_("Corrupted booh file..."))
2974 if !dest = $xmldoc.root.attributes['destination']
2975 return utf8(_("Corrupted booh file..."))
2978 if !theme = $xmldoc.root.attributes['theme']
2979 return utf8(_("Corrupted booh file..."))
2982 if $xmldoc.root.attributes['version'] < $VERSION
2983 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2984 mark_document_as_dirty
2985 if $xmldoc.root.attributes['version'] < '0.8.4'
2986 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2987 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2988 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2989 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2990 if old_dest_dir != new_dest_dir
2991 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2993 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2994 xmldir.elements.each { |element|
2995 if %w(image video).include?(element.name) && !element.attributes['deleted']
2996 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2997 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2998 Dir[old_name + '*'].each { |file|
2999 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
3000 file != new_file and sys("mv '#{file}' '#{new_file}'")
3003 if element.name == 'dir' && !element.attributes['deleted']
3004 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3005 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3006 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
3010 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3014 $xmldoc.root.add_attribute('version', $VERSION)
3017 select_current_theme
3019 $filename = filename
3020 set_mainwindow_title(nil)
3021 $default_size['thumbnails'] =~ /(.*)x(.*)/
3022 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3023 $albums_thumbnail_size =~ /(.*)x(.*)/
3024 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3026 populate_subalbums_treeview(true)
3028 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
3032 def open_file_user(filename)
3033 result = open_file(filename)
3035 $config['last-opens'] ||= []
3036 if $config['last-opens'][-1] != utf8(filename)
3037 $config['last-opens'] << utf8(filename)
3039 $orig_filename = $filename
3040 $main_window.title = 'booh - ' + File.basename($orig_filename)
3041 tmp = Tempfile.new("boohtemp")
3042 Thread.critical = true
3043 $filename = tmp.path
3046 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3047 Thread.critical = false
3049 $tempfiles << $filename << "#{$filename}.backup"
3051 $orig_filename = nil
3057 if !ask_save_modifications(utf8(_("Save this album?")),
3058 utf8(_("Do you want to save the changes to this album?")),
3059 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3062 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3064 Gtk::FileChooser::ACTION_OPEN,
3066 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3067 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3068 fc.set_current_folder(File.expand_path("~/.booh"))
3069 fc.transient_for = $main_window
3070 fc.preview_widget = previewlabel = Gtk::Label.new.show
3071 fc.signal_connect('update-preview') { |w|
3072 if fc.preview_filename
3074 push_mousecursor_wait(fc)
3075 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3079 xmldoc.elements.each('//*') { |elem|
3080 if elem.name == 'dir'
3082 elsif elem.name == 'image'
3084 elsif elem.name == 'video'
3092 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3093 fc.preview_widget_active = false
3095 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") %
3096 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3097 fc.preview_widget_active = true
3103 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3104 push_mousecursor_wait(fc)
3105 msg = open_file_user(fc.filename)
3120 def additional_booh_options
3123 options += "--mproc #{$config['mproc'].to_i} "
3125 options += "--comments-format '#{$config['comments-format']}' "
3126 if $config['transcode-videos']
3127 options += "--transcode-videos '#{$config['transcode-videos']}' "
3129 if $config['use-flv'] == 'true'
3130 options += "--flv-generator '#{$config['flv-generator']}' "
3135 def ask_multi_languages(value)
3137 spl = value.split(',')
3138 value = [ spl[0..-2], spl[-1] ]
3141 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3144 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3145 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3147 lbl = Gtk::Label.new
3149 _("You can choose to activate <b>multi-languages</b> support for this web-album
3150 (it will work only if you publish your web-album on an Apache web-server). This will
3151 use the MultiViews feature of Apache; the pages will be served according to the
3152 value of the Accept-Language HTTP header sent by the web browsers, so that people
3153 with different languages preferences will be able to browse your web-album with
3154 navigation in their language (if language is available).
3157 dialog.vbox.add(lbl)
3158 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3159 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3160 add(languages = Gtk::Button.new))))
3162 pick_languages = proc {
3163 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3166 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3167 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3169 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3170 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3172 SUPPORTED_LANGUAGES.each { |lang|
3173 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3174 if ! value.nil? && value[0].include?(lang)
3180 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3181 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3182 fallback_language = nil
3183 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3184 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3185 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3186 fbl_rb.active = true
3187 fallback_language = SUPPORTED_LANGUAGES[0]
3189 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3190 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3191 rb.signal_connect('clicked') { fallback_language = lang }
3192 if ! value.nil? && value[1] == lang
3197 dialog2.window_position = Gtk::Window::POS_MOUSE
3201 dialog2.run { |response|
3203 if resp == Gtk::Dialog::RESPONSE_OK
3205 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3206 value[1] = fallback_language
3207 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3214 languages.signal_connect('clicked') {
3217 dialog.window_position = Gtk::Window::POS_MOUSE
3221 rb_yes.active = true
3222 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3224 rb_no.signal_connect('clicked') {
3228 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3241 dialog.run { |response|
3246 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3248 return [ true, nil ]
3250 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3259 if !ask_save_modifications(utf8(_("Save this album?")),
3260 utf8(_("Do you want to save the changes to this album?")),
3261 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3264 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3266 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3267 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3268 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3270 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3271 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3272 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3273 tbl.attach(src = Gtk::Entry.new,
3274 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3275 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3276 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3277 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3278 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3279 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3280 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3281 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3282 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3283 tbl.attach(dest = Gtk::Entry.new,
3284 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3285 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3286 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3287 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3288 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3289 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3290 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3291 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3292 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3294 tooltips = Gtk::Tooltips.new
3295 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3296 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3297 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3298 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3299 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3300 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3301 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)
3302 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3303 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3304 nperpage_model = Gtk::ListStore.new(String, String)
3305 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3306 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3307 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3308 nperpagecombo.set_attributes(crt, { :markup => 0 })
3309 iter = nperpage_model.append
3310 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3312 [ 12, 20, 30, 40, 50 ].each { |v|
3313 iter = nperpage_model.append
3314 iter[0] = iter[1] = v.to_s
3316 nperpagecombo.active = 0
3318 multilanguages_value = nil
3319 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3320 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3321 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)
3322 multilanguages.signal_connect('clicked') {
3323 retval = ask_multi_languages(multilanguages_value)
3325 multilanguages_value = retval[1]
3327 if multilanguages_value
3328 ml_label.text = utf8(_("Multi-languages: enabled."))
3330 ml_label.text = utf8(_("Multi-languages: disabled."))
3333 if $config['default-multi-languages']
3334 multilanguages_value = $config['default-multi-languages']
3335 ml_label.text = utf8(_("Multi-languages: enabled."))
3337 ml_label.text = utf8(_("Multi-languages: disabled."))
3340 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3341 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3342 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)
3343 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3344 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3345 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)
3346 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3347 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3348 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)
3350 src_nb_calculated_for = ''
3351 src_nb_process = nil
3352 process_src_nb = proc {
3353 if src.text != src_nb_calculated_for
3354 src_nb_calculated_for = src.text
3357 Process.kill(9, src_nb_process)
3359 #- process doesn't exist anymore - race condition
3362 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3363 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3365 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3366 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3369 while src_nb_process
3370 msg 3, "sleeping for completion of previous process"
3373 gtk_thread_flush #- flush to avoid race condition in src_nb markup update
3375 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3376 total = { 'image' => 0, 'video' => 0, nil => 0 }
3377 if src_nb_process = fork
3378 msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3382 rd.readlines.each { |dir|
3383 if File.basename(dir) =~ /^\./
3387 Dir.entries(dir.chomp).each { |file|
3388 total[entry2type(file)] += 1
3390 rescue Errno::EACCES, Errno::ENOENT
3395 msg 3, "ripping #{src_nb_process}"
3396 dummy, exitstatus = Process.waitpid2(src_nb_process)
3398 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3400 src_nb_process = nil
3406 wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3407 Process.exit!(0) #- _exit
3410 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3413 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3419 timeout_src_nb = Gtk.timeout_add(100) {
3423 src_browse.signal_connect('clicked') {
3424 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3426 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3428 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3429 fc.transient_for = $main_window
3430 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3431 src.text = utf8(fc.filename)
3433 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3438 dest_browse.signal_connect('clicked') {
3439 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3441 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3443 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3444 fc.transient_for = $main_window
3445 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3446 dest.text = utf8(fc.filename)
3451 conf_browse.signal_connect('clicked') {
3452 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3454 Gtk::FileChooser::ACTION_SAVE,
3456 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3457 fc.transient_for = $main_window
3458 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3459 fc.set_current_folder(File.expand_path("~/.booh"))
3460 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3461 conf.text = utf8(fc.filename)
3468 recreate_theme_config = proc {
3469 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3471 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3472 $images_size.each { |s|
3473 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3477 tooltips.set_tip(cb, utf8(s['description']), nil)
3478 theme_sizes << { :widget => cb, :value => s['name'] }
3480 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3481 tooltips = Gtk::Tooltips.new
3482 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3483 theme_sizes << { :widget => cb, :value => 'original' }
3486 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3489 $allowed_N_values.each { |n|
3491 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3493 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3495 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3499 nperrows << { :widget => rb, :value => n }
3501 nperrowradios.show_all
3503 recreate_theme_config.call
3505 theme_button.signal_connect('clicked') {
3506 if newtheme = theme_choose(theme_button.label)
3507 theme_button.label = newtheme
3508 recreate_theme_config.call
3512 dialog.vbox.add(frame1)
3513 dialog.vbox.add(frame2)