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-2010 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-2010 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 || /usr/bin/vlc %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 -remote 'openURL(%f,new-window)' || /usr/bin/firefox %f"
129 $config['use-flv'] ||= "true"
130 $config['flv-generator'] ||= "/usr/bin/ffmpeg -i %f -b 800k -ar 22050 -ab 32k %o"
131 $config['comments-format'] ||= '%t'
132 if !FileTest.directory?(File.expand_path('~/.booh'))
133 system("mkdir ~/.booh")
135 if $config['mproc'].nil?
137 for line in IO.readlines('/proc/cpuinfo') do
138 line =~ /^processor/ and cpus += 1
141 $config['mproc'] = cpus
144 $config['rotate-set-exif'] ||= 'true'
149 def check_config_preferences_dep
150 viewer_binary = $config['video-viewer'].split.first
151 if viewer_binary && !File.executable?(viewer_binary)
152 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
153 You should fix this in Edit/Preferences so that you can view videos.
155 Problem was: '%s' is not an executable file.
156 Hint: don't forget to specify the full path to the executable,
157 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
160 flv_generator_binary = $config['use-flv'] == 'true' && $config['flv-generator'].split.first
161 if flv_generator_binary && !File.executable?(flv_generator_binary)
162 show_popup($main_window, utf8(_("The configured .flv generator seems to be unavailable.
163 You should fix this in Edit/Preferences so that you can have working
164 embedded flash videos.
166 Problem was: '%s' is not an executable file.
167 Hint: don't forget to specify the full path to the executable,
168 e.g. '/usr/bin/ffmpeg' is correct but 'ffmpeg' only is not.") % flv_generator_binary), { :pos_centered => true, :not_transient => true })
173 if !system("which convert >/dev/null 2>/dev/null")
174 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
175 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
178 if !system("which identify >/dev/null 2>/dev/null")
179 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
180 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
182 if !system("which exif >/dev/null 2>/dev/null")
183 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
185 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
187 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
190 check_config_preferences_dep
193 def check_image_editor
194 if last_failed_binary = check_multi_binaries($config['image-editor'])
195 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
196 You should fix this in Edit/Preferences so that you can edit photos externally.
198 Problem was: '%s' is not an executable file.
199 Hint: don't forget to specify the full path to the executable,
200 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
208 if $config['last-opens'] && $config['last-opens'].size > 10
209 $config['last-opens'] = $config['last-opens'][-10, 10]
212 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
213 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
214 $config.each_pair { |key, value|
215 elem = xmldoc.root.add_element key
217 $config[key].each_pair { |subkey, subvalue|
218 subelem = elem.add_element subkey
219 subelem.add_text subvalue.to_s
221 elsif value.is_a? Array
222 elem.add_text value.join('~~~')
227 elem.add_text value.to_s
231 ios = File.open($config_file, "w")
235 $tempfiles.each { |f|
242 def set_mousecursor(what, *widget)
243 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
244 if widget[0] && widget[0].window
245 widget[0].window.cursor = cursor
247 if $main_window && $main_window.window
248 $main_window.window.cursor = cursor
250 $current_cursor = what
252 def set_mousecursor_wait(*widget)
253 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
254 if Thread.current == Thread.main
255 Gtk.main_iteration while Gtk.events_pending?
258 def set_mousecursor_normal(*widget)
259 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
261 def push_mousecursor_wait(*widget)
262 if $current_cursor != Gdk::Cursor::WATCH
263 $save_cursor = $current_cursor
264 gtk_thread_protect { set_mousecursor_wait(*widget) }
267 def pop_mousecursor(*widget)
268 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
272 source = $xmldoc.root.attributes['source']
273 dest = $xmldoc.root.attributes['destination']
274 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
277 def full_src_dir_to_rel(path, source)
278 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
281 def build_full_dest_filename(filename)
282 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
285 def save_undo(name, closure, *params)
286 UndoHandler.save_undo(name, closure, [ *params ])
287 $undo_tb.sensitive = $undo_mb.sensitive = true
288 $redo_tb.sensitive = $redo_mb.sensitive = false
291 def view_element(filename, closures)
292 if entry2type(filename) == 'video'
293 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
299 w = create_window.set_title(filename)
301 msg 3, "filename: #{filename}"
302 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
303 #- typically this file won't exist in case of videos; try with the largest thumbnail around
304 if !File.exists?(dest_img)
305 if entry2type(filename) == 'video'
306 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
307 if not alternatives.empty?
308 dest_img = alternatives[-1]
311 push_mousecursor_wait
312 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
314 if !File.exists?(dest_img)
315 msg 2, _("Could not generate fullscreen thumbnail!")
320 aspect = utf8(_("Aspect: unknown"))
321 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
323 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
325 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
326 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(vbox).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
327 evt.signal_connect('button-press-event') { |this, event|
328 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
329 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
331 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
333 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
334 delete_item.signal_connect('activate') {
336 closures[:delete].call(false)
339 menu.popup(nil, nil, event.button, event.time)
342 evt.signal_connect('button-release-event') { |this, event|
344 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
345 msg 3, "gesture delete: click-drag right button to the bottom"
347 closures[:delete].call(false)
348 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
352 tooltips = Gtk::Tooltips.new
353 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
355 w.signal_connect('key-press-event') { |w,event|
356 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
358 closures[:delete].call(false)
362 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
363 b.signal_connect('clicked') { w.destroy }
366 vb.pack_start(evt, false, false)
367 vb.pack_end(bottom, false, false)
370 w.signal_connect('delete-event') { w.destroy }
371 w.window_position = Gtk::Window::POS_CENTER
375 def scroll_upper(scrolledwindow, ypos_top)
376 newval = scrolledwindow.vadjustment.value -
377 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
378 if newval < scrolledwindow.vadjustment.lower
379 newval = scrolledwindow.vadjustment.lower
381 scrolledwindow.vadjustment.value = newval
384 def scroll_lower(scrolledwindow, ypos_bottom)
385 newval = scrolledwindow.vadjustment.value +
386 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
387 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
388 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
390 scrolledwindow.vadjustment.value = newval
393 def autoscroll_if_needed(scrolledwindow, image, textview)
394 #- autoscroll if cursor or image is not visible, if possible
395 if image && image.window || textview.window
396 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
397 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
398 current_miny_visible = scrolledwindow.vadjustment.value
399 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
400 if ypos_top < current_miny_visible
401 scroll_upper(scrolledwindow, ypos_top)
402 elsif ypos_bottom > current_maxy_visible
403 scroll_lower(scrolledwindow, ypos_bottom)
408 def create_editzone(scrolledwindow, pagenum, image)
409 frame = Gtk::Frame.new
410 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
411 frame.set_shadow_type(Gtk::SHADOW_IN)
412 textview.signal_connect('key-press-event') { |w, event|
413 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
414 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
415 scrolledwindow.signal_emit('key-press-event', event)
417 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
418 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
419 if event.keyval == Gdk::Keyval::GDK_Up
420 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
421 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
423 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
426 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
427 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
429 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
436 candidate_undo_text = nil
437 textview.signal_connect('focus-in-event') { |w, event|
438 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
439 candidate_undo_text = textview.buffer.text
443 textview.signal_connect('key-release-event') { |w, event|
444 if candidate_undo_text && candidate_undo_text != textview.buffer.text
446 save_undo(_("text edit"),
448 save_text = textview.buffer.text
449 textview.buffer.text = text
451 $notebook.set_page(pagenum)
453 textview.buffer.text = save_text
455 $notebook.set_page(pagenum)
457 }, candidate_undo_text)
458 candidate_undo_text = nil
461 if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
462 autoscroll_if_needed(scrolledwindow, image, textview)
467 return [ frame, textview ]
470 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
472 if !$modified_pixbufs[thumbnail_img]
473 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
474 elsif !$modified_pixbufs[thumbnail_img][:orig]
475 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
478 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
481 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
482 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
483 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
484 if pixbuf.height > desired_y
485 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
486 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
487 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
492 if $modified_pixbufs[thumbnail_img][:whitebalance]
493 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
496 #- fix gamma correction
497 if $modified_pixbufs[thumbnail_img][:gammacorrect]
498 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
501 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
504 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
507 #- update rotate attribute
508 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
509 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
511 #- change exif orientation if configured so (but forget in case of thumbnails caption)
512 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
513 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
516 $modified_pixbufs[thumbnail_img] ||= {}
517 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
518 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
520 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
523 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
526 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
528 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
530 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
531 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
533 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
534 $notebook.set_page(0)
535 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
540 def color_swap(xmldir, attributes_prefix)
542 rexml_thread_protect {
543 if xmldir.attributes["#{attributes_prefix}color-swap"]
544 xmldir.delete_attribute("#{attributes_prefix}color-swap")
546 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
551 def enhance(xmldir, attributes_prefix)
553 rexml_thread_protect {
554 if xmldir.attributes["#{attributes_prefix}enhance"]
555 xmldir.delete_attribute("#{attributes_prefix}enhance")
557 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
562 def change_seektime(xmldir, attributes_prefix, value)
564 rexml_thread_protect {
565 xmldir.add_attribute("#{attributes_prefix}seektime", value)
569 def ask_new_seektime(xmldir, attributes_prefix)
570 value = rexml_thread_protect {
572 xmldir.attributes["#{attributes_prefix}seektime"]
578 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
580 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
581 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
582 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
586 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
590 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
591 entry.signal_connect('key-press-event') { |w, event|
592 if event.keyval == Gdk::Keyval::GDK_Return
593 dialog.response(Gtk::Dialog::RESPONSE_OK)
595 elsif event.keyval == Gdk::Keyval::GDK_Escape
596 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
599 false #- propagate if needed
603 dialog.window_position = Gtk::Window::POS_MOUSE
606 dialog.run { |response|
609 if response == Gtk::Dialog::RESPONSE_OK
611 msg 3, "changing seektime to #{newval}"
612 return { :old => value, :new => newval }
619 def change_pano_amount(xmldir, attributes_prefix, value)
621 rexml_thread_protect {
623 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
625 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
630 def ask_new_pano_amount(xmldir, attributes_prefix)
631 value = rexml_thread_protect {
633 xmldir.attributes["#{attributes_prefix}pano-amount"]
639 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
641 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
642 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
643 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
647 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
648 of this panorama image compared to other regular images. For example, if the panorama
649 was taken out of four photos on one row, counting the necessary overlap, the width of
650 this panorama image should probably be roughly three times the width of regular images.
652 With this information, booh will be able to generate panorama thumbnails looking
653 the right 'size', since the height of the thumbnail for this image will be similar
654 to the height of other thumbnails.
657 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
658 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
659 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
660 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
661 spin.signal_connect('value-changed') {
664 dialog.window_position = Gtk::Window::POS_MOUSE
667 spin.value = value.to_f
674 dialog.run { |response|
678 newval = spin.value.to_f
681 if response == Gtk::Dialog::RESPONSE_OK
683 msg 3, "changing panorama amount to #{newval}"
684 return { :old => value, :new => newval }
691 def change_whitebalance(xmlelem, attributes_prefix, value)
693 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
696 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
698 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
699 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
700 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
701 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
702 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
703 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
704 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
705 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
706 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
707 $modified_pixbufs[thumbnail_img] ||= {}
708 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
709 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
711 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
712 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
714 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
717 $modified_pixbufs[thumbnail_img] ||= {}
718 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
720 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
723 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
724 #- init $modified_pixbufs correctly
725 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
727 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
729 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
731 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
732 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
733 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
737 _("You can fix the <b>white balance</b> of the image, if your image is too blue
738 or too yellow because the recorder didn't detect the light correctly. Drag the
739 slider below the image to the left for more blue, to the right for more yellow.
743 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
745 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
747 dialog.window_position = Gtk::Window::POS_MOUSE
751 timeout = Gtk.timeout_add(100) {
752 if hs.value != lastval
755 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
761 dialog.run { |response|
762 Gtk.timeout_remove(timeout)
763 if response == Gtk::Dialog::RESPONSE_OK
765 newval = hs.value.to_s
766 msg 3, "changing white balance to #{newval}"
768 return { :old => value, :new => newval }
771 $modified_pixbufs[thumbnail_img] ||= {}
772 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
773 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
781 def change_gammacorrect(xmlelem, attributes_prefix, value)
783 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
786 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
788 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
789 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
790 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
791 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
792 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
793 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
794 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
795 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
796 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
797 $modified_pixbufs[thumbnail_img] ||= {}
798 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
799 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
801 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
802 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
804 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
807 $modified_pixbufs[thumbnail_img] ||= {}
808 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
810 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
813 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
814 #- init $modified_pixbufs correctly
815 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
817 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
819 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
821 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
822 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
823 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
827 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
828 or too bright. Drag the slider below the image.
832 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
834 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
836 dialog.window_position = Gtk::Window::POS_MOUSE
840 timeout = Gtk.timeout_add(100) {
841 if hs.value != lastval
844 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
850 dialog.run { |response|
851 Gtk.timeout_remove(timeout)
852 if response == Gtk::Dialog::RESPONSE_OK
854 newval = hs.value.to_s
855 msg 3, "gamma correction to #{newval}"
857 return { :old => value, :new => newval }
860 $modified_pixbufs[thumbnail_img] ||= {}
861 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
862 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
870 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
871 if File.exists?(destfile)
872 File.delete(destfile)
874 #- type can be 'element' or 'subdir'
876 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
878 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
882 $max_gen_thumbnail_threads = nil
883 $current_gen_thumbnail_threads = 0
884 $gen_thumbnail_monitor = Monitor.new
886 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
887 if $max_gen_thumbnail_threads.nil?
888 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
891 push_mousecursor_wait
892 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
895 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
900 $gen_thumbnail_monitor.synchronize {
901 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
902 $current_gen_thumbnail_threads += 1
907 msg 3, "generate thumbnail from new thread"
910 $gen_thumbnail_monitor.synchronize {
911 $current_gen_thumbnail_threads -= 1
915 msg 3, "generate thumbnail from current thread"
920 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
921 distribute_multiple_call = Proc.new { |action, arg|
922 $selected_elements.each_key { |path|
923 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
925 if possible_actions[:can_multiple] && $selected_elements.length > 0
926 UndoHandler.begin_batch
927 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
928 UndoHandler.end_batch
930 closures[action].call(arg)
932 $selected_elements = {}
935 if optionals.include?('change_image')
936 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
937 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
938 changeimg.signal_connect('activate') { closures[:change].call }
939 menu.append(Gtk::SeparatorMenuItem.new)
941 if !possible_actions[:can_multiple] || $selected_elements.length == 0
944 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
945 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
946 view.signal_connect('activate') { closures[:view].call }
948 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
949 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
950 view.signal_connect('activate') { closures[:view].call }
951 menu.append(Gtk::SeparatorMenuItem.new)
954 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
955 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
956 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
957 exif.signal_connect('activate') { show_popup($main_window,
958 utf8(`exif -m '#{fullpath}'`),
959 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
960 menu.append(Gtk::SeparatorMenuItem.new)
963 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
964 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
965 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
966 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
967 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
968 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
969 if !possible_actions[:can_multiple] || $selected_elements.length == 0
970 menu.append(Gtk::SeparatorMenuItem.new)
971 if !possible_actions[:forbid_left]
972 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
973 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
974 moveleft.signal_connect('activate') { closures[:move].call('left') }
975 if !possible_actions[:can_left]
976 moveleft.sensitive = false
979 if !possible_actions[:forbid_right]
980 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
981 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
982 moveright.signal_connect('activate') { closures[:move].call('right') }
983 if !possible_actions[:can_right]
984 moveright.sensitive = false
987 if optionals.include?('move_top')
988 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
989 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
990 movetop.signal_connect('activate') { closures[:move].call('top') }
991 if !possible_actions[:can_top]
992 movetop.sensitive = false
995 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
996 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
997 moveup.signal_connect('activate') { closures[:move].call('up') }
998 if !possible_actions[:can_up]
999 moveup.sensitive = false
1001 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
1002 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
1003 movedown.signal_connect('activate') { closures[:move].call('down') }
1004 if !possible_actions[:can_down]
1005 movedown.sensitive = false
1007 if optionals.include?('move_bottom')
1008 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
1009 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
1010 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
1011 if !possible_actions[:can_bottom]
1012 movebottom.sensitive = false
1017 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
1018 menu.append(Gtk::SeparatorMenuItem.new)
1019 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
1020 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
1021 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
1022 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1023 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1024 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1025 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1026 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1027 seektime.signal_connect('activate') {
1028 if possible_actions[:can_multiple] && $selected_elements.length > 0
1029 if values = ask_new_seektime(nil, '')
1030 distribute_multiple_call.call(:seektime, values)
1033 closures[:seektime].call
1038 menu.append( Gtk::SeparatorMenuItem.new)
1039 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1040 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1041 gammacorrect.signal_connect('activate') {
1042 if possible_actions[:can_multiple] && $selected_elements.length > 0
1043 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1044 distribute_multiple_call.call(:gammacorrect, values)
1047 closures[:gammacorrect].call
1050 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1051 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1052 whitebalance.signal_connect('activate') {
1053 if possible_actions[:can_multiple] && $selected_elements.length > 0
1054 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1055 distribute_multiple_call.call(:whitebalance, values)
1058 closures[:whitebalance].call
1061 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1062 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1063 _("Enhance constrast"))))
1065 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1067 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1068 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1069 if type == 'image' && possible_actions[:can_panorama]
1070 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1071 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1072 panorama.signal_connect('activate') {
1073 if possible_actions[:can_multiple] && $selected_elements.length > 0
1074 if values = ask_new_pano_amount(nil, '')
1075 distribute_multiple_call.call(:pano, values)
1078 distribute_multiple_call.call(:pano)
1082 menu.append( Gtk::SeparatorMenuItem.new)
1083 if optionals.include?('delete')
1084 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1085 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1086 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1087 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1088 paste_item.signal_connect('activate') { closures[:paste].call }
1089 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1090 clear_item.signal_connect('activate') { $cuts = [] }
1092 paste_item.sensitive = clear_item.sensitive = false
1095 menu.append( Gtk::SeparatorMenuItem.new)
1097 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1098 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1099 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1100 editexternally.signal_connect('activate') {
1101 if check_image_editor
1102 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1108 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1109 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1110 if optionals.include?('delete')
1111 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1112 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1115 menu.popup(nil, nil, event.button, event.time)
1118 def delete_current_subalbum
1120 sel = $albums_tv.selection.selected_rows
1121 $xmldir.elements.each { |e|
1122 if e.name == 'image' || e.name == 'video'
1123 e.add_attribute('deleted', 'true')
1126 #- branch if we have a non deleted subalbum
1127 if $xmldir.child_byname_notattr('dir', 'deleted')
1128 $xmldir.delete_attribute('thumbnails-caption')
1129 $xmldir.delete_attribute('thumbnails-captionfile')
1131 $xmldir.add_attribute('deleted', 'true')
1133 while moveup.parent.name == 'dir'
1134 moveup = moveup.parent
1135 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1136 moveup.add_attribute('deleted', 'true')
1143 save_changes('forced')
1144 populate_subalbums_treeview(false)
1145 $albums_tv.selection.select_path(sel[0])
1151 $current_path = nil #- prevent save_changes from being rerun again
1152 sel = $albums_tv.selection.selected_rows
1153 restore_one = proc { |xmldir|
1154 xmldir.elements.each { |e|
1155 if e.name == 'dir' && e.attributes['deleted']
1158 e.delete_attribute('deleted')
1161 restore_one.call($xmldir)
1162 populate_subalbums_treeview(false)
1163 $albums_tv.selection.select_path(sel[0])
1166 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1169 frame1 = Gtk::Frame.new
1170 fullpath = from_utf8("#{$current_path}/#{filename}")
1172 my_gen_real_thumbnail = proc {
1173 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1177 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1178 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1179 pack_start(img = Gtk::Image.new).
1180 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1181 px, mask = pxb.render_pixmap_and_mask(0)
1182 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1183 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1185 frame1.add(img = Gtk::Image.new)
1188 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1189 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1190 my_gen_real_thumbnail.call
1192 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1195 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1197 tooltips = Gtk::Tooltips.new
1198 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1199 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1201 frame2, textview = create_editzone($autotable_sw, 1, img)
1202 textview.buffer.text = caption
1203 textview.set_justification(Gtk::Justification::CENTER)
1205 vbox = Gtk::VBox.new(false, 5)
1206 vbox.pack_start(evtbox, false, false)
1207 vbox.pack_start(frame2, false, false)
1208 autotable.append(vbox, filename)
1210 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1211 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1213 #- to be able to find widgets by name
1214 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1216 cleanup_all_thumbnails = proc {
1217 #- remove out of sync images
1218 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1219 for sizeobj in $images_size
1220 #- cannot use sizeobj because panoramic images will have a larger width
1221 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1229 cleanup_all_thumbnails.call
1230 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1232 rexml_thread_protect {
1233 $xmldir.delete_attribute('already-generated')
1235 my_gen_real_thumbnail.call
1238 rotate_and_cleanup = proc { |angle|
1239 cleanup_all_thumbnails.call
1240 rexml_thread_protect {
1241 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1245 move = proc { |direction|
1246 do_method = "move_#{direction}"
1247 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1249 done = autotable.method(do_method).call(vbox)
1250 textview.grab_focus #- because if moving, focus is stolen
1254 save_undo(_("move %s") % direction,
1256 autotable.method(undo_method).call(vbox)
1257 textview.grab_focus #- because if moving, focus is stolen
1258 autoscroll_if_needed($autotable_sw, img, textview)
1259 $notebook.set_page(1)
1261 autotable.method(do_method).call(vbox)
1262 textview.grab_focus #- because if moving, focus is stolen
1263 autoscroll_if_needed($autotable_sw, img, textview)
1264 $notebook.set_page(1)
1270 color_swap_and_cleanup = proc {
1271 perform_color_swap_and_cleanup = proc {
1272 cleanup_all_thumbnails.call
1273 rexml_thread_protect {
1274 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1276 my_gen_real_thumbnail.call
1279 perform_color_swap_and_cleanup.call
1281 save_undo(_("color swap"),
1283 perform_color_swap_and_cleanup.call
1285 autoscroll_if_needed($autotable_sw, img, textview)
1286 $notebook.set_page(1)
1288 perform_color_swap_and_cleanup.call
1290 autoscroll_if_needed($autotable_sw, img, textview)
1291 $notebook.set_page(1)
1296 change_seektime_and_cleanup_real = proc { |values|
1297 perform_change_seektime_and_cleanup = proc { |val|
1298 cleanup_all_thumbnails.call
1299 rexml_thread_protect {
1300 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1302 my_gen_real_thumbnail.call
1304 perform_change_seektime_and_cleanup.call(values[:new])
1306 save_undo(_("specify seektime"),
1308 perform_change_seektime_and_cleanup.call(values[:old])
1310 autoscroll_if_needed($autotable_sw, img, textview)
1311 $notebook.set_page(1)
1313 perform_change_seektime_and_cleanup.call(values[:new])
1315 autoscroll_if_needed($autotable_sw, img, textview)
1316 $notebook.set_page(1)
1321 change_seektime_and_cleanup = proc {
1322 rexml_thread_protect {
1323 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1324 change_seektime_and_cleanup_real.call(values)
1329 change_pano_amount_and_cleanup_real = proc { |values|
1330 perform_change_pano_amount_and_cleanup = proc { |val|
1331 cleanup_all_thumbnails.call
1332 rexml_thread_protect {
1333 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1336 perform_change_pano_amount_and_cleanup.call(values[:new])
1338 save_undo(_("change panorama amount"),
1340 perform_change_pano_amount_and_cleanup.call(values[:old])
1342 autoscroll_if_needed($autotable_sw, img, textview)
1343 $notebook.set_page(1)
1345 perform_change_pano_amount_and_cleanup.call(values[:new])
1347 autoscroll_if_needed($autotable_sw, img, textview)
1348 $notebook.set_page(1)
1353 change_pano_amount_and_cleanup = proc {
1354 rexml_thread_protect {
1355 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1356 change_pano_amount_and_cleanup_real.call(values)
1361 whitebalance_and_cleanup_real = proc { |values|
1362 perform_change_whitebalance_and_cleanup = proc { |val|
1363 cleanup_all_thumbnails.call
1364 rexml_thread_protect {
1365 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1366 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1367 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1370 perform_change_whitebalance_and_cleanup.call(values[:new])
1372 save_undo(_("fix white balance"),
1374 perform_change_whitebalance_and_cleanup.call(values[:old])
1376 autoscroll_if_needed($autotable_sw, img, textview)
1377 $notebook.set_page(1)
1379 perform_change_whitebalance_and_cleanup.call(values[:new])
1381 autoscroll_if_needed($autotable_sw, img, textview)
1382 $notebook.set_page(1)
1387 whitebalance_and_cleanup = proc {
1388 rexml_thread_protect {
1389 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1390 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1391 whitebalance_and_cleanup_real.call(values)
1396 gammacorrect_and_cleanup_real = proc { |values|
1397 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1398 cleanup_all_thumbnails.call
1399 rexml_thread_protect {
1400 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1401 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1402 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1405 perform_change_gammacorrect_and_cleanup.call(values[:new])
1407 save_undo(_("gamma correction"),
1409 perform_change_gammacorrect_and_cleanup.call(values[:old])
1411 autoscroll_if_needed($autotable_sw, img, textview)
1412 $notebook.set_page(1)
1414 perform_change_gammacorrect_and_cleanup.call(values[:new])
1416 autoscroll_if_needed($autotable_sw, img, textview)
1417 $notebook.set_page(1)
1422 gammacorrect_and_cleanup = Proc.new {
1423 rexml_thread_protect {
1424 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1425 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1426 gammacorrect_and_cleanup_real.call(values)
1431 enhance_and_cleanup = proc {
1432 perform_enhance_and_cleanup = proc {
1433 cleanup_all_thumbnails.call
1434 rexml_thread_protect {
1435 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1437 my_gen_real_thumbnail.call
1440 cleanup_all_thumbnails.call
1441 perform_enhance_and_cleanup.call
1443 save_undo(_("enhance"),
1445 perform_enhance_and_cleanup.call
1447 autoscroll_if_needed($autotable_sw, img, textview)
1448 $notebook.set_page(1)
1450 perform_enhance_and_cleanup.call
1452 autoscroll_if_needed($autotable_sw, img, textview)
1453 $notebook.set_page(1)
1458 delete = proc { |isacut|
1459 if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1462 perform_delete = proc {
1463 after = autotable.get_next_widget(vbox)
1465 after = autotable.get_previous_widget(vbox)
1467 if $config['deleteondisk'] && !isacut
1468 msg 3, "scheduling for delete: #{fullpath}"
1469 $todelete << fullpath
1471 autotable.remove_widget(vbox)
1473 $vbox2widgets[after][:textview].grab_focus
1474 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1478 previous_pos = autotable.get_current_number(vbox)
1482 delete_current_subalbum
1484 save_undo(_("delete"),
1486 autotable.reinsert(pos, vbox, filename)
1487 $notebook.set_page(1)
1488 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1490 msg 3, "removing deletion schedule of: #{fullpath}"
1491 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1494 $notebook.set_page(1)
1503 $cuts << { :vbox => vbox, :filename => filename }
1504 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1509 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1512 autotable.queue_draws << proc {
1513 $vbox2widgets[last[:vbox]][:textview].grab_focus
1514 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1516 save_undo(_("paste"),
1518 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1519 $notebook.set_page(1)
1522 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1524 $notebook.set_page(1)
1527 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1532 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1533 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1534 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1535 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1537 textview.signal_connect('key-press-event') { |w, event|
1540 x, y = autotable.get_current_pos(vbox)
1541 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1542 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1543 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1544 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1546 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1547 $vbox2widgets[widget_up][:textview].grab_focus
1554 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1556 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1557 $vbox2widgets[widget_down][:textview].grab_focus
1564 if event.keyval == Gdk::Keyval::GDK_Left
1567 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1574 rotate_and_cleanup.call(-90)
1577 if event.keyval == Gdk::Keyval::GDK_Right
1578 next_ = autotable.get_next_widget(vbox)
1579 if next_ && autotable.get_current_pos(next_)[0] > x
1581 $vbox2widgets[next_][:textview].grab_focus
1588 rotate_and_cleanup.call(90)
1591 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1594 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1595 view_element(filename, { :delete => delete })
1598 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1601 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1605 !propagate #- propagate if needed
1608 $ignore_next_release = false
1609 evtbox.signal_connect('button-press-event') { |w, event|
1610 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1611 if event.state & Gdk::Window::BUTTON3_MASK != 0
1612 #- gesture redo: hold right mouse button then click left mouse button
1613 $config['nogestures'] or perform_redo
1614 $ignore_next_release = true
1616 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1618 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1620 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1621 elsif $enhance.active?
1622 enhance_and_cleanup.call
1623 elsif $delete.active?
1627 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1630 $button1_pressed_autotable = true
1631 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1632 if event.state & Gdk::Window::BUTTON1_MASK != 0
1633 #- gesture undo: hold left mouse button then click right mouse button
1634 $config['nogestures'] or perform_undo
1635 $ignore_next_release = true
1637 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1638 view_element(filename, { :delete => delete })
1643 evtbox.signal_connect('button-release-event') { |w, event|
1644 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1645 if !$ignore_next_release
1646 x, y = autotable.get_current_pos(vbox)
1647 next_ = autotable.get_next_widget(vbox)
1648 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1649 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1650 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1651 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1652 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1653 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1654 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1656 $ignore_next_release = false
1657 $gesture_press = nil
1662 #- handle reordering with drag and drop
1663 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1664 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1665 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1666 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1669 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1671 #- mouse gesture first (dnd disables button-release-event)
1672 if $gesture_press && $gesture_press[:filename] == filename
1673 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1674 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1675 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1676 rotate_and_cleanup.call(angle)
1677 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1679 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1680 msg 3, "gesture delete: click-drag right button to the bottom"
1682 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1687 ctxt.targets.each { |target|
1688 if target.name == 'reorder-elements'
1689 move_dnd = proc { |from,to|
1692 autotable.move(from, to)
1693 save_undo(_("reorder"),
1696 autotable.move(to - 1, from)
1698 autotable.move(to, from + 1)
1700 $notebook.set_page(1)
1702 autotable.move(from, to)
1703 $notebook.set_page(1)
1708 if $multiple_dnd.size == 0
1709 move_dnd.call(selection_data.data.to_i,
1710 autotable.get_current_number(vbox))
1712 UndoHandler.begin_batch
1713 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1715 #- need to update current position between each call
1716 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1717 autotable.get_current_number(vbox))
1719 UndoHandler.end_batch
1730 def create_auto_table
1732 $autotable = Gtk::AutoTable.new(5)
1734 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1735 thumbnails_vb = Gtk::VBox.new(false, 5)
1737 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1738 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1739 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1740 thumbnails_vb.add($autotable)
1742 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1743 $autotable_sw.add_with_viewport(thumbnails_vb)
1745 #- follows stuff for handling multiple elements selection
1746 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1748 update_selected = proc {
1749 $autotable.current_order.each { |path|
1750 w = $name2widgets[path][:evtbox].window
1751 xm = w.position[0] + w.size[0]/2
1752 ym = w.position[1] + w.size[1]/2
1753 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1754 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1755 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1756 if $name2widgets[path][:img].pixbuf
1757 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1761 if $selected_elements[path] && ! $selected_elements[path][:keep]
1762 if ((xm < press_x && xm < pos_x || xm > pos_x && xm > press_x) || (ym < press_y && ym < pos_y || ym > pos_y && ym > press_y))
1763 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1764 $selected_elements.delete(path)
1769 $autotable.signal_connect('realize') { |w,e|
1770 gc = Gdk::GC.new($autotable.window)
1771 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1772 gc.function = Gdk::GC::INVERT
1773 #- autoscroll handling for DND and multiple selections
1774 Gtk.timeout_add(100) {
1775 if ! $autotable.window.nil?
1776 w, x, y, mask = $autotable.window.pointer
1777 if mask & Gdk::Window::BUTTON1_MASK != 0
1778 if y < $autotable_sw.vadjustment.value
1780 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1782 if $button1_pressed_autotable || press_x
1783 scroll_upper($autotable_sw, y)
1786 w, pos_x, pos_y = $autotable.window.pointer
1787 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1788 update_selected.call
1791 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1793 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1795 if $button1_pressed_autotable || press_x
1796 scroll_lower($autotable_sw, y)
1799 w, pos_x, pos_y = $autotable.window.pointer
1800 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1801 update_selected.call
1806 ! $autotable.window.nil?
1810 $autotable.signal_connect('button-press-event') { |w,e|
1812 if !$button1_pressed_autotable
1815 if e.state & Gdk::Window::SHIFT_MASK == 0
1816 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1817 $selected_elements = {}
1818 $statusbar.push(0, utf8(_("Nothing selected.")))
1820 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1822 set_mousecursor(Gdk::Cursor::TCROSS)
1826 $autotable.signal_connect('button-release-event') { |w,e|
1828 if $button1_pressed_autotable
1829 #- unselect all only now
1830 $multiple_dnd = $selected_elements.keys
1831 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1832 $selected_elements = {}
1833 $button1_pressed_autotable = false
1836 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1837 if $selected_elements.length > 0
1838 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1841 press_x = press_y = pos_x = pos_y = nil
1842 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1846 $autotable.signal_connect('motion-notify-event') { |w,e|
1849 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1853 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1854 update_selected.call
1860 def create_subalbums_page
1862 subalbums_hb = Gtk::HBox.new
1863 $subalbums_vb = Gtk::VBox.new(false, 5)
1864 subalbums_hb.pack_start($subalbums_vb, false, false)
1865 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1866 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1867 $subalbums_sw.add_with_viewport(subalbums_hb)
1870 def save_current_file
1876 ios = File.open($filename, "w")
1879 rescue Iconv::IllegalSequence
1880 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1881 if ! ios.nil? && ! ios.closed?
1884 $xmldoc.xml_decl.encoding = 'UTF-8'
1885 ios = File.open($filename, "w")
1897 def save_current_file_user
1898 save_tempfilename = $filename
1899 $filename = $orig_filename
1900 if ! save_current_file
1901 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1902 $filename = save_tempfilename
1906 $generated_outofline = false
1907 $filename = save_tempfilename
1909 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1910 $todelete.each { |f|
1914 puts "Failed to delete #{f}: #{$!}"
1920 def mark_document_as_dirty
1921 $xmldoc.elements.each('//dir') { |elem|
1922 elem.delete_attribute('already-generated')
1926 #- ret: true => ok false => cancel
1927 def ask_save_modifications(msg1, msg2, *options)
1929 options = options.size > 0 ? options[0] : {}
1931 if options[:disallow_cancel]
1932 dialog = Gtk::Dialog.new(msg1,
1934 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1935 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1936 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1938 dialog = Gtk::Dialog.new(msg1,
1940 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1941 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1942 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1943 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1945 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1946 dialog.vbox.add(Gtk::Label.new(msg2))
1947 dialog.window_position = Gtk::Window::POS_CENTER
1950 dialog.run { |response|
1952 if response == Gtk::Dialog::RESPONSE_YES
1953 if ! save_current_file_user
1954 return ask_save_modifications(msg1, msg2, options)
1957 #- if we have generated an album but won't save modifications, we must remove
1958 #- already-generated markers in original file
1959 if $generated_outofline
1961 $xmldoc = REXML::Document.new(File.new($orig_filename))
1962 mark_document_as_dirty
1963 ios = File.open($orig_filename, "w")
1967 puts "exception: #{$!}"
1971 if response == Gtk::Dialog::RESPONSE_CANCEL
1979 def try_quit(*options)
1980 if ask_save_modifications(utf8(_("Save before quitting?")),
1981 utf8(_("Do you want to save your changes before quitting?")),
1987 def show_popup(parent, msg, *options)
1988 dialog = Gtk::Dialog.new
1989 if options[0] && options[0][:title]
1990 dialog.title = options[0][:title]
1992 dialog.title = utf8(_("Booh message"))
1994 lbl = Gtk::Label.new
1995 if options[0] && options[0][:nomarkup]
2000 if options[0] && options[0][:centered]
2001 lbl.set_justify(Gtk::Justification::CENTER)
2003 if options[0] && options[0][:selectable]
2004 lbl.selectable = true
2006 if options[0] && options[0][:scrolled]
2007 sw = Gtk::ScrolledWindow.new(nil, nil)
2008 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
2009 sw.add_with_viewport(lbl)
2011 dialog.set_default_size(500, 600)
2013 dialog.vbox.add(lbl)
2014 dialog.set_default_size(200, 120)
2016 if options[0] && options[0][:okcancel]
2017 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2019 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2021 if options[0] && options[0][:pos_centered]
2022 dialog.window_position = Gtk::Window::POS_CENTER
2024 dialog.window_position = Gtk::Window::POS_MOUSE
2027 if options[0] && options[0][:linkurl]
2028 linkbut = Gtk::Button.new('')
2029 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2030 linkbut.signal_connect('clicked') {
2031 open_url(options[0][:linkurl])
2032 dialog.response(Gtk::Dialog::RESPONSE_OK)
2033 set_mousecursor_normal
2035 linkbut.relief = Gtk::RELIEF_NONE
2036 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2037 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2038 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2043 if !options[0] || !options[0][:not_transient]
2044 dialog.transient_for = parent
2045 dialog.run { |response|
2047 if options[0] && options[0][:okcancel]
2048 return response == Gtk::Dialog::RESPONSE_OK
2052 dialog.signal_connect('response') { dialog.destroy }
2056 def set_mainwindow_title(progress)
2057 filename = $orig_filename || $filename
2060 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2062 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2066 $main_window.title = 'booh - ' + File.basename(filename)
2068 $main_window.title = 'booh'
2073 def backend_wait_message(parent, msg, infopipe_path, mode)
2075 w.set_transient_for(parent)
2078 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2079 vb.pack_start(Gtk::Label.new(msg), false, false)
2081 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2082 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2083 if mode != 'one dir scan'
2084 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2086 if mode == 'web-album'
2087 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2088 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2090 vb.pack_start(Gtk::HSeparator.new, false, false)
2092 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2093 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2094 vb.pack_end(bottom, false, false)
2097 update_progression_title_pb1 = proc {
2098 if mode == 'web-album'
2099 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2100 elsif mode != 'one dir scan'
2101 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2103 set_mainwindow_title(pb1_1.fraction)
2107 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2108 refresh_thread = Thread.new {
2109 directories_counter = 0
2110 while line = infopipe.gets
2111 msg 3, "infopipe got data: #{line}"
2112 if line =~ /^directories: (\d+), sizes: (\d+)/
2113 directories = $1.to_f + 1
2115 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2116 elements = $3.to_f + 1
2117 if mode == 'web-album'
2121 gtk_thread_protect { pb1_1.fraction = 0 }
2122 if mode != 'one dir scan'
2123 newtext = utf8(full_src_dir_to_rel($1, $2))
2124 newtext = '/' if newtext == ''
2125 gtk_thread_protect { pb1_2.text = newtext }
2126 directories_counter += 1
2127 gtk_thread_protect {
2128 pb1_2.fraction = directories_counter / directories
2129 update_progression_title_pb1.call
2132 elsif line =~ /^processing element$/
2133 element_counter += 1
2134 gtk_thread_protect {
2135 pb1_1.fraction = element_counter / elements
2136 update_progression_title_pb1.call
2138 elsif line =~ /^processing size$/
2139 element_counter += 1
2140 gtk_thread_protect {
2141 pb1_1.fraction = element_counter / elements
2142 update_progression_title_pb1.call
2144 elsif line =~ /^finished processing sizes$/
2145 gtk_thread_protect { pb1_1.fraction = 1 }
2146 elsif line =~ /^creating index.html$/
2147 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2148 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2149 directories_counter = 0
2150 elsif line =~ /^index.html: (.+)\|(.+)/
2151 newtext = utf8(full_src_dir_to_rel($1, $2))
2152 newtext = '/' if newtext == ''
2153 gtk_thread_protect { pb2.text = newtext }
2154 directories_counter += 1
2155 gtk_thread_protect {
2156 pb2.fraction = directories_counter / directories
2157 set_mainwindow_title(0.9 + pb2.fraction / 10)
2159 elsif line =~ /^die: (.*)$/
2166 w.signal_connect('delete-event') { w.destroy }
2167 w.signal_connect('destroy') {
2168 Thread.kill(refresh_thread)
2169 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2172 File.delete(infopipe_path)
2174 set_mainwindow_title(nil)
2176 w.window_position = Gtk::Window::POS_CENTER
2182 def call_backend(cmd, waitmsg, mode, params)
2183 pipe = Tempfile.new("boohpipe")
2186 system("mkfifo #{path}")
2187 cmd += " --info-pipe #{path}"
2188 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2193 id, exitstatus = Process.waitpid2(pid)
2194 gtk_thread_protect { w8.destroy }
2196 if params[:successmsg]
2197 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2199 if params[:closure_after]
2200 gtk_thread_protect(¶ms[:closure_after])
2202 elsif exitstatus == 15
2203 #- say nothing, user aborted
2205 gtk_thread_protect { show_popup($main_window,
2206 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2212 button.signal_connect('clicked') {
2213 Process.kill('SIGTERM', pid)
2217 def save_changes(*forced)
2218 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2222 $xmldir.delete_attribute('already-generated')
2224 propagate_children = proc { |xmldir|
2225 if xmldir.attributes['subdirs-caption']
2226 xmldir.delete_attribute('already-generated')
2228 xmldir.elements.each('dir') { |element|
2229 propagate_children.call(element)
2233 if $xmldir.child_byname_notattr('dir', 'deleted')
2234 new_title = $subalbums_title.buffer.text
2235 if new_title != $xmldir.attributes['subdirs-caption']
2236 parent = $xmldir.parent
2237 if parent.name == 'dir'
2238 parent.delete_attribute('already-generated')
2240 propagate_children.call($xmldir)
2242 $xmldir.add_attribute('subdirs-caption', new_title)
2243 $xmldir.elements.each('dir') { |element|
2244 if !element.attributes['deleted']
2245 path = element.attributes['path']
2246 newtext = $subalbums_edits[path][:editzone].buffer.text
2247 if element.attributes['subdirs-caption']
2248 if element.attributes['subdirs-caption'] != newtext
2249 propagate_children.call(element)
2251 element.add_attribute('subdirs-caption', newtext)
2252 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2254 if element.attributes['thumbnails-caption'] != newtext
2255 element.delete_attribute('already-generated')
2257 element.add_attribute('thumbnails-caption', newtext)
2258 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2264 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2265 if $xmldir.attributes['thumbnails-caption']
2266 path = $xmldir.attributes['path']
2267 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2269 elsif $xmldir.attributes['thumbnails-caption']
2270 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2273 if $xmldir.attributes['thumbnails-caption']
2274 if edit = $subalbums_edits[$xmldir.attributes['path']]
2275 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2279 #- remove and reinsert elements to reflect new ordering
2282 $xmldir.elements.each { |element|
2283 if element.name == 'image' || element.name == 'video'
2284 saves[element.attributes['filename']] = element.remove
2288 $autotable.current_order.each { |path|
2289 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2290 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2293 saves.each_key { |path|
2294 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2295 chld.add_attribute('deleted', 'true')
2299 def sort_by_exif_date
2303 rexml_thread_protect {
2304 $xmldir.elements.each { |element|
2305 if element.name == 'image' || element.name == 'video'
2306 current_order << element.attributes['filename']
2311 #- look for EXIF dates
2314 if current_order.size > 20
2316 w.set_transient_for($main_window)
2318 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2319 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2320 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2321 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2322 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2323 vb.pack_end(bottom, false, false)
2325 w.signal_connect('delete-event') { w.destroy }
2326 w.window_position = Gtk::Window::POS_CENTER
2330 b.signal_connect('clicked') { aborted = true }
2332 current_order.each { |f|
2334 if entry2type(f) == 'image'
2336 pb.fraction = i.to_f / current_order.size
2337 Gtk.main_iteration while Gtk.events_pending?
2338 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2340 dates[f] = date_time
2353 current_order.each { |f|
2354 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2356 dates[f] = date_time
2362 rexml_thread_protect {
2363 $xmldir.elements.each { |element|
2364 if element.name == 'image' || element.name == 'video'
2365 saves[element.attributes['filename']] = element.remove
2370 neworder = smartsort(current_order, dates)
2372 rexml_thread_protect {
2374 $xmldir.add_element(saves[f].name, saves[f].attributes)
2378 #- let the auto-table reflect new ordering
2382 def remove_all_captions
2385 $autotable.current_order.each { |path|
2386 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2387 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2389 save_undo(_("remove all captions"),
2391 texts.each_key { |key|
2392 $name2widgets[key][:textview].buffer.text = texts[key]
2394 $notebook.set_page(1)
2396 texts.each_key { |key|
2397 $name2widgets[key][:textview].buffer.text = ''
2399 $notebook.set_page(1)
2405 $selected_elements.each_key { |path|
2406 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2412 $selected_elements = {}
2416 $undo_tb.sensitive = $undo_mb.sensitive = false
2417 $redo_tb.sensitive = $redo_mb.sensitive = false
2423 $subalbums_vb.children.each { |chld|
2424 $subalbums_vb.remove(chld)
2426 $subalbums = Gtk::Table.new(0, 0, true)
2427 current_y_sub_albums = 0
2429 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2430 $subalbums_edits = {}
2431 subalbums_counter = 0
2432 subalbums_edits_bypos = {}
2434 add_subalbum = proc { |xmldir, counter|
2435 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2436 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2437 if xmldir == $xmldir
2438 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2439 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2440 caption = xmldir.attributes['thumbnails-caption']
2441 infotype = 'thumbnails'
2443 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2444 captionfile, caption = find_subalbum_caption_info(xmldir)
2445 infotype = find_subalbum_info_type(xmldir)
2447 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2448 hbox = Gtk::HBox.new
2449 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2451 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2454 my_gen_real_thumbnail = proc {
2455 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2458 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2459 f.add(img = Gtk::Image.new)
2460 my_gen_real_thumbnail.call
2462 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2464 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2465 $subalbums.attach(hbox,
2466 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2468 frame, textview = create_editzone($subalbums_sw, 0, img)
2469 textview.buffer.text = caption
2470 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2471 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2473 change_image = proc {
2474 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2476 Gtk::FileChooser::ACTION_OPEN,
2478 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2479 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2480 fc.transient_for = $main_window
2481 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))
2482 f.add(preview_img = Gtk::Image.new)
2484 fc.signal_connect('update-preview') { |w|
2485 if fc.preview_filename
2486 if entry2type(fc.preview_filename) == 'video'
2490 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2492 fc.preview_widget_active = false
2494 tmpimage = "#{tmpdir}/00000001.jpg"
2496 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2497 fc.preview_widget_active = true
2498 rescue Gdk::PixbufError
2499 fc.preview_widget_active = false
2501 File.delete(tmpimage)
2508 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2509 fc.preview_widget_active = true
2510 rescue Gdk::PixbufError
2511 fc.preview_widget_active = false
2516 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2518 old_file = captionfile
2519 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2520 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2521 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2522 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2524 new_file = fc.filename
2525 msg 3, "new captionfile is: #{fc.filename}"
2526 perform_changefile = proc {
2527 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2528 $modified_pixbufs.delete(thumbnail_file)
2529 xmldir.delete_attribute("#{infotype}-rotate")
2530 xmldir.delete_attribute("#{infotype}-color-swap")
2531 xmldir.delete_attribute("#{infotype}-enhance")
2532 xmldir.delete_attribute("#{infotype}-seektime")
2533 my_gen_real_thumbnail.call
2535 perform_changefile.call
2537 save_undo(_("change caption file for sub-album"),
2539 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2540 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2541 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2542 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2543 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2544 my_gen_real_thumbnail.call
2545 $notebook.set_page(0)
2547 perform_changefile.call
2548 $notebook.set_page(0)
2556 if File.exists?(thumbnail_file)
2557 File.delete(thumbnail_file)
2559 my_gen_real_thumbnail.call
2562 rotate_and_cleanup = proc { |angle|
2563 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2564 if File.exists?(thumbnail_file)
2565 File.delete(thumbnail_file)
2569 move = proc { |direction|
2572 save_changes('forced')
2573 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2574 if direction == 'up'
2575 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2576 subalbums_edits_bypos[oldpos - 1][:position] += 1
2578 if direction == 'down'
2579 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2580 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2582 if direction == 'top'
2583 for i in 1 .. oldpos - 1
2584 subalbums_edits_bypos[i][:position] += 1
2586 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2588 if direction == 'bottom'
2589 for i in oldpos + 1 .. subalbums_counter
2590 subalbums_edits_bypos[i][:position] -= 1
2592 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2596 $xmldir.elements.each('dir') { |element|
2597 if (!element.attributes['deleted'])
2598 elems << [ element.attributes['path'], element.remove ]
2601 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2602 each { |e| $xmldir.add_element(e[1]) }
2603 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2604 $xmldir.elements.each('descendant::dir') { |elem|
2605 elem.delete_attribute('already-generated')
2608 sel = $albums_tv.selection.selected_rows
2610 populate_subalbums_treeview(false)
2611 $albums_tv.selection.select_path(sel[0])
2614 color_swap_and_cleanup = proc {
2615 perform_color_swap_and_cleanup = proc {
2616 color_swap(xmldir, "#{infotype}-")
2617 my_gen_real_thumbnail.call
2619 perform_color_swap_and_cleanup.call
2621 save_undo(_("color swap"),
2623 perform_color_swap_and_cleanup.call
2624 $notebook.set_page(0)
2626 perform_color_swap_and_cleanup.call
2627 $notebook.set_page(0)
2632 change_seektime_and_cleanup = proc {
2633 if values = ask_new_seektime(xmldir, "#{infotype}-")
2634 perform_change_seektime_and_cleanup = proc { |val|
2635 change_seektime(xmldir, "#{infotype}-", val)
2636 my_gen_real_thumbnail.call
2638 perform_change_seektime_and_cleanup.call(values[:new])
2640 save_undo(_("specify seektime"),
2642 perform_change_seektime_and_cleanup.call(values[:old])
2643 $notebook.set_page(0)
2645 perform_change_seektime_and_cleanup.call(values[:new])
2646 $notebook.set_page(0)
2652 whitebalance_and_cleanup = proc {
2653 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2654 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2655 perform_change_whitebalance_and_cleanup = proc { |val|
2656 change_whitebalance(xmldir, "#{infotype}-", val)
2657 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2658 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2659 if File.exists?(thumbnail_file)
2660 File.delete(thumbnail_file)
2663 perform_change_whitebalance_and_cleanup.call(values[:new])
2665 save_undo(_("fix white balance"),
2667 perform_change_whitebalance_and_cleanup.call(values[:old])
2668 $notebook.set_page(0)
2670 perform_change_whitebalance_and_cleanup.call(values[:new])
2671 $notebook.set_page(0)
2677 gammacorrect_and_cleanup = proc {
2678 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2679 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2680 perform_change_gammacorrect_and_cleanup = proc { |val|
2681 change_gammacorrect(xmldir, "#{infotype}-", val)
2682 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2683 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2684 if File.exists?(thumbnail_file)
2685 File.delete(thumbnail_file)
2688 perform_change_gammacorrect_and_cleanup.call(values[:new])
2690 save_undo(_("gamma correction"),
2692 perform_change_gammacorrect_and_cleanup.call(values[:old])
2693 $notebook.set_page(0)
2695 perform_change_gammacorrect_and_cleanup.call(values[:new])
2696 $notebook.set_page(0)
2702 enhance_and_cleanup = proc {
2703 perform_enhance_and_cleanup = proc {
2704 enhance(xmldir, "#{infotype}-")
2705 my_gen_real_thumbnail.call
2708 perform_enhance_and_cleanup.call
2710 save_undo(_("enhance"),
2712 perform_enhance_and_cleanup.call
2713 $notebook.set_page(0)
2715 perform_enhance_and_cleanup.call
2716 $notebook.set_page(0)
2721 evtbox.signal_connect('button-press-event') { |w, event|
2722 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2724 rotate_and_cleanup.call(90)
2726 rotate_and_cleanup.call(-90)
2727 elsif $enhance.active?
2728 enhance_and_cleanup.call
2731 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2732 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2733 { :forbid_left => true, :forbid_right => true,
2734 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2735 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2736 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2737 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2738 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2740 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2745 evtbox.signal_connect('button-press-event') { |w, event|
2746 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2750 evtbox.signal_connect('button-release-event') { |w, event|
2751 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2752 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2753 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2754 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2755 msg 3, "gesture rotate: #{angle}"
2756 rotate_and_cleanup.call(angle)
2759 $gesture_press = nil
2762 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2763 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2764 current_y_sub_albums += 1
2767 if $xmldir.child_byname_notattr('dir', 'deleted')
2769 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2770 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2771 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2772 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2773 #- this album image/caption
2774 if $xmldir.attributes['thumbnails-caption']
2775 add_subalbum.call($xmldir, 0)
2778 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2779 $xmldir.elements.each { |element|
2780 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2781 #- element (image or video) of this album
2782 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2783 msg 3, "dest_img: #{dest_img}"
2784 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2785 total[element.name] += 1
2787 if element.name == 'dir' && !element.attributes['deleted']
2788 #- sub-album image/caption
2789 add_subalbum.call(element, subalbums_counter += 1)
2790 total[element.name] += 1
2793 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2794 total['image'], total['video'], total['dir'] ]))
2795 $subalbums_vb.add($subalbums)
2796 $subalbums_vb.show_all
2798 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2799 $notebook.get_tab_label($autotable_sw).sensitive = false
2800 $notebook.set_page(0)
2801 $thumbnails_title.buffer.text = ''
2803 $notebook.get_tab_label($autotable_sw).sensitive = true
2804 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2807 if !$xmldir.child_byname_notattr('dir', 'deleted')
2808 $notebook.get_tab_label($subalbums_sw).sensitive = false
2809 $notebook.set_page(1)
2811 $notebook.get_tab_label($subalbums_sw).sensitive = true
2815 def pixbuf_or_nil(filename)
2817 return Gdk::Pixbuf.new(filename)
2823 def theme_choose(current)
2824 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2826 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2827 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2828 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2830 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2831 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2832 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2833 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2834 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2835 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2836 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2837 treeview.signal_connect('button-press-event') { |w, event|
2838 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2839 dialog.response(Gtk::Dialog::RESPONSE_OK)
2843 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2845 ([ $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|
2848 iter[0] = File.basename(dir)
2849 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2850 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2851 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2852 if File.basename(dir) == current
2853 treeview.selection.select_iter(iter)
2856 dialog.set_default_size(-1, 500)
2857 dialog.vbox.show_all
2859 dialog.run { |response|
2860 iter = treeview.selection.selected
2862 if response == Gtk::Dialog::RESPONSE_OK && iter
2863 return model.get_value(iter, 0)
2869 def show_password_protections
2870 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2871 child_iter = $albums_iters[xmldir.attributes['path']]
2872 if xmldir.attributes['password-protect']
2873 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2874 already_protected = true
2875 elsif already_protected
2876 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2878 pix = pix.saturate_and_pixelate(1, true)
2884 xmldir.elements.each('dir') { |elem|
2885 if !elem.attributes['deleted']
2886 examine_dir_elem.call(child_iter, elem, already_protected)
2890 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2893 def populate_subalbums_treeview(select_first)
2897 $subalbums_vb.children.each { |chld|
2898 $subalbums_vb.remove(chld)
2901 source = $xmldoc.root.attributes['source']
2902 msg 3, "source: #{source}"
2904 xmldir = $xmldoc.elements['//dir']
2905 if !xmldir || xmldir.attributes['path'] != source
2906 msg 1, _("Corrupted booh file...")
2910 append_dir_elem = proc { |parent_iter, xmldir|
2911 child_iter = $albums_ts.append(parent_iter)
2912 child_iter[0] = File.basename(xmldir.attributes['path'])
2913 child_iter[1] = xmldir.attributes['path']
2914 $albums_iters[xmldir.attributes['path']] = child_iter
2915 msg 3, "puttin location: #{xmldir.attributes['path']}"
2916 xmldir.elements.each('dir') { |elem|
2917 if !elem.attributes['deleted']
2918 append_dir_elem.call(child_iter, elem)
2922 append_dir_elem.call(nil, xmldir)
2923 show_password_protections
2925 $albums_tv.expand_all
2927 $albums_tv.selection.select_iter($albums_ts.iter_first)
2931 def select_current_theme
2932 select_theme($xmldoc.root.attributes['theme'],
2933 $xmldoc.root.attributes['limit-sizes'],
2934 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2935 $xmldoc.root.attributes['thumbnails-per-row'])
2938 def open_file(filename)
2942 $current_path = nil #- invalidate
2943 $modified_pixbufs = {}
2946 $subalbums_vb.children.each { |chld|
2947 $subalbums_vb.remove(chld)
2950 if !File.exists?(filename)
2951 return utf8(_("File not found."))
2955 $xmldoc = REXML::Document.new(File.new(filename))
2960 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2961 if entry2type(filename).nil?
2962 return utf8(_("Not a booh file!"))
2964 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."))
2968 if !source = $xmldoc.root.attributes['source']
2969 return utf8(_("Corrupted booh file..."))
2972 if !dest = $xmldoc.root.attributes['destination']
2973 return utf8(_("Corrupted booh file..."))
2976 if !theme = $xmldoc.root.attributes['theme']
2977 return utf8(_("Corrupted booh file..."))
2980 if $xmldoc.root.attributes['version'] < $VERSION
2981 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2982 mark_document_as_dirty
2983 if $xmldoc.root.attributes['version'] < '0.8.4'
2984 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2985 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2986 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2987 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2988 if old_dest_dir != new_dest_dir
2989 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2991 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2992 xmldir.elements.each { |element|
2993 if %w(image video).include?(element.name) && !element.attributes['deleted']
2994 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2995 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2996 Dir[old_name + '*'].each { |file|
2997 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2998 file != new_file and sys("mv '#{file}' '#{new_file}'")
3001 if element.name == 'dir' && !element.attributes['deleted']
3002 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3003 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3004 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
3008 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3012 $xmldoc.root.add_attribute('version', $VERSION)
3015 select_current_theme
3017 $filename = filename
3018 set_mainwindow_title(nil)
3019 $default_size['thumbnails'] =~ /(.*)x(.*)/
3020 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3021 $albums_thumbnail_size =~ /(.*)x(.*)/
3022 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3024 populate_subalbums_treeview(true)
3026 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $upload.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
3030 def open_file_user(filename)
3031 result = open_file(filename)
3033 $config['last-opens'] ||= []
3034 if $config['last-opens'][-1] != utf8(filename)
3035 $config['last-opens'] << utf8(filename)
3037 $orig_filename = $filename
3038 $main_window.title = 'booh - ' + File.basename($orig_filename)
3039 tmp = Tempfile.new("boohtemp")
3040 $filename = tmp.path
3043 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3045 $tempfiles << $filename << "#{$filename}.backup"
3047 $orig_filename = nil
3053 if !ask_save_modifications(utf8(_("Save this album?")),
3054 utf8(_("Do you want to save the changes to this album?")),
3055 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3058 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3060 Gtk::FileChooser::ACTION_OPEN,
3062 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3063 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3064 fc.set_current_folder(File.expand_path("~/.booh"))
3065 fc.transient_for = $main_window
3066 fc.preview_widget = previewlabel = Gtk::Label.new.show
3067 fc.signal_connect('update-preview') { |w|
3068 if fc.preview_filename
3070 push_mousecursor_wait(fc)
3071 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3075 xmldoc.elements.each('//*') { |elem|
3076 if elem.name == 'dir'
3078 elsif elem.name == 'image'
3080 elsif elem.name == 'video'
3088 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3089 fc.preview_widget_active = false
3091 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") %
3092 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3093 fc.preview_widget_active = true
3099 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3100 push_mousecursor_wait(fc)
3101 msg = open_file_user(fc.filename)
3116 def additional_booh_options
3119 options += "--mproc #{$config['mproc'].to_i} "
3121 options += "--comments-format '#{$config['comments-format']}' "
3122 if $config['transcode-videos']
3123 options += "--transcode-videos '#{$config['transcode-videos']}' "
3125 if $config['use-flv'] == 'true'
3126 options += "--flv-generator '#{$config['flv-generator']}' "
3131 def ask_multi_languages(value)
3133 spl = value.split(',')
3134 value = [ spl[0..-2], spl[-1] ]
3137 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3140 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3141 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3143 lbl = Gtk::Label.new
3145 _("You can choose to activate <b>multi-languages</b> support for this web-album
3146 (it will work only if you publish your web-album on an Apache web-server). This will
3147 use the MultiViews feature of Apache; the pages will be served according to the
3148 value of the Accept-Language HTTP header sent by the web browsers, so that people
3149 with different languages preferences will be able to browse your web-album with
3150 navigation in their language (if language is available).
3153 dialog.vbox.add(lbl)
3154 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3155 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3156 add(languages = Gtk::Button.new))))
3158 pick_languages = proc {
3159 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3162 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3163 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3165 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3166 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3168 SUPPORTED_LANGUAGES.each { |lang|
3169 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3170 if ! value.nil? && value[0].include?(lang)
3176 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3177 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3178 fallback_language = nil
3179 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3180 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3181 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3182 fbl_rb.active = true
3183 fallback_language = SUPPORTED_LANGUAGES[0]
3185 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3186 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3187 rb.signal_connect('clicked') { fallback_language = lang }
3188 if ! value.nil? && value[1] == lang
3193 dialog2.window_position = Gtk::Window::POS_MOUSE
3197 dialog2.run { |response|
3199 if resp == Gtk::Dialog::RESPONSE_OK
3201 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3202 value[1] = fallback_language
3203 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3210 languages.signal_connect('clicked') {
3213 dialog.window_position = Gtk::Window::POS_MOUSE
3217 rb_yes.active = true
3218 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3220 rb_no.signal_connect('clicked') {
3224 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3237 dialog.run { |response|
3242 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3244 return [ true, nil ]
3246 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3255 if !ask_save_modifications(utf8(_("Save this album?")),
3256 utf8(_("Do you want to save the changes to this album?")),
3257 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3260 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3262 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3263 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3264 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3266 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3267 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3268 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3269 tbl.attach(src = Gtk::Entry.new,
3270 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3271 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3272 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3273 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3274 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3275 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3276 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3277 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3278 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3279 tbl.attach(dest = Gtk::Entry.new,
3280 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3281 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3282 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3283 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3284 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3285 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3286 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3287 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3288 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3290 tooltips = Gtk::Tooltips.new
3291 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3292 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3293 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3294 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3295 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3296 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3297 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)
3298 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3299 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3300 nperpage_model = Gtk::ListStore.new(String, String)
3301 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3302 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3303 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3304 nperpagecombo.set_attributes(crt, { :markup => 0 })
3305 iter = nperpage_model.append
3306 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3308 [ 12, 20, 30, 40, 50 ].each { |v|
3309 iter = nperpage_model.append
3310 iter[0] = iter[1] = v.to_s
3312 nperpagecombo.active = 0
3314 multilanguages_value = nil
3315 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3316 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3317 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)
3318 multilanguages.signal_connect('clicked') {
3319 retval = ask_multi_languages(multilanguages_value)
3321 multilanguages_value = retval[1]
3323 if multilanguages_value
3324 ml_label.text = utf8(_("Multi-languages: enabled."))
3326 ml_label.text = utf8(_("Multi-languages: disabled."))
3329 if $config['default-multi-languages']
3330 multilanguages_value = $config['default-multi-languages']
3331 ml_label.text = utf8(_("Multi-languages: enabled."))
3333 ml_label.text = utf8(_("Multi-languages: disabled."))
3336 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3337 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3338 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)
3339 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3340 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3341 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)
3342 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3343 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3344 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)
3346 src_nb_calculated_for = ''
3347 src_nb_process = nil
3348 process_src_nb = proc {
3349 if src.text != src_nb_calculated_for
3350 src_nb_calculated_for = src.text
3353 Process.kill(9, src_nb_process)
3355 #- process doesn't exist anymore - race condition
3358 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3359 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3361 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3362 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3365 while src_nb_process
3366 msg 3, "sleeping for completion of previous process"
3369 gtk_thread_flush #- flush to avoid race condition in src_nb markup update
3371 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3372 total = { 'image' => 0, 'video' => 0, nil => 0 }
3373 if src_nb_process = fork
3374 msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3378 rd.readlines.each { |dir|
3379 if File.basename(dir) =~ /^\./
3383 Dir.entries(dir.chomp).each { |file|
3384 total[entry2type(file)] += 1
3386 rescue Errno::EACCES, Errno::ENOENT
3391 msg 3, "ripping #{src_nb_process}"
3392 dummy, exitstatus = Process.waitpid2(src_nb_process)
3394 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3396 src_nb_process = nil
3402 wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3403 Process.exit!(0) #- _exit
3406 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3409 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3415 timeout_src_nb = Gtk.timeout_add(100) {
3419 src_browse.signal_connect('clicked') {
3420 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3422 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3424 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3425 fc.transient_for = $main_window
3426 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3427 src.text = utf8(fc.filename)
3429 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3434 dest_browse.signal_connect('clicked') {
3435 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3437 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3439 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3440 fc.transient_for = $main_window
3441 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3442 dest.text = utf8(fc.filename)
3447 conf_browse.signal_connect('clicked') {
3448 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3450 Gtk::FileChooser::ACTION_SAVE,
3452 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3453 fc.transient_for = $main_window
3454 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3455 fc.set_current_folder(File.expand_path("~/.booh"))
3456 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3457 conf.text = utf8(fc.filename)
3464 recreate_theme_config = proc {
3465 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3467 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3468 $images_size.each { |s|
3469 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3473 tooltips.set_tip(cb, utf8(s['description']), nil)
3474 theme_sizes << { :widget => cb, :value => s['name'] }
3476 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3477 tooltips = Gtk::Tooltips.new
3478 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3479 theme_sizes << { :widget => cb, :value => 'original' }
3482 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3485 $allowed_N_values.each { |n|
3487 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3489 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3491 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3495 nperrows << { :widget => rb, :value => n }
3497 nperrowradios.show_all
3499 recreate_theme_config.call
3501 theme_button.signal_connect('clicked') {
3502 if newtheme = theme_choose(theme_button.label)
3503 theme_button.label = newtheme
3504 recreate_theme_config.call
3508 dialog.vbox.add(frame1)
3509 dialog.vbox.add(frame2)
<