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][:topwidget]
2007 dialog.vbox.add(options[0][:topwidget])
2009 if options[0] && options[0][:scrolled]
2010 sw = Gtk::ScrolledWindow.new(nil, nil)
2011 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
2012 sw.add_with_viewport(lbl)
2014 dialog.set_default_size(500, 600)
2016 dialog.vbox.add(lbl)
2017 dialog.set_default_size(200, 120)
2019 if options[0] && options[0][:okcancel]
2020 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2022 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2024 if options[0] && options[0][:pos_centered]
2025 dialog.window_position = Gtk::Window::POS_CENTER
2027 dialog.window_position = Gtk::Window::POS_MOUSE
2030 if options[0] && options[0][:linkurl]
2031 linkbut = Gtk::Button.new('')
2032 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2033 linkbut.signal_connect('clicked') {
2034 open_url(options[0][:linkurl])
2035 dialog.response(Gtk::Dialog::RESPONSE_OK)
2036 set_mousecursor_normal
2038 linkbut.relief = Gtk::RELIEF_NONE
2039 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2040 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2041 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2046 if !options[0] || !options[0][:not_transient]
2047 dialog.transient_for = parent
2048 dialog.run { |response|
2050 if options[0] && options[0][:okcancel]
2051 return response == Gtk::Dialog::RESPONSE_OK
2055 dialog.signal_connect('response') { dialog.destroy }
2059 def set_mainwindow_title(progress)
2060 filename = $orig_filename || $filename
2063 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2065 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2069 $main_window.title = 'booh - ' + File.basename(filename)
2071 $main_window.title = 'booh'
2076 def backend_wait_message(parent, msg, infopipe_path, mode)
2078 w.set_transient_for(parent)
2081 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2082 vb.pack_start(Gtk::Label.new(msg), false, false)
2084 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2085 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2086 if mode != 'one dir scan'
2087 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2089 if mode == 'web-album'
2090 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2091 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2093 vb.pack_start(Gtk::HSeparator.new, false, false)
2095 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2096 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2097 vb.pack_end(bottom, false, false)
2100 update_progression_title_pb1 = proc {
2101 if mode == 'web-album'
2102 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2103 elsif mode != 'one dir scan'
2104 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2106 set_mainwindow_title(pb1_1.fraction)
2110 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2111 refresh_thread = Thread.new {
2112 directories_counter = 0
2113 while line = infopipe.gets
2114 msg 3, "infopipe got data: #{line}"
2115 if line =~ /^directories: (\d+), sizes: (\d+)/
2116 directories = $1.to_f + 1
2118 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2119 elements = $3.to_f + 1
2120 if mode == 'web-album'
2124 gtk_thread_protect { pb1_1.fraction = 0 }
2125 if mode != 'one dir scan'
2126 newtext = utf8(full_src_dir_to_rel($1, $2))
2127 newtext = '/' if newtext == ''
2128 gtk_thread_protect { pb1_2.text = newtext }
2129 directories_counter += 1
2130 gtk_thread_protect {
2131 pb1_2.fraction = directories_counter / directories
2132 update_progression_title_pb1.call
2135 elsif line =~ /^processing element$/
2136 element_counter += 1
2137 gtk_thread_protect {
2138 pb1_1.fraction = element_counter / elements
2139 update_progression_title_pb1.call
2141 elsif line =~ /^processing size$/
2142 element_counter += 1
2143 gtk_thread_protect {
2144 pb1_1.fraction = element_counter / elements
2145 update_progression_title_pb1.call
2147 elsif line =~ /^finished processing sizes$/
2148 gtk_thread_protect { pb1_1.fraction = 1 }
2149 elsif line =~ /^creating index.html$/
2150 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2151 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2152 directories_counter = 0
2153 elsif line =~ /^index.html: (.+)\|(.+)/
2154 newtext = utf8(full_src_dir_to_rel($1, $2))
2155 newtext = '/' if newtext == ''
2156 gtk_thread_protect { pb2.text = newtext }
2157 directories_counter += 1
2158 gtk_thread_protect {
2159 pb2.fraction = directories_counter / directories
2160 set_mainwindow_title(0.9 + pb2.fraction / 10)
2162 elsif line =~ /^die: (.*)$/
2169 w.signal_connect('delete-event') { w.destroy }
2170 w.signal_connect('destroy') {
2171 Thread.kill(refresh_thread)
2172 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2175 File.delete(infopipe_path)
2177 set_mainwindow_title(nil)
2179 w.window_position = Gtk::Window::POS_CENTER
2185 def call_backend(cmd, waitmsg, mode, params)
2186 pipe = Tempfile.new("boohpipe")
2187 Thread.critical = true
2190 system("mkfifo #{path}")
2191 Thread.critical = false
2192 cmd += " --info-pipe #{path}"
2193 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2198 id, exitstatus = Process.waitpid2(pid)
2199 gtk_thread_protect { w8.destroy }
2201 if params[:successmsg]
2202 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2204 if params[:closure_after]
2205 gtk_thread_protect(¶ms[:closure_after])
2207 elsif exitstatus == 15
2208 #- say nothing, user aborted
2210 gtk_thread_protect { show_popup($main_window,
2211 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2217 button.signal_connect('clicked') {
2218 Process.kill('SIGTERM', pid)
2222 def save_changes(*forced)
2223 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2227 $xmldir.delete_attribute('already-generated')
2229 propagate_children = proc { |xmldir|
2230 if xmldir.attributes['subdirs-caption']
2231 xmldir.delete_attribute('already-generated')
2233 xmldir.elements.each('dir') { |element|
2234 propagate_children.call(element)
2238 if $xmldir.child_byname_notattr('dir', 'deleted')
2239 new_title = $subalbums_title.buffer.text
2240 if new_title != $xmldir.attributes['subdirs-caption']
2241 parent = $xmldir.parent
2242 if parent.name == 'dir'
2243 parent.delete_attribute('already-generated')
2245 propagate_children.call($xmldir)
2247 $xmldir.add_attribute('subdirs-caption', new_title)
2248 $xmldir.elements.each('dir') { |element|
2249 if !element.attributes['deleted']
2250 path = element.attributes['path']
2251 newtext = $subalbums_edits[path][:editzone].buffer.text
2252 if element.attributes['subdirs-caption']
2253 if element.attributes['subdirs-caption'] != newtext
2254 propagate_children.call(element)
2256 element.add_attribute('subdirs-caption', newtext)
2257 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2259 if element.attributes['thumbnails-caption'] != newtext
2260 element.delete_attribute('already-generated')
2262 element.add_attribute('thumbnails-caption', newtext)
2263 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2269 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2270 if $xmldir.attributes['thumbnails-caption']
2271 path = $xmldir.attributes['path']
2272 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2274 elsif $xmldir.attributes['thumbnails-caption']
2275 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2278 if $xmldir.attributes['thumbnails-caption']
2279 if edit = $subalbums_edits[$xmldir.attributes['path']]
2280 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2284 #- remove and reinsert elements to reflect new ordering
2287 $xmldir.elements.each { |element|
2288 if element.name == 'image' || element.name == 'video'
2289 saves[element.attributes['filename']] = element.remove
2293 $autotable.current_order.each { |path|
2294 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2295 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2298 saves.each_key { |path|
2299 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2300 chld.add_attribute('deleted', 'true')
2304 def sort_by_exif_date
2308 rexml_thread_protect {
2309 $xmldir.elements.each { |element|
2310 if element.name == 'image' || element.name == 'video'
2311 current_order << element.attributes['filename']
2316 #- look for EXIF dates
2319 if current_order.size > 20
2321 w.set_transient_for($main_window)
2323 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2324 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2325 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2326 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2327 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2328 vb.pack_end(bottom, false, false)
2330 w.signal_connect('delete-event') { w.destroy }
2331 w.window_position = Gtk::Window::POS_CENTER
2335 b.signal_connect('clicked') { aborted = true }
2337 current_order.each { |f|
2339 if entry2type(f) == 'image'
2341 pb.fraction = i.to_f / current_order.size
2342 Gtk.main_iteration while Gtk.events_pending?
2343 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2345 dates[f] = date_time
2358 current_order.each { |f|
2359 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2361 dates[f] = date_time
2367 rexml_thread_protect {
2368 $xmldir.elements.each { |element|
2369 if element.name == 'image' || element.name == 'video'
2370 saves[element.attributes['filename']] = element.remove
2375 neworder = smartsort(current_order, dates)
2377 rexml_thread_protect {
2379 $xmldir.add_element(saves[f].name, saves[f].attributes)
2383 #- let the auto-table reflect new ordering
2387 def remove_all_captions
2390 $autotable.current_order.each { |path|
2391 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2392 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2394 save_undo(_("remove all captions"),
2396 texts.each_key { |key|
2397 $name2widgets[key][:textview].buffer.text = texts[key]
2399 $notebook.set_page(1)
2401 texts.each_key { |key|
2402 $name2widgets[key][:textview].buffer.text = ''
2404 $notebook.set_page(1)
2410 $selected_elements.each_key { |path|
2411 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2417 $selected_elements = {}
2421 $undo_tb.sensitive = $undo_mb.sensitive = false
2422 $redo_tb.sensitive = $redo_mb.sensitive = false
2428 $subalbums_vb.children.each { |chld|
2429 $subalbums_vb.remove(chld)
2431 $subalbums = Gtk::Table.new(0, 0, true)
2432 current_y_sub_albums = 0
2434 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2435 $subalbums_edits = {}
2436 subalbums_counter = 0
2437 subalbums_edits_bypos = {}
2439 add_subalbum = proc { |xmldir, counter|
2440 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2441 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2442 if xmldir == $xmldir
2443 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2444 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2445 caption = xmldir.attributes['thumbnails-caption']
2446 infotype = 'thumbnails'
2448 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2449 captionfile, caption = find_subalbum_caption_info(xmldir)
2450 infotype = find_subalbum_info_type(xmldir)
2452 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2453 hbox = Gtk::HBox.new
2454 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2456 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2459 my_gen_real_thumbnail = proc {
2460 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2463 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2464 f.add(img = Gtk::Image.new)
2465 my_gen_real_thumbnail.call
2467 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2469 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2470 $subalbums.attach(hbox,
2471 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2473 frame, textview = create_editzone($subalbums_sw, 0, img)
2474 textview.buffer.text = caption
2475 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2476 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2478 change_image = proc {
2479 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2481 Gtk::FileChooser::ACTION_OPEN,
2483 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2484 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2485 fc.transient_for = $main_window
2486 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))
2487 f.add(preview_img = Gtk::Image.new)
2489 fc.signal_connect('update-preview') { |w|
2490 if fc.preview_filename
2491 if entry2type(fc.preview_filename) == 'video'
2495 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2497 fc.preview_widget_active = false
2499 tmpimage = "#{tmpdir}/00000001.jpg"
2501 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2502 fc.preview_widget_active = true
2503 rescue Gdk::PixbufError
2504 fc.preview_widget_active = false
2506 File.delete(tmpimage)
2513 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2514 fc.preview_widget_active = true
2515 rescue Gdk::PixbufError
2516 fc.preview_widget_active = false
2521 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2523 old_file = captionfile
2524 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2525 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2526 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2527 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2529 new_file = fc.filename
2530 msg 3, "new captionfile is: #{fc.filename}"
2531 perform_changefile = proc {
2532 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2533 $modified_pixbufs.delete(thumbnail_file)
2534 xmldir.delete_attribute("#{infotype}-rotate")
2535 xmldir.delete_attribute("#{infotype}-color-swap")
2536 xmldir.delete_attribute("#{infotype}-enhance")
2537 xmldir.delete_attribute("#{infotype}-seektime")
2538 my_gen_real_thumbnail.call
2540 perform_changefile.call
2542 save_undo(_("change caption file for sub-album"),
2544 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2545 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2546 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2547 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2548 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2549 my_gen_real_thumbnail.call
2550 $notebook.set_page(0)
2552 perform_changefile.call
2553 $notebook.set_page(0)
2561 if File.exists?(thumbnail_file)
2562 File.delete(thumbnail_file)
2564 my_gen_real_thumbnail.call
2567 rotate_and_cleanup = proc { |angle|
2568 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2569 if File.exists?(thumbnail_file)
2570 File.delete(thumbnail_file)
2574 move = proc { |direction|
2577 save_changes('forced')
2578 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2579 if direction == 'up'
2580 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2581 subalbums_edits_bypos[oldpos - 1][:position] += 1
2583 if direction == 'down'
2584 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2585 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2587 if direction == 'top'
2588 for i in 1 .. oldpos - 1
2589 subalbums_edits_bypos[i][:position] += 1
2591 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2593 if direction == 'bottom'
2594 for i in oldpos + 1 .. subalbums_counter
2595 subalbums_edits_bypos[i][:position] -= 1
2597 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2601 $xmldir.elements.each('dir') { |element|
2602 if (!element.attributes['deleted'])
2603 elems << [ element.attributes['path'], element.remove ]
2606 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2607 each { |e| $xmldir.add_element(e[1]) }
2608 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2609 $xmldir.elements.each('descendant::dir') { |elem|
2610 elem.delete_attribute('already-generated')
2613 sel = $albums_tv.selection.selected_rows
2615 populate_subalbums_treeview(false)
2616 $albums_tv.selection.select_path(sel[0])
2619 color_swap_and_cleanup = proc {
2620 perform_color_swap_and_cleanup = proc {
2621 color_swap(xmldir, "#{infotype}-")
2622 my_gen_real_thumbnail.call
2624 perform_color_swap_and_cleanup.call
2626 save_undo(_("color swap"),
2628 perform_color_swap_and_cleanup.call
2629 $notebook.set_page(0)
2631 perform_color_swap_and_cleanup.call
2632 $notebook.set_page(0)
2637 change_seektime_and_cleanup = proc {
2638 if values = ask_new_seektime(xmldir, "#{infotype}-")
2639 perform_change_seektime_and_cleanup = proc { |val|
2640 change_seektime(xmldir, "#{infotype}-", val)
2641 my_gen_real_thumbnail.call
2643 perform_change_seektime_and_cleanup.call(values[:new])
2645 save_undo(_("specify seektime"),
2647 perform_change_seektime_and_cleanup.call(values[:old])
2648 $notebook.set_page(0)
2650 perform_change_seektime_and_cleanup.call(values[:new])
2651 $notebook.set_page(0)
2657 whitebalance_and_cleanup = proc {
2658 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2659 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2660 perform_change_whitebalance_and_cleanup = proc { |val|
2661 change_whitebalance(xmldir, "#{infotype}-", val)
2662 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2663 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2664 if File.exists?(thumbnail_file)
2665 File.delete(thumbnail_file)
2668 perform_change_whitebalance_and_cleanup.call(values[:new])
2670 save_undo(_("fix white balance"),
2672 perform_change_whitebalance_and_cleanup.call(values[:old])
2673 $notebook.set_page(0)
2675 perform_change_whitebalance_and_cleanup.call(values[:new])
2676 $notebook.set_page(0)
2682 gammacorrect_and_cleanup = proc {
2683 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2684 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2685 perform_change_gammacorrect_and_cleanup = proc { |val|
2686 change_gammacorrect(xmldir, "#{infotype}-", val)
2687 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2688 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2689 if File.exists?(thumbnail_file)
2690 File.delete(thumbnail_file)
2693 perform_change_gammacorrect_and_cleanup.call(values[:new])
2695 save_undo(_("gamma correction"),
2697 perform_change_gammacorrect_and_cleanup.call(values[:old])
2698 $notebook.set_page(0)
2700 perform_change_gammacorrect_and_cleanup.call(values[:new])
2701 $notebook.set_page(0)
2707 enhance_and_cleanup = proc {
2708 perform_enhance_and_cleanup = proc {
2709 enhance(xmldir, "#{infotype}-")
2710 my_gen_real_thumbnail.call
2713 perform_enhance_and_cleanup.call
2715 save_undo(_("enhance"),
2717 perform_enhance_and_cleanup.call
2718 $notebook.set_page(0)
2720 perform_enhance_and_cleanup.call
2721 $notebook.set_page(0)
2726 evtbox.signal_connect('button-press-event') { |w, event|
2727 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2729 rotate_and_cleanup.call(90)
2731 rotate_and_cleanup.call(-90)
2732 elsif $enhance.active?
2733 enhance_and_cleanup.call
2736 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2737 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2738 { :forbid_left => true, :forbid_right => true,
2739 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2740 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2741 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2742 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2743 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2745 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2750 evtbox.signal_connect('button-press-event') { |w, event|
2751 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2755 evtbox.signal_connect('button-release-event') { |w, event|
2756 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2757 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2758 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2759 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2760 msg 3, "gesture rotate: #{angle}"
2761 rotate_and_cleanup.call(angle)
2764 $gesture_press = nil
2767 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2768 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2769 current_y_sub_albums += 1
2772 if $xmldir.child_byname_notattr('dir', 'deleted')
2774 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2775 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2776 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2777 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2778 #- this album image/caption
2779 if $xmldir.attributes['thumbnails-caption']
2780 add_subalbum.call($xmldir, 0)
2783 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2784 $xmldir.elements.each { |element|
2785 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2786 #- element (image or video) of this album
2787 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2788 msg 3, "dest_img: #{dest_img}"
2789 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2790 total[element.name] += 1
2792 if element.name == 'dir' && !element.attributes['deleted']
2793 #- sub-album image/caption
2794 add_subalbum.call(element, subalbums_counter += 1)
2795 total[element.name] += 1
2798 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2799 total['image'], total['video'], total['dir'] ]))
2800 $subalbums_vb.add($subalbums)
2801 $subalbums_vb.show_all
2803 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2804 $notebook.get_tab_label($autotable_sw).sensitive = false
2805 $notebook.set_page(0)
2806 $thumbnails_title.buffer.text = ''
2808 $notebook.get_tab_label($autotable_sw).sensitive = true
2809 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2812 if !$xmldir.child_byname_notattr('dir', 'deleted')
2813 $notebook.get_tab_label($subalbums_sw).sensitive = false
2814 $notebook.set_page(1)
2816 $notebook.get_tab_label($subalbums_sw).sensitive = true
2820 def pixbuf_or_nil(filename)
2822 return Gdk::Pixbuf.new(filename)
2828 def theme_choose(current)
2829 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2831 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2832 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2833 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2835 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2836 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2837 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2838 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2839 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2840 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2841 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2842 treeview.signal_connect('button-press-event') { |w, event|
2843 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2844 dialog.response(Gtk::Dialog::RESPONSE_OK)
2848 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2850 ([ $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|
2853 iter[0] = File.basename(dir)
2854 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2855 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2856 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2857 if File.basename(dir) == current
2858 treeview.selection.select_iter(iter)
2861 dialog.set_default_size(-1, 500)
2862 dialog.vbox.show_all
2864 dialog.run { |response|
2865 iter = treeview.selection.selected
2867 if response == Gtk::Dialog::RESPONSE_OK && iter
2868 return model.get_value(iter, 0)
2874 def show_password_protections
2875 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2876 child_iter = $albums_iters[xmldir.attributes['path']]
2877 if xmldir.attributes['password-protect']
2878 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2879 already_protected = true
2880 elsif already_protected
2881 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2883 pix = pix.saturate_and_pixelate(1, true)
2889 xmldir.elements.each('dir') { |elem|
2890 if !elem.attributes['deleted']
2891 examine_dir_elem.call(child_iter, elem, already_protected)
2895 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2898 def populate_subalbums_treeview(select_first)
2902 $subalbums_vb.children.each { |chld|
2903 $subalbums_vb.remove(chld)
2906 source = $xmldoc.root.attributes['source']
2907 msg 3, "source: #{source}"
2909 xmldir = $xmldoc.elements['//dir']
2910 if !xmldir || xmldir.attributes['path'] != source
2911 msg 1, _("Corrupted booh file...")
2915 append_dir_elem = proc { |parent_iter, xmldir|
2916 child_iter = $albums_ts.append(parent_iter)
2917 child_iter[0] = File.basename(xmldir.attributes['path'])
2918 child_iter[1] = xmldir.attributes['path']
2919 $albums_iters[xmldir.attributes['path']] = child_iter
2920 msg 3, "puttin location: #{xmldir.attributes['path']}"
2921 xmldir.elements.each('dir') { |elem|
2922 if !elem.attributes['deleted']
2923 append_dir_elem.call(child_iter, elem)
2927 append_dir_elem.call(nil, xmldir)
2928 show_password_protections
2930 $albums_tv.expand_all
2932 $albums_tv.selection.select_iter($albums_ts.iter_first)
2936 def select_current_theme
2937 select_theme($xmldoc.root.attributes['theme'],
2938 $xmldoc.root.attributes['limit-sizes'],
2939 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2940 $xmldoc.root.attributes['thumbnails-per-row'])
2943 def open_file(filename)
2947 $current_path = nil #- invalidate
2948 $modified_pixbufs = {}
2951 $subalbums_vb.children.each { |chld|
2952 $subalbums_vb.remove(chld)
2955 if !File.exists?(filename)
2956 return utf8(_("File not found."))
2960 $xmldoc = REXML::Document.new(File.new(filename))
2965 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2966 if entry2type(filename).nil?
2967 return utf8(_("Not a booh file!"))
2969 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."))
2973 if !source = $xmldoc.root.attributes['source']
2974 return utf8(_("Corrupted booh file..."))
2977 if !dest = $xmldoc.root.attributes['destination']
2978 return utf8(_("Corrupted booh file..."))
2981 if !theme = $xmldoc.root.attributes['theme']
2982 return utf8(_("Corrupted booh file..."))
2985 if $xmldoc.root.attributes['version'] < $VERSION
2986 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2987 mark_document_as_dirty
2988 if $xmldoc.root.attributes['version'] < '0.8.4'
2989 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2990 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2991 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2992 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2993 if old_dest_dir != new_dest_dir
2994 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2996 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2997 xmldir.elements.each { |element|
2998 if %w(image video).include?(element.name) && !element.attributes['deleted']
2999 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3000 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3001 Dir[old_name + '*'].each { |file|
3002 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
3003 file != new_file and sys("mv '#{file}' '#{new_file}'")
3006 if element.name == 'dir' && !element.attributes['deleted']
3007 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3008 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3009 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
3013 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3017 $xmldoc.root.add_attribute('version', $VERSION)
3020 select_current_theme
3022 $filename = filename
3023 set_mainwindow_title(nil)
3024 $default_size['thumbnails'] =~ /(.*)x(.*)/
3025 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3026 $albums_thumbnail_size =~ /(.*)x(.*)/
3027 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3029 populate_subalbums_treeview(true)
3031 $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
3035 def open_file_user(filename)
3036 result = open_file(filename)
3038 $config['last-opens'] ||= []
3039 if $config['last-opens'][-1] != utf8(filename)
3040 $config['last-opens'] << utf8(filename)
3042 $orig_filename = $filename
3043 $main_window.title = 'booh - ' + File.basename($orig_filename)
3044 tmp = Tempfile.new("boohtemp")
3045 Thread.critical = true
3046 $filename = tmp.path
3049 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3050 Thread.critical = false
3052 $tempfiles << $filename << "#{$filename}.backup"
3054 $orig_filename = nil
3060 if !ask_save_modifications(utf8(_("Save this album?")),
3061 utf8(_("Do you want to save the changes to this album?")),
3062 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3065 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3067 Gtk::FileChooser::ACTION_OPEN,
3069 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3070 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3071 fc.set_current_folder(File.expand_path("~/.booh"))
3072 fc.transient_for = $main_window
3073 fc.preview_widget = previewlabel = Gtk::Label.new.show
3074 fc.signal_connect('update-preview') { |w|
3075 if fc.preview_filename
3077 push_mousecursor_wait(fc)
3078 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3082 xmldoc.elements.each('//*') { |elem|
3083 if elem.name == 'dir'
3085 elsif elem.name == 'image'
3087 elsif elem.name == 'video'
3095 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3096 fc.preview_widget_active = false
3098 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") %
3099 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3100 fc.preview_widget_active = true
3106 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3107 push_mousecursor_wait(fc)
3108 msg = open_file_user(fc.filename)
3123 def additional_booh_options
3126 options += "--mproc #{$config['mproc'].to_i} "
3128 options += "--comments-format '#{$config['comments-format']}' "
3129 if $config['transcode-videos']
3130 options += "--transcode-videos '#{$config['transcode-videos']}' "
3132 if $config['use-flv'] == 'true'
3133 options += "--flv-generator '#{$config['flv-generator']}' "
3138 def ask_multi_languages(value)
3140 spl = value.split(',')
3141 value = [ spl[0..-2], spl[-1] ]
3144 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3147 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3148 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3150 lbl = Gtk::Label.new
3152 _("You can choose to activate <b>multi-languages</b> support for this web-album
3153 (it will work only if you publish your web-album on an Apache web-server). This will
3154 use the MultiViews feature of Apache; the pages will be served according to the
3155 value of the Accept-Language HTTP header sent by the web browsers, so that people
3156 with different languages preferences will be able to browse your web-album with
3157 navigation in their language (if language is available).
3160 dialog.vbox.add(lbl)
3161 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3162 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3163 add(languages = Gtk::Button.new))))
3165 pick_languages = proc {
3166 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3169 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3170 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3172 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3173 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3175 SUPPORTED_LANGUAGES.each { |lang|
3176 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3177 if ! value.nil? && value[0].include?(lang)
3183 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3184 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3185 fallback_language = nil
3186 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3187 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3188 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3189 fbl_rb.active = true
3190 fallback_language = SUPPORTED_LANGUAGES[0]
3192 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3193 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3194 rb.signal_connect('clicked') { fallback_language = lang }
3195 if ! value.nil? && value[1] == lang
3200 dialog2.window_position = Gtk::Window::POS_MOUSE
3204 dialog2.run { |response|
3206 if resp == Gtk::Dialog::RESPONSE_OK
3208 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3209 value[1] = fallback_language
3210 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3217 languages.signal_connect('clicked') {
3220 dialog.window_position = Gtk::Window::POS_MOUSE
3224 rb_yes.active = true
3225 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3227 rb_no.signal_connect('clicked') {
3231 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3244 dialog.run { |response|
3249 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3251 return [ true, nil ]
3253 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3262 if !ask_save_modifications(utf8(_("Save this album?")),
3263 utf8(_("Do you want to save the changes to this album?")),
3264 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3267 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3269 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3270 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3271 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3273 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3274 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3275 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3276 tbl.attach(src = Gtk::Entry.new,
3277 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3278 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3279 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3280 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3281 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3282 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3283 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3284 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3285 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3286 tbl.attach(dest = Gtk::Entry.new,
3287 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3288 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3289 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3290 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3291 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3292 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3293 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3294 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3295 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3297 tooltips = Gtk::Tooltips.new
3298 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3299 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3300 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3301 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3302 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3303 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3304 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)
3305 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3306 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3307 nperpage_model = Gtk::ListStore.new(String, String)
3308 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3309 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3310 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3311 nperpagecombo.set_attributes(crt, { :markup => 0 })
3312 iter = nperpage_model.append
3313 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3315 [ 12, 20, 30, 40, 50 ].each { |v|
3316 iter = nperpage_model.append
3317 iter[0] = iter[1] = v.to_s
3319 nperpagecombo.active = 0
3321 multilanguages_value = nil
3322 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3323 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3324 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)
3325 multilanguages.signal_connect('clicked') {
3326 retval = ask_multi_languages(multilanguages_value)
3328 multilanguages_value = retval[1]
3330 if multilanguages_value
3331 ml_label.text = utf8(_("Multi-languages: enabled."))
3333 ml_label.text = utf8(_("Multi-languages: disabled."))
3336 if $config['default-multi-languages']
3337 multilanguages_value = $config['default-multi-languages']
3338 ml_label.text = utf8(_("Multi-languages: enabled."))
3340 ml_label.text = utf8(_("Multi-languages: disabled."))
3343 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3344 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3345 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)
3346 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3347 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3348 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)
3349 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3350 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3351 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)
3353 src_nb_calculated_for = ''
3354 src_nb_process = nil
3355 process_src_nb = proc {
3356 if src.text != src_nb_calculated_for
3357 src_nb_calculated_for = src.text
3360 Process.kill(9, src_nb_process)
3362 #- process doesn't exist anymore - race condition
3365 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3366 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3368 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3369 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3372 while src_nb_process
3373 msg 3, "sleeping for completion of previous process"
3376 gtk_thread_flush #- flush to avoid race condition in src_nb markup update
3378 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3379 total = { 'image' => 0, 'video' => 0, nil => 0 }
3380 if src_nb_process = fork
3381 msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3385 rd.readlines.each { |dir|
3386 if File.basename(dir) =~ /^\./
3390 Dir.entries(dir.chomp).each { |file|
3391 total[entry2type(file)] += 1
3393 rescue Errno::EACCES, Errno::ENOENT
3398 msg 3, "ripping #{src_nb_process}"
3399 dummy, exitstatus = Process.waitpid2(src_nb_process)
3401 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3403 src_nb_process = nil
3409 wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3410 Process.exit!(0) #- _exit
3413 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3416 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3422 timeout_src_nb = Gtk.timeout_add(100) {
3426 src_browse.signal_connect('clicked') {
3427 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3429 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3431 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3432 fc.transient_for = $main_window
3433 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3434 src.text = utf8(fc.filename)
3436 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3441 dest_browse.signal_connect('clicked') {
3442 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3444 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3446 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3447 fc.transient_for = $main_window
3448 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3449 dest.text = utf8(fc.filename)
3454 conf_browse.signal_connect('clicked') {
3455 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3457 Gtk::FileChooser::ACTION_SAVE,
3459 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3460 fc.transient_for = $main_window
3461 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3462 fc.set_current_folder(File.expand_path("~/.booh"))
3463 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3464 conf.text = utf8(fc.filename)
3471 recreate_theme_config = proc {
3472 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3474 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3475 $images_size.each { |s|
3476 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3480 tooltips.set_tip(cb, utf8(s['description']), nil)
3481 theme_sizes << { :widget => cb, :value => s['name'] }
3483 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3484 tooltips = Gtk::Tooltips.new
3485 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3486 theme_sizes << { :widget => cb, :value => 'original' }
3489 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3492 $allowed_N_values.each { |n|
3494 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3496 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3498 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3502 nperrows << { :widget => rb, :value => n }
3504 nperrowradios.show_all
3506 recreate_theme_config.call
3508 theme_button.signal_connect('clicked') {
3509 if newtheme = theme_choose(theme_button.label)
3510 theme_button.label = newtheme
3511 recreate_theme_config.call