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-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'booh/rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
40 require 'booh/Synchronizator'
45 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
46 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
47 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
50 #- default values for some globals
52 $xmlaccesslock = Object.new
55 $ignore_videos = false
56 $button1_pressed_autotable = false
57 $generated_outofline = false
60 puts _("Usage: %s [OPTION]...") % File.basename($0)
62 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
67 parser = GetoptLong.new
68 parser.set_options(*$options.collect { |ary| ary[0..2] })
70 parser.each_option do |name, arg|
77 puts _("Booh version %s
79 Copyright (c) 2005-2008 Guillaume Cottenceau.
80 This is free software; see the source for copying conditions. There is NO
81 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
85 when '--verbose-level'
86 $verbose_level = arg.to_i
99 $config_file = File.expand_path('~/.booh-gui-rc')
100 if File.readable?($config_file)
101 $xmldoc = Synchronizator.new(REXML::Document.new(File.new($config_file)), $xmlaccesslock)
102 $xmldoc.root.elements.each { |element|
103 txt = element.get_text
105 if txt.value =~ /~~~/ || element.name == 'last-opens'
106 $config[element.name] = txt.value.split(/~~~/)
108 $config[element.name] = txt.value
110 elsif element.elements.size == 0
111 $config[element.name] = ''
113 $config[element.name] = {}
114 element.each { |chld|
116 $config[element.name][chld.name] = txt ? txt.value : nil
121 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
122 $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
123 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
124 $config['comments-format'] ||= '%t'
125 if !FileTest.directory?(File.expand_path('~/.booh'))
126 system("mkdir ~/.booh")
128 if $config['mproc'].nil?
130 for line in IO.readlines('/proc/cpuinfo') do
131 line =~ /^processor/ and cpus += 1
134 $config['mproc'] = cpus
137 $config['rotate-set-exif'] ||= 'true'
143 if !system("which convert >/dev/null 2>/dev/null")
144 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
145 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
148 if !system("which identify >/dev/null 2>/dev/null")
149 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
150 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
152 if !system("which exif >/dev/null 2>/dev/null")
153 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
155 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
157 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
160 viewer_binary = $config['video-viewer'].split.first
161 if viewer_binary && !File.executable?(viewer_binary)
162 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
163 You should fix this in Edit/Preferences so that you can view videos.
165 Problem was: '%s' is not an executable file.
166 Hint: don't forget to specify the full path to the executable,
167 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
169 image_editor_binary = $config['image-editor'].split.first
170 if image_editor_binary && !File.executable?(image_editor_binary)
171 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
172 You should fix this in Edit/Preferences so that you can edit photos externally.
174 Problem was: '%s' is not an executable file.
175 Hint: don't forget to specify the full path to the executable,
176 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
178 browser_binary = $config['browser'].split.first
179 if browser_binary && !File.executable?(browser_binary)
180 show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
181 You should fix this in Edit/Preferences so that you can open URLs.
183 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
188 if $config['last-opens'] && $config['last-opens'].size > 10
189 $config['last-opens'] = $config['last-opens'][-10, 10]
192 $xmldoc = Synchronizator.new(Document.new("<booh-gui-rc version='#{$VERSION}'/>"), $xmlaccesslock)
193 $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
194 $config.each_pair { |key, value|
195 elem = $xmldoc.root.add_element key
197 $config[key].each_pair { |subkey, subvalue|
198 subelem = elem.add_element subkey
199 subelem.add_text subvalue.to_s
201 elsif value.is_a? Array
202 elem.add_text value.join('~~~')
207 elem.add_text value.to_s
211 ios = File.open($config_file, "w")
212 $xmldoc.write(ios, 0)
215 $tempfiles.each { |f|
222 def set_mousecursor(what, *widget)
223 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
224 if widget[0] && widget[0].window
225 widget[0].window.cursor = cursor
227 if $main_window && $main_window.window
228 $main_window.window.cursor = cursor
230 $current_cursor = what
232 def set_mousecursor_wait(*widget)
233 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
234 if Thread.current == Thread.main
235 Gtk.main_iteration while Gtk.events_pending?
238 def set_mousecursor_normal(*widget)
239 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
241 def push_mousecursor_wait(*widget)
242 if $current_cursor != Gdk::Cursor::WATCH
243 $save_cursor = $current_cursor
244 gtk_thread_protect { set_mousecursor_wait(*widget) }
247 def pop_mousecursor(*widget)
248 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
252 source = $xmldoc.root.attributes['source']
253 dest = $xmldoc.root.attributes['destination']
254 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
257 def full_src_dir_to_rel(path, source)
258 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
261 def build_full_dest_filename(filename)
262 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
265 def save_undo(name, closure, *params)
266 UndoHandler.save_undo(name, closure, [ *params ])
267 $undo_tb.sensitive = $undo_mb.sensitive = true
268 $redo_tb.sensitive = $redo_mb.sensitive = false
271 def view_element(filename, closures)
272 if entry2type(filename) == 'video'
273 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
279 w = create_window.set_title(filename)
281 msg 3, "filename: #{filename}"
282 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
283 #- typically this file won't exist in case of videos; try with the largest thumbnail around
284 if !File.exists?(dest_img)
285 if entry2type(filename) == 'video'
286 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
287 if not alternatives.empty?
288 dest_img = alternatives[-1]
291 push_mousecursor_wait
292 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
294 if !File.exists?(dest_img)
295 msg 2, _("Could not generate fullscreen thumbnail!")
300 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
301 evt.signal_connect('button-press-event') { |this, event|
302 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
303 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
305 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
307 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
308 delete_item.signal_connect('activate') {
310 closures[:delete].call(false)
313 menu.popup(nil, nil, event.button, event.time)
316 evt.signal_connect('button-release-event') { |this, event|
318 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
319 msg 3, "gesture delete: click-drag right button to the bottom"
321 closures[:delete].call(false)
322 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
326 tooltips = Gtk::Tooltips.new
327 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
329 w.signal_connect('key-press-event') { |w,event|
330 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
332 closures[:delete].call(false)
336 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
337 b.signal_connect('clicked') { w.destroy }
340 vb.pack_start(evt, false, false)
341 vb.pack_end(bottom, false, false)
344 w.signal_connect('delete-event') { w.destroy }
345 w.window_position = Gtk::Window::POS_CENTER
349 def scroll_upper(scrolledwindow, ypos_top)
350 newval = scrolledwindow.vadjustment.value -
351 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
352 if newval < scrolledwindow.vadjustment.lower
353 newval = scrolledwindow.vadjustment.lower
355 scrolledwindow.vadjustment.value = newval
358 def scroll_lower(scrolledwindow, ypos_bottom)
359 newval = scrolledwindow.vadjustment.value +
360 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
361 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
362 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
364 scrolledwindow.vadjustment.value = newval
367 def autoscroll_if_needed(scrolledwindow, image, textview)
368 #- autoscroll if cursor or image is not visible, if possible
369 if image && image.window || textview.window
370 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
371 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
372 current_miny_visible = scrolledwindow.vadjustment.value
373 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
374 if ypos_top < current_miny_visible
375 scroll_upper(scrolledwindow, ypos_top)
376 elsif ypos_bottom > current_maxy_visible
377 scroll_lower(scrolledwindow, ypos_bottom)
382 def create_editzone(scrolledwindow, pagenum, image)
383 frame = Gtk::Frame.new
384 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
385 frame.set_shadow_type(Gtk::SHADOW_IN)
386 textview.signal_connect('key-press-event') { |w, event|
387 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
388 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
389 scrolledwindow.signal_emit('key-press-event', event)
391 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
392 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
393 if event.keyval == Gdk::Keyval::GDK_Up
394 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
395 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
397 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
400 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
401 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
403 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
410 candidate_undo_text = nil
411 textview.signal_connect('focus-in-event') { |w, event|
412 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
413 candidate_undo_text = textview.buffer.text
417 textview.signal_connect('key-release-event') { |w, event|
418 if candidate_undo_text && candidate_undo_text != textview.buffer.text
420 save_undo(_("text edit"),
422 save_text = textview.buffer.text
423 textview.buffer.text = text
425 $notebook.set_page(pagenum)
427 textview.buffer.text = save_text
429 $notebook.set_page(pagenum)
431 }, candidate_undo_text)
432 candidate_undo_text = nil
435 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)
436 autoscroll_if_needed(scrolledwindow, image, textview)
441 return [ frame, textview ]
444 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
446 if !$modified_pixbufs[thumbnail_img]
447 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
448 elsif !$modified_pixbufs[thumbnail_img][:orig]
449 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
452 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
455 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
456 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
457 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
458 if pixbuf.height > desired_y
459 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
460 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
461 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
466 if $modified_pixbufs[thumbnail_img][:whitebalance]
467 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
470 #- fix gamma correction
471 if $modified_pixbufs[thumbnail_img][:gammacorrect]
472 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
475 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
478 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
481 #- update rotate attribute
482 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
483 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
485 if $config['rotate-set-exif'] == 'true'
486 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
489 $modified_pixbufs[thumbnail_img] ||= {}
490 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
491 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
493 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
496 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
499 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
501 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
503 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
504 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
506 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
507 $notebook.set_page(0)
508 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
513 def color_swap(xmldir, attributes_prefix)
515 if xmldir.attributes["#{attributes_prefix}color-swap"]
516 xmldir.delete_attribute("#{attributes_prefix}color-swap")
518 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
522 def enhance(xmldir, attributes_prefix)
524 if xmldir.attributes["#{attributes_prefix}enhance"]
525 xmldir.delete_attribute("#{attributes_prefix}enhance")
527 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
531 def change_seektime(xmldir, attributes_prefix, value)
533 xmldir.add_attribute("#{attributes_prefix}seektime", value)
536 def ask_new_seektime(xmldir, attributes_prefix)
538 value = xmldir.attributes["#{attributes_prefix}seektime"]
543 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
545 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
546 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
547 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
551 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
555 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
556 entry.signal_connect('key-press-event') { |w, event|
557 if event.keyval == Gdk::Keyval::GDK_Return
558 dialog.response(Gtk::Dialog::RESPONSE_OK)
560 elsif event.keyval == Gdk::Keyval::GDK_Escape
561 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
564 false #- propagate if needed
568 dialog.window_position = Gtk::Window::POS_MOUSE
571 dialog.run { |response|
574 if response == Gtk::Dialog::RESPONSE_OK
576 msg 3, "changing seektime to #{newval}"
577 return { :old => value, :new => newval }
584 def change_pano_amount(xmldir, attributes_prefix, value)
587 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
589 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
593 def ask_new_pano_amount(xmldir, attributes_prefix)
595 value = xmldir.attributes["#{attributes_prefix}pano-amount"]
600 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
602 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
603 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
604 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
608 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
609 of this panorama image compared to other regular images. For example, if the panorama
610 was taken out of four photos on one row, counting the necessary overlap, the width of
611 this panorama image should probably be roughly three times the width of regular images.
613 With this information, booh will be able to generate panorama thumbnails looking
614 the right 'size', since the height of the thumbnail for this image will be similar
615 to the height of other thumbnails.
618 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)")))).
619 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
620 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
621 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
622 spin.signal_connect('value-changed') {
625 dialog.window_position = Gtk::Window::POS_MOUSE
628 spin.value = value.to_f
635 dialog.run { |response|
639 newval = spin.value.to_f
642 if response == Gtk::Dialog::RESPONSE_OK
644 msg 3, "changing panorama amount to #{newval}"
645 return { :old => value, :new => newval }
652 def change_whitebalance(xmlelem, attributes_prefix, value)
654 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
657 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
659 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
660 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
661 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
662 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
663 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
664 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
665 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
666 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
667 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
668 $modified_pixbufs[thumbnail_img] ||= {}
669 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
670 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
672 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
673 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
675 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
678 $modified_pixbufs[thumbnail_img] ||= {}
679 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
681 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
684 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
685 #- init $modified_pixbufs correctly
686 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
688 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
690 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
692 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
693 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
694 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
698 _("You can fix the <b>white balance</b> of the image, if your image is too blue
699 or too yellow because the recorder didn't detect the light correctly. Drag the
700 slider below the image to the left for more blue, to the right for more yellow.
704 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
706 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
708 dialog.window_position = Gtk::Window::POS_MOUSE
712 timeout = Gtk.timeout_add(100) {
713 if hs.value != lastval
716 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
722 dialog.run { |response|
723 Gtk.timeout_remove(timeout)
724 if response == Gtk::Dialog::RESPONSE_OK
726 newval = hs.value.to_s
727 msg 3, "changing white balance to #{newval}"
729 return { :old => value, :new => newval }
732 $modified_pixbufs[thumbnail_img] ||= {}
733 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
734 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
742 def change_gammacorrect(xmlelem, attributes_prefix, value)
744 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
747 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
749 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
750 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
751 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
752 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
753 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
754 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
755 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
756 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
757 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
758 $modified_pixbufs[thumbnail_img] ||= {}
759 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
760 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
762 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
763 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
765 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
768 $modified_pixbufs[thumbnail_img] ||= {}
769 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
771 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
774 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
775 #- init $modified_pixbufs correctly
776 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
778 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
780 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
782 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
783 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
784 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
788 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
789 or too bright. Drag the slider below the image.
793 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
795 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
797 dialog.window_position = Gtk::Window::POS_MOUSE
801 timeout = Gtk.timeout_add(100) {
802 if hs.value != lastval
805 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
811 dialog.run { |response|
812 Gtk.timeout_remove(timeout)
813 if response == Gtk::Dialog::RESPONSE_OK
815 newval = hs.value.to_s
816 msg 3, "gamma correction to #{newval}"
818 return { :old => value, :new => newval }
821 $modified_pixbufs[thumbnail_img] ||= {}
822 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
823 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
831 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
832 if File.exists?(destfile)
833 File.delete(destfile)
835 #- type can be 'element' or 'subdir'
837 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
839 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
843 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
845 push_mousecursor_wait
846 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
849 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
855 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
856 distribute_multiple_call = Proc.new { |action, arg|
857 $selected_elements.each_key { |path|
858 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
860 if possible_actions[:can_multiple] && $selected_elements.length > 0
861 UndoHandler.begin_batch
862 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
863 UndoHandler.end_batch
865 closures[action].call(arg)
867 $selected_elements = {}
870 if optionals.include?('change_image')
871 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
872 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
873 changeimg.signal_connect('activate') { closures[:change].call }
874 menu.append(Gtk::SeparatorMenuItem.new)
876 if !possible_actions[:can_multiple] || $selected_elements.length == 0
879 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
880 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
881 view.signal_connect('activate') { closures[:view].call }
883 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
884 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
885 view.signal_connect('activate') { closures[:view].call }
886 menu.append(Gtk::SeparatorMenuItem.new)
889 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
890 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
891 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
892 exif.signal_connect('activate') { show_popup($main_window,
893 utf8(`exif -m '#{fullpath}'`),
894 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
895 menu.append(Gtk::SeparatorMenuItem.new)
898 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
899 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
900 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
901 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
902 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
903 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
904 if !possible_actions[:can_multiple] || $selected_elements.length == 0
905 menu.append(Gtk::SeparatorMenuItem.new)
906 if !possible_actions[:forbid_left]
907 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
908 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
909 moveleft.signal_connect('activate') { closures[:move].call('left') }
910 if !possible_actions[:can_left]
911 moveleft.sensitive = false
914 if !possible_actions[:forbid_right]
915 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
916 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
917 moveright.signal_connect('activate') { closures[:move].call('right') }
918 if !possible_actions[:can_right]
919 moveright.sensitive = false
922 if optionals.include?('move_top')
923 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
924 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
925 movetop.signal_connect('activate') { closures[:move].call('top') }
926 if !possible_actions[:can_top]
927 movetop.sensitive = false
930 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
931 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
932 moveup.signal_connect('activate') { closures[:move].call('up') }
933 if !possible_actions[:can_up]
934 moveup.sensitive = false
936 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
937 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
938 movedown.signal_connect('activate') { closures[:move].call('down') }
939 if !possible_actions[:can_down]
940 movedown.sensitive = false
942 if optionals.include?('move_bottom')
943 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
944 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
945 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
946 if !possible_actions[:can_bottom]
947 movebottom.sensitive = false
952 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
953 menu.append(Gtk::SeparatorMenuItem.new)
954 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
955 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
956 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
957 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
958 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
959 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
960 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
961 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
962 seektime.signal_connect('activate') {
963 if possible_actions[:can_multiple] && $selected_elements.length > 0
964 if values = ask_new_seektime(nil, '')
965 distribute_multiple_call.call(:seektime, values)
968 closures[:seektime].call
973 menu.append( Gtk::SeparatorMenuItem.new)
974 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
975 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
976 gammacorrect.signal_connect('activate') {
977 if possible_actions[:can_multiple] && $selected_elements.length > 0
978 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
979 distribute_multiple_call.call(:gammacorrect, values)
982 closures[:gammacorrect].call
985 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
986 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
987 whitebalance.signal_connect('activate') {
988 if possible_actions[:can_multiple] && $selected_elements.length > 0
989 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
990 distribute_multiple_call.call(:whitebalance, values)
993 closures[:whitebalance].call
996 if !possible_actions[:can_multiple] || $selected_elements.length == 0
997 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
998 _("Enhance constrast"))))
1000 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1002 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1003 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1004 if type == 'image' && possible_actions[:can_panorama]
1005 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1006 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1007 panorama.signal_connect('activate') {
1008 if possible_actions[:can_multiple] && $selected_elements.length > 0
1009 if values = ask_new_pano_amount(nil, '')
1010 distribute_multiple_call.call(:pano, values)
1013 distribute_multiple_call.call(:pano)
1017 menu.append( Gtk::SeparatorMenuItem.new)
1018 if optionals.include?('delete')
1019 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1020 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1021 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1022 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1023 paste_item.signal_connect('activate') { closures[:paste].call }
1024 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1025 clear_item.signal_connect('activate') { $cuts = [] }
1027 paste_item.sensitive = clear_item.sensitive = false
1030 menu.append( Gtk::SeparatorMenuItem.new)
1032 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1033 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1034 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1035 editexternally.signal_connect('activate') {
1036 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1041 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1042 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1043 if optionals.include?('delete')
1044 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1045 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1048 menu.popup(nil, nil, event.button, event.time)
1051 def delete_current_subalbum
1053 sel = $albums_tv.selection.selected_rows
1054 $xmldir.elements.each { |e|
1055 if e.name == 'image' || e.name == 'video'
1056 e.add_attribute('deleted', 'true')
1059 #- branch if we have a non deleted subalbum
1060 if $xmldir.child_byname_notattr('dir', 'deleted')
1061 $xmldir.delete_attribute('thumbnails-caption')
1062 $xmldir.delete_attribute('thumbnails-captionfile')
1064 $xmldir.add_attribute('deleted', 'true')
1066 while moveup.parent.name == 'dir'
1067 moveup = moveup.parent
1068 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1069 moveup.add_attribute('deleted', 'true')
1076 save_changes('forced')
1077 populate_subalbums_treeview(false)
1078 $albums_tv.selection.select_path(sel[0])
1084 $current_path = nil #- prevent save_changes from being rerun again
1085 sel = $albums_tv.selection.selected_rows
1086 restore_one = proc { |xmldir|
1087 xmldir.elements.each { |e|
1088 if e.name == 'dir' && e.attributes['deleted']
1091 e.delete_attribute('deleted')
1094 restore_one.call($xmldir)
1095 populate_subalbums_treeview(false)
1096 $albums_tv.selection.select_path(sel[0])
1099 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1102 frame1 = Gtk::Frame.new
1103 fullpath = from_utf8("#{$current_path}/#{filename}")
1105 my_gen_real_thumbnail = proc {
1106 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1110 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1111 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1112 pack_start(img = Gtk::Image.new).
1113 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1114 px, mask = pxb.render_pixmap_and_mask(0)
1115 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1116 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1118 frame1.add(img = Gtk::Image.new)
1121 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1122 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1123 my_gen_real_thumbnail.call
1125 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1128 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1130 tooltips = Gtk::Tooltips.new
1131 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1132 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1134 frame2, textview = create_editzone($autotable_sw, 1, img)
1135 textview.buffer.text = caption
1136 textview.set_justification(Gtk::Justification::CENTER)
1138 vbox = Gtk::VBox.new(false, 5)
1139 vbox.pack_start(evtbox, false, false)
1140 vbox.pack_start(frame2, false, false)
1141 autotable.append(vbox, filename)
1143 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1144 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1146 #- to be able to find widgets by name
1147 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1149 cleanup_all_thumbnails = proc {
1150 #- remove out of sync images
1151 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1152 for sizeobj in $images_size
1153 #- cannot use sizeobj because panoramic images will have a larger width
1154 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1162 cleanup_all_thumbnails.call
1163 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1165 $xmldir.delete_attribute('already-generated')
1166 my_gen_real_thumbnail.call
1169 rotate_and_cleanup = proc { |angle|
1170 cleanup_all_thumbnails.call
1171 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1174 move = proc { |direction|
1175 do_method = "move_#{direction}"
1176 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1178 done = autotable.method(do_method).call(vbox)
1179 textview.grab_focus #- because if moving, focus is stolen
1183 save_undo(_("move %s") % direction,
1185 autotable.method(undo_method).call(vbox)
1186 textview.grab_focus #- because if moving, focus is stolen
1187 autoscroll_if_needed($autotable_sw, img, textview)
1188 $notebook.set_page(1)
1190 autotable.method(do_method).call(vbox)
1191 textview.grab_focus #- because if moving, focus is stolen
1192 autoscroll_if_needed($autotable_sw, img, textview)
1193 $notebook.set_page(1)
1199 color_swap_and_cleanup = proc {
1200 perform_color_swap_and_cleanup = proc {
1201 cleanup_all_thumbnails.call
1202 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1203 my_gen_real_thumbnail.call
1206 perform_color_swap_and_cleanup.call
1208 save_undo(_("color swap"),
1210 perform_color_swap_and_cleanup.call
1212 autoscroll_if_needed($autotable_sw, img, textview)
1213 $notebook.set_page(1)
1215 perform_color_swap_and_cleanup.call
1217 autoscroll_if_needed($autotable_sw, img, textview)
1218 $notebook.set_page(1)
1223 change_seektime_and_cleanup_real = proc { |values|
1224 perform_change_seektime_and_cleanup = proc { |val|
1225 cleanup_all_thumbnails.call
1226 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1227 my_gen_real_thumbnail.call
1229 perform_change_seektime_and_cleanup.call(values[:new])
1231 save_undo(_("specify seektime"),
1233 perform_change_seektime_and_cleanup.call(values[:old])
1235 autoscroll_if_needed($autotable_sw, img, textview)
1236 $notebook.set_page(1)
1238 perform_change_seektime_and_cleanup.call(values[:new])
1240 autoscroll_if_needed($autotable_sw, img, textview)
1241 $notebook.set_page(1)
1246 change_seektime_and_cleanup = proc {
1247 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1248 change_seektime_and_cleanup_real.call(values)
1252 change_pano_amount_and_cleanup_real = proc { |values|
1253 perform_change_pano_amount_and_cleanup = proc { |val|
1254 cleanup_all_thumbnails.call
1255 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1257 perform_change_pano_amount_and_cleanup.call(values[:new])
1259 save_undo(_("change panorama amount"),
1261 perform_change_pano_amount_and_cleanup.call(values[:old])
1263 autoscroll_if_needed($autotable_sw, img, textview)
1264 $notebook.set_page(1)
1266 perform_change_pano_amount_and_cleanup.call(values[:new])
1268 autoscroll_if_needed($autotable_sw, img, textview)
1269 $notebook.set_page(1)
1274 change_pano_amount_and_cleanup = proc {
1275 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1276 change_pano_amount_and_cleanup_real.call(values)
1280 whitebalance_and_cleanup_real = proc { |values|
1281 perform_change_whitebalance_and_cleanup = proc { |val|
1282 cleanup_all_thumbnails.call
1283 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1284 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1285 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1287 perform_change_whitebalance_and_cleanup.call(values[:new])
1289 save_undo(_("fix white balance"),
1291 perform_change_whitebalance_and_cleanup.call(values[:old])
1293 autoscroll_if_needed($autotable_sw, img, textview)
1294 $notebook.set_page(1)
1296 perform_change_whitebalance_and_cleanup.call(values[:new])
1298 autoscroll_if_needed($autotable_sw, img, textview)
1299 $notebook.set_page(1)
1304 whitebalance_and_cleanup = proc {
1305 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1306 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1307 whitebalance_and_cleanup_real.call(values)
1311 gammacorrect_and_cleanup_real = proc { |values|
1312 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1313 cleanup_all_thumbnails.call
1314 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1315 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1316 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1318 perform_change_gammacorrect_and_cleanup.call(values[:new])
1320 save_undo(_("gamma correction"),
1322 perform_change_gammacorrect_and_cleanup.call(values[:old])
1324 autoscroll_if_needed($autotable_sw, img, textview)
1325 $notebook.set_page(1)
1327 perform_change_gammacorrect_and_cleanup.call(values[:new])
1329 autoscroll_if_needed($autotable_sw, img, textview)
1330 $notebook.set_page(1)
1335 gammacorrect_and_cleanup = Proc.new {
1336 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1337 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1338 gammacorrect_and_cleanup_real.call(values)
1342 enhance_and_cleanup = proc {
1343 perform_enhance_and_cleanup = proc {
1344 cleanup_all_thumbnails.call
1345 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1346 my_gen_real_thumbnail.call
1349 cleanup_all_thumbnails.call
1350 perform_enhance_and_cleanup.call
1352 save_undo(_("enhance"),
1354 perform_enhance_and_cleanup.call
1356 autoscroll_if_needed($autotable_sw, img, textview)
1357 $notebook.set_page(1)
1359 perform_enhance_and_cleanup.call
1361 autoscroll_if_needed($autotable_sw, img, textview)
1362 $notebook.set_page(1)
1367 delete = proc { |isacut|
1368 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 })
1371 perform_delete = proc {
1372 after = autotable.get_next_widget(vbox)
1374 after = autotable.get_previous_widget(vbox)
1376 if $config['deleteondisk'] && !isacut
1377 msg 3, "scheduling for delete: #{fullpath}"
1378 $todelete << fullpath
1380 autotable.remove_widget(vbox)
1382 $vbox2widgets[after][:textview].grab_focus
1383 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1387 previous_pos = autotable.get_current_number(vbox)
1391 delete_current_subalbum
1393 save_undo(_("delete"),
1395 autotable.reinsert(pos, vbox, filename)
1396 $notebook.set_page(1)
1397 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1399 msg 3, "removing deletion schedule of: #{fullpath}"
1400 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1403 $notebook.set_page(1)
1412 $cuts << { :vbox => vbox, :filename => filename }
1413 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1418 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1421 autotable.queue_draws << proc {
1422 $vbox2widgets[last[:vbox]][:textview].grab_focus
1423 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1425 save_undo(_("paste"),
1427 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1428 $notebook.set_page(1)
1431 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1433 $notebook.set_page(1)
1436 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1441 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1442 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1443 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1444 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1446 textview.signal_connect('key-press-event') { |w, event|
1449 x, y = autotable.get_current_pos(vbox)
1450 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1451 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1452 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1453 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1455 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1456 $vbox2widgets[widget_up][:textview].grab_focus
1463 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1465 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1466 $vbox2widgets[widget_down][:textview].grab_focus
1473 if event.keyval == Gdk::Keyval::GDK_Left
1476 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1483 rotate_and_cleanup.call(-90)
1486 if event.keyval == Gdk::Keyval::GDK_Right
1487 next_ = autotable.get_next_widget(vbox)
1488 if next_ && autotable.get_current_pos(next_)[0] > x
1490 $vbox2widgets[next_][:textview].grab_focus
1497 rotate_and_cleanup.call(90)
1500 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1503 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1504 view_element(filename, { :delete => delete })
1507 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1510 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1514 !propagate #- propagate if needed
1517 $ignore_next_release = false
1518 evtbox.signal_connect('button-press-event') { |w, event|
1519 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1520 if event.state & Gdk::Window::BUTTON3_MASK != 0
1521 #- gesture redo: hold right mouse button then click left mouse button
1522 $config['nogestures'] or perform_redo
1523 $ignore_next_release = true
1525 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1527 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1529 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1530 elsif $enhance.active?
1531 enhance_and_cleanup.call
1532 elsif $delete.active?
1536 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1539 $button1_pressed_autotable = true
1540 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1541 if event.state & Gdk::Window::BUTTON1_MASK != 0
1542 #- gesture undo: hold left mouse button then click right mouse button
1543 $config['nogestures'] or perform_undo
1544 $ignore_next_release = true
1546 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1547 view_element(filename, { :delete => delete })
1552 evtbox.signal_connect('button-release-event') { |w, event|
1553 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1554 if !$ignore_next_release
1555 x, y = autotable.get_current_pos(vbox)
1556 next_ = autotable.get_next_widget(vbox)
1557 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1558 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1559 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1560 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1561 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1562 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1563 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1565 $ignore_next_release = false
1566 $gesture_press = nil
1571 #- handle reordering with drag and drop
1572 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1573 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1574 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1575 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1578 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1580 #- mouse gesture first (dnd disables button-release-event)
1581 if $gesture_press && $gesture_press[:filename] == filename
1582 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1583 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1584 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1585 rotate_and_cleanup.call(angle)
1586 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1588 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1589 msg 3, "gesture delete: click-drag right button to the bottom"
1591 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1596 ctxt.targets.each { |target|
1597 if target.name == 'reorder-elements'
1598 move_dnd = proc { |from,to|
1601 autotable.move(from, to)
1602 save_undo(_("reorder"),
1605 autotable.move(to - 1, from)
1607 autotable.move(to, from + 1)
1609 $notebook.set_page(1)
1611 autotable.move(from, to)
1612 $notebook.set_page(1)
1617 if $multiple_dnd.size == 0
1618 move_dnd.call(selection_data.data.to_i,
1619 autotable.get_current_number(vbox))
1621 UndoHandler.begin_batch
1622 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1624 #- need to update current position between each call
1625 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1626 autotable.get_current_number(vbox))
1628 UndoHandler.end_batch
1639 def create_auto_table
1641 $autotable = Gtk::AutoTable.new(5)
1643 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1644 thumbnails_vb = Gtk::VBox.new(false, 5)
1646 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1647 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1648 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1649 thumbnails_vb.add($autotable)
1651 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1652 $autotable_sw.add_with_viewport(thumbnails_vb)
1654 #- follows stuff for handling multiple elements selection
1655 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1657 update_selected = proc {
1658 $autotable.current_order.each { |path|
1659 w = $name2widgets[path][:evtbox].window
1660 xm = w.position[0] + w.size[0]/2
1661 ym = w.position[1] + w.size[1]/2
1662 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1663 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1664 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1665 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1668 if $selected_elements[path] && ! $selected_elements[path][:keep]
1669 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))
1670 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1671 $selected_elements.delete(path)
1676 $autotable.signal_connect('realize') { |w,e|
1677 gc = Gdk::GC.new($autotable.window)
1678 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1679 gc.function = Gdk::GC::INVERT
1680 #- autoscroll handling for DND and multiple selections
1681 Gtk.timeout_add(100) {
1682 if ! $autotable.window.nil?
1683 w, x, y, mask = $autotable.window.pointer
1684 if mask & Gdk::Window::BUTTON1_MASK != 0
1685 if y < $autotable_sw.vadjustment.value
1687 $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]])
1689 if $button1_pressed_autotable || press_x
1690 scroll_upper($autotable_sw, y)
1693 w, pos_x, pos_y = $autotable.window.pointer
1694 $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]])
1695 update_selected.call
1698 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1700 $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]])
1702 if $button1_pressed_autotable || press_x
1703 scroll_lower($autotable_sw, y)
1706 w, pos_x, pos_y = $autotable.window.pointer
1707 $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]])
1708 update_selected.call
1713 ! $autotable.window.nil?
1717 $autotable.signal_connect('button-press-event') { |w,e|
1719 if !$button1_pressed_autotable
1722 if e.state & Gdk::Window::SHIFT_MASK == 0
1723 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1724 $selected_elements = {}
1725 $statusbar.push(0, utf8(_("Nothing selected.")))
1727 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1729 set_mousecursor(Gdk::Cursor::TCROSS)
1733 $autotable.signal_connect('button-release-event') { |w,e|
1735 if $button1_pressed_autotable
1736 #- unselect all only now
1737 $multiple_dnd = $selected_elements.keys
1738 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1739 $selected_elements = {}
1740 $button1_pressed_autotable = false
1743 $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]])
1744 if $selected_elements.length > 0
1745 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1748 press_x = press_y = pos_x = pos_y = nil
1749 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1753 $autotable.signal_connect('motion-notify-event') { |w,e|
1756 $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]])
1760 $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]])
1761 update_selected.call
1767 def create_subalbums_page
1769 subalbums_hb = Gtk::HBox.new
1770 $subalbums_vb = Gtk::VBox.new(false, 5)
1771 subalbums_hb.pack_start($subalbums_vb, false, false)
1772 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1773 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1774 $subalbums_sw.add_with_viewport(subalbums_hb)
1777 def save_current_file
1783 ios = File.open($filename, "w")
1784 $xmldoc.write(ios, 0)
1786 rescue Iconv::IllegalSequence
1787 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1788 if ! ios.nil? && ! ios.closed?
1791 $xmldoc.xml_decl.encoding = 'UTF-8'
1792 ios = File.open($filename, "w")
1793 $xmldoc.write(ios, 0)
1804 def save_current_file_user
1805 save_tempfilename = $filename
1806 $filename = $orig_filename
1807 if ! save_current_file
1808 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1809 $filename = save_tempfilename
1813 $generated_outofline = false
1814 $filename = save_tempfilename
1816 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1817 $todelete.each { |f|
1822 def mark_document_as_dirty
1823 $xmldoc.elements.each('//dir') { |elem|
1824 elem.delete_attribute('already-generated')
1828 #- ret: true => ok false => cancel
1829 def ask_save_modifications(msg1, msg2, *options)
1831 options = options.size > 0 ? options[0] : {}
1833 if options[:disallow_cancel]
1834 dialog = Gtk::Dialog.new(msg1,
1836 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1837 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1838 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1840 dialog = Gtk::Dialog.new(msg1,
1842 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1843 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1844 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1845 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1847 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1848 dialog.vbox.add(Gtk::Label.new(msg2))
1849 dialog.window_position = Gtk::Window::POS_CENTER
1852 dialog.run { |response|
1854 if response == Gtk::Dialog::RESPONSE_YES
1855 if ! save_current_file_user
1856 return ask_save_modifications(msg1, msg2, options)
1859 #- if we have generated an album but won't save modifications, we must remove
1860 #- already-generated markers in original file
1861 if $generated_outofline
1863 $xmldoc = Synchronizator.new(REXML::Document.new(File.new($orig_filename)), $xmlaccesslock)
1864 mark_document_as_dirty
1865 ios = File.open($orig_filename, "w")
1866 $xmldoc.write(ios, 0)
1869 puts "exception: #{$!}"
1873 if response == Gtk::Dialog::RESPONSE_CANCEL
1876 $todelete = [] #- unconditionally clear the list of images/videos to delete
1882 def try_quit(*options)
1883 if ask_save_modifications(utf8(_("Save before quitting?")),
1884 utf8(_("Do you want to save your changes before quitting?")),
1890 def show_popup(parent, msg, *options)
1891 dialog = Gtk::Dialog.new
1892 if options[0] && options[0][:title]
1893 dialog.title = options[0][:title]
1895 dialog.title = utf8(_("Booh message"))
1897 lbl = Gtk::Label.new
1898 if options[0] && options[0][:nomarkup]
1903 if options[0] && options[0][:centered]
1904 lbl.set_justify(Gtk::Justification::CENTER)
1906 if options[0] && options[0][:selectable]
1907 lbl.selectable = true
1909 if options[0] && options[0][:topwidget]
1910 dialog.vbox.add(options[0][:topwidget])
1912 if options[0] && options[0][:scrolled]
1913 sw = Gtk::ScrolledWindow.new(nil, nil)
1914 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1915 sw.add_with_viewport(lbl)
1917 dialog.set_default_size(500, 600)
1919 dialog.vbox.add(lbl)
1920 dialog.set_default_size(200, 120)
1922 if options[0] && options[0][:okcancel]
1923 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1925 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1927 if options[0] && options[0][:pos_centered]
1928 dialog.window_position = Gtk::Window::POS_CENTER
1930 dialog.window_position = Gtk::Window::POS_MOUSE
1933 if options[0] && options[0][:linkurl]
1934 linkbut = Gtk::Button.new('')
1935 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1936 linkbut.signal_connect('clicked') {
1937 open_url(options[0][:linkurl])
1938 dialog.response(Gtk::Dialog::RESPONSE_OK)
1939 set_mousecursor_normal
1941 linkbut.relief = Gtk::RELIEF_NONE
1942 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1943 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1944 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1949 if !options[0] || !options[0][:not_transient]
1950 dialog.transient_for = parent
1951 dialog.run { |response|
1953 if options[0] && options[0][:okcancel]
1954 return response == Gtk::Dialog::RESPONSE_OK
1958 dialog.signal_connect('response') { dialog.destroy }
1962 def set_mainwindow_title(progress)
1963 filename = $orig_filename || $filename
1966 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
1968 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
1972 $main_window.title = 'booh - ' + File.basename(filename)
1974 $main_window.title = 'booh'
1979 def backend_wait_message(parent, msg, infopipe_path, mode)
1981 w.set_transient_for(parent)
1984 vb = Gtk::VBox.new(false, 5).set_border_width(5)
1985 vb.pack_start(Gtk::Label.new(msg), false, false)
1987 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1988 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
1989 if mode != 'one dir scan'
1990 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1992 if mode == 'web-album'
1993 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1994 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1996 vb.pack_start(Gtk::HSeparator.new, false, false)
1998 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1999 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2000 vb.pack_end(bottom, false, false)
2003 update_progression_title_pb1 = proc {
2004 if mode == 'web-album'
2005 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2006 elsif mode != 'one dir scan'
2007 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2009 set_mainwindow_title(pb1_1.fraction)
2013 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2014 refresh_thread = Thread.new {
2015 directories_counter = 0
2016 while line = infopipe.gets
2017 if line =~ /^directories: (\d+), sizes: (\d+)/
2018 directories = $1.to_f + 1
2020 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2021 elements = $3.to_f + 1
2022 if mode == 'web-album'
2026 gtk_thread_protect { pb1_1.fraction = 0 }
2027 if mode != 'one dir scan'
2028 newtext = utf8(full_src_dir_to_rel($1, $2))
2029 newtext = '/' if newtext == ''
2030 gtk_thread_protect { pb1_2.text = newtext }
2031 directories_counter += 1
2032 gtk_thread_protect {
2033 pb1_2.fraction = directories_counter / directories
2034 update_progression_title_pb1.call
2037 elsif line =~ /^processing element$/
2038 element_counter += 1
2039 gtk_thread_protect {
2040 pb1_1.fraction = element_counter / elements
2041 update_progression_title_pb1.call
2043 elsif line =~ /^processing size$/
2044 element_counter += 1
2045 gtk_thread_protect {
2046 pb1_1.fraction = element_counter / elements
2047 update_progression_title_pb1.call
2049 elsif line =~ /^finished processing sizes$/
2050 gtk_thread_protect { pb1_1.fraction = 1 }
2051 elsif line =~ /^creating index.html$/
2052 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2053 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2054 directories_counter = 0
2055 elsif line =~ /^index.html: (.+)\|(.+)/
2056 newtext = utf8(full_src_dir_to_rel($1, $2))
2057 newtext = '/' if newtext == ''
2058 gtk_thread_protect { pb2.text = newtext }
2059 directories_counter += 1
2060 gtk_thread_protect {
2061 pb2.fraction = directories_counter / directories
2062 set_mainwindow_title(0.9 + pb2.fraction / 10)
2064 elsif line =~ /^die: (.*)$/
2071 w.signal_connect('delete-event') { w.destroy }
2072 w.signal_connect('destroy') {
2073 Thread.kill(refresh_thread)
2074 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2077 File.delete(infopipe_path)
2079 set_mainwindow_title(nil)
2081 w.window_position = Gtk::Window::POS_CENTER
2087 def call_backend(cmd, waitmsg, mode, params)
2088 pipe = Tempfile.new("boohpipe")
2089 Thread.critical = true
2092 system("mkfifo #{path}")
2093 Thread.critical = false
2094 cmd += " --info-pipe #{path}"
2095 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2100 id, exitstatus = Process.waitpid2(pid)
2101 gtk_thread_protect { w8.destroy }
2103 if params[:successmsg]
2104 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2106 if params[:closure_after]
2107 gtk_thread_protect(¶ms[:closure_after])
2109 elsif exitstatus == 15
2110 #- say nothing, user aborted
2112 gtk_thread_protect { show_popup($main_window,
2113 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2119 button.signal_connect('clicked') {
2120 Process.kill('SIGTERM', pid)
2124 def save_changes(*forced)
2125 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2129 $xmldir.delete_attribute('already-generated')
2131 propagate_children = proc { |xmldir|
2132 if xmldir.attributes['subdirs-caption']
2133 xmldir.delete_attribute('already-generated')
2135 xmldir.elements.each('dir') { |element|
2136 propagate_children.call(element)
2140 if $xmldir.child_byname_notattr('dir', 'deleted')
2141 new_title = $subalbums_title.buffer.text
2142 if new_title != $xmldir.attributes['subdirs-caption']
2143 parent = $xmldir.parent
2144 if parent.name == 'dir'
2145 parent.delete_attribute('already-generated')
2147 propagate_children.call($xmldir)
2149 $xmldir.add_attribute('subdirs-caption', new_title)
2150 $xmldir.elements.each('dir') { |element|
2151 if !element.attributes['deleted']
2152 path = element.attributes['path']
2153 newtext = $subalbums_edits[path][:editzone].buffer.text
2154 if element.attributes['subdirs-caption']
2155 if element.attributes['subdirs-caption'] != newtext
2156 propagate_children.call(element)
2158 element.add_attribute('subdirs-caption', newtext)
2159 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2161 if element.attributes['thumbnails-caption'] != newtext
2162 element.delete_attribute('already-generated')
2164 element.add_attribute('thumbnails-caption', newtext)
2165 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2171 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2172 if $xmldir.attributes['thumbnails-caption']
2173 path = $xmldir.attributes['path']
2174 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2176 elsif $xmldir.attributes['thumbnails-caption']
2177 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2180 if $xmldir.attributes['thumbnails-caption']
2181 if edit = $subalbums_edits[$xmldir.attributes['path']]
2182 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2186 #- remove and reinsert elements to reflect new ordering
2189 $xmldir.elements.each { |element|
2190 if element.name == 'image' || element.name == 'video'
2191 saves[element.attributes['filename']] = element.remove
2195 $autotable.current_order.each { |path|
2196 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2197 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2200 saves.each_key { |path|
2201 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2202 chld.add_attribute('deleted', 'true')
2206 def sort_by_exif_date
2210 $xmldir.elements.each { |element|
2211 if element.name == 'image' || element.name == 'video'
2212 current_order << element.attributes['filename']
2216 #- look for EXIF dates
2219 if current_order.size > 20
2221 w.set_transient_for($main_window)
2223 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2224 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2225 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2226 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2227 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2228 vb.pack_end(bottom, false, false)
2230 w.signal_connect('delete-event') { w.destroy }
2231 w.window_position = Gtk::Window::POS_CENTER
2235 b.signal_connect('clicked') { aborted = true }
2237 current_order.each { |f|
2239 if entry2type(f) == 'image'
2241 pb.fraction = i.to_f / current_order.size
2242 Gtk.main_iteration while Gtk.events_pending?
2243 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2245 dates[f] = date_time
2258 current_order.each { |f|
2259 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2261 dates[f] = date_time
2267 $xmldir.elements.each { |element|
2268 if element.name == 'image' || element.name == 'video'
2269 saves[element.attributes['filename']] = element.remove
2273 neworder = smartsort(current_order, dates)
2276 $xmldir.add_element(saves[f].name, saves[f].attributes)
2279 #- let the auto-table reflect new ordering
2283 def remove_all_captions
2286 $autotable.current_order.each { |path|
2287 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2288 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2290 save_undo(_("remove all captions"),
2292 texts.each_key { |key|
2293 $name2widgets[key][:textview].buffer.text = texts[key]
2295 $notebook.set_page(1)
2297 texts.each_key { |key|
2298 $name2widgets[key][:textview].buffer.text = ''
2300 $notebook.set_page(1)
2306 $selected_elements.each_key { |path|
2307 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2313 $selected_elements = {}
2317 $undo_tb.sensitive = $undo_mb.sensitive = false
2318 $redo_tb.sensitive = $redo_mb.sensitive = false
2324 $subalbums_vb.children.each { |chld|
2325 $subalbums_vb.remove(chld)
2327 $subalbums = Gtk::Table.new(0, 0, true)
2328 current_y_sub_albums = 0
2330 $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"], $xmlaccesslock)
2331 $subalbums_edits = {}
2332 subalbums_counter = 0
2333 subalbums_edits_bypos = {}
2335 add_subalbum = proc { |xmldir, counter|
2336 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2337 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2338 if xmldir == $xmldir
2339 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2340 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2341 caption = xmldir.attributes['thumbnails-caption']
2342 infotype = 'thumbnails'
2344 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2345 captionfile, caption = find_subalbum_caption_info(xmldir)
2346 infotype = find_subalbum_info_type(xmldir)
2348 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2349 hbox = Gtk::HBox.new
2350 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2352 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2355 my_gen_real_thumbnail = proc {
2356 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2359 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2360 f.add(img = Gtk::Image.new)
2361 my_gen_real_thumbnail.call
2363 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2365 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2366 $subalbums.attach(hbox,
2367 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2369 frame, textview = create_editzone($subalbums_sw, 0, img)
2370 textview.buffer.text = caption
2371 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2372 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2374 change_image = proc {
2375 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2377 Gtk::FileChooser::ACTION_OPEN,
2379 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2380 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2381 fc.transient_for = $main_window
2382 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))
2383 f.add(preview_img = Gtk::Image.new)
2385 fc.signal_connect('update-preview') { |w|
2387 if fc.preview_filename
2388 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2389 fc.preview_widget_active = true
2391 rescue Gdk::PixbufError
2392 fc.preview_widget_active = false
2395 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2397 old_file = captionfile
2398 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2399 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2400 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2401 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2403 new_file = fc.filename
2404 msg 3, "new captionfile is: #{fc.filename}"
2405 perform_changefile = proc {
2406 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2407 $modified_pixbufs.delete(thumbnail_file)
2408 xmldir.delete_attribute("#{infotype}-rotate")
2409 xmldir.delete_attribute("#{infotype}-color-swap")
2410 xmldir.delete_attribute("#{infotype}-enhance")
2411 xmldir.delete_attribute("#{infotype}-seektime")
2412 my_gen_real_thumbnail.call
2414 perform_changefile.call
2416 save_undo(_("change caption file for sub-album"),
2418 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2419 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2420 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2421 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2422 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2423 my_gen_real_thumbnail.call
2424 $notebook.set_page(0)
2426 perform_changefile.call
2427 $notebook.set_page(0)
2435 if File.exists?(thumbnail_file)
2436 File.delete(thumbnail_file)
2438 my_gen_real_thumbnail.call
2441 rotate_and_cleanup = proc { |angle|
2442 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2443 if File.exists?(thumbnail_file)
2444 File.delete(thumbnail_file)
2448 move = proc { |direction|
2451 save_changes('forced')
2452 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2453 if direction == 'up'
2454 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2455 subalbums_edits_bypos[oldpos - 1][:position] += 1
2457 if direction == 'down'
2458 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2459 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2461 if direction == 'top'
2462 for i in 1 .. oldpos - 1
2463 subalbums_edits_bypos[i][:position] += 1
2465 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2467 if direction == 'bottom'
2468 for i in oldpos + 1 .. subalbums_counter
2469 subalbums_edits_bypos[i][:position] -= 1
2471 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2475 $xmldir.elements.each('dir') { |element|
2476 if (!element.attributes['deleted'])
2477 elems << [ element.attributes['path'], element.remove ]
2480 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2481 each { |e| $xmldir.add_element(e[1]) }
2482 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2483 $xmldir.elements.each('descendant::dir') { |elem|
2484 elem.delete_attribute('already-generated')
2487 sel = $albums_tv.selection.selected_rows
2489 populate_subalbums_treeview(false)
2490 $albums_tv.selection.select_path(sel[0])
2493 color_swap_and_cleanup = proc {
2494 perform_color_swap_and_cleanup = proc {
2495 color_swap(xmldir, "#{infotype}-")
2496 my_gen_real_thumbnail.call
2498 perform_color_swap_and_cleanup.call
2500 save_undo(_("color swap"),
2502 perform_color_swap_and_cleanup.call
2503 $notebook.set_page(0)
2505 perform_color_swap_and_cleanup.call
2506 $notebook.set_page(0)
2511 change_seektime_and_cleanup = proc {
2512 if values = ask_new_seektime(xmldir, "#{infotype}-")
2513 perform_change_seektime_and_cleanup = proc { |val|
2514 change_seektime(xmldir, "#{infotype}-", val)
2515 my_gen_real_thumbnail.call
2517 perform_change_seektime_and_cleanup.call(values[:new])
2519 save_undo(_("specify seektime"),
2521 perform_change_seektime_and_cleanup.call(values[:old])
2522 $notebook.set_page(0)
2524 perform_change_seektime_and_cleanup.call(values[:new])
2525 $notebook.set_page(0)
2531 whitebalance_and_cleanup = proc {
2532 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2533 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2534 perform_change_whitebalance_and_cleanup = proc { |val|
2535 change_whitebalance(xmldir, "#{infotype}-", val)
2536 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2537 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2538 if File.exists?(thumbnail_file)
2539 File.delete(thumbnail_file)
2542 perform_change_whitebalance_and_cleanup.call(values[:new])
2544 save_undo(_("fix white balance"),
2546 perform_change_whitebalance_and_cleanup.call(values[:old])
2547 $notebook.set_page(0)
2549 perform_change_whitebalance_and_cleanup.call(values[:new])
2550 $notebook.set_page(0)
2556 gammacorrect_and_cleanup = proc {
2557 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2558 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2559 perform_change_gammacorrect_and_cleanup = proc { |val|
2560 change_gammacorrect(xmldir, "#{infotype}-", val)
2561 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2562 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2563 if File.exists?(thumbnail_file)
2564 File.delete(thumbnail_file)
2567 perform_change_gammacorrect_and_cleanup.call(values[:new])
2569 save_undo(_("gamma correction"),
2571 perform_change_gammacorrect_and_cleanup.call(values[:old])
2572 $notebook.set_page(0)
2574 perform_change_gammacorrect_and_cleanup.call(values[:new])
2575 $notebook.set_page(0)
2581 enhance_and_cleanup = proc {
2582 perform_enhance_and_cleanup = proc {
2583 enhance(xmldir, "#{infotype}-")
2584 my_gen_real_thumbnail.call
2587 perform_enhance_and_cleanup.call
2589 save_undo(_("enhance"),
2591 perform_enhance_and_cleanup.call
2592 $notebook.set_page(0)
2594 perform_enhance_and_cleanup.call
2595 $notebook.set_page(0)
2600 evtbox.signal_connect('button-press-event') { |w, event|
2601 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2603 rotate_and_cleanup.call(90)
2605 rotate_and_cleanup.call(-90)
2606 elsif $enhance.active?
2607 enhance_and_cleanup.call
2610 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2611 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2612 { :forbid_left => true, :forbid_right => true,
2613 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2614 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2615 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2616 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2617 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2619 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2624 evtbox.signal_connect('button-press-event') { |w, event|
2625 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2629 evtbox.signal_connect('button-release-event') { |w, event|
2630 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2631 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2632 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2633 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2634 msg 3, "gesture rotate: #{angle}"
2635 rotate_and_cleanup.call(angle)
2638 $gesture_press = nil
2641 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2642 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2643 current_y_sub_albums += 1
2646 if $xmldir.child_byname_notattr('dir', 'deleted')
2648 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2649 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2650 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2651 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2652 #- this album image/caption
2653 if $xmldir.attributes['thumbnails-caption']
2654 add_subalbum.call($xmldir, 0)
2657 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2658 $xmldir.elements.each { |element|
2659 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2660 #- element (image or video) of this album
2661 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2662 msg 3, "dest_img: #{dest_img}"
2663 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2664 total[element.name] += 1
2666 if element.name == 'dir' && !element.attributes['deleted']
2667 #- sub-album image/caption
2668 add_subalbum.call(element, subalbums_counter += 1)
2669 total[element.name] += 1
2672 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2673 total['image'], total['video'], total['dir'] ]))
2674 $subalbums_vb.add($subalbums)
2675 $subalbums_vb.show_all
2677 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2678 $notebook.get_tab_label($autotable_sw).sensitive = false
2679 $notebook.set_page(0)
2680 $thumbnails_title.buffer.text = ''
2682 $notebook.get_tab_label($autotable_sw).sensitive = true
2683 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2686 if !$xmldir.child_byname_notattr('dir', 'deleted')
2687 $notebook.get_tab_label($subalbums_sw).sensitive = false
2688 $notebook.set_page(1)
2690 $notebook.get_tab_label($subalbums_sw).sensitive = true
2694 def pixbuf_or_nil(filename)
2696 return Gdk::Pixbuf.new(filename)
2702 def theme_choose(current)
2703 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2705 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2706 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2707 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2709 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2710 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2711 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2712 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2713 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2714 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2715 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2716 treeview.signal_connect('button-press-event') { |w, event|
2717 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2718 dialog.response(Gtk::Dialog::RESPONSE_OK)
2722 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2724 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2727 iter[0] = File.basename(dir)
2728 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2729 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2730 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2731 if File.basename(dir) == current
2732 treeview.selection.select_iter(iter)
2735 dialog.set_default_size(-1, 500)
2736 dialog.vbox.show_all
2738 dialog.run { |response|
2739 iter = treeview.selection.selected
2741 if response == Gtk::Dialog::RESPONSE_OK && iter
2742 return model.get_value(iter, 0)
2748 def show_password_protections
2749 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2750 child_iter = $albums_iters[xmldir.attributes['path']]
2751 if xmldir.attributes['password-protect']
2752 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2753 already_protected = true
2754 elsif already_protected
2755 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2757 pix = pix.saturate_and_pixelate(1, true)
2763 xmldir.elements.each('dir') { |elem|
2764 if !elem.attributes['deleted']
2765 examine_dir_elem.call(child_iter, elem, already_protected)
2769 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2772 def populate_subalbums_treeview(select_first)
2776 $subalbums_vb.children.each { |chld|
2777 $subalbums_vb.remove(chld)
2780 source = $xmldoc.root.attributes['source']
2781 msg 3, "source: #{source}"
2783 xmldir = $xmldoc.elements['//dir']
2784 if !xmldir || xmldir.attributes['path'] != source
2785 msg 1, _("Corrupted booh file...")
2789 append_dir_elem = proc { |parent_iter, xmldir|
2790 child_iter = $albums_ts.append(parent_iter)
2791 child_iter[0] = File.basename(xmldir.attributes['path'])
2792 child_iter[1] = xmldir.attributes['path']
2793 $albums_iters[xmldir.attributes['path']] = child_iter
2794 msg 3, "puttin location: #{xmldir.attributes['path']}"
2795 xmldir.elements.each('dir') { |elem|
2796 if !elem.attributes['deleted']
2797 append_dir_elem.call(child_iter, elem)
2801 append_dir_elem.call(nil, xmldir)
2802 show_password_protections
2804 $albums_tv.expand_all
2806 $albums_tv.selection.select_iter($albums_ts.iter_first)
2810 def select_current_theme
2811 select_theme($xmldoc.root.attributes['theme'],
2812 $xmldoc.root.attributes['limit-sizes'],
2813 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2814 $xmldoc.root.attributes['thumbnails-per-row'])
2817 def open_file(filename)
2821 $current_path = nil #- invalidate
2822 $modified_pixbufs = {}
2825 $subalbums_vb.children.each { |chld|
2826 $subalbums_vb.remove(chld)
2829 if !File.exists?(filename)
2830 return utf8(_("File not found."))
2834 $xmldoc = Synchronizator.new(REXML::Document.new(File.new(filename)), $xmlaccesslock)
2839 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2840 if entry2type(filename).nil?
2841 return utf8(_("Not a booh file!"))
2843 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."))
2847 if !source = $xmldoc.root.attributes['source']
2848 return utf8(_("Corrupted booh file..."))
2851 if !dest = $xmldoc.root.attributes['destination']
2852 return utf8(_("Corrupted booh file..."))
2855 if !theme = $xmldoc.root.attributes['theme']
2856 return utf8(_("Corrupted booh file..."))
2859 if $xmldoc.root.attributes['version'] < '0.9.0'
2860 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2861 mark_document_as_dirty
2862 if $xmldoc.root.attributes['version'] < '0.8.4'
2863 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2864 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2865 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2866 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2867 if old_dest_dir != new_dest_dir
2868 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2870 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2871 xmldir.elements.each { |element|
2872 if %w(image video).include?(element.name) && !element.attributes['deleted']
2873 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2874 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2875 Dir[old_name + '*'].each { |file|
2876 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2877 file != new_file and sys("mv '#{file}' '#{new_file}'")
2880 if element.name == 'dir' && !element.attributes['deleted']
2881 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2882 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2883 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2887 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2891 $xmldoc.root.add_attribute('version', $VERSION)
2894 select_current_theme
2896 $filename = filename
2897 set_mainwindow_title(nil)
2898 $default_size['thumbnails'] =~ /(.*)x(.*)/
2899 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2900 $albums_thumbnail_size =~ /(.*)x(.*)/
2901 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2903 populate_subalbums_treeview(true)
2905 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2909 def open_file_user(filename)
2910 result = open_file(filename)
2912 $config['last-opens'] ||= []
2913 if $config['last-opens'][-1] != utf8(filename)
2914 $config['last-opens'] << utf8(filename)
2916 $orig_filename = $filename
2917 $main_window.title = 'booh - ' + File.basename($orig_filename)
2918 tmp = Tempfile.new("boohtemp")
2919 Thread.critical = true
2920 $filename = tmp.path
2923 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
2924 Thread.critical = false
2926 $tempfiles << $filename << "#{$filename}.backup"
2928 $orig_filename = nil
2934 if !ask_save_modifications(utf8(_("Save this album?")),
2935 utf8(_("Do you want to save the changes to this album?")),
2936 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2939 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2941 Gtk::FileChooser::ACTION_OPEN,
2943 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2944 fc.add_shortcut_folder(File.expand_path("~/.booh"))
2945 fc.set_current_folder(File.expand_path("~/.booh"))
2946 fc.transient_for = $main_window
2949 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2950 push_mousecursor_wait(fc)
2951 msg = open_file_user(fc.filename)
2966 def additional_booh_options
2969 options += "--mproc #{$config['mproc'].to_i} "
2971 options += "--comments-format '#{$config['comments-format']}' "
2972 if $config['transcode-videos']
2973 options += "--transcode-videos '#{$config['transcode-videos']}' "
2978 def ask_multi_languages(value)
2980 spl = value.split(',')
2981 value = [ spl[0..-2], spl[-1] ]
2984 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2987 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2988 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2990 lbl = Gtk::Label.new
2992 _("You can choose to activate <b>multi-languages</b> support for this web-album
2993 (it will work only if you publish your web-album on an Apache web-server). This will
2994 use the MultiViews feature of Apache; the pages will be served according to the
2995 value of the Accept-Language HTTP header sent by the web browsers, so that people
2996 with different languages preferences will be able to browse your web-album with
2997 navigation in their language (if language is available).
3000 dialog.vbox.add(lbl)
3001 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3002 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3003 add(languages = Gtk::Button.new))))
3005 pick_languages = proc {
3006 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3009 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3010 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3012 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3013 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3015 SUPPORTED_LANGUAGES.each { |lang|
3016 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3017 if ! value.nil? && value[0].include?(lang)
3023 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3024 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3025 fallback_language = nil
3026 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3027 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3028 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3029 fbl_rb.active = true
3030 fallback_language = SUPPORTED_LANGUAGES[0]
3032 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3033 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3034 rb.signal_connect('clicked') { fallback_language = lang }
3035 if ! value.nil? && value[1] == lang
3040 dialog2.window_position = Gtk::Window::POS_MOUSE
3044 dialog2.run { |response|
3046 if resp == Gtk::Dialog::RESPONSE_OK
3048 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3049 value[1] = fallback_language
3050 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3057 languages.signal_connect('clicked') {
3060 dialog.window_position = Gtk::Window::POS_MOUSE
3064 rb_yes.active = true
3065 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3067 rb_no.signal_connect('clicked') {
3071 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3084 dialog.run { |response|
3089 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3091 return [ true, nil ]
3093 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3102 if !ask_save_modifications(utf8(_("Save this album?")),
3103 utf8(_("Do you want to save the changes to this album?")),
3104 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3107 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3109 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3110 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3111 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3113 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3114 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3115 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3116 tbl.attach(src = Gtk::Entry.new,
3117 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3118 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3119 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3120 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3121 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3122 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3123 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3124 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3125 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3126 tbl.attach(dest = Gtk::Entry.new,
3127 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3128 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3129 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3130 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3131 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3132 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3133 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3134 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3135 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3137 tooltips = Gtk::Tooltips.new
3138 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3139 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3140 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3141 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3142 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3143 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3144 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)
3145 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3146 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3147 nperpage_model = Gtk::ListStore.new(String, String)
3148 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3149 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3150 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3151 nperpagecombo.set_attributes(crt, { :markup => 0 })
3152 iter = nperpage_model.append
3153 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3155 [ 12, 20, 30, 40, 50 ].each { |v|
3156 iter = nperpage_model.append
3157 iter[0] = iter[1] = v.to_s
3159 nperpagecombo.active = 0
3161 multilanguages_value = nil
3162 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3163 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3164 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)
3165 multilanguages.signal_connect('clicked') {
3166 retval = ask_multi_languages(multilanguages_value)
3168 multilanguages_value = retval[1]
3170 if multilanguages_value
3171 ml_label.text = utf8(_("Multi-languages: enabled."))
3173 ml_label.text = utf8(_("Multi-languages: disabled."))
3176 if $config['default-multi-languages']
3177 multilanguages_value = $config['default-multi-languages']
3178 ml_label.text = utf8(_("Multi-languages: enabled."))
3180 ml_label.text = utf8(_("Multi-languages: disabled."))
3183 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3184 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3185 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)
3186 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3187 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3188 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)
3190 src_nb_calculated_for = ''
3192 process_src_nb = proc {
3193 if src.text != src_nb_calculated_for
3194 src_nb_calculated_for = src.text
3196 Thread.kill(src_nb_thread)
3199 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3200 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3202 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3203 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3204 src_nb_thread = Thread.new {
3205 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3206 total = { 'image' => 0, 'video' => 0, nil => 0 }
3207 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3208 if File.basename(dir) =~ /^\./
3212 Dir.entries(dir.chomp).each { |file|
3213 total[entry2type(file)] += 1
3215 rescue Errno::EACCES, Errno::ENOENT
3219 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3223 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3226 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3232 timeout_src_nb = Gtk.timeout_add(100) {
3236 src_browse.signal_connect('clicked') {
3237 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3239 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3241 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3242 fc.transient_for = $main_window
3243 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3244 src.text = utf8(fc.filename)
3246 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3251 dest_browse.signal_connect('clicked') {
3252 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3254 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3256 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3257 fc.transient_for = $main_window
3258 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3259 dest.text = utf8(fc.filename)
3264 conf_browse.signal_connect('clicked') {
3265 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3267 Gtk::FileChooser::ACTION_SAVE,
3269 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3270 fc.transient_for = $main_window
3271 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3272 fc.set_current_folder(File.expand_path("~/.booh"))
3273 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3274 conf.text = utf8(fc.filename)
3281 recreate_theme_config = proc {
3282 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3284 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3285 $images_size.each { |s|
3286 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3290 tooltips.set_tip(cb, utf8(s['description']), nil)
3291 theme_sizes << { :widget => cb, :value => s['name'] }
3293 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3294 tooltips = Gtk::Tooltips.new
3295 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3296 theme_sizes << { :widget => cb, :value => 'original' }
3299 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3302 $allowed_N_values.each { |n|
3304 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3306 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3308 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3312 nperrows << { :widget => rb, :value => n }
3314 nperrowradios.show_all
3316 recreate_theme_config.call
3318 theme_button.signal_connect('clicked') {
3319 if newtheme = theme_choose(theme_button.label)
3320 theme_button.label = newtheme
3321 recreate_theme_config.call
3325 dialog.vbox.add(frame1)
3326 dialog.vbox.add(frame2)
3332 dialog.run { |response|
3333 if response == Gtk::Dialog::RESPONSE_OK
3334 srcdir = from_utf8_safe(src.text)
3335 destdir = from_utf8_safe(dest.text)
3336 confpath = from_utf8_safe(conf.text)
3337 if src.text != '' && srcdir == ''
3338 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3340 elsif !File.directory?(srcdir)
3341 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3343 elsif dest.text != '' && destdir == ''
3344 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3346 elsif destdir != make_dest_filename(destdir)
3347 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3349 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3350 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3351 inside it will be permanently removed before creating the web-album!
3352 Are you sure you want to continue?")), { :okcancel => true })
3354 elsif File.exists?(destdir) && !File.directory?(destdir)
3355 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3357 elsif conf.text == ''
3358 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3360 elsif conf.text != '' && confpath == ''
3361 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3363 elsif File.directory?(confpath)
3364 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3366 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3367 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3369 system("mkdir '#{destdir}'")
3370 if !File.directory?(destdir)
3371 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3383 srcdir = from_utf8(src.text)
3384 destdir = from_utf8(dest.text)
3385 configskel = File.expand_path(from_utf8(conf.text))
3386 theme = theme_button.label
3387 #- some sort of automatic theme preference
3388 $config['default-theme'] = theme
3389 $config['default-multi-languages'] = multilanguages_value
3390 $config['default-optimize32'] = optimize432.active?.to_s
3391 sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3392 nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3393 nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3394 opt432 = optimize432.active?
3395 madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
3396 indexlink = indexlinkentry.text.gsub('\'', ''')
3399 Thread.kill(src_nb_thread)
3400 gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3403 Gtk.timeout_remove(timeout_src_nb)
3406 call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3407 "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3408 (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3409 (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3410 "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3411 utf8(_("Please wait while scanning source directory...")),
3413 { :closure_after => proc {
3414 open_file_user(configskel)
3415 $main_window.urgency_hint = true
3421 dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3423 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3424 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3425 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3427 source = $xmldoc.root.attributes['source']
3428 dest = $xmldoc.root.attributes['destination']
3429 theme = $xmldoc.root.attributes['theme']
3430 opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3431 nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3432 nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3433 limit_sizes = $xmldoc.root.attributes['limit-sizes']
3435 limit_sizes = limit_sizes.split(/,/)
3437 madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
3438 indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
3439 save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3441 tooltips = Gtk::Tooltips.new
3442 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3443 tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3444 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3445 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3446 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3447 tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3448 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3449 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3450 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3451 tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3452 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3453 tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3454 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3456 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3457 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3458 pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3459 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3460 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3461 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3462 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)
3463 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3464 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3465 nperpage_model = Gtk::ListStore.new(String, String)
3466 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3467 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3468 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3469 nperpagecombo.set_attributes(crt, { :markup => 0 })
3470 iter = nperpage_model.append
3471 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3473 [ 12, 20, 30, 40, 50 ].each { |v|
3474 iter = nperpage_model.append
3475 iter[0] = iter[1] = v.to_s
3476 if nperpage && nperpage == v.to_s
3477 nperpagecombo.active_iter = iter
3480 if nperpagecombo.active_iter.nil?
3481 nperpagecombo.active = 0
3484 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3485 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3486 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)
3488 if save_multilanguages_value
3489 ml_label.text = utf8(_("Multi-languages: enabled."))
3491 ml_label.text = utf8(_("Multi-languages: disabled."))
3495 multilanguages.signal_connect('clicked') {
3496 retval = ask_multi_languages(save_multilanguages_value)
3498 save_multilanguages_value = retval[1]
3503 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3504 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3506 indexlinkentry.text = indexlink
3508 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)