5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2011 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
32 require 'booh/libadds'
33 require 'booh/GtkAutoTable'
37 bindtextdomain("booh")
39 require 'booh/rexml/document'
42 require 'booh/booh-lib'
44 require 'booh/UndoHandler'
49 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
50 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
51 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
54 #- default values for some globals
58 $ignore_videos = false
59 $button1_pressed_autotable = false
60 $generated_outofline = false
63 puts _("Usage: %s [OPTION]...") % File.basename($0)
65 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
70 parser = GetoptLong.new
71 parser.set_options(*$options.collect { |ary| ary[0..2] })
73 parser.each_option do |name, arg|
80 puts _("Booh version %s
82 Copyright (c) 2005-2011 Guillaume Cottenceau.
83 This is free software; see the source for copying conditions. There is NO
84 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
88 when '--verbose-level'
89 $verbose_level = arg.to_i
102 $config_file = File.expand_path('~/.booh-gui-rc')
103 if File.readable?($config_file)
105 xmldoc = REXML::Document.new(File.new($config_file))
107 #- encoding unsupported anymore? file edited manually? ignore then
108 msg 1, "Ignoring #{$config_file}, failed to parse it: #{$!}"
111 xmldoc.root.elements.each { |element|
112 txt = element.get_text
114 if txt.value =~ /~~~/ || element.name == 'last-opens'
115 $config[element.name] = txt.value.split(/~~~/)
117 $config[element.name] = txt.value
119 elsif element.elements.size == 0
120 $config[element.name] = ''
122 $config[element.name] = {}
123 element.each { |chld|
125 $config[element.name][chld.name] = txt ? txt.value : nil
131 $config['video-viewer'] ||= '/usr/bin/mplayer %f || /usr/bin/vlc %f'
132 $config['image-editor'] ||= '/usr/bin/gimp-remote %f || /usr/bin/gimp %f'
133 $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"
134 $config['use-flv'] ||= "true"
135 $config['flv-generator'] ||= "/usr/bin/ffmpeg -i %f -b 800k -ar 22050 -ab 32k %o"
136 $config['comments-format'] ||= '%t'
137 if !FileTest.directory?(File.expand_path('~/.booh'))
138 system("mkdir ~/.booh")
140 if $config['mproc'].nil?
142 for line in IO.readlines('/proc/cpuinfo') do
143 line =~ /^processor/ and cpus += 1
146 $config['mproc'] = cpus
149 $config['rotate-set-exif'] ||= 'true'
154 def check_config_preferences_dep
155 viewer_binary = $config['video-viewer'].split.first
156 if viewer_binary && !File.executable?(viewer_binary)
157 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
158 You should fix this in Edit/Preferences so that you can view videos.
160 Problem was: '%s' is not an executable file.
161 Hint: don't forget to specify the full path to the executable,
162 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
165 flv_generator_binary = $config['use-flv'] == 'true' && $config['flv-generator'].split.first
166 if flv_generator_binary && !File.executable?(flv_generator_binary)
167 show_popup($main_window, utf8(_("The configured .flv generator seems to be unavailable.
168 You should fix this in Edit/Preferences so that you can have working
169 embedded flash videos.
171 Problem was: '%s' is not an executable file.
172 Hint: don't forget to specify the full path to the executable,
173 e.g. '/usr/bin/ffmpeg' is correct but 'ffmpeg' only is not.") % flv_generator_binary), { :pos_centered => true, :not_transient => true })
178 if !system("which convert >/dev/null 2>/dev/null")
179 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
180 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
183 if !system("which identify >/dev/null 2>/dev/null")
184 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
185 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
187 if !system("which exif >/dev/null 2>/dev/null")
188 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
190 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
192 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
195 check_config_preferences_dep
198 def check_image_editor
199 if last_failed_binary = check_multi_binaries($config['image-editor'])
200 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
201 You should fix this in Edit/Preferences so that you can edit photos externally.
203 Problem was: '%s' is not an executable file.
204 Hint: don't forget to specify the full path to the executable,
205 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
213 if $config['last-opens'] && $config['last-opens'].size > 10
214 $config['last-opens'] = $config['last-opens'][-10, 10]
217 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
218 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
219 $config.each_pair { |key, value|
220 elem = xmldoc.root.add_element key
222 $config[key].each_pair { |subkey, subvalue|
223 subelem = elem.add_element subkey
224 subelem.add_text subvalue.to_s
226 elsif value.is_a? Array
227 elem.add_text value.join('~~~')
232 elem.add_text value.to_s
236 ios = File.open($config_file, "w")
240 $tempfiles.each { |f|
247 def set_mousecursor(what, *widget)
248 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
249 if widget[0] && widget[0].window
250 widget[0].window.cursor = cursor
252 if $main_window && $main_window.window
253 $main_window.window.cursor = cursor
255 $current_cursor = what
257 def set_mousecursor_wait(*widget)
258 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
259 if Thread.current == Thread.main
260 Gtk.main_iteration while Gtk.events_pending?
263 def set_mousecursor_normal(*widget)
264 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
266 def push_mousecursor_wait(*widget)
267 if $current_cursor != Gdk::Cursor::WATCH
268 $save_cursor = $current_cursor
269 gtk_thread_protect { set_mousecursor_wait(*widget) }
272 def pop_mousecursor(*widget)
273 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
277 source = $xmldoc.root.attributes['source']
278 dest = $xmldoc.root.attributes['destination']
279 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
282 def full_src_dir_to_rel(path, source)
283 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
286 def build_full_dest_filename(filename)
287 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
290 def save_undo(name, closure, *params)
291 UndoHandler.save_undo(name, closure, [ *params ])
292 $undo_tb.sensitive = $undo_mb.sensitive = true
293 $redo_tb.sensitive = $redo_mb.sensitive = false
296 def view_element(filename, closures)
297 if entry2type(filename) == 'video'
298 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
304 w = create_window.set_title(filename)
306 msg 3, "filename: #{filename}"
307 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
308 #- typically this file won't exist in case of videos; try with the largest thumbnail around
309 if !File.exists?(dest_img)
310 if entry2type(filename) == 'video'
311 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
312 if not alternatives.empty?
313 dest_img = alternatives[-1]
316 push_mousecursor_wait
317 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
319 if !File.exists?(dest_img)
320 msg 2, _("Could not generate fullscreen thumbnail!")
325 aspect = utf8(_("Aspect: unknown"))
326 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
328 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
330 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
331 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)))
332 evt.signal_connect('button-press-event') { |this, event|
333 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
334 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
336 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
338 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
339 delete_item.signal_connect('activate') {
341 closures[:delete].call(false)
344 menu.popup(nil, nil, event.button, event.time)
347 evt.signal_connect('button-release-event') { |this, event|
349 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
350 msg 3, "gesture delete: click-drag right button to the bottom"
352 closures[:delete].call(false)
353 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
357 tooltips = Gtk::Tooltips.new
358 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
360 w.signal_connect('key-press-event') { |w,event|
361 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
363 closures[:delete].call(false)
367 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
368 b.signal_connect('clicked') { w.destroy }
371 vb.pack_start(evt, false, false)
372 vb.pack_end(bottom, false, false)
375 w.signal_connect('delete-event') { w.destroy }
376 w.window_position = Gtk::Window::POS_CENTER
380 def scroll_upper(scrolledwindow, ypos_top)
381 newval = scrolledwindow.vadjustment.value -
382 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
383 if newval < scrolledwindow.vadjustment.lower
384 newval = scrolledwindow.vadjustment.lower
386 scrolledwindow.vadjustment.value = newval
389 def scroll_lower(scrolledwindow, ypos_bottom)
390 newval = scrolledwindow.vadjustment.value +
391 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
392 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
393 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
395 scrolledwindow.vadjustment.value = newval
398 def autoscroll_if_needed(scrolledwindow, image, textview)
399 #- autoscroll if cursor or image is not visible, if possible
400 if image && image.window || textview.window
401 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
402 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
403 current_miny_visible = scrolledwindow.vadjustment.value
404 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
405 if ypos_top < current_miny_visible
406 scroll_upper(scrolledwindow, ypos_top)
407 elsif ypos_bottom > current_maxy_visible
408 scroll_lower(scrolledwindow, ypos_bottom)
413 def create_editzone(scrolledwindow, pagenum, image)
414 frame = Gtk::Frame.new
415 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
416 frame.set_shadow_type(Gtk::SHADOW_IN)
417 textview.signal_connect('key-press-event') { |w, event|
418 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
419 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
420 scrolledwindow.signal_emit('key-press-event', event)
422 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
423 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
424 if event.keyval == Gdk::Keyval::GDK_Up
425 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
426 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
428 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
431 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
432 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
434 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
441 candidate_undo_text = nil
442 textview.signal_connect('focus-in-event') { |w, event|
443 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
444 candidate_undo_text = textview.buffer.text
448 textview.signal_connect('key-release-event') { |w, event|
449 if candidate_undo_text && candidate_undo_text != textview.buffer.text
451 save_undo(_("text edit"),
453 save_text = textview.buffer.text
454 textview.buffer.text = text
456 $notebook.set_page(pagenum)
458 textview.buffer.text = save_text
460 $notebook.set_page(pagenum)
462 }, candidate_undo_text)
463 candidate_undo_text = nil
466 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)
467 autoscroll_if_needed(scrolledwindow, image, textview)
472 return [ frame, textview ]
475 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
477 if !$modified_pixbufs[thumbnail_img]
478 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
479 elsif !$modified_pixbufs[thumbnail_img][:orig]
480 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
483 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
486 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
487 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
488 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
489 if pixbuf.height > desired_y
490 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
491 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
492 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
497 if $modified_pixbufs[thumbnail_img][:whitebalance]
498 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
501 #- fix gamma correction
502 if $modified_pixbufs[thumbnail_img][:gammacorrect]
503 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
506 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
509 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
512 #- update rotate attribute
513 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
514 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
516 #- change exif orientation if configured so (but forget in case of thumbnails caption)
517 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
518 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
521 $modified_pixbufs[thumbnail_img] ||= {}
522 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
523 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
525 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
528 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
531 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
533 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
535 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
536 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
538 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
539 $notebook.set_page(0)
540 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
545 def color_swap(xmldir, attributes_prefix)
547 rexml_thread_protect {
548 if xmldir.attributes["#{attributes_prefix}color-swap"]
549 xmldir.delete_attribute("#{attributes_prefix}color-swap")
551 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
556 def enhance(xmldir, attributes_prefix)
558 rexml_thread_protect {
559 if xmldir.attributes["#{attributes_prefix}enhance"]
560 xmldir.delete_attribute("#{attributes_prefix}enhance")
562 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
567 def change_seektime(xmldir, attributes_prefix, value)
569 rexml_thread_protect {
570 xmldir.add_attribute("#{attributes_prefix}seektime", value)
574 def ask_new_seektime(xmldir, attributes_prefix)
575 value = rexml_thread_protect {
577 xmldir.attributes["#{attributes_prefix}seektime"]
583 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
585 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
586 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
587 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
591 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
595 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
596 entry.signal_connect('key-press-event') { |w, event|
597 if event.keyval == Gdk::Keyval::GDK_Return
598 dialog.response(Gtk::Dialog::RESPONSE_OK)
600 elsif event.keyval == Gdk::Keyval::GDK_Escape
601 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
604 false #- propagate if needed
608 dialog.window_position = Gtk::Window::POS_MOUSE
611 dialog.run { |response|
614 if response == Gtk::Dialog::RESPONSE_OK
616 msg 3, "changing seektime to #{newval}"
617 return { :old => value, :new => newval }
624 def change_pano_amount(xmldir, attributes_prefix, value)
626 rexml_thread_protect {
628 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
630 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
635 def ask_new_pano_amount(xmldir, attributes_prefix)
636 value = rexml_thread_protect {
638 xmldir.attributes["#{attributes_prefix}pano-amount"]
644 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
646 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
647 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
648 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
652 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
653 of this panorama image compared to other regular images. For example, if the panorama
654 was taken out of four photos on one row, counting the necessary overlap, the width of
655 this panorama image should probably be roughly three times the width of regular images.
657 With this information, booh will be able to generate panorama thumbnails looking
658 the right 'size', since the height of the thumbnail for this image will be similar
659 to the height of other thumbnails.
662 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)")))).
663 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
664 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
665 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
666 spin.signal_connect('value-changed') {
669 dialog.window_position = Gtk::Window::POS_MOUSE
672 spin.value = value.to_f
679 dialog.run { |response|
683 newval = spin.value.to_f
686 if response == Gtk::Dialog::RESPONSE_OK
688 msg 3, "changing panorama amount to #{newval}"
689 return { :old => value, :new => newval }
696 def change_whitebalance(xmlelem, attributes_prefix, value)
698 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
701 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
703 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
704 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
705 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
706 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
707 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
708 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
709 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
710 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
711 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
712 $modified_pixbufs[thumbnail_img] ||= {}
713 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
714 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
716 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
717 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
719 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
722 $modified_pixbufs[thumbnail_img] ||= {}
723 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
725 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
728 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
729 #- init $modified_pixbufs correctly
730 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
732 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
734 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
736 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
737 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
738 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
742 _("You can fix the <b>white balance</b> of the image, if your image is too blue
743 or too yellow because the recorder didn't detect the light correctly. Drag the
744 slider below the image to the left for more blue, to the right for more yellow.
748 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
750 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
752 dialog.window_position = Gtk::Window::POS_MOUSE
756 timeout = Gtk.timeout_add(100) {
757 if hs.value != lastval
760 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
766 dialog.run { |response|
767 Gtk.timeout_remove(timeout)
768 if response == Gtk::Dialog::RESPONSE_OK
770 newval = hs.value.to_s
771 msg 3, "changing white balance to #{newval}"
773 return { :old => value, :new => newval }
776 $modified_pixbufs[thumbnail_img] ||= {}
777 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
778 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
786 def change_gammacorrect(xmlelem, attributes_prefix, value)
788 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
791 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
793 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
794 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
795 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
796 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
797 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
798 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
799 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
800 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
801 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
802 $modified_pixbufs[thumbnail_img] ||= {}
803 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
804 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
806 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
807 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
809 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
812 $modified_pixbufs[thumbnail_img] ||= {}
813 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
815 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
818 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
819 #- init $modified_pixbufs correctly
820 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
822 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
824 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
826 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
827 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
828 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
832 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
833 or too bright. Drag the slider below the image.
837 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
839 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
841 dialog.window_position = Gtk::Window::POS_MOUSE
845 timeout = Gtk.timeout_add(100) {
846 if hs.value != lastval
849 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
855 dialog.run { |response|
856 Gtk.timeout_remove(timeout)
857 if response == Gtk::Dialog::RESPONSE_OK
859 newval = hs.value.to_s
860 msg 3, "gamma correction to #{newval}"
862 return { :old => value, :new => newval }
865 $modified_pixbufs[thumbnail_img] ||= {}
866 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
867 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
875 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
876 if File.exists?(destfile)
877 File.delete(destfile)
879 #- type can be 'element' or 'subdir'
881 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
883 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
887 $max_gen_thumbnail_threads = nil
888 $current_gen_thumbnail_threads = 0
889 $gen_thumbnail_monitor = Monitor.new
891 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
892 if $max_gen_thumbnail_threads.nil?
893 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
896 push_mousecursor_wait
897 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
900 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
905 $gen_thumbnail_monitor.synchronize {
906 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
907 $current_gen_thumbnail_threads += 1
912 msg 3, "generate thumbnail from new thread"
915 $gen_thumbnail_monitor.synchronize {
916 $current_gen_thumbnail_threads -= 1
920 msg 3, "generate thumbnail from current thread"
925 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
926 distribute_multiple_call = Proc.new { |action, arg|
927 $selected_elements.each_key { |path|
928 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
930 if possible_actions[:can_multiple] && $selected_elements.length > 0
931 UndoHandler.begin_batch
932 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
933 UndoHandler.end_batch
935 closures[action].call(arg)
937 $selected_elements = {}
940 if optionals.include?('change_image')
941 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
942 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
943 changeimg.signal_connect('activate') { closures[:change].call }
944 menu.append(Gtk::SeparatorMenuItem.new)
946 if !possible_actions[:can_multiple] || $selected_elements.length == 0
949 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
950 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
951 view.signal_connect('activate') { closures[:view].call }
953 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
954 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
955 view.signal_connect('activate') { closures[:view].call }
956 menu.append(Gtk::SeparatorMenuItem.new)
959 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
960 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
961 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
962 exif.signal_connect('activate') { show_popup($main_window,
963 utf8(`exif -m '#{fullpath}'`),
964 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
965 menu.append(Gtk::SeparatorMenuItem.new)
968 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
969 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
970 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
971 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
972 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
973 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
974 if !possible_actions[:can_multiple] || $selected_elements.length == 0
975 menu.append(Gtk::SeparatorMenuItem.new)
976 if !possible_actions[:forbid_left]
977 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
978 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
979 moveleft.signal_connect('activate') { closures[:move].call('left') }
980 if !possible_actions[:can_left]
981 moveleft.sensitive = false
984 if !possible_actions[:forbid_right]
985 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
986 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
987 moveright.signal_connect('activate') { closures[:move].call('right') }
988 if !possible_actions[:can_right]
989 moveright.sensitive = false
992 if optionals.include?('move_top')
993 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
994 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
995 movetop.signal_connect('activate') { closures[:move].call('top') }
996 if !possible_actions[:can_top]
997 movetop.sensitive = false
1000 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
1001 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
1002 moveup.signal_connect('activate') { closures[:move].call('up') }
1003 if !possible_actions[:can_up]
1004 moveup.sensitive = false
1006 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
1007 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
1008 movedown.signal_connect('activate') { closures[:move].call('down') }
1009 if !possible_actions[:can_down]
1010 movedown.sensitive = false
1012 if optionals.include?('move_bottom')
1013 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
1014 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
1015 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
1016 if !possible_actions[:can_bottom]
1017 movebottom.sensitive = false
1022 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
1023 menu.append(Gtk::SeparatorMenuItem.new)
1024 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
1025 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
1026 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
1027 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1028 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1029 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1030 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1031 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1032 seektime.signal_connect('activate') {
1033 if possible_actions[:can_multiple] && $selected_elements.length > 0
1034 if values = ask_new_seektime(nil, '')
1035 distribute_multiple_call.call(:seektime, values)
1038 closures[:seektime].call
1043 menu.append( Gtk::SeparatorMenuItem.new)
1044 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1045 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1046 gammacorrect.signal_connect('activate') {
1047 if possible_actions[:can_multiple] && $selected_elements.length > 0
1048 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1049 distribute_multiple_call.call(:gammacorrect, values)
1052 closures[:gammacorrect].call
1055 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1056 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1057 whitebalance.signal_connect('activate') {
1058 if possible_actions[:can_multiple] && $selected_elements.length > 0
1059 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1060 distribute_multiple_call.call(:whitebalance, values)
1063 closures[:whitebalance].call
1066 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1067 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1068 _("Enhance constrast"))))
1070 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1072 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1073 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1074 if type == 'image' && possible_actions[:can_panorama]
1075 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1076 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1077 panorama.signal_connect('activate') {
1078 if possible_actions[:can_multiple] && $selected_elements.length > 0
1079 if values = ask_new_pano_amount(nil, '')
1080 distribute_multiple_call.call(:pano, values)
1083 distribute_multiple_call.call(:pano)
1087 menu.append( Gtk::SeparatorMenuItem.new)
1088 if optionals.include?('delete')
1089 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1090 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1091 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1092 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1093 paste_item.signal_connect('activate') { closures[:paste].call }
1094 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1095 clear_item.signal_connect('activate') { $cuts = [] }
1097 paste_item.sensitive = clear_item.sensitive = false
1100 menu.append( Gtk::SeparatorMenuItem.new)
1102 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1103 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1104 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1105 editexternally.signal_connect('activate') {
1106 if check_image_editor
1107 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1113 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1114 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1115 if optionals.include?('delete')
1116 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1117 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1120 menu.popup(nil, nil, event.button, event.time)
1123 def delete_current_subalbum
1125 sel = $albums_tv.selection.selected_rows
1126 $xmldir.elements.each { |e|
1127 if e.name == 'image' || e.name == 'video'
1128 e.add_attribute('deleted', 'true')
1131 #- branch if we have a non deleted subalbum
1132 if $xmldir.child_byname_notattr('dir', 'deleted')
1133 $xmldir.delete_attribute('thumbnails-caption')
1134 $xmldir.delete_attribute('thumbnails-captionfile')
1136 $xmldir.add_attribute('deleted', 'true')
1138 while moveup.parent.name == 'dir'
1139 moveup = moveup.parent
1140 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1141 moveup.add_attribute('deleted', 'true')
1148 save_changes('forced')
1149 populate_subalbums_treeview(false)
1150 $albums_tv.selection.select_path(sel[0])
1156 $current_path = nil #- prevent save_changes from being rerun again
1157 sel = $albums_tv.selection.selected_rows
1158 restore_one = proc { |xmldir|
1159 xmldir.elements.each { |e|
1160 if e.name == 'dir' && e.attributes['deleted']
1163 e.delete_attribute('deleted')
1166 restore_one.call($xmldir)
1167 populate_subalbums_treeview(false)
1168 $albums_tv.selection.select_path(sel[0])
1171 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1174 frame1 = Gtk::Frame.new
1175 fullpath = from_utf8("#{$current_path}/#{filename}")
1177 my_gen_real_thumbnail = proc {
1178 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1182 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1183 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1184 pack_start(img = Gtk::Image.new).
1185 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1186 px, mask = pxb.render_pixmap_and_mask(0)
1187 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1188 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1190 frame1.add(img = Gtk::Image.new)
1193 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1194 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1195 my_gen_real_thumbnail.call
1197 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1200 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1202 tooltips = Gtk::Tooltips.new
1203 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1204 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1206 frame2, textview = create_editzone($autotable_sw, 1, img)
1207 textview.buffer.text = caption
1208 textview.set_justification(Gtk::Justification::CENTER)
1210 vbox = Gtk::VBox.new(false, 5)
1211 vbox.pack_start(evtbox, false, false)
1212 vbox.pack_start(frame2, false, false)
1213 autotable.append(vbox, filename)
1215 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1216 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1218 #- to be able to find widgets by name
1219 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1221 cleanup_all_thumbnails = proc {
1222 #- remove out of sync images
1223 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1224 for sizeobj in $images_size
1225 #- cannot use sizeobj because panoramic images will have a larger width
1226 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1234 cleanup_all_thumbnails.call
1235 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1237 rexml_thread_protect {
1238 $xmldir.delete_attribute('already-generated')
1240 my_gen_real_thumbnail.call
1243 rotate_and_cleanup = proc { |angle|
1244 cleanup_all_thumbnails.call
1245 rexml_thread_protect {
1246 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1250 move = proc { |direction|
1251 do_method = "move_#{direction}"
1252 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1254 done = autotable.method(do_method).call(vbox)
1255 textview.grab_focus #- because if moving, focus is stolen
1259 save_undo(_("move %s") % direction,
1261 autotable.method(undo_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)
1266 autotable.method(do_method).call(vbox)
1267 textview.grab_focus #- because if moving, focus is stolen
1268 autoscroll_if_needed($autotable_sw, img, textview)
1269 $notebook.set_page(1)
1275 color_swap_and_cleanup = proc {
1276 perform_color_swap_and_cleanup = proc {
1277 cleanup_all_thumbnails.call
1278 rexml_thread_protect {
1279 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1281 my_gen_real_thumbnail.call
1284 perform_color_swap_and_cleanup.call
1286 save_undo(_("color swap"),
1288 perform_color_swap_and_cleanup.call
1290 autoscroll_if_needed($autotable_sw, img, textview)
1291 $notebook.set_page(1)
1293 perform_color_swap_and_cleanup.call
1295 autoscroll_if_needed($autotable_sw, img, textview)
1296 $notebook.set_page(1)
1301 change_seektime_and_cleanup_real = proc { |values|
1302 perform_change_seektime_and_cleanup = proc { |val|
1303 cleanup_all_thumbnails.call
1304 rexml_thread_protect {
1305 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1307 my_gen_real_thumbnail.call
1309 perform_change_seektime_and_cleanup.call(values[:new])
1311 save_undo(_("specify seektime"),
1313 perform_change_seektime_and_cleanup.call(values[:old])
1315 autoscroll_if_needed($autotable_sw, img, textview)
1316 $notebook.set_page(1)
1318 perform_change_seektime_and_cleanup.call(values[:new])
1320 autoscroll_if_needed($autotable_sw, img, textview)
1321 $notebook.set_page(1)
1326 change_seektime_and_cleanup = proc {
1327 rexml_thread_protect {
1328 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1329 change_seektime_and_cleanup_real.call(values)
1334 change_pano_amount_and_cleanup_real = proc { |values|
1335 perform_change_pano_amount_and_cleanup = proc { |val|
1336 cleanup_all_thumbnails.call
1337 rexml_thread_protect {
1338 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1341 perform_change_pano_amount_and_cleanup.call(values[:new])
1343 save_undo(_("change panorama amount"),
1345 perform_change_pano_amount_and_cleanup.call(values[:old])
1347 autoscroll_if_needed($autotable_sw, img, textview)
1348 $notebook.set_page(1)
1350 perform_change_pano_amount_and_cleanup.call(values[:new])
1352 autoscroll_if_needed($autotable_sw, img, textview)
1353 $notebook.set_page(1)
1358 change_pano_amount_and_cleanup = proc {
1359 rexml_thread_protect {
1360 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1361 change_pano_amount_and_cleanup_real.call(values)
1366 whitebalance_and_cleanup_real = proc { |values|
1367 perform_change_whitebalance_and_cleanup = proc { |val|
1368 cleanup_all_thumbnails.call
1369 rexml_thread_protect {
1370 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1371 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1372 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1375 perform_change_whitebalance_and_cleanup.call(values[:new])
1377 save_undo(_("fix white balance"),
1379 perform_change_whitebalance_and_cleanup.call(values[:old])
1381 autoscroll_if_needed($autotable_sw, img, textview)
1382 $notebook.set_page(1)
1384 perform_change_whitebalance_and_cleanup.call(values[:new])
1386 autoscroll_if_needed($autotable_sw, img, textview)
1387 $notebook.set_page(1)
1392 whitebalance_and_cleanup = proc {
1393 rexml_thread_protect {
1394 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1395 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1396 whitebalance_and_cleanup_real.call(values)
1401 gammacorrect_and_cleanup_real = proc { |values|
1402 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1403 cleanup_all_thumbnails.call
1404 rexml_thread_protect {
1405 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1406 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1407 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1410 perform_change_gammacorrect_and_cleanup.call(values[:new])
1412 save_undo(_("gamma correction"),
1414 perform_change_gammacorrect_and_cleanup.call(values[:old])
1416 autoscroll_if_needed($autotable_sw, img, textview)
1417 $notebook.set_page(1)
1419 perform_change_gammacorrect_and_cleanup.call(values[:new])
1421 autoscroll_if_needed($autotable_sw, img, textview)
1422 $notebook.set_page(1)
1427 gammacorrect_and_cleanup = Proc.new {
1428 rexml_thread_protect {
1429 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1430 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1431 gammacorrect_and_cleanup_real.call(values)
1436 enhance_and_cleanup = proc {
1437 perform_enhance_and_cleanup = proc {
1438 cleanup_all_thumbnails.call
1439 rexml_thread_protect {
1440 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1442 my_gen_real_thumbnail.call
1445 cleanup_all_thumbnails.call
1446 perform_enhance_and_cleanup.call
1448 save_undo(_("enhance"),
1450 perform_enhance_and_cleanup.call
1452 autoscroll_if_needed($autotable_sw, img, textview)
1453 $notebook.set_page(1)
1455 perform_enhance_and_cleanup.call
1457 autoscroll_if_needed($autotable_sw, img, textview)
1458 $notebook.set_page(1)
1463 delete = proc { |isacut|
1464 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 })
1467 perform_delete = proc {
1468 after = autotable.get_next_widget(vbox)
1470 after = autotable.get_previous_widget(vbox)
1472 if $config['deleteondisk'] && !isacut
1473 msg 3, "scheduling for delete: #{fullpath}"
1474 $todelete << fullpath
1476 autotable.remove_widget(vbox)
1478 $vbox2widgets[after][:textview].grab_focus
1479 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1483 previous_pos = autotable.get_current_number(vbox)
1487 delete_current_subalbum
1489 save_undo(_("delete"),
1491 autotable.reinsert(pos, vbox, filename)
1492 $notebook.set_page(1)
1493 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1495 msg 3, "removing deletion schedule of: #{fullpath}"
1496 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1499 $notebook.set_page(1)
1508 $cuts << { :vbox => vbox, :filename => filename }
1509 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1514 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1517 autotable.queue_draws << proc {
1518 $vbox2widgets[last[:vbox]][:textview].grab_focus
1519 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1521 save_undo(_("paste"),
1523 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1524 $notebook.set_page(1)
1527 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1529 $notebook.set_page(1)
1532 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1537 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1538 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1539 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1540 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1542 textview.signal_connect('key-press-event') { |w, event|
1545 x, y = autotable.get_current_pos(vbox)
1546 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1547 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1548 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1549 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1551 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1552 $vbox2widgets[widget_up][:textview].grab_focus
1559 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1561 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1562 $vbox2widgets[widget_down][:textview].grab_focus
1569 if event.keyval == Gdk::Keyval::GDK_Left
1572 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1579 rotate_and_cleanup.call(-90)
1582 if event.keyval == Gdk::Keyval::GDK_Right
1583 next_ = autotable.get_next_widget(vbox)
1584 if next_ && autotable.get_current_pos(next_)[0] > x
1586 $vbox2widgets[next_][:textview].grab_focus
1593 rotate_and_cleanup.call(90)
1596 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1599 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1600 view_element(filename, { :delete => delete })
1603 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1606 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1610 !propagate #- propagate if needed
1613 $ignore_next_release = false
1614 evtbox.signal_connect('button-press-event') { |w, event|
1615 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1616 if event.state & Gdk::Window::BUTTON3_MASK != 0
1617 #- gesture redo: hold right mouse button then click left mouse button
1618 $config['nogestures'] or perform_redo
1619 $ignore_next_release = true
1621 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1623 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1625 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1626 elsif $enhance.active?
1627 enhance_and_cleanup.call
1628 elsif $delete.active?
1632 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1635 $button1_pressed_autotable = true
1636 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1637 if event.state & Gdk::Window::BUTTON1_MASK != 0
1638 #- gesture undo: hold left mouse button then click right mouse button
1639 $config['nogestures'] or perform_undo
1640 $ignore_next_release = true
1642 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1643 view_element(filename, { :delete => delete })
1648 evtbox.signal_connect('button-release-event') { |w, event|
1649 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1650 if !$ignore_next_release
1651 x, y = autotable.get_current_pos(vbox)
1652 next_ = autotable.get_next_widget(vbox)
1653 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1654 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1655 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1656 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1657 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1658 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1659 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1661 $ignore_next_release = false
1662 $gesture_press = nil
1667 #- handle reordering with drag and drop
1668 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1669 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1670 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1671 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1674 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1676 #- mouse gesture first (dnd disables button-release-event)
1677 if $gesture_press && $gesture_press[:filename] == filename
1678 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1679 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1680 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1681 rotate_and_cleanup.call(angle)
1682 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1684 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1685 msg 3, "gesture delete: click-drag right button to the bottom"
1687 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1692 ctxt.targets.each { |target|
1693 if target.name == 'reorder-elements'
1694 move_dnd = proc { |from,to|
1697 autotable.move(from, to)
1698 save_undo(_("reorder"),
1701 autotable.move(to - 1, from)
1703 autotable.move(to, from + 1)
1705 $notebook.set_page(1)
1707 autotable.move(from, to)
1708 $notebook.set_page(1)
1713 if $multiple_dnd.size == 0
1714 move_dnd.call(selection_data.data.to_i,
1715 autotable.get_current_number(vbox))
1717 UndoHandler.begin_batch
1718 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1720 #- need to update current position between each call
1721 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1722 autotable.get_current_number(vbox))
1724 UndoHandler.end_batch
1735 def create_auto_table
1737 $autotable = Gtk::AutoTable.new(5)
1739 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1740 thumbnails_vb = Gtk::VBox.new(false, 5)
1742 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1743 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1744 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1745 thumbnails_vb.add($autotable)
1747 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1748 $autotable_sw.add_with_viewport(thumbnails_vb)
1750 #- follows stuff for handling multiple elements selection
1751 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1753 update_selected = proc {
1754 $autotable.current_order.each { |path|
1755 w = $name2widgets[path][:evtbox].window
1756 xm = w.position[0] + w.size[0]/2
1757 ym = w.position[1] + w.size[1]/2
1758 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1759 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1760 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1761 if $name2widgets[path][:img].pixbuf
1762 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1766 if $selected_elements[path] && ! $selected_elements[path][:keep]
1767 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))
1768 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1769 $selected_elements.delete(path)
1774 $autotable.signal_connect('realize') { |w,e|
1775 gc = Gdk::GC.new($autotable.window)
1776 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1777 gc.function = Gdk::GC::INVERT
1778 #- autoscroll handling for DND and multiple selections
1779 Gtk.timeout_add(100) {
1780 if ! $autotable.window.nil?
1781 w, x, y, mask = $autotable.window.pointer
1782 if mask & Gdk::Window::BUTTON1_MASK != 0
1783 if y < $autotable_sw.vadjustment.value
1785 $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]])
1787 if $button1_pressed_autotable || press_x
1788 scroll_upper($autotable_sw, y)
1791 w, pos_x, pos_y = $autotable.window.pointer
1792 $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]])
1793 update_selected.call
1796 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1798 $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]])
1800 if $button1_pressed_autotable || press_x
1801 scroll_lower($autotable_sw, y)
1804 w, pos_x, pos_y = $autotable.window.pointer
1805 $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]])
1806 update_selected.call
1811 ! $autotable.window.nil?
1815 $autotable.signal_connect('button-press-event') { |w,e|
1817 if !$button1_pressed_autotable
1820 if e.state & Gdk::Window::SHIFT_MASK == 0
1821 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1822 $selected_elements = {}
1823 $statusbar.push(0, utf8(_("Nothing selected.")))
1825 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1827 set_mousecursor(Gdk::Cursor::TCROSS)
1831 $autotable.signal_connect('button-release-event') { |w,e|
1833 if $button1_pressed_autotable
1834 #- unselect all only now
1835 $multiple_dnd = $selected_elements.keys
1836 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1837 $selected_elements = {}
1838 $button1_pressed_autotable = false
1841 $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]])
1842 if $selected_elements.length > 0
1843 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1846 press_x = press_y = pos_x = pos_y = nil
1847 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1851 $autotable.signal_connect('motion-notify-event') { |w,e|
1854 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1858 $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]])
1859 update_selected.call
1865 def create_subalbums_page
1867 subalbums_hb = Gtk::HBox.new
1868 $subalbums_vb = Gtk::VBox.new(false, 5)
1869 subalbums_hb.pack_start($subalbums_vb, false, false)
1870 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1871 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1872 $subalbums_sw.add_with_viewport(subalbums_hb)
1875 def save_current_file
1881 ios = File.open($filename, "w")
1884 rescue Iconv::IllegalSequence
1885 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1886 if ! ios.nil? && ! ios.closed?
1889 $xmldoc.xml_decl.encoding = 'UTF-8'
1890 ios = File.open($filename, "w")
1902 def save_current_file_user
1903 save_tempfilename = $filename
1904 $filename = $orig_filename
1905 if ! save_current_file
1906 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1907 $filename = save_tempfilename
1911 $generated_outofline = false
1912 $filename = save_tempfilename
1914 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1915 $todelete.each { |f|
1919 puts "Failed to delete #{f}: #{$!}"
1925 def mark_document_as_dirty
1926 $xmldoc.elements.each('//dir') { |elem|
1927 elem.delete_attribute('already-generated')
1931 #- ret: true => ok false => cancel
1932 def ask_save_modifications(msg1, msg2, *options)
1934 options = options.size > 0 ? options[0] : {}
1936 if options[:disallow_cancel]
1937 dialog = Gtk::Dialog.new(msg1,
1939 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1940 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1941 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1943 dialog = Gtk::Dialog.new(msg1,
1945 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1946 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1947 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1948 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1950 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1951 dialog.vbox.add(Gtk::Label.new(msg2))
1952 dialog.window_position = Gtk::Window::POS_CENTER
1955 dialog.run { |response|
1957 if response == Gtk::Dialog::RESPONSE_YES
1958 if ! save_current_file_user
1959 return ask_save_modifications(msg1, msg2, options)
1962 #- if we have generated an album but won't save modifications, we must remove
1963 #- already-generated markers in original file
1964 if $generated_outofline
1966 $xmldoc = REXML::Document.new(File.new($orig_filename))
1967 mark_document_as_dirty
1968 ios = File.open($orig_filename, "w")
1972 puts "exception: #{$!}"
1976 if response == Gtk::Dialog::RESPONSE_CANCEL
1984 def try_quit(*options)
1985 if ask_save_modifications(utf8(_("Save before quitting?")),
1986 utf8(_("Do you want to save your changes before quitting?")),
1992 def show_popup(parent, msg, *options)
1993 dialog = Gtk::Dialog.new
1994 if options[0] && options[0][:title]
1995 dialog.title = options[0][:title]
1997 dialog.title = utf8(_("Booh message"))
1999 lbl = Gtk::Label.new
2000 if options[0] && options[0][:nomarkup]
2005 if options[0] && options[0][:centered]
2006 lbl.set_justify(Gtk::Justification::CENTER)
2008 if options[0] && options[0][:selectable]
2009 lbl.selectable = true
2011 if options[0] && options[0][:scrolled]
2012 sw = Gtk::ScrolledWindow.new(nil, nil)
2013 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
2014 sw.add_with_viewport(lbl)
2016 dialog.set_default_size(500, 600)
2018 dialog.vbox.add(lbl)
2019 dialog.set_default_size(200, 120)
2021 if options[0] && options[0][:okcancel]
2022 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2024 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2026 if options[0] && options[0][:pos_centered]
2027 dialog.window_position = Gtk::Window::POS_CENTER
2029 dialog.window_position = Gtk::Window::POS_MOUSE
2032 if options[0] && options[0][:linkurl]
2033 linkbut = Gtk::Button.new('')
2034 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2035 linkbut.signal_connect('clicked') {
2036 open_url(options[0][:linkurl])
2037 dialog.response(Gtk::Dialog::RESPONSE_OK)
2038 set_mousecursor_normal
2040 linkbut.relief = Gtk::RELIEF_NONE
2041 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2042 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2043 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2048 if !options[0] || !options[0][:not_transient]
2049 dialog.transient_for = parent
2050 dialog.run { |response|
2052 if options[0] && options[0][:okcancel]
2053 return response == Gtk::Dialog::RESPONSE_OK
2057 dialog.signal_connect('response') { dialog.destroy }
2061 def set_mainwindow_title(progress)
2062 filename = $orig_filename || $filename
2065 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2067 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2071 $main_window.title = 'booh - ' + File.basename(filename)
2073 $main_window.title = 'booh'
2078 def backend_wait_message(parent, msg, infopipe_path, mode)
2080 w.set_transient_for(parent)
2083 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2084 vb.pack_start(Gtk::Label.new(msg), false, false)
2086 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2087 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2088 if mode != 'one dir scan'
2089 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2091 if mode == 'web-album'
2092 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2093 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2095 vb.pack_start(Gtk::HSeparator.new, false, false)
2097 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2098 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2099 vb.pack_end(bottom, false, false)
2102 update_progression_title_pb1 = proc {
2103 if mode == 'web-album'
2104 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2105 elsif mode != 'one dir scan'
2106 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2108 set_mainwindow_title(pb1_1.fraction)
2112 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2113 refresh_thread = Thread.new {
2114 directories_counter = 0
2115 while line = infopipe.gets
2116 msg 3, "infopipe got data: #{line}"
2117 if line =~ /^directories: (\d+), sizes: (\d+)/
2118 directories = $1.to_f + 1
2120 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2121 elements = $3.to_f + 1
2122 if mode == 'web-album'
2126 gtk_thread_protect { pb1_1.fraction = 0 }
2127 if mode != 'one dir scan'
2128 newtext = utf8(full_src_dir_to_rel($1, $2))
2129 newtext = '/' if newtext == ''
2130 gtk_thread_protect { pb1_2.text = newtext }
2131 directories_counter += 1
2132 gtk_thread_protect {
2133 pb1_2.fraction = directories_counter / directories
2134 update_progression_title_pb1.call
2137 elsif line =~ /^processing element$/
2138 element_counter += 1
2139 gtk_thread_protect {
2140 pb1_1.fraction = element_counter / elements
2141 update_progression_title_pb1.call
2143 elsif line =~ /^processing size$/
2144 element_counter += 1
2145 gtk_thread_protect {
2146 pb1_1.fraction = element_counter / elements
2147 update_progression_title_pb1.call
2149 elsif line =~ /^finished processing sizes$/
2150 gtk_thread_protect { pb1_1.fraction = 1 }
2151 elsif line =~ /^creating index.html$/
2152 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2153 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2154 directories_counter = 0
2155 elsif line =~ /^index.html: (.+)\|(.+)/
2156 newtext = utf8(full_src_dir_to_rel($1, $2))
2157 newtext = '/' if newtext == ''
2158 gtk_thread_protect { pb2.text = newtext }
2159 directories_counter += 1
2160 gtk_thread_protect {
2161 pb2.fraction = directories_counter / directories
2162 set_mainwindow_title(0.9 + pb2.fraction / 10)
2164 elsif line =~ /^die: (.*)$/
2171 w.signal_connect('delete-event') { w.destroy }
2172 w.signal_connect('destroy') {
2173 Thread.kill(refresh_thread)
2174 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2177 File.delete(infopipe_path)
2179 set_mainwindow_title(nil)
2181 w.window_position = Gtk::Window::POS_CENTER
2187 def call_backend(cmd, waitmsg, mode, params)
2188 pipe = Tempfile.new("boohpipe")
2191 system("mkfifo #{path}")
2192 cmd += " --info-pipe #{path}"
2193 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2198 id, exitstatus = Process.waitpid2(pid)
2199 gtk_thread_protect { w8.destroy }
2201 if params[:successmsg]
2202 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2204 if params[:closure_after]
2205 gtk_thread_protect(¶ms[:closure_after])
2207 elsif exitstatus == 15
2208 #- say nothing, user aborted
2210 gtk_thread_protect { show_popup($main_window,
2211 utf8($diemsg ? _("Unexpected internal error, sorry:\n\n%s") % $diemsg :
2212 _("Unexpected internal error, sorry.\nCheck console for error message."))) }
2219 button.signal_connect('clicked') {
2220 Process.kill('SIGTERM', pid)
2224 def save_changes(*forced)
2225 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2229 $xmldir.delete_attribute('already-generated')
2231 propagate_children = proc { |xmldir|
2232 if xmldir.attributes['subdirs-caption']
2233 xmldir.delete_attribute('already-generated')
2235 xmldir.elements.each('dir') { |element|
2236 propagate_children.call(element)
2240 if $xmldir.child_byname_notattr('dir', 'deleted')
2241 new_title = $subalbums_title.buffer.text
2242 if new_title != $xmldir.attributes['subdirs-caption']
2243 parent = $xmldir.parent
2244 if parent.name == 'dir'
2245 parent.delete_attribute('already-generated')
2247 propagate_children.call($xmldir)
2249 $xmldir.add_attribute('subdirs-caption', new_title)
2250 $xmldir.elements.each('dir') { |element|
2251 if !element.attributes['deleted']
2252 path = element.attributes['path']
2253 newtext = $subalbums_edits[path][:editzone].buffer.text
2254 if element.attributes['subdirs-caption']
2255 if element.attributes['subdirs-caption'] != newtext
2256 propagate_children.call(element)
2258 element.add_attribute('subdirs-caption', newtext)
2259 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2261 if element.attributes['thumbnails-caption'] != newtext
2262 element.delete_attribute('already-generated')
2264 element.add_attribute('thumbnails-caption', newtext)
2265 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2271 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2272 if $xmldir.attributes['thumbnails-caption']
2273 path = $xmldir.attributes['path']
2274 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2276 elsif $xmldir.attributes['thumbnails-caption']
2277 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2280 if $xmldir.attributes['thumbnails-caption']
2281 if edit = $subalbums_edits[$xmldir.attributes['path']]
2282 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2286 #- remove and reinsert elements to reflect new ordering
2289 $xmldir.elements.each { |element|
2290 if element.name == 'image' || element.name == 'video'
2291 saves[element.attributes['filename']] = element.remove
2295 $autotable.current_order.each { |path|
2296 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2297 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2300 saves.each_key { |path|
2301 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2302 chld.add_attribute('deleted', 'true')
2306 def sort_by_exif_date
2310 rexml_thread_protect {
2311 $xmldir.elements.each { |element|
2312 if element.name == 'image' || element.name == 'video'
2313 current_order << element.attributes['filename']
2318 #- look for EXIF dates
2321 if current_order.size > 20
2323 w.set_transient_for($main_window)
2325 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2326 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2327 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2328 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2329 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2330 vb.pack_end(bottom, false, false)
2332 w.signal_connect('delete-event') { w.destroy }
2333 w.window_position = Gtk::Window::POS_CENTER
2337 b.signal_connect('clicked') { aborted = true }
2339 current_order.each { |f|
2341 if entry2type(f) == 'image'
2343 pb.fraction = i.to_f / current_order.size
2344 Gtk.main_iteration while Gtk.events_pending?
2345 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2347 dates[f] = date_time
2360 current_order.each { |f|
2361 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2363 dates[f] = date_time
2369 rexml_thread_protect {
2370 $xmldir.elements.each { |element|
2371 if element.name == 'image' || element.name == 'video'
2372 saves[element.attributes['filename']] = element.remove
2377 neworder = smartsort(current_order, dates)
2379 rexml_thread_protect {
2381 $xmldir.add_element(saves[f].name, saves[f].attributes)
2385 #- let the auto-table reflect new ordering
2389 def remove_all_captions
2392 $autotable.current_order.each { |path|
2393 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2394 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2396 save_undo(_("remove all captions"),
2398 texts.each_key { |key|
2399 $name2widgets[key][:textview].buffer.text = texts[key]
2401 $notebook.set_page(1)
2403 texts.each_key { |key|
2404 $name2widgets[key][:textview].buffer.text = ''
2406 $notebook.set_page(1)
2412 $selected_elements.each_key { |path|
2413 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2419 $selected_elements = {}
2423 $undo_tb.sensitive = $undo_mb.sensitive = false
2424 $redo_tb.sensitive = $redo_mb.sensitive = false
2430 $subalbums_vb.children.each { |chld|
2431 $subalbums_vb.remove(chld)
2433 $subalbums = Gtk::Table.new(0, 0, true)
2434 current_y_sub_albums = 0
2436 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2437 $subalbums_edits = {}
2438 subalbums_counter = 0
2439 subalbums_edits_bypos = {}
2441 add_subalbum = proc { |xmldir, counter|
2442 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2443 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2444 if xmldir == $xmldir
2445 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2446 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2447 caption = xmldir.attributes['thumbnails-caption']
2448 infotype = 'thumbnails'
2450 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2451 captionfile, caption = find_subalbum_caption_info(xmldir)
2452 infotype = find_subalbum_info_type(xmldir)
2454 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2455 hbox = Gtk::HBox.new
2456 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2458 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2461 my_gen_real_thumbnail = proc {
2462 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2465 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2466 f.add(img = Gtk::Image.new)
2467 my_gen_real_thumbnail.call
2469 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2471 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2472 $subalbums.attach(hbox,
2473 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2475 frame, textview = create_editzone($subalbums_sw, 0, img)
2476 textview.buffer.text = caption
2477 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2478 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2480 change_image = proc {
2481 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2483 Gtk::FileChooser::ACTION_OPEN,
2485 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2486 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2487 fc.transient_for = $main_window
2488 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))
2489 f.add(preview_img = Gtk::Image.new)
2491 fc.signal_connect('update-preview') { |w|
2492 if fc.preview_filename
2493 if entry2type(fc.preview_filename) == 'video'
2497 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2499 fc.preview_widget_active = false
2501 tmpimage = "#{tmpdir}/00000001.jpg"
2503 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2504 fc.preview_widget_active = true
2505 rescue Gdk::PixbufError
2506 fc.preview_widget_active = false
2508 File.delete(tmpimage)
2515 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2516 fc.preview_widget_active = true
2517 rescue Gdk::PixbufError
2518 fc.preview_widget_active = false
2523 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2525 old_file = captionfile
2526 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2527 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2528 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2529 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2531 new_file = fc.filename
2532 msg 3, "new captionfile is: #{fc.filename}"
2533 perform_changefile = proc {
2534 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2535 $modified_pixbufs.delete(thumbnail_file)
2536 xmldir.delete_attribute("#{infotype}-rotate")
2537 xmldir.delete_attribute("#{infotype}-color-swap")
2538 xmldir.delete_attribute("#{infotype}-enhance")
2539 xmldir.delete_attribute("#{infotype}-seektime")
2540 my_gen_real_thumbnail.call
2542 perform_changefile.call
2544 save_undo(_("change caption file for sub-album"),
2546 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2547 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2548 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2549 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2550 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2551 my_gen_real_thumbnail.call
2552 $notebook.set_page(0)
2554 perform_changefile.call
2555 $notebook.set_page(0)
2563 if File.exists?(thumbnail_file)
2564 File.delete(thumbnail_file)
2566 my_gen_real_thumbnail.call
2569 rotate_and_cleanup = proc { |angle|
2570 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2571 if File.exists?(thumbnail_file)
2572 File.delete(thumbnail_file)
2576 move = proc { |direction|
2579 save_changes('forced')
2580 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2581 if direction == 'up'
2582 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2583 subalbums_edits_bypos[oldpos - 1][:position] += 1
2585 if direction == 'down'
2586 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2587 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2589 if direction == 'top'
2590 for i in 1 .. oldpos - 1
2591 subalbums_edits_bypos[i][:position] += 1
2593 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2595 if direction == 'bottom'
2596 for i in oldpos + 1 .. subalbums_counter
2597 subalbums_edits_bypos[i][:position] -= 1
2599 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2603 $xmldir.elements.each('dir') { |element|
2604 if (!element.attributes['deleted'])
2605 elems << [ element.attributes['path'], element.remove ]
2608 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2609 each { |e| $xmldir.add_element(e[1]) }
2610 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2611 $xmldir.elements.each('descendant::dir') { |elem|
2612 elem.delete_attribute('already-generated')
2615 sel = $albums_tv.selection.selected_rows
2617 populate_subalbums_treeview(false)
2618 $albums_tv.selection.select_path(sel[0])
2621 color_swap_and_cleanup = proc {
2622 perform_color_swap_and_cleanup = proc {
2623 color_swap(xmldir, "#{infotype}-")
2624 my_gen_real_thumbnail.call
2626 perform_color_swap_and_cleanup.call
2628 save_undo(_("color swap"),
2630 perform_color_swap_and_cleanup.call
2631 $notebook.set_page(0)
2633 perform_color_swap_and_cleanup.call
2634 $notebook.set_page(0)
2639 change_seektime_and_cleanup = proc {
2640 if values = ask_new_seektime(xmldir, "#{infotype}-")
2641 perform_change_seektime_and_cleanup = proc { |val|
2642 change_seektime(xmldir, "#{infotype}-", val)
2643 my_gen_real_thumbnail.call
2645 perform_change_seektime_and_cleanup.call(values[:new])
2647 save_undo(_("specify seektime"),
2649 perform_change_seektime_and_cleanup.call(values[:old])
2650 $notebook.set_page(0)
2652 perform_change_seektime_and_cleanup.call(values[:new])
2653 $notebook.set_page(0)
2659 whitebalance_and_cleanup = proc {
2660 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2661 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2662 perform_change_whitebalance_and_cleanup = proc { |val|
2663 change_whitebalance(xmldir, "#{infotype}-", val)
2664 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2665 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2666 if File.exists?(thumbnail_file)
2667 File.delete(thumbnail_file)
2670 perform_change_whitebalance_and_cleanup.call(values[:new])
2672 save_undo(_("fix white balance"),
2674 perform_change_whitebalance_and_cleanup.call(values[:old])
2675 $notebook.set_page(0)
2677 perform_change_whitebalance_and_cleanup.call(values[:new])
2678 $notebook.set_page(0)
2684 gammacorrect_and_cleanup = proc {
2685 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2686 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2687 perform_change_gammacorrect_and_cleanup = proc { |val|
2688 change_gammacorrect(xmldir, "#{infotype}-", val)
2689 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2690 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2691 if File.exists?(thumbnail_file)
2692 File.delete(thumbnail_file)
2695 perform_change_gammacorrect_and_cleanup.call(values[:new])
2697 save_undo(_("gamma correction"),
2699 perform_change_gammacorrect_and_cleanup.call(values[:old])
2700 $notebook.set_page(0)
2702 perform_change_gammacorrect_and_cleanup.call(values[:new])
2703 $notebook.set_page(0)
2709 enhance_and_cleanup = proc {
2710 perform_enhance_and_cleanup = proc {
2711 enhance(xmldir, "#{infotype}-")
2712 my_gen_real_thumbnail.call
2715 perform_enhance_and_cleanup.call
2717 save_undo(_("enhance"),
2719 perform_enhance_and_cleanup.call
2720 $notebook.set_page(0)
2722 perform_enhance_and_cleanup.call
2723 $notebook.set_page(0)
2728 evtbox.signal_connect('button-press-event') { |w, event|
2729 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2731 rotate_and_cleanup.call(90)
2733 rotate_and_cleanup.call(-90)
2734 elsif $enhance.active?
2735 enhance_and_cleanup.call
2738 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2739 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2740 { :forbid_left => true, :forbid_right => true,
2741 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2742 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2743 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2744 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2745 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2747 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2752 evtbox.signal_connect('button-press-event') { |w, event|
2753 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2757 evtbox.signal_connect('button-release-event') { |w, event|
2758 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2759 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2760 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2761 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2762 msg 3, "gesture rotate: #{angle}"
2763 rotate_and_cleanup.call(angle)
2766 $gesture_press = nil
2769 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2770 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2771 current_y_sub_albums += 1
2774 if $xmldir.child_byname_notattr('dir', 'deleted')
2776 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2777 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2778 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2779 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2780 #- this album image/caption
2781 if $xmldir.attributes['thumbnails-caption']
2782 add_subalbum.call($xmldir, 0)
2785 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2786 $xmldir.elements.each { |element|
2787 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2788 #- element (image or video) of this album
2789 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2790 msg 3, "dest_img: #{dest_img}"
2791 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2792 total[element.name] += 1
2794 if element.name == 'dir' && !element.attributes['deleted']
2795 #- sub-album image/caption
2796 add_subalbum.call(element, subalbums_counter += 1)
2797 total[element.name] += 1
2800 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2801 total['image'], total['video'], total['dir'] ]))
2802 $subalbums_vb.add($subalbums)
2803 $subalbums_vb.show_all
2805 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2806 $notebook.get_tab_label($autotable_sw).sensitive = false
2807 $notebook.set_page(0)
2808 $thumbnails_title.buffer.text = ''
2810 $notebook.get_tab_label($autotable_sw).sensitive = true
2811 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2814 if !$xmldir.child_byname_notattr('dir', 'deleted')
2815 $notebook.get_tab_label($subalbums_sw).sensitive = false
2816 $notebook.set_page(1)
2818 $notebook.get_tab_label($subalbums_sw).sensitive = true
2822 def pixbuf_or_nil(filename)
2824 return Gdk::Pixbuf.new(filename)
2830 def theme_choose(current)
2831 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2833 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2834 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2835 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2837 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2838 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2839 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2840 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2841 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2842 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2843 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2844 treeview.signal_connect('button-press-event') { |w, event|
2845 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2846 dialog.response(Gtk::Dialog::RESPONSE_OK)
2850 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2852 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.split("\n").find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2855 iter[0] = File.basename(dir)
2856 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2857 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2858 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2859 if File.basename(dir) == current
2860 treeview.selection.select_iter(iter)
2863 dialog.set_default_size(-1, 500)
2864 dialog.vbox.show_all
2866 dialog.run { |response|
2867 iter = treeview.selection.selected
2869 if response == Gtk::Dialog::RESPONSE_OK && iter
2870 return model.get_value(iter, 0)
2876 def show_password_protections
2877 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2878 child_iter = $albums_iters[xmldir.attributes['path']]
2879 if xmldir.attributes['password-protect']
2880 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2881 already_protected = true
2882 elsif already_protected
2883 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2885 pix = pix.saturate_and_pixelate(1, true)
2891 xmldir.elements.each('dir') { |elem|
2892 if !elem.attributes['deleted']
2893 examine_dir_elem.call(child_iter, elem, already_protected)
2897 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2900 def populate_subalbums_treeview(select_first)
2904 $subalbums_vb.children.each { |chld|
2905 $subalbums_vb.remove(chld)
2908 source = $xmldoc.root.attributes['source']
2909 msg 3, "source: #{source}"
2911 xmldir = $xmldoc.elements['//dir']
2912 if !xmldir || xmldir.attributes['path'] != source
2913 msg 1, _("Corrupted booh file...")
2917 append_dir_elem = proc { |parent_iter, xmldir|
2918 child_iter = $albums_ts.append(parent_iter)
2919 child_iter[0] = File.basename(xmldir.attributes['path'])
2920 child_iter[1] = xmldir.attributes['path']
2921 $albums_iters[xmldir.attributes['path']] = child_iter
2922 msg 3, "puttin location: #{xmldir.attributes['path']}"
2923 xmldir.elements.each('dir') { |elem|
2924 if !elem.attributes['deleted']
2925 append_dir_elem.call(child_iter, elem)
2929 append_dir_elem.call(nil, xmldir)
2930 show_password_protections
2932 $albums_tv.expand_all
2934 $albums_tv.selection.select_iter($albums_ts.iter_first)
2938 def select_current_theme
2939 select_theme($xmldoc.root.attributes['theme'],
2940 $xmldoc.root.attributes['limit-sizes'],
2941 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2942 $xmldoc.root.attributes['thumbnails-per-row'])
2945 def open_file(filename)
2949 $current_path = nil #- invalidate
2950 $modified_pixbufs = {}
2953 $subalbums_vb.children.each { |chld|
2954 $subalbums_vb.remove(chld)
2957 if !File.exists?(filename)
2958 return utf8(_("File not found."))
2962 $xmldoc = REXML::Document.new(File.new(filename))
2967 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2968 if entry2type(filename).nil?
2969 return utf8(_("Not a booh file!"))
2971 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."))
2975 if !source = $xmldoc.root.attributes['source']
2976 return utf8(_("Corrupted booh file..."))
2979 if !dest = $xmldoc.root.attributes['destination']
2980 return utf8(_("Corrupted booh file..."))
2983 if !theme = $xmldoc.root.attributes['theme']
2984 return utf8(_("Corrupted booh file..."))
2987 if $xmldoc.root.attributes['version'] < $VERSION
2988 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2989 mark_document_as_dirty
2990 if $xmldoc.root.attributes['version'] < '0.8.4'
2991 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2992 `find '#{source}' -type d -follow`.split("\n").sort.collect { |v| v.chomp }.each { |dir|
2993 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2994 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2995 if old_dest_dir != new_dest_dir
2996 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2998 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2999 xmldir.elements.each { |element|
3000 if %w(image video).include?(element.name) && !element.attributes['deleted']
3001 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3002 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
3003 Dir[old_name + '*'].each { |file|
3004 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
3005 file != new_file and sys("mv '#{file}' '#{new_file}'")
3008 if element.name == 'dir' && !element.attributes['deleted']
3009 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3010 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
3011 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
3015 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
3019 $xmldoc.root.add_attribute('version', $VERSION)
3022 select_current_theme
3024 $filename = filename
3025 set_mainwindow_title(nil)
3026 $default_size['thumbnails'] =~ /(.*)x(.*)/
3027 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3028 $albums_thumbnail_size =~ /(.*)x(.*)/
3029 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3031 populate_subalbums_treeview(true)
3033 $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
3037 def open_file_user(filename)
3038 result = open_file(filename)
3040 $config['last-opens'] ||= []
3041 if $config['last-opens'][-1] != utf8(filename)
3042 $config['last-opens'] << utf8(filename)
3044 $orig_filename = $filename
3045 $main_window.title = 'booh - ' + File.basename($orig_filename)
3046 tmp = Tempfile.new("boohtemp")
3047 $filename = tmp.path
3050 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3052 $tempfiles << $filename << "#{$filename}.backup"
3054 $orig_filename = nil
3060 if !ask_save_modifications(utf8(_("Save this album?")),
3061 utf8(_("Do you want to save the changes to this album?")),
3062 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3065 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3067 Gtk::FileChooser::ACTION_OPEN,
3069 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3070 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3071 fc.set_current_folder(File.expand_path("~/.booh"))
3072 fc.transient_for = $main_window
3073 fc.preview_widget = previewlabel = Gtk::Label.new.show
3074 fc.signal_connect('update-preview') { |w|
3075 if fc.preview_filename
3077 push_mousecursor_wait(fc)
3078 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3082 xmldoc.elements.each('//*') { |elem|
3083 if elem.name == 'dir'
3085 elsif elem.name == 'image'
3087 elsif elem.name == 'video'
3095 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3096 fc.preview_widget_active = false
3098 previewlabel.markup = utf8(_("<i>Source:</i> %s\n<i>Destination:</i> %s\n<i>Subalbums:</i> %s\n<i>Images:</i> %s\n<i>Videos:</i> %s") %
3099 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3100 fc.preview_widget_active = true
3106 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3107 push_mousecursor_wait(fc)
3108 msg = open_file_user(fc.filename)
3123 def additional_booh_options
3126 options += "--mproc #{$config['mproc'].to_i} "
3128 options += "--comments-format '#{$config['comments-format']}' "
3129 if $config['transcode-videos']
3130 options += "--transcode-videos '#{$config['transcode-videos']}' "
3132 if $config['use-flv'] == 'true'
3133 options += "--flv-generator '#{$config['flv-generator']}' "
3138 def ask_multi_languages(value)
3140 spl = value.split(',')
3141 value = [ spl[0..-2], spl[-1] ]
3144 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3147 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3148 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3150 lbl = Gtk::Label.new
3152 _("You can choose to activate <b>multi-languages</b> support for this web-album
3153 (it will work only if you publish your web-album on an Apache web-server). This will
3154 use the MultiViews feature of Apache; the pages will be served according to the
3155 value of the Accept-Language HTTP header sent by the web browsers, so that people
3156 with different languages preferences will be able to browse your web-album with
3157 navigation in their language (if language is available).
3160 dialog.vbox.add(lbl)
3161 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3162 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3163 add(languages = Gtk::Button.new))))
3165 pick_languages = proc {
3166 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3169 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3170 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3172 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3173 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3175 SUPPORTED_LANGUAGES.each { |lang|
3176 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3177 if ! value.nil? && value[0].include?(lang)
3183 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3184 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3185 fallback_language = nil
3186 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3187 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3188 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3189 fbl_rb.active = true
3190 fallback_language = SUPPORTED_LANGUAGES[0]
3192 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3193 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3194 rb.signal_connect('clicked') { fallback_language = lang }
3195 if ! value.nil? && value[1] == lang
3200 dialog2.window_position = Gtk::Window::POS_MOUSE
3204 dialog2.run { |response|
3206 if resp == Gtk::Dialog::RESPONSE_OK
3208 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3209 value[1] = fallback_language
3210 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3217 languages.signal_connect('clicked') {
3220 dialog.window_position = Gtk::Window::POS_MOUSE
3224 rb_yes.active = true
3225 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3227 rb_no.signal_connect('clicked') {
3231 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3244 dialog.run { |response|
3249 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3251 return [ true, nil ]
3253 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3262 if !ask_save_modifications(utf8(_("Save this album?")),
3263 utf8(_("Do you want to save the changes to this album?")),
3264 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3267 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3269 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3270 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3271 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3273 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3274 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3275 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3276 tbl.attach(src = Gtk::Entry.new,
3277 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3278 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3279 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3280 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3281 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3282 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3283 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3284 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3285 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3286 tbl.attach(dest = Gtk::Entry.new,
3287 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3288 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3289 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3290 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3291 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3292 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3293 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3294 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3295 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3297 tooltips = Gtk::Tooltips.new
3298 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3299 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3300 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3301 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3302 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3303 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3304 tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
3305 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3306 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3307 nperpage_model = Gtk::ListStore.new(String, String)
3308 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3309 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3310 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3311 nperpagecombo.set_attributes(crt, { :markup => 0 })
3312 iter = nperpage_model.append
3313 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3315 [ 12, 20, 30, 40, 50 ].each { |v|
3316 iter = nperpage_model.append
3317 iter[0] = iter[1] = v.to_s
3319 nperpagecombo.active = 0
3321 multilanguages_value = nil
3322 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3323 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3324 tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3325 multilanguages.signal_connect('clicked') {
3326 retval = ask_multi_languages(multilanguages_value)
3328 multilanguages_value = retval[1]
3330 if multilanguages_value
3331 ml_label.text = utf8(_("Multi-languages: enabled."))
3333 ml_label.text = utf8(_("Multi-languages: disabled."))
3336 if $config['default-multi-languages']
3337 multilanguages_value = $config['default-multi-languages']
3338 ml_label.text = utf8(_("Multi-languages: enabled."))
3340 ml_label.text = utf8(_("Multi-languages: disabled."))
3343 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3344 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3345 tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3346 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3347 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3348 tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
3349 vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3350 vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3351 tooltips.set_tip(quotehtml, utf8(_("If checked, text using markup special characters such as '<grin>' will be shown properly; if unchecked, markup such as '<a href..' links will be interpreted by the browser properly")), nil)
3353 src_nb_calculated_for = ''
3354 src_nb_process = nil
3355 process_src_nb = proc {
3356 if src.text != src_nb_calculated_for
3357 src_nb_calculated_for = src.text
3360 Process.kill(9, src_nb_process)
3362 #- process doesn't exist anymore - race condition
3365 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3366 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3368 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3369 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3372 while src_nb_process
3373 msg 3, "sleeping for completion of previous process"
3376 gtk_thread_flush #- flush to avoid race condition in src_nb markup update
3378 src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3379 total = { 'image' => 0, 'video' => 0, nil => 0 }
3380 if src_nb_process = fork
3381 msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3385 rd.readlines.each { |dir|
3386 if File.basename(dir) =~ /^\./
3390 Dir.entries(dir.chomp).each { |file|
3391 total[entry2type(file)] += 1
3393 rescue Errno::EACCES, Errno::ENOENT
3398 msg 3, "ripping #{src_nb_process}"
3399 dummy, exitstatus = Process.waitpid2(src_nb_process)
3401 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3403 src_nb_process = nil
3409 wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3410 Process.exit!(0) #- _exit
3413 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3416 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3422 timeout_src_nb = Gtk.timeout_add(100) {
3426 src_browse.signal_connect('clicked') {
3427 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3429 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3431 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3432 fc.transient_for = $main_window
3433 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3434 src.text = utf8(fc.filename)
3436 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3441 dest_browse.signal_connect('clicked') {
3442 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3444 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3446 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3447 fc.transient_for = $main_window
3448 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3449 dest.text = utf8(fc.filename)
3454 conf_browse.signal_connect('clicked') {
3455 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3457 Gtk::FileChooser::ACTION_SAVE,
3459 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3460 fc.transient_for = $main_window
3461 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3462 fc.set_current_folder(File.expand_path("~/.booh"))
3463 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
3464 conf.text = utf8(fc.filename)
3471 recreate_theme_config = proc {
3472 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3474 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3475 $images_size.each { |s|
3476 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3480 tooltips.set_tip(cb, utf8(s['description']), nil)
3481 theme_sizes << { :widget => cb, :value => s['name'] }
3483 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3484 tooltips = Gtk::Tooltips.new
3485 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3486 theme_sizes << { :widget => cb, :value => 'original' }
3489 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3492 $allowed_N_values.each { |n|
3494 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3496 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3498 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3502 nperrows << { :widget => rb, :value => n }
3504 nperrowradios.show_all
3506 recreate_theme_config.call
3508 theme_button.signal_connect('clicked') {
3509 if newtheme = theme_choose(theme_button.label)