5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2009 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'booh/rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
44 [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
45 [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
46 [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
49 #- default values for some globals
53 $ignore_videos = false
54 $button1_pressed_autotable = false
55 $generated_outofline = false
58 puts _("Usage: %s [OPTION]...") % File.basename($0)
60 printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
65 parser = GetoptLong.new
66 parser.set_options(*$options.collect { |ary| ary[0..2] })
68 parser.each_option do |name, arg|
75 puts _("Booh version %s
77 Copyright (c) 2005-2009 Guillaume Cottenceau.
78 This is free software; see the source for copying conditions. There is NO
79 warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
83 when '--verbose-level'
84 $verbose_level = arg.to_i
97 $config_file = File.expand_path('~/.booh-gui-rc')
98 if File.readable?($config_file)
99 xmldoc = REXML::Document.new(File.new($config_file))
100 xmldoc.root.elements.each { |element|
101 txt = element.get_text
103 if txt.value =~ /~~~/ || element.name == 'last-opens'
104 $config[element.name] = txt.value.split(/~~~/)
106 $config[element.name] = txt.value
108 elsif element.elements.size == 0
109 $config[element.name] = ''
111 $config[element.name] = {}
112 element.each { |chld|
114 $config[element.name][chld.name] = txt ? txt.value : nil
119 $config['video-viewer'] ||= '/usr/bin/mplayer %f'
120 $config['image-editor'] ||= '/usr/bin/gimp-remote %f || /usr/bin/gimp %f'
121 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f || /usr/bin/firefox %f"
122 $config['comments-format'] ||= '%t'
123 if !FileTest.directory?(File.expand_path('~/.booh'))
124 system("mkdir ~/.booh")
126 if $config['mproc'].nil?
128 for line in IO.readlines('/proc/cpuinfo') do
129 line =~ /^processor/ and cpus += 1
132 $config['mproc'] = cpus
135 $config['rotate-set-exif'] ||= 'true'
141 if !system("which convert >/dev/null 2>/dev/null")
142 show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
143 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
146 if !system("which identify >/dev/null 2>/dev/null")
147 show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
148 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
150 if !system("which exif >/dev/null 2>/dev/null")
151 show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
153 missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
155 show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
158 viewer_binary = $config['video-viewer'].split.first
159 if viewer_binary && !File.executable?(viewer_binary)
160 show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
161 You should fix this in Edit/Preferences so that you can view videos.
163 Problem was: '%s' is not an executable file.
164 Hint: don't forget to specify the full path to the executable,
165 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
169 def check_image_editor
170 if last_failed_binary = check_multi_binaries($config['image-editor'])
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.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
184 if $config['last-opens'] && $config['last-opens'].size > 10
185 $config['last-opens'] = $config['last-opens'][-10, 10]
188 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
189 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
190 $config.each_pair { |key, value|
191 elem = xmldoc.root.add_element key
193 $config[key].each_pair { |subkey, subvalue|
194 subelem = elem.add_element subkey
195 subelem.add_text subvalue.to_s
197 elsif value.is_a? Array
198 elem.add_text value.join('~~~')
203 elem.add_text value.to_s
207 ios = File.open($config_file, "w")
211 $tempfiles.each { |f|
218 def set_mousecursor(what, *widget)
219 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
220 if widget[0] && widget[0].window
221 widget[0].window.cursor = cursor
223 if $main_window && $main_window.window
224 $main_window.window.cursor = cursor
226 $current_cursor = what
228 def set_mousecursor_wait(*widget)
229 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
230 if Thread.current == Thread.main
231 Gtk.main_iteration while Gtk.events_pending?
234 def set_mousecursor_normal(*widget)
235 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
237 def push_mousecursor_wait(*widget)
238 if $current_cursor != Gdk::Cursor::WATCH
239 $save_cursor = $current_cursor
240 gtk_thread_protect { set_mousecursor_wait(*widget) }
243 def pop_mousecursor(*widget)
244 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
248 source = $xmldoc.root.attributes['source']
249 dest = $xmldoc.root.attributes['destination']
250 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
253 def full_src_dir_to_rel(path, source)
254 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
257 def build_full_dest_filename(filename)
258 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
261 def save_undo(name, closure, *params)
262 UndoHandler.save_undo(name, closure, [ *params ])
263 $undo_tb.sensitive = $undo_mb.sensitive = true
264 $redo_tb.sensitive = $redo_mb.sensitive = false
267 def view_element(filename, closures)
268 if entry2type(filename) == 'video'
269 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
275 w = create_window.set_title(filename)
277 msg 3, "filename: #{filename}"
278 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
279 #- typically this file won't exist in case of videos; try with the largest thumbnail around
280 if !File.exists?(dest_img)
281 if entry2type(filename) == 'video'
282 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
283 if not alternatives.empty?
284 dest_img = alternatives[-1]
287 push_mousecursor_wait
288 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
290 if !File.exists?(dest_img)
291 msg 2, _("Could not generate fullscreen thumbnail!")
296 aspect = utf8(_("Aspect: unknown"))
297 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
299 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
301 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
302 evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(vbox).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
303 evt.signal_connect('button-press-event') { |this, event|
304 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
305 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
307 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
309 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
310 delete_item.signal_connect('activate') {
312 closures[:delete].call(false)
315 menu.popup(nil, nil, event.button, event.time)
318 evt.signal_connect('button-release-event') { |this, event|
320 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
321 msg 3, "gesture delete: click-drag right button to the bottom"
323 closures[:delete].call(false)
324 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
328 tooltips = Gtk::Tooltips.new
329 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
331 w.signal_connect('key-press-event') { |w,event|
332 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
334 closures[:delete].call(false)
338 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
339 b.signal_connect('clicked') { w.destroy }
342 vb.pack_start(evt, false, false)
343 vb.pack_end(bottom, false, false)
346 w.signal_connect('delete-event') { w.destroy }
347 w.window_position = Gtk::Window::POS_CENTER
351 def scroll_upper(scrolledwindow, ypos_top)
352 newval = scrolledwindow.vadjustment.value -
353 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
354 if newval < scrolledwindow.vadjustment.lower
355 newval = scrolledwindow.vadjustment.lower
357 scrolledwindow.vadjustment.value = newval
360 def scroll_lower(scrolledwindow, ypos_bottom)
361 newval = scrolledwindow.vadjustment.value +
362 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
363 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
364 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
366 scrolledwindow.vadjustment.value = newval
369 def autoscroll_if_needed(scrolledwindow, image, textview)
370 #- autoscroll if cursor or image is not visible, if possible
371 if image && image.window || textview.window
372 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
373 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
374 current_miny_visible = scrolledwindow.vadjustment.value
375 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
376 if ypos_top < current_miny_visible
377 scroll_upper(scrolledwindow, ypos_top)
378 elsif ypos_bottom > current_maxy_visible
379 scroll_lower(scrolledwindow, ypos_bottom)
384 def create_editzone(scrolledwindow, pagenum, image)
385 frame = Gtk::Frame.new
386 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
387 frame.set_shadow_type(Gtk::SHADOW_IN)
388 textview.signal_connect('key-press-event') { |w, event|
389 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
390 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
391 scrolledwindow.signal_emit('key-press-event', event)
393 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
394 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
395 if event.keyval == Gdk::Keyval::GDK_Up
396 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
397 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
399 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
402 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
403 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
405 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
412 candidate_undo_text = nil
413 textview.signal_connect('focus-in-event') { |w, event|
414 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
415 candidate_undo_text = textview.buffer.text
419 textview.signal_connect('key-release-event') { |w, event|
420 if candidate_undo_text && candidate_undo_text != textview.buffer.text
422 save_undo(_("text edit"),
424 save_text = textview.buffer.text
425 textview.buffer.text = text
427 $notebook.set_page(pagenum)
429 textview.buffer.text = save_text
431 $notebook.set_page(pagenum)
433 }, candidate_undo_text)
434 candidate_undo_text = nil
437 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)
438 autoscroll_if_needed(scrolledwindow, image, textview)
443 return [ frame, textview ]
446 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
448 if !$modified_pixbufs[thumbnail_img]
449 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
450 elsif !$modified_pixbufs[thumbnail_img][:orig]
451 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
454 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
457 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
458 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
459 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
460 if pixbuf.height > desired_y
461 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
462 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
463 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
468 if $modified_pixbufs[thumbnail_img][:whitebalance]
469 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
472 #- fix gamma correction
473 if $modified_pixbufs[thumbnail_img][:gammacorrect]
474 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
477 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
480 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
483 #- update rotate attribute
484 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
485 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
487 #- change exif orientation if configured so (but forget in case of thumbnails caption)
488 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
489 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
492 $modified_pixbufs[thumbnail_img] ||= {}
493 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
494 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
496 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
499 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
502 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
504 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
506 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
507 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
509 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
510 $notebook.set_page(0)
511 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
516 def color_swap(xmldir, attributes_prefix)
518 rexml_thread_protect {
519 if xmldir.attributes["#{attributes_prefix}color-swap"]
520 xmldir.delete_attribute("#{attributes_prefix}color-swap")
522 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
527 def enhance(xmldir, attributes_prefix)
529 rexml_thread_protect {
530 if xmldir.attributes["#{attributes_prefix}enhance"]
531 xmldir.delete_attribute("#{attributes_prefix}enhance")
533 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
538 def change_seektime(xmldir, attributes_prefix, value)
540 rexml_thread_protect {
541 xmldir.add_attribute("#{attributes_prefix}seektime", value)
545 def ask_new_seektime(xmldir, attributes_prefix)
546 value = rexml_thread_protect {
548 xmldir.attributes["#{attributes_prefix}seektime"]
554 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
556 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
557 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
558 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
562 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
566 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
567 entry.signal_connect('key-press-event') { |w, event|
568 if event.keyval == Gdk::Keyval::GDK_Return
569 dialog.response(Gtk::Dialog::RESPONSE_OK)
571 elsif event.keyval == Gdk::Keyval::GDK_Escape
572 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
575 false #- propagate if needed
579 dialog.window_position = Gtk::Window::POS_MOUSE
582 dialog.run { |response|
585 if response == Gtk::Dialog::RESPONSE_OK
587 msg 3, "changing seektime to #{newval}"
588 return { :old => value, :new => newval }
595 def change_pano_amount(xmldir, attributes_prefix, value)
597 rexml_thread_protect {
599 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
601 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
606 def ask_new_pano_amount(xmldir, attributes_prefix)
607 value = rexml_thread_protect {
609 xmldir.attributes["#{attributes_prefix}pano-amount"]
615 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
617 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
618 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
619 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
623 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
624 of this panorama image compared to other regular images. For example, if the panorama
625 was taken out of four photos on one row, counting the necessary overlap, the width of
626 this panorama image should probably be roughly three times the width of regular images.
628 With this information, booh will be able to generate panorama thumbnails looking
629 the right 'size', since the height of the thumbnail for this image will be similar
630 to the height of other thumbnails.
633 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)")))).
634 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
635 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
636 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
637 spin.signal_connect('value-changed') {
640 dialog.window_position = Gtk::Window::POS_MOUSE
643 spin.value = value.to_f
650 dialog.run { |response|
654 newval = spin.value.to_f
657 if response == Gtk::Dialog::RESPONSE_OK
659 msg 3, "changing panorama amount to #{newval}"
660 return { :old => value, :new => newval }
667 def change_whitebalance(xmlelem, attributes_prefix, value)
669 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
672 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
674 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
675 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
676 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
677 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
678 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
679 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
680 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
681 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
682 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
683 $modified_pixbufs[thumbnail_img] ||= {}
684 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
685 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
687 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
688 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
690 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
693 $modified_pixbufs[thumbnail_img] ||= {}
694 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
696 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
699 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
700 #- init $modified_pixbufs correctly
701 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
703 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
705 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
707 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
708 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
709 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
713 _("You can fix the <b>white balance</b> of the image, if your image is too blue
714 or too yellow because the recorder didn't detect the light correctly. Drag the
715 slider below the image to the left for more blue, to the right for more yellow.
719 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
721 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
723 dialog.window_position = Gtk::Window::POS_MOUSE
727 timeout = Gtk.timeout_add(100) {
728 if hs.value != lastval
731 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
737 dialog.run { |response|
738 Gtk.timeout_remove(timeout)
739 if response == Gtk::Dialog::RESPONSE_OK
741 newval = hs.value.to_s
742 msg 3, "changing white balance to #{newval}"
744 return { :old => value, :new => newval }
747 $modified_pixbufs[thumbnail_img] ||= {}
748 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
749 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
757 def change_gammacorrect(xmlelem, attributes_prefix, value)
759 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
762 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
764 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
765 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
766 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
767 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
768 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
769 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
770 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
771 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
772 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
773 $modified_pixbufs[thumbnail_img] ||= {}
774 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
775 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
777 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
778 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
780 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
783 $modified_pixbufs[thumbnail_img] ||= {}
784 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
786 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
789 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
790 #- init $modified_pixbufs correctly
791 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
793 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
795 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
797 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
798 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
799 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
803 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
804 or too bright. Drag the slider below the image.
808 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
810 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
812 dialog.window_position = Gtk::Window::POS_MOUSE
816 timeout = Gtk.timeout_add(100) {
817 if hs.value != lastval
820 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
826 dialog.run { |response|
827 Gtk.timeout_remove(timeout)
828 if response == Gtk::Dialog::RESPONSE_OK
830 newval = hs.value.to_s
831 msg 3, "gamma correction to #{newval}"
833 return { :old => value, :new => newval }
836 $modified_pixbufs[thumbnail_img] ||= {}
837 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
838 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
846 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
847 if File.exists?(destfile)
848 File.delete(destfile)
850 #- type can be 'element' or 'subdir'
852 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
854 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
858 $max_gen_thumbnail_threads = nil
859 $current_gen_thumbnail_threads = 0
860 $gen_thumbnail_monitor = Monitor.new
862 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
863 if $max_gen_thumbnail_threads.nil?
864 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
867 push_mousecursor_wait
868 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
871 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
876 $gen_thumbnail_monitor.synchronize {
877 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
878 $current_gen_thumbnail_threads += 1
883 msg 3, "generate thumbnail from new thread"
886 $gen_thumbnail_monitor.synchronize {
887 $current_gen_thumbnail_threads -= 1
891 msg 3, "generate thumbnail from current thread"
896 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
897 distribute_multiple_call = Proc.new { |action, arg|
898 $selected_elements.each_key { |path|
899 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
901 if possible_actions[:can_multiple] && $selected_elements.length > 0
902 UndoHandler.begin_batch
903 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
904 UndoHandler.end_batch
906 closures[action].call(arg)
908 $selected_elements = {}
911 if optionals.include?('change_image')
912 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
913 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
914 changeimg.signal_connect('activate') { closures[:change].call }
915 menu.append(Gtk::SeparatorMenuItem.new)
917 if !possible_actions[:can_multiple] || $selected_elements.length == 0
920 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
921 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
922 view.signal_connect('activate') { closures[:view].call }
924 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
925 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
926 view.signal_connect('activate') { closures[:view].call }
927 menu.append(Gtk::SeparatorMenuItem.new)
930 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
931 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
932 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
933 exif.signal_connect('activate') { show_popup($main_window,
934 utf8(`exif -m '#{fullpath}'`),
935 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
936 menu.append(Gtk::SeparatorMenuItem.new)
939 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
940 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
941 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
942 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
943 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
944 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
945 if !possible_actions[:can_multiple] || $selected_elements.length == 0
946 menu.append(Gtk::SeparatorMenuItem.new)
947 if !possible_actions[:forbid_left]
948 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
949 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
950 moveleft.signal_connect('activate') { closures[:move].call('left') }
951 if !possible_actions[:can_left]
952 moveleft.sensitive = false
955 if !possible_actions[:forbid_right]
956 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
957 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
958 moveright.signal_connect('activate') { closures[:move].call('right') }
959 if !possible_actions[:can_right]
960 moveright.sensitive = false
963 if optionals.include?('move_top')
964 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
965 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
966 movetop.signal_connect('activate') { closures[:move].call('top') }
967 if !possible_actions[:can_top]
968 movetop.sensitive = false
971 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
972 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
973 moveup.signal_connect('activate') { closures[:move].call('up') }
974 if !possible_actions[:can_up]
975 moveup.sensitive = false
977 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
978 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
979 movedown.signal_connect('activate') { closures[:move].call('down') }
980 if !possible_actions[:can_down]
981 movedown.sensitive = false
983 if optionals.include?('move_bottom')
984 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
985 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
986 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
987 if !possible_actions[:can_bottom]
988 movebottom.sensitive = false
993 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
994 menu.append(Gtk::SeparatorMenuItem.new)
995 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
996 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
997 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
998 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
999 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1000 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1001 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1002 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1003 seektime.signal_connect('activate') {
1004 if possible_actions[:can_multiple] && $selected_elements.length > 0
1005 if values = ask_new_seektime(nil, '')
1006 distribute_multiple_call.call(:seektime, values)
1009 closures[:seektime].call
1014 menu.append( Gtk::SeparatorMenuItem.new)
1015 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1016 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1017 gammacorrect.signal_connect('activate') {
1018 if possible_actions[:can_multiple] && $selected_elements.length > 0
1019 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1020 distribute_multiple_call.call(:gammacorrect, values)
1023 closures[:gammacorrect].call
1026 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1027 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1028 whitebalance.signal_connect('activate') {
1029 if possible_actions[:can_multiple] && $selected_elements.length > 0
1030 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1031 distribute_multiple_call.call(:whitebalance, values)
1034 closures[:whitebalance].call
1037 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1038 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1039 _("Enhance constrast"))))
1041 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1043 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1044 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1045 if type == 'image' && possible_actions[:can_panorama]
1046 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1047 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1048 panorama.signal_connect('activate') {
1049 if possible_actions[:can_multiple] && $selected_elements.length > 0
1050 if values = ask_new_pano_amount(nil, '')
1051 distribute_multiple_call.call(:pano, values)
1054 distribute_multiple_call.call(:pano)
1058 menu.append( Gtk::SeparatorMenuItem.new)
1059 if optionals.include?('delete')
1060 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1061 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1062 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1063 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1064 paste_item.signal_connect('activate') { closures[:paste].call }
1065 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1066 clear_item.signal_connect('activate') { $cuts = [] }
1068 paste_item.sensitive = clear_item.sensitive = false
1071 menu.append( Gtk::SeparatorMenuItem.new)
1073 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1074 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1075 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1076 editexternally.signal_connect('activate') {
1077 if check_image_editor
1078 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1084 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1085 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1086 if optionals.include?('delete')
1087 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1088 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1091 menu.popup(nil, nil, event.button, event.time)
1094 def delete_current_subalbum
1096 sel = $albums_tv.selection.selected_rows
1097 $xmldir.elements.each { |e|
1098 if e.name == 'image' || e.name == 'video'
1099 e.add_attribute('deleted', 'true')
1102 #- branch if we have a non deleted subalbum
1103 if $xmldir.child_byname_notattr('dir', 'deleted')
1104 $xmldir.delete_attribute('thumbnails-caption')
1105 $xmldir.delete_attribute('thumbnails-captionfile')
1107 $xmldir.add_attribute('deleted', 'true')
1109 while moveup.parent.name == 'dir'
1110 moveup = moveup.parent
1111 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1112 moveup.add_attribute('deleted', 'true')
1119 save_changes('forced')
1120 populate_subalbums_treeview(false)
1121 $albums_tv.selection.select_path(sel[0])
1127 $current_path = nil #- prevent save_changes from being rerun again
1128 sel = $albums_tv.selection.selected_rows
1129 restore_one = proc { |xmldir|
1130 xmldir.elements.each { |e|
1131 if e.name == 'dir' && e.attributes['deleted']
1134 e.delete_attribute('deleted')
1137 restore_one.call($xmldir)
1138 populate_subalbums_treeview(false)
1139 $albums_tv.selection.select_path(sel[0])
1142 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1145 frame1 = Gtk::Frame.new
1146 fullpath = from_utf8("#{$current_path}/#{filename}")
1148 my_gen_real_thumbnail = proc {
1149 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1153 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1154 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1155 pack_start(img = Gtk::Image.new).
1156 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1157 px, mask = pxb.render_pixmap_and_mask(0)
1158 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1159 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1161 frame1.add(img = Gtk::Image.new)
1164 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1165 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1166 my_gen_real_thumbnail.call
1168 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1171 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1173 tooltips = Gtk::Tooltips.new
1174 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1175 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1177 frame2, textview = create_editzone($autotable_sw, 1, img)
1178 textview.buffer.text = caption
1179 textview.set_justification(Gtk::Justification::CENTER)
1181 vbox = Gtk::VBox.new(false, 5)
1182 vbox.pack_start(evtbox, false, false)
1183 vbox.pack_start(frame2, false, false)
1184 autotable.append(vbox, filename)
1186 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1187 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1189 #- to be able to find widgets by name
1190 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1192 cleanup_all_thumbnails = proc {
1193 #- remove out of sync images
1194 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1195 for sizeobj in $images_size
1196 #- cannot use sizeobj because panoramic images will have a larger width
1197 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1205 cleanup_all_thumbnails.call
1206 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1208 rexml_thread_protect {
1209 $xmldir.delete_attribute('already-generated')
1211 my_gen_real_thumbnail.call
1214 rotate_and_cleanup = proc { |angle|
1215 cleanup_all_thumbnails.call
1216 rexml_thread_protect {
1217 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1221 move = proc { |direction|
1222 do_method = "move_#{direction}"
1223 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1225 done = autotable.method(do_method).call(vbox)
1226 textview.grab_focus #- because if moving, focus is stolen
1230 save_undo(_("move %s") % direction,
1232 autotable.method(undo_method).call(vbox)
1233 textview.grab_focus #- because if moving, focus is stolen
1234 autoscroll_if_needed($autotable_sw, img, textview)
1235 $notebook.set_page(1)
1237 autotable.method(do_method).call(vbox)
1238 textview.grab_focus #- because if moving, focus is stolen
1239 autoscroll_if_needed($autotable_sw, img, textview)
1240 $notebook.set_page(1)
1246 color_swap_and_cleanup = proc {
1247 perform_color_swap_and_cleanup = proc {
1248 cleanup_all_thumbnails.call
1249 rexml_thread_protect {
1250 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1252 my_gen_real_thumbnail.call
1255 perform_color_swap_and_cleanup.call
1257 save_undo(_("color swap"),
1259 perform_color_swap_and_cleanup.call
1261 autoscroll_if_needed($autotable_sw, img, textview)
1262 $notebook.set_page(1)
1264 perform_color_swap_and_cleanup.call
1266 autoscroll_if_needed($autotable_sw, img, textview)
1267 $notebook.set_page(1)
1272 change_seektime_and_cleanup_real = proc { |values|
1273 perform_change_seektime_and_cleanup = proc { |val|
1274 cleanup_all_thumbnails.call
1275 rexml_thread_protect {
1276 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1278 my_gen_real_thumbnail.call
1280 perform_change_seektime_and_cleanup.call(values[:new])
1282 save_undo(_("specify seektime"),
1284 perform_change_seektime_and_cleanup.call(values[:old])
1286 autoscroll_if_needed($autotable_sw, img, textview)
1287 $notebook.set_page(1)
1289 perform_change_seektime_and_cleanup.call(values[:new])
1291 autoscroll_if_needed($autotable_sw, img, textview)
1292 $notebook.set_page(1)
1297 change_seektime_and_cleanup = proc {
1298 rexml_thread_protect {
1299 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1300 change_seektime_and_cleanup_real.call(values)
1305 change_pano_amount_and_cleanup_real = proc { |values|
1306 perform_change_pano_amount_and_cleanup = proc { |val|
1307 cleanup_all_thumbnails.call
1308 rexml_thread_protect {
1309 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1312 perform_change_pano_amount_and_cleanup.call(values[:new])
1314 save_undo(_("change panorama amount"),
1316 perform_change_pano_amount_and_cleanup.call(values[:old])
1318 autoscroll_if_needed($autotable_sw, img, textview)
1319 $notebook.set_page(1)
1321 perform_change_pano_amount_and_cleanup.call(values[:new])
1323 autoscroll_if_needed($autotable_sw, img, textview)
1324 $notebook.set_page(1)
1329 change_pano_amount_and_cleanup = proc {
1330 rexml_thread_protect {
1331 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1332 change_pano_amount_and_cleanup_real.call(values)
1337 whitebalance_and_cleanup_real = proc { |values|
1338 perform_change_whitebalance_and_cleanup = proc { |val|
1339 cleanup_all_thumbnails.call
1340 rexml_thread_protect {
1341 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1342 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1343 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1346 perform_change_whitebalance_and_cleanup.call(values[:new])
1348 save_undo(_("fix white balance"),
1350 perform_change_whitebalance_and_cleanup.call(values[:old])
1352 autoscroll_if_needed($autotable_sw, img, textview)
1353 $notebook.set_page(1)
1355 perform_change_whitebalance_and_cleanup.call(values[:new])
1357 autoscroll_if_needed($autotable_sw, img, textview)
1358 $notebook.set_page(1)
1363 whitebalance_and_cleanup = proc {
1364 rexml_thread_protect {
1365 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1366 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1367 whitebalance_and_cleanup_real.call(values)
1372 gammacorrect_and_cleanup_real = proc { |values|
1373 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1374 cleanup_all_thumbnails.call
1375 rexml_thread_protect {
1376 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1377 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1378 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1381 perform_change_gammacorrect_and_cleanup.call(values[:new])
1383 save_undo(_("gamma correction"),
1385 perform_change_gammacorrect_and_cleanup.call(values[:old])
1387 autoscroll_if_needed($autotable_sw, img, textview)
1388 $notebook.set_page(1)
1390 perform_change_gammacorrect_and_cleanup.call(values[:new])
1392 autoscroll_if_needed($autotable_sw, img, textview)
1393 $notebook.set_page(1)
1398 gammacorrect_and_cleanup = Proc.new {
1399 rexml_thread_protect {
1400 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1401 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1402 gammacorrect_and_cleanup_real.call(values)
1407 enhance_and_cleanup = proc {
1408 perform_enhance_and_cleanup = proc {
1409 cleanup_all_thumbnails.call
1410 rexml_thread_protect {
1411 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1413 my_gen_real_thumbnail.call
1416 cleanup_all_thumbnails.call
1417 perform_enhance_and_cleanup.call
1419 save_undo(_("enhance"),
1421 perform_enhance_and_cleanup.call
1423 autoscroll_if_needed($autotable_sw, img, textview)
1424 $notebook.set_page(1)
1426 perform_enhance_and_cleanup.call
1428 autoscroll_if_needed($autotable_sw, img, textview)
1429 $notebook.set_page(1)
1434 delete = proc { |isacut|
1435 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 })
1438 perform_delete = proc {
1439 after = autotable.get_next_widget(vbox)
1441 after = autotable.get_previous_widget(vbox)
1443 if $config['deleteondisk'] && !isacut
1444 msg 3, "scheduling for delete: #{fullpath}"
1445 $todelete << fullpath
1447 autotable.remove_widget(vbox)
1449 $vbox2widgets[after][:textview].grab_focus
1450 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1454 previous_pos = autotable.get_current_number(vbox)
1458 delete_current_subalbum
1460 save_undo(_("delete"),
1462 autotable.reinsert(pos, vbox, filename)
1463 $notebook.set_page(1)
1464 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1466 msg 3, "removing deletion schedule of: #{fullpath}"
1467 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1470 $notebook.set_page(1)
1479 $cuts << { :vbox => vbox, :filename => filename }
1480 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1485 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1488 autotable.queue_draws << proc {
1489 $vbox2widgets[last[:vbox]][:textview].grab_focus
1490 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1492 save_undo(_("paste"),
1494 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1495 $notebook.set_page(1)
1498 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1500 $notebook.set_page(1)
1503 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1508 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1509 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1510 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1511 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1513 textview.signal_connect('key-press-event') { |w, event|
1516 x, y = autotable.get_current_pos(vbox)
1517 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1518 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1519 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1520 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1522 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1523 $vbox2widgets[widget_up][:textview].grab_focus
1530 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1532 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1533 $vbox2widgets[widget_down][:textview].grab_focus
1540 if event.keyval == Gdk::Keyval::GDK_Left
1543 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1550 rotate_and_cleanup.call(-90)
1553 if event.keyval == Gdk::Keyval::GDK_Right
1554 next_ = autotable.get_next_widget(vbox)
1555 if next_ && autotable.get_current_pos(next_)[0] > x
1557 $vbox2widgets[next_][:textview].grab_focus
1564 rotate_and_cleanup.call(90)
1567 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1570 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1571 view_element(filename, { :delete => delete })
1574 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1577 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1581 !propagate #- propagate if needed
1584 $ignore_next_release = false
1585 evtbox.signal_connect('button-press-event') { |w, event|
1586 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1587 if event.state & Gdk::Window::BUTTON3_MASK != 0
1588 #- gesture redo: hold right mouse button then click left mouse button
1589 $config['nogestures'] or perform_redo
1590 $ignore_next_release = true
1592 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1594 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1596 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1597 elsif $enhance.active?
1598 enhance_and_cleanup.call
1599 elsif $delete.active?
1603 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1606 $button1_pressed_autotable = true
1607 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1608 if event.state & Gdk::Window::BUTTON1_MASK != 0
1609 #- gesture undo: hold left mouse button then click right mouse button
1610 $config['nogestures'] or perform_undo
1611 $ignore_next_release = true
1613 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1614 view_element(filename, { :delete => delete })
1619 evtbox.signal_connect('button-release-event') { |w, event|
1620 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1621 if !$ignore_next_release
1622 x, y = autotable.get_current_pos(vbox)
1623 next_ = autotable.get_next_widget(vbox)
1624 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1625 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1626 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1627 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1628 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1629 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1630 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1632 $ignore_next_release = false
1633 $gesture_press = nil
1638 #- handle reordering with drag and drop
1639 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1640 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1641 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1642 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1645 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1647 #- mouse gesture first (dnd disables button-release-event)
1648 if $gesture_press && $gesture_press[:filename] == filename
1649 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1650 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1651 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1652 rotate_and_cleanup.call(angle)
1653 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1655 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1656 msg 3, "gesture delete: click-drag right button to the bottom"
1658 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1663 ctxt.targets.each { |target|
1664 if target.name == 'reorder-elements'
1665 move_dnd = proc { |from,to|
1668 autotable.move(from, to)
1669 save_undo(_("reorder"),
1672 autotable.move(to - 1, from)
1674 autotable.move(to, from + 1)
1676 $notebook.set_page(1)
1678 autotable.move(from, to)
1679 $notebook.set_page(1)
1684 if $multiple_dnd.size == 0
1685 move_dnd.call(selection_data.data.to_i,
1686 autotable.get_current_number(vbox))
1688 UndoHandler.begin_batch
1689 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1691 #- need to update current position between each call
1692 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1693 autotable.get_current_number(vbox))
1695 UndoHandler.end_batch
1706 def create_auto_table
1708 $autotable = Gtk::AutoTable.new(5)
1710 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1711 thumbnails_vb = Gtk::VBox.new(false, 5)
1713 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1714 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1715 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1716 thumbnails_vb.add($autotable)
1718 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1719 $autotable_sw.add_with_viewport(thumbnails_vb)
1721 #- follows stuff for handling multiple elements selection
1722 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1724 update_selected = proc {
1725 $autotable.current_order.each { |path|
1726 w = $name2widgets[path][:evtbox].window
1727 xm = w.position[0] + w.size[0]/2
1728 ym = w.position[1] + w.size[1]/2
1729 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1730 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1731 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1732 if $name2widgets[path][:img].pixbuf
1733 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1737 if $selected_elements[path] && ! $selected_elements[path][:keep]
1738 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))
1739 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1740 $selected_elements.delete(path)
1745 $autotable.signal_connect('realize') { |w,e|
1746 gc = Gdk::GC.new($autotable.window)
1747 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1748 gc.function = Gdk::GC::INVERT
1749 #- autoscroll handling for DND and multiple selections
1750 Gtk.timeout_add(100) {
1751 if ! $autotable.window.nil?
1752 w, x, y, mask = $autotable.window.pointer
1753 if mask & Gdk::Window::BUTTON1_MASK != 0
1754 if y < $autotable_sw.vadjustment.value
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]])
1758 if $button1_pressed_autotable || press_x
1759 scroll_upper($autotable_sw, y)
1762 w, pos_x, pos_y = $autotable.window.pointer
1763 $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]])
1764 update_selected.call
1767 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1769 $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]])
1771 if $button1_pressed_autotable || press_x
1772 scroll_lower($autotable_sw, y)
1775 w, pos_x, pos_y = $autotable.window.pointer
1776 $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]])
1777 update_selected.call
1782 ! $autotable.window.nil?
1786 $autotable.signal_connect('button-press-event') { |w,e|
1788 if !$button1_pressed_autotable
1791 if e.state & Gdk::Window::SHIFT_MASK == 0
1792 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1793 $selected_elements = {}
1794 $statusbar.push(0, utf8(_("Nothing selected.")))
1796 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1798 set_mousecursor(Gdk::Cursor::TCROSS)
1802 $autotable.signal_connect('button-release-event') { |w,e|
1804 if $button1_pressed_autotable
1805 #- unselect all only now
1806 $multiple_dnd = $selected_elements.keys
1807 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1808 $selected_elements = {}
1809 $button1_pressed_autotable = false
1812 $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]])
1813 if $selected_elements.length > 0
1814 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1817 press_x = press_y = pos_x = pos_y = nil
1818 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1822 $autotable.signal_connect('motion-notify-event') { |w,e|
1825 $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]])
1829 $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]])
1830 update_selected.call
1836 def create_subalbums_page
1838 subalbums_hb = Gtk::HBox.new
1839 $subalbums_vb = Gtk::VBox.new(false, 5)
1840 subalbums_hb.pack_start($subalbums_vb, false, false)
1841 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1842 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1843 $subalbums_sw.add_with_viewport(subalbums_hb)
1846 def save_current_file
1852 ios = File.open($filename, "w")
1853 $xmldoc.write(ios, 0)
1855 rescue Iconv::IllegalSequence
1856 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1857 if ! ios.nil? && ! ios.closed?
1860 $xmldoc.xml_decl.encoding = 'UTF-8'
1861 ios = File.open($filename, "w")
1862 $xmldoc.write(ios, 0)
1873 def save_current_file_user
1874 save_tempfilename = $filename
1875 $filename = $orig_filename
1876 if ! save_current_file
1877 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1878 $filename = save_tempfilename
1882 $generated_outofline = false
1883 $filename = save_tempfilename
1885 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1886 $todelete.each { |f|
1891 def mark_document_as_dirty
1892 $xmldoc.elements.each('//dir') { |elem|
1893 elem.delete_attribute('already-generated')
1897 #- ret: true => ok false => cancel
1898 def ask_save_modifications(msg1, msg2, *options)
1900 options = options.size > 0 ? options[0] : {}
1902 if options[:disallow_cancel]
1903 dialog = Gtk::Dialog.new(msg1,
1905 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1906 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1907 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1909 dialog = Gtk::Dialog.new(msg1,
1911 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1912 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1913 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1914 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1916 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1917 dialog.vbox.add(Gtk::Label.new(msg2))
1918 dialog.window_position = Gtk::Window::POS_CENTER
1921 dialog.run { |response|
1923 if response == Gtk::Dialog::RESPONSE_YES
1924 if ! save_current_file_user
1925 return ask_save_modifications(msg1, msg2, options)
1928 #- if we have generated an album but won't save modifications, we must remove
1929 #- already-generated markers in original file
1930 if $generated_outofline
1932 $xmldoc = REXML::Document.new(File.new($orig_filename))
1933 mark_document_as_dirty
1934 ios = File.open($orig_filename, "w")
1935 $xmldoc.write(ios, 0)
1938 puts "exception: #{$!}"
1942 if response == Gtk::Dialog::RESPONSE_CANCEL
1945 $todelete = [] #- unconditionally clear the list of images/videos to delete
1951 def try_quit(*options)
1952 if ask_save_modifications(utf8(_("Save before quitting?")),
1953 utf8(_("Do you want to save your changes before quitting?")),
1959 def show_popup(parent, msg, *options)
1960 dialog = Gtk::Dialog.new
1961 if options[0] && options[0][:title]
1962 dialog.title = options[0][:title]
1964 dialog.title = utf8(_("Booh message"))
1966 lbl = Gtk::Label.new
1967 if options[0] && options[0][:nomarkup]
1972 if options[0] && options[0][:centered]
1973 lbl.set_justify(Gtk::Justification::CENTER)
1975 if options[0] && options[0][:selectable]
1976 lbl.selectable = true
1978 if options[0] && options[0][:topwidget]
1979 dialog.vbox.add(options[0][:topwidget])
1981 if options[0] && options[0][:scrolled]
1982 sw = Gtk::ScrolledWindow.new(nil, nil)
1983 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1984 sw.add_with_viewport(lbl)
1986 dialog.set_default_size(500, 600)
1988 dialog.vbox.add(lbl)
1989 dialog.set_default_size(200, 120)
1991 if options[0] && options[0][:okcancel]
1992 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1994 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1996 if options[0] && options[0][:pos_centered]
1997 dialog.window_position = Gtk::Window::POS_CENTER
1999 dialog.window_position = Gtk::Window::POS_MOUSE
2002 if options[0] && options[0][:linkurl]
2003 linkbut = Gtk::Button.new('')
2004 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2005 linkbut.signal_connect('clicked') {
2006 open_url(options[0][:linkurl])
2007 dialog.response(Gtk::Dialog::RESPONSE_OK)
2008 set_mousecursor_normal
2010 linkbut.relief = Gtk::RELIEF_NONE
2011 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2012 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2013 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2018 if !options[0] || !options[0][:not_transient]
2019 dialog.transient_for = parent
2020 dialog.run { |response|
2022 if options[0] && options[0][:okcancel]
2023 return response == Gtk::Dialog::RESPONSE_OK
2027 dialog.signal_connect('response') { dialog.destroy }
2031 def set_mainwindow_title(progress)
2032 filename = $orig_filename || $filename
2035 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2037 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2041 $main_window.title = 'booh - ' + File.basename(filename)
2043 $main_window.title = 'booh'
2048 def backend_wait_message(parent, msg, infopipe_path, mode)
2050 w.set_transient_for(parent)
2053 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2054 vb.pack_start(Gtk::Label.new(msg), false, false)
2056 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2057 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2058 if mode != 'one dir scan'
2059 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2061 if mode == 'web-album'
2062 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2063 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2065 vb.pack_start(Gtk::HSeparator.new, false, false)
2067 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2068 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2069 vb.pack_end(bottom, false, false)
2072 update_progression_title_pb1 = proc {
2073 if mode == 'web-album'
2074 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2075 elsif mode != 'one dir scan'
2076 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2078 set_mainwindow_title(pb1_1.fraction)
2082 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2083 refresh_thread = Thread.new {
2084 directories_counter = 0
2085 while line = infopipe.gets
2086 msg 3, "infopipe got data: #{line}"
2087 if line =~ /^directories: (\d+), sizes: (\d+)/
2088 directories = $1.to_f + 1
2090 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2091 elements = $3.to_f + 1
2092 if mode == 'web-album'
2096 gtk_thread_protect { pb1_1.fraction = 0 }
2097 if mode != 'one dir scan'
2098 newtext = utf8(full_src_dir_to_rel($1, $2))
2099 newtext = '/' if newtext == ''
2100 gtk_thread_protect { pb1_2.text = newtext }
2101 directories_counter += 1
2102 gtk_thread_protect {
2103 pb1_2.fraction = directories_counter / directories
2104 update_progression_title_pb1.call
2107 elsif line =~ /^processing element$/
2108 element_counter += 1
2109 gtk_thread_protect {
2110 pb1_1.fraction = element_counter / elements
2111 update_progression_title_pb1.call
2113 elsif line =~ /^processing size$/
2114 element_counter += 1
2115 gtk_thread_protect {
2116 pb1_1.fraction = element_counter / elements
2117 update_progression_title_pb1.call
2119 elsif line =~ /^finished processing sizes$/
2120 gtk_thread_protect { pb1_1.fraction = 1 }
2121 elsif line =~ /^creating index.html$/
2122 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2123 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2124 directories_counter = 0
2125 elsif line =~ /^index.html: (.+)\|(.+)/
2126 newtext = utf8(full_src_dir_to_rel($1, $2))
2127 newtext = '/' if newtext == ''
2128 gtk_thread_protect { pb2.text = newtext }
2129 directories_counter += 1
2130 gtk_thread_protect {
2131 pb2.fraction = directories_counter / directories
2132 set_mainwindow_title(0.9 + pb2.fraction / 10)
2134 elsif line =~ /^die: (.*)$/
2141 w.signal_connect('delete-event') { w.destroy }
2142 w.signal_connect('destroy') {
2143 Thread.kill(refresh_thread)
2144 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2147 File.delete(infopipe_path)
2149 set_mainwindow_title(nil)
2151 w.window_position = Gtk::Window::POS_CENTER
2157 def call_backend(cmd, waitmsg, mode, params)
2158 pipe = Tempfile.new("boohpipe")
2159 Thread.critical = true
2162 system("mkfifo #{path}")
2163 Thread.critical = false
2164 cmd += " --info-pipe #{path}"
2165 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2170 id, exitstatus = Process.waitpid2(pid)
2171 gtk_thread_protect { w8.destroy }
2173 if params[:successmsg]
2174 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2176 if params[:closure_after]
2177 gtk_thread_protect(¶ms[:closure_after])
2179 elsif exitstatus == 15
2180 #- say nothing, user aborted
2182 gtk_thread_protect { show_popup($main_window,
2183 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2189 button.signal_connect('clicked') {
2190 Process.kill('SIGTERM', pid)
2194 def save_changes(*forced)
2195 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2199 $xmldir.delete_attribute('already-generated')
2201 propagate_children = proc { |xmldir|
2202 if xmldir.attributes['subdirs-caption']
2203 xmldir.delete_attribute('already-generated')
2205 xmldir.elements.each('dir') { |element|
2206 propagate_children.call(element)
2210 if $xmldir.child_byname_notattr('dir', 'deleted')
2211 new_title = $subalbums_title.buffer.text
2212 if new_title != $xmldir.attributes['subdirs-caption']
2213 parent = $xmldir.parent
2214 if parent.name == 'dir'
2215 parent.delete_attribute('already-generated')
2217 propagate_children.call($xmldir)
2219 $xmldir.add_attribute('subdirs-caption', new_title)
2220 $xmldir.elements.each('dir') { |element|
2221 if !element.attributes['deleted']
2222 path = element.attributes['path']
2223 newtext = $subalbums_edits[path][:editzone].buffer.text
2224 if element.attributes['subdirs-caption']
2225 if element.attributes['subdirs-caption'] != newtext
2226 propagate_children.call(element)
2228 element.add_attribute('subdirs-caption', newtext)
2229 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2231 if element.attributes['thumbnails-caption'] != newtext
2232 element.delete_attribute('already-generated')
2234 element.add_attribute('thumbnails-caption', newtext)
2235 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2241 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2242 if $xmldir.attributes['thumbnails-caption']
2243 path = $xmldir.attributes['path']
2244 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2246 elsif $xmldir.attributes['thumbnails-caption']
2247 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2250 if $xmldir.attributes['thumbnails-caption']
2251 if edit = $subalbums_edits[$xmldir.attributes['path']]
2252 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2256 #- remove and reinsert elements to reflect new ordering
2259 $xmldir.elements.each { |element|
2260 if element.name == 'image' || element.name == 'video'
2261 saves[element.attributes['filename']] = element.remove
2265 $autotable.current_order.each { |path|
2266 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2267 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2270 saves.each_key { |path|
2271 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2272 chld.add_attribute('deleted', 'true')
2276 def sort_by_exif_date
2280 rexml_thread_protect {
2281 $xmldir.elements.each { |element|
2282 if element.name == 'image' || element.name == 'video'
2283 current_order << element.attributes['filename']
2288 #- look for EXIF dates
2291 if current_order.size > 20
2293 w.set_transient_for($main_window)
2295 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2296 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2297 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2298 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2299 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2300 vb.pack_end(bottom, false, false)
2302 w.signal_connect('delete-event') { w.destroy }
2303 w.window_position = Gtk::Window::POS_CENTER
2307 b.signal_connect('clicked') { aborted = true }
2309 current_order.each { |f|
2311 if entry2type(f) == 'image'
2313 pb.fraction = i.to_f / current_order.size
2314 Gtk.main_iteration while Gtk.events_pending?
2315 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2317 dates[f] = date_time
2330 current_order.each { |f|
2331 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2333 dates[f] = date_time
2339 rexml_thread_protect {
2340 $xmldir.elements.each { |element|
2341 if element.name == 'image' || element.name == 'video'
2342 saves[element.attributes['filename']] = element.remove
2347 neworder = smartsort(current_order, dates)
2349 rexml_thread_protect {
2351 $xmldir.add_element(saves[f].name, saves[f].attributes)
2355 #- let the auto-table reflect new ordering
2359 def remove_all_captions
2362 $autotable.current_order.each { |path|
2363 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2364 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2366 save_undo(_("remove all captions"),
2368 texts.each_key { |key|
2369 $name2widgets[key][:textview].buffer.text = texts[key]
2371 $notebook.set_page(1)
2373 texts.each_key { |key|
2374 $name2widgets[key][:textview].buffer.text = ''
2376 $notebook.set_page(1)
2382 $selected_elements.each_key { |path|
2383 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2389 $selected_elements = {}
2393 $undo_tb.sensitive = $undo_mb.sensitive = false
2394 $redo_tb.sensitive = $redo_mb.sensitive = false
2400 $subalbums_vb.children.each { |chld|
2401 $subalbums_vb.remove(chld)
2403 $subalbums = Gtk::Table.new(0, 0, true)
2404 current_y_sub_albums = 0
2406 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2407 $subalbums_edits = {}
2408 subalbums_counter = 0
2409 subalbums_edits_bypos = {}
2411 add_subalbum = proc { |xmldir, counter|
2412 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2413 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2414 if xmldir == $xmldir
2415 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2416 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2417 caption = xmldir.attributes['thumbnails-caption']
2418 infotype = 'thumbnails'
2420 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2421 captionfile, caption = find_subalbum_caption_info(xmldir)
2422 infotype = find_subalbum_info_type(xmldir)
2424 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2425 hbox = Gtk::HBox.new
2426 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2428 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2431 my_gen_real_thumbnail = proc {
2432 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2435 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2436 f.add(img = Gtk::Image.new)
2437 my_gen_real_thumbnail.call
2439 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2441 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2442 $subalbums.attach(hbox,
2443 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2445 frame, textview = create_editzone($subalbums_sw, 0, img)
2446 textview.buffer.text = caption
2447 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2448 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2450 change_image = proc {
2451 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2453 Gtk::FileChooser::ACTION_OPEN,
2455 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2456 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2457 fc.transient_for = $main_window
2458 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))
2459 f.add(preview_img = Gtk::Image.new)
2461 fc.signal_connect('update-preview') { |w|
2462 if fc.preview_filename
2463 if entry2type(fc.preview_filename) == 'video'
2467 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2469 fc.preview_widget_active = false
2471 tmpimage = "#{tmpdir}/00000001.jpg"
2473 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2474 fc.preview_widget_active = true
2475 rescue Gdk::PixbufError
2476 fc.preview_widget_active = false
2478 File.delete(tmpimage)
2485 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2486 fc.preview_widget_active = true
2487 rescue Gdk::PixbufError
2488 fc.preview_widget_active = false
2493 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2495 old_file = captionfile
2496 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2497 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2498 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2499 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2501 new_file = fc.filename
2502 msg 3, "new captionfile is: #{fc.filename}"
2503 perform_changefile = proc {
2504 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2505 $modified_pixbufs.delete(thumbnail_file)
2506 xmldir.delete_attribute("#{infotype}-rotate")
2507 xmldir.delete_attribute("#{infotype}-color-swap")
2508 xmldir.delete_attribute("#{infotype}-enhance")
2509 xmldir.delete_attribute("#{infotype}-seektime")
2510 my_gen_real_thumbnail.call
2512 perform_changefile.call
2514 save_undo(_("change caption file for sub-album"),
2516 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2517 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2518 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2519 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2520 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2521 my_gen_real_thumbnail.call
2522 $notebook.set_page(0)
2524 perform_changefile.call
2525 $notebook.set_page(0)
2533 if File.exists?(thumbnail_file)
2534 File.delete(thumbnail_file)
2536 my_gen_real_thumbnail.call
2539 rotate_and_cleanup = proc { |angle|
2540 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2541 if File.exists?(thumbnail_file)
2542 File.delete(thumbnail_file)
2546 move = proc { |direction|
2549 save_changes('forced')
2550 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2551 if direction == 'up'
2552 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2553 subalbums_edits_bypos[oldpos - 1][:position] += 1
2555 if direction == 'down'
2556 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2557 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2559 if direction == 'top'
2560 for i in 1 .. oldpos - 1
2561 subalbums_edits_bypos[i][:position] += 1
2563 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2565 if direction == 'bottom'
2566 for i in oldpos + 1 .. subalbums_counter
2567 subalbums_edits_bypos[i][:position] -= 1
2569 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2573 $xmldir.elements.each('dir') { |element|
2574 if (!element.attributes['deleted'])
2575 elems << [ element.attributes['path'], element.remove ]
2578 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2579 each { |e| $xmldir.add_element(e[1]) }
2580 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2581 $xmldir.elements.each('descendant::dir') { |elem|
2582 elem.delete_attribute('already-generated')
2585 sel = $albums_tv.selection.selected_rows
2587 populate_subalbums_treeview(false)
2588 $albums_tv.selection.select_path(sel[0])
2591 color_swap_and_cleanup = proc {
2592 perform_color_swap_and_cleanup = proc {
2593 color_swap(xmldir, "#{infotype}-")
2594 my_gen_real_thumbnail.call
2596 perform_color_swap_and_cleanup.call
2598 save_undo(_("color swap"),
2600 perform_color_swap_and_cleanup.call
2601 $notebook.set_page(0)
2603 perform_color_swap_and_cleanup.call
2604 $notebook.set_page(0)
2609 change_seektime_and_cleanup = proc {
2610 if values = ask_new_seektime(xmldir, "#{infotype}-")
2611 perform_change_seektime_and_cleanup = proc { |val|
2612 change_seektime(xmldir, "#{infotype}-", val)
2613 my_gen_real_thumbnail.call
2615 perform_change_seektime_and_cleanup.call(values[:new])
2617 save_undo(_("specify seektime"),
2619 perform_change_seektime_and_cleanup.call(values[:old])
2620 $notebook.set_page(0)
2622 perform_change_seektime_and_cleanup.call(values[:new])
2623 $notebook.set_page(0)
2629 whitebalance_and_cleanup = proc {
2630 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2631 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2632 perform_change_whitebalance_and_cleanup = proc { |val|
2633 change_whitebalance(xmldir, "#{infotype}-", val)
2634 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2635 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2636 if File.exists?(thumbnail_file)
2637 File.delete(thumbnail_file)
2640 perform_change_whitebalance_and_cleanup.call(values[:new])
2642 save_undo(_("fix white balance"),
2644 perform_change_whitebalance_and_cleanup.call(values[:old])
2645 $notebook.set_page(0)
2647 perform_change_whitebalance_and_cleanup.call(values[:new])
2648 $notebook.set_page(0)
2654 gammacorrect_and_cleanup = proc {
2655 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2656 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2657 perform_change_gammacorrect_and_cleanup = proc { |val|
2658 change_gammacorrect(xmldir, "#{infotype}-", val)
2659 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2660 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2661 if File.exists?(thumbnail_file)
2662 File.delete(thumbnail_file)
2665 perform_change_gammacorrect_and_cleanup.call(values[:new])
2667 save_undo(_("gamma correction"),
2669 perform_change_gammacorrect_and_cleanup.call(values[:old])
2670 $notebook.set_page(0)
2672 perform_change_gammacorrect_and_cleanup.call(values[:new])
2673 $notebook.set_page(0)
2679 enhance_and_cleanup = proc {
2680 perform_enhance_and_cleanup = proc {
2681 enhance(xmldir, "#{infotype}-")
2682 my_gen_real_thumbnail.call
2685 perform_enhance_and_cleanup.call
2687 save_undo(_("enhance"),
2689 perform_enhance_and_cleanup.call
2690 $notebook.set_page(0)
2692 perform_enhance_and_cleanup.call
2693 $notebook.set_page(0)
2698 evtbox.signal_connect('button-press-event') { |w, event|
2699 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2701 rotate_and_cleanup.call(90)
2703 rotate_and_cleanup.call(-90)
2704 elsif $enhance.active?
2705 enhance_and_cleanup.call
2708 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2709 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2710 { :forbid_left => true, :forbid_right => true,
2711 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2712 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2713 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2714 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2715 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2717 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2722 evtbox.signal_connect('button-press-event') { |w, event|
2723 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2727 evtbox.signal_connect('button-release-event') { |w, event|
2728 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2729 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2730 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2731 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2732 msg 3, "gesture rotate: #{angle}"
2733 rotate_and_cleanup.call(angle)
2736 $gesture_press = nil
2739 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2740 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2741 current_y_sub_albums += 1
2744 if $xmldir.child_byname_notattr('dir', 'deleted')
2746 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2747 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2748 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2749 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2750 #- this album image/caption
2751 if $xmldir.attributes['thumbnails-caption']
2752 add_subalbum.call($xmldir, 0)
2755 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2756 $xmldir.elements.each { |element|
2757 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2758 #- element (image or video) of this album
2759 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2760 msg 3, "dest_img: #{dest_img}"
2761 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2762 total[element.name] += 1
2764 if element.name == 'dir' && !element.attributes['deleted']
2765 #- sub-album image/caption
2766 add_subalbum.call(element, subalbums_counter += 1)
2767 total[element.name] += 1
2770 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2771 total['image'], total['video'], total['dir'] ]))
2772 $subalbums_vb.add($subalbums)
2773 $subalbums_vb.show_all
2775 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2776 $notebook.get_tab_label($autotable_sw).sensitive = false
2777 $notebook.set_page(0)
2778 $thumbnails_title.buffer.text = ''
2780 $notebook.get_tab_label($autotable_sw).sensitive = true
2781 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2784 if !$xmldir.child_byname_notattr('dir', 'deleted')
2785 $notebook.get_tab_label($subalbums_sw).sensitive = false
2786 $notebook.set_page(1)
2788 $notebook.get_tab_label($subalbums_sw).sensitive = true
2792 def pixbuf_or_nil(filename)
2794 return Gdk::Pixbuf.new(filename)
2800 def theme_choose(current)
2801 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2803 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2804 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2805 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2807 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2808 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2809 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2810 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2811 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2812 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2813 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2814 treeview.signal_connect('button-press-event') { |w, event|
2815 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2816 dialog.response(Gtk::Dialog::RESPONSE_OK)
2820 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2822 ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
2825 iter[0] = File.basename(dir)
2826 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2827 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2828 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2829 if File.basename(dir) == current
2830 treeview.selection.select_iter(iter)
2833 dialog.set_default_size(-1, 500)
2834 dialog.vbox.show_all
2836 dialog.run { |response|
2837 iter = treeview.selection.selected
2839 if response == Gtk::Dialog::RESPONSE_OK && iter
2840 return model.get_value(iter, 0)
2846 def show_password_protections
2847 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2848 child_iter = $albums_iters[xmldir.attributes['path']]
2849 if xmldir.attributes['password-protect']
2850 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2851 already_protected = true
2852 elsif already_protected
2853 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2855 pix = pix.saturate_and_pixelate(1, true)
2861 xmldir.elements.each('dir') { |elem|
2862 if !elem.attributes['deleted']
2863 examine_dir_elem.call(child_iter, elem, already_protected)
2867 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2870 def populate_subalbums_treeview(select_first)
2874 $subalbums_vb.children.each { |chld|
2875 $subalbums_vb.remove(chld)
2878 source = $xmldoc.root.attributes['source']
2879 msg 3, "source: #{source}"
2881 xmldir = $xmldoc.elements['//dir']
2882 if !xmldir || xmldir.attributes['path'] != source
2883 msg 1, _("Corrupted booh file...")
2887 append_dir_elem = proc { |parent_iter, xmldir|
2888 child_iter = $albums_ts.append(parent_iter)
2889 child_iter[0] = File.basename(xmldir.attributes['path'])
2890 child_iter[1] = xmldir.attributes['path']
2891 $albums_iters[xmldir.attributes['path']] = child_iter
2892 msg 3, "puttin location: #{xmldir.attributes['path']}"
2893 xmldir.elements.each('dir') { |elem|
2894 if !elem.attributes['deleted']
2895 append_dir_elem.call(child_iter, elem)
2899 append_dir_elem.call(nil, xmldir)
2900 show_password_protections
2902 $albums_tv.expand_all
2904 $albums_tv.selection.select_iter($albums_ts.iter_first)
2908 def select_current_theme
2909 select_theme($xmldoc.root.attributes['theme'],
2910 $xmldoc.root.attributes['limit-sizes'],
2911 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2912 $xmldoc.root.attributes['thumbnails-per-row'])
2915 def open_file(filename)
2919 $current_path = nil #- invalidate
2920 $modified_pixbufs = {}
2923 $subalbums_vb.children.each { |chld|
2924 $subalbums_vb.remove(chld)
2927 if !File.exists?(filename)
2928 return utf8(_("File not found."))
2932 $xmldoc = REXML::Document.new(File.new(filename))
2937 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2938 if entry2type(filename).nil?
2939 return utf8(_("Not a booh file!"))
2941 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."))
2945 if !source = $xmldoc.root.attributes['source']
2946 return utf8(_("Corrupted booh file..."))
2949 if !dest = $xmldoc.root.attributes['destination']
2950 return utf8(_("Corrupted booh file..."))
2953 if !theme = $xmldoc.root.attributes['theme']
2954 return utf8(_("Corrupted booh file..."))
2957 if $xmldoc.root.attributes['version'] < '0.9.0'
2958 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2959 mark_document_as_dirty
2960 if $xmldoc.root.attributes['version'] < '0.8.4'
2961 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2962 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2963 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2964 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2965 if old_dest_dir != new_dest_dir
2966 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2968 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2969 xmldir.elements.each { |element|
2970 if %w(image video).include?(element.name) && !element.attributes['deleted']
2971 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2972 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2973 Dir[old_name + '*'].each { |file|
2974 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2975 file != new_file and sys("mv '#{file}' '#{new_file}'")
2978 if element.name == 'dir' && !element.attributes['deleted']
2979 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2980 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2981 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2985 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2989 $xmldoc.root.add_attribute('version', $VERSION)
2992 select_current_theme
2994 $filename = filename
2995 set_mainwindow_title(nil)
2996 $default_size['thumbnails'] =~ /(.*)x(.*)/
2997 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2998 $albums_thumbnail_size =~ /(.*)x(.*)/
2999 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3001 populate_subalbums_treeview(true)
3003 $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
3007 def open_file_user(filename)
3008 result = open_file(filename)
3010 $config['last-opens'] ||= []
3011 if $config['last-opens'][-1] != utf8(filename)
3012 $config['last-opens'] << utf8(filename)
3014 $orig_filename = $filename
3015 $main_window.title = 'booh - ' + File.basename($orig_filename)
3016 tmp = Tempfile.new("boohtemp")
3017 Thread.critical = true
3018 $filename = tmp.path
3021 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3022 Thread.critical = false
3024 $tempfiles << $filename << "#{$filename}.backup"
3026 $orig_filename = nil
3032 if !ask_save_modifications(utf8(_("Save this album?")),
3033 utf8(_("Do you want to save the changes to this album?")),
3034 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3037 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3039 Gtk::FileChooser::ACTION_OPEN,
3041 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3042 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3043 fc.set_current_folder(File.expand_path("~/.booh"))
3044 fc.transient_for = $main_window
3045 fc.preview_widget = previewlabel = Gtk::Label.new.show
3046 fc.signal_connect('update-preview') { |w|
3047 if fc.preview_filename
3049 push_mousecursor_wait(fc)
3050 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3054 xmldoc.elements.each('//*') { |elem|
3055 if elem.name == 'dir'
3057 elsif elem.name == 'image'
3059 elsif elem.name == 'video'
3067 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3068 fc.preview_widget_active = false
3070 previewlabel.markup = utf8(_("<i>Source:</i> %s\n<i>Destination:</i> %s\n<i>Subalbums:</i> %s\n<i>Images:</i> %s\n<i>Videos:</i> %s") %
3071 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3072 fc.preview_widget_active = true
3078 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3079 push_mousecursor_wait(fc)
3080 msg = open_file_user(fc.filename)
3095 def additional_booh_options
3098 options += "--mproc #{$config['mproc'].to_i} "
3100 options += "--comments-format '#{$config['comments-format']}' "
3101 if $config['transcode-videos']
3102 options += "--transcode-videos '#{$config['transcode-videos']}' "
3107 def ask_multi_languages(value)
3109 spl = value.split(',')
3110 value = [ spl[0..-2], spl[-1] ]
3113 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3116 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3117 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3119 lbl = Gtk::Label.new
3121 _("You can choose to activate <b>multi-languages</b> support for this web-album
3122 (it will work only if you publish your web-album on an Apache web-server). This will
3123 use the MultiViews feature of Apache; the pages will be served according to the
3124 value of the Accept-Language HTTP header sent by the web browsers, so that people
3125 with different languages preferences will be able to browse your web-album with
3126 navigation in their language (if language is available).
3129 dialog.vbox.add(lbl)
3130 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3131 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3132 add(languages = Gtk::Button.new))))
3134 pick_languages = proc {
3135 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3138 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3139 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3141 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3142 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3144 SUPPORTED_LANGUAGES.each { |lang|
3145 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3146 if ! value.nil? && value[0].include?(lang)
3152 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3153 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3154 fallback_language = nil
3155 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3156 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3157 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3158 fbl_rb.active = true
3159 fallback_language = SUPPORTED_LANGUAGES[0]
3161 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3162 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3163 rb.signal_connect('clicked') { fallback_language = lang }
3164 if ! value.nil? && value[1] == lang
3169 dialog2.window_position = Gtk::Window::POS_MOUSE
3173 dialog2.run { |response|
3175 if resp == Gtk::Dialog::RESPONSE_OK
3177 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3178 value[1] = fallback_language
3179 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3186 languages.signal_connect('clicked') {
3189 dialog.window_position = Gtk::Window::POS_MOUSE
3193 rb_yes.active = true
3194 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3196 rb_no.signal_connect('clicked') {
3200 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3213 dialog.run { |response|
3218 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3220 return [ true, nil ]
3222 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3231 if !ask_save_modifications(utf8(_("Save this album?")),
3232 utf8(_("Do you want to save the changes to this album?")),
3233 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3236 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3238 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3239 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3240 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3242 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3243 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3244 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3245 tbl.attach(src = Gtk::Entry.new,
3246 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3247 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3248 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3249 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3250 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3251 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3252 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3253 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3254 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3255 tbl.attach(dest = Gtk::Entry.new,
3256 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3257 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3258 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3259 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3260 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3261 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3262 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3263 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3264 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3266 tooltips = Gtk::Tooltips.new
3267 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3268 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3269 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3270 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3271 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3272 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3273 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)
3274 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3275 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3276 nperpage_model = Gtk::ListStore.new(String, String)
3277 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3278 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3279 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3280 nperpagecombo.set_attributes(crt, { :markup => 0 })
3281 iter = nperpage_model.append
3282 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3284 [ 12, 20, 30, 40, 50 ].each { |v|
3285 iter = nperpage_model.append
3286 iter[0] = iter[1] = v.to_s
3288 nperpagecombo.active = 0
3290 multilanguages_value = nil
3291 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3292 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3293 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)
3294 multilanguages.signal_connect('clicked') {
3295 retval = ask_multi_languages(multilanguages_value)
3297 multilanguages_value = retval[1]
3299 if multilanguages_value
3300 ml_label.text = utf8(_("Multi-languages: enabled."))
3302 ml_label.text = utf8(_("Multi-languages: disabled."))
3305 if $config['default-multi-languages']
3306 multilanguages_value = $config['default-multi-languages']
3307 ml_label.text = utf8(_("Multi-languages: enabled."))
3309 ml_label.text = utf8(_("Multi-languages: disabled."))
3312 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3313 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3314 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)
3315 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3316 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3317 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)
3319 src_nb_calculated_for = ''
3321 process_src_nb = proc {
3322 if src.text != src_nb_calculated_for
3323 src_nb_calculated_for = src.text
3325 Thread.kill(src_nb_thread)
3328 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3329 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3331 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3332 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3333 src_nb_thread = Thread.new {
3334 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3335 total = { 'image' => 0, 'video' => 0, nil => 0 }
3336 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3337 if File.basename(dir) =~ /^\./
3341 Dir.entries(dir.chomp).each { |file|
3342 total[entry2type(file)] += 1
3344 rescue Errno::EACCES, Errno::ENOENT
3348 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3352 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3355 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3361 timeout_src_nb = Gtk.timeout_add(100) {
3365 src_browse.signal_connect('clicked') {
3366 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3368 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3370 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3371 fc.transient_for = $main_window
3372 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3373 src.text = utf8(fc.filename)
3375 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3380 dest_browse.signal_connect('clicked') {
3381 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3383 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3385 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3386 fc.transient_for = $main_window
3387 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3388 dest.text = utf8(fc.filename)
3393 conf_browse.signal_connect('clicked') {
3394 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3396 Gtk::FileChooser::ACTION_SAVE,
3398 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3399 fc.transient_for = $main_window
3400 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3401 fc.set_current_folder(File.expand_path("~/.booh"))
3402 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3403 conf.text = utf8(fc.filename)
3410 recreate_theme_config = proc {
3411 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3413 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3414 $images_size.each { |s|
3415 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3419 tooltips.set_tip(cb, utf8(s['description']), nil)
3420 theme_sizes << { :widget => cb, :value => s['name'] }
3422 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3423 tooltips = Gtk::Tooltips.new
3424 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3425 theme_sizes << { :widget => cb, :value => 'original' }
3428 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3431 $allowed_N_values.each { |n|
3433 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3435 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3437 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3441 nperrows << { :widget => rb, :value => n }
3443 nperrowradios.show_all
3445 recreate_theme_config.call
3447 theme_button.signal_connect('clicked') {
3448 if newtheme = theme_choose(theme_button.label)
3449 theme_button.label = newtheme
3450 recreate_theme_config.call
3454 dialog.vbox.add(frame1)
3455 dialog.vbox.add(frame2)
3461 dialog.run { |response|
3462 if response == Gtk::Dialog::RESPONSE_OK
3463 srcdir = from_utf8_safe(src.text)
3464 destdir = from_utf8_safe(dest.text)
3465 confpath = from_utf8_safe(conf.text)
3466 if src.text != '' && srcdir == ''
3467 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3469 elsif !File.directory?(srcdir)
3470 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3472 elsif dest.text != '' && destdir == ''
3473 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3475 elsif destdir != make_dest_filename(destdir)
3476 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3478 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3479 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3480 inside it will be permanently removed before creating the web-album!
3481 Are you sure you want to continue?")), { :okcancel => true })
3483 elsif File.exists?(destdir) && !File.directory?(destdir)
3484 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3486 elsif conf.text == ''
3487 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3489 elsif conf.text != '' && confpath == ''
3490 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3492 elsif File.directory?(confpath)
3493 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3495 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3496 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3498 system("mkdir '#{destdir}'")
3499 if !File.directory?(destdir)
3500 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3512 srcdir = from_utf8(src.text)
3513 destdir = from_utf8(dest.text)