5 # A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
13 # Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
32 bindtextdomain("booh")
34 require 'booh/rexml/document'
37 require 'booh/booh-lib'
39 require 'booh/UndoHandler'
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-2008 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'
121 $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-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 image_editor_binary = $config['image-editor'].split.first
171 if image_editor_binary && !File.executable?(image_editor_binary)
172 show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
173 You should fix this in Edit/Preferences so that you can edit photos externally.
175 Problem was: '%s' is not an executable file.
176 Hint: don't forget to specify the full path to the executable,
177 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
185 if $config['last-opens'] && $config['last-opens'].size > 10
186 $config['last-opens'] = $config['last-opens'][-10, 10]
189 xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
190 xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
191 $config.each_pair { |key, value|
192 elem = xmldoc.root.add_element key
194 $config[key].each_pair { |subkey, subvalue|
195 subelem = elem.add_element subkey
196 subelem.add_text subvalue.to_s
198 elsif value.is_a? Array
199 elem.add_text value.join('~~~')
204 elem.add_text value.to_s
208 ios = File.open($config_file, "w")
212 $tempfiles.each { |f|
219 def set_mousecursor(what, *widget)
220 cursor = what.nil? ? nil : Gdk::Cursor.new(what)
221 if widget[0] && widget[0].window
222 widget[0].window.cursor = cursor
224 if $main_window && $main_window.window
225 $main_window.window.cursor = cursor
227 $current_cursor = what
229 def set_mousecursor_wait(*widget)
230 gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
231 if Thread.current == Thread.main
232 Gtk.main_iteration while Gtk.events_pending?
235 def set_mousecursor_normal(*widget)
236 gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
238 def push_mousecursor_wait(*widget)
239 if $current_cursor != Gdk::Cursor::WATCH
240 $save_cursor = $current_cursor
241 gtk_thread_protect { set_mousecursor_wait(*widget) }
244 def pop_mousecursor(*widget)
245 gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
249 source = $xmldoc.root.attributes['source']
250 dest = $xmldoc.root.attributes['destination']
251 return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
254 def full_src_dir_to_rel(path, source)
255 return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
258 def build_full_dest_filename(filename)
259 return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
262 def save_undo(name, closure, *params)
263 UndoHandler.save_undo(name, closure, [ *params ])
264 $undo_tb.sensitive = $undo_mb.sensitive = true
265 $redo_tb.sensitive = $redo_mb.sensitive = false
268 def view_element(filename, closures)
269 if entry2type(filename) == 'video'
270 cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
276 w = create_window.set_title(filename)
278 msg 3, "filename: #{filename}"
279 dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
280 #- typically this file won't exist in case of videos; try with the largest thumbnail around
281 if !File.exists?(dest_img)
282 if entry2type(filename) == 'video'
283 alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
284 if not alternatives.empty?
285 dest_img = alternatives[-1]
288 push_mousecursor_wait
289 gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
291 if !File.exists?(dest_img)
292 msg 2, _("Could not generate fullscreen thumbnail!")
297 aspect = utf8(_("Aspect: unknown"))
298 size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
300 aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
302 vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
303 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)))
304 evt.signal_connect('button-press-event') { |this, event|
305 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
306 $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
308 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
310 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
311 delete_item.signal_connect('activate') {
313 closures[:delete].call(false)
316 menu.popup(nil, nil, event.button, event.time)
319 evt.signal_connect('button-release-event') { |this, event|
321 if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
322 msg 3, "gesture delete: click-drag right button to the bottom"
324 closures[:delete].call(false)
325 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
329 tooltips = Gtk::Tooltips.new
330 tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
332 w.signal_connect('key-press-event') { |w,event|
333 if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
335 closures[:delete].call(false)
339 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
340 b.signal_connect('clicked') { w.destroy }
343 vb.pack_start(evt, false, false)
344 vb.pack_end(bottom, false, false)
347 w.signal_connect('delete-event') { w.destroy }
348 w.window_position = Gtk::Window::POS_CENTER
352 def scroll_upper(scrolledwindow, ypos_top)
353 newval = scrolledwindow.vadjustment.value -
354 ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
355 if newval < scrolledwindow.vadjustment.lower
356 newval = scrolledwindow.vadjustment.lower
358 scrolledwindow.vadjustment.value = newval
361 def scroll_lower(scrolledwindow, ypos_bottom)
362 newval = scrolledwindow.vadjustment.value +
363 ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
364 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
365 newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
367 scrolledwindow.vadjustment.value = newval
370 def autoscroll_if_needed(scrolledwindow, image, textview)
371 #- autoscroll if cursor or image is not visible, if possible
372 if image && image.window || textview.window
373 ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
374 ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
375 current_miny_visible = scrolledwindow.vadjustment.value
376 current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
377 if ypos_top < current_miny_visible
378 scroll_upper(scrolledwindow, ypos_top)
379 elsif ypos_bottom > current_maxy_visible
380 scroll_lower(scrolledwindow, ypos_bottom)
385 def create_editzone(scrolledwindow, pagenum, image)
386 frame = Gtk::Frame.new
387 frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
388 frame.set_shadow_type(Gtk::SHADOW_IN)
389 textview.signal_connect('key-press-event') { |w, event|
390 textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
391 if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
392 scrolledwindow.signal_emit('key-press-event', event)
394 if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
395 event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
396 if event.keyval == Gdk::Keyval::GDK_Up
397 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
398 scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
400 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
403 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
404 scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
406 scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
413 candidate_undo_text = nil
414 textview.signal_connect('focus-in-event') { |w, event|
415 textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
416 candidate_undo_text = textview.buffer.text
420 textview.signal_connect('key-release-event') { |w, event|
421 if candidate_undo_text && candidate_undo_text != textview.buffer.text
423 save_undo(_("text edit"),
425 save_text = textview.buffer.text
426 textview.buffer.text = text
428 $notebook.set_page(pagenum)
430 textview.buffer.text = save_text
432 $notebook.set_page(pagenum)
434 }, candidate_undo_text)
435 candidate_undo_text = nil
438 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)
439 autoscroll_if_needed(scrolledwindow, image, textview)
444 return [ frame, textview ]
447 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
449 if !$modified_pixbufs[thumbnail_img]
450 $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
451 elsif !$modified_pixbufs[thumbnail_img][:orig]
452 $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
455 pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
458 if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
459 pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
460 msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
461 if pixbuf.height > desired_y
462 pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
463 elsif pixbuf.width < desired_x && pixbuf.height < desired_y
464 pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
469 if $modified_pixbufs[thumbnail_img][:whitebalance]
470 pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
473 #- fix gamma correction
474 if $modified_pixbufs[thumbnail_img][:gammacorrect]
475 pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
478 img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
481 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
484 #- update rotate attribute
485 new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
486 xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
488 #- change exif orientation if configured so (but forget in case of thumbnails caption)
489 if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
490 Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
493 $modified_pixbufs[thumbnail_img] ||= {}
494 $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
495 msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
497 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
500 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
503 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
505 save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
507 rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
508 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
510 rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
511 $notebook.set_page(0)
512 $notebook.set_page(attributes_prefix != '' ? 0 : 1)
517 def color_swap(xmldir, attributes_prefix)
519 rexml_thread_protect {
520 if xmldir.attributes["#{attributes_prefix}color-swap"]
521 xmldir.delete_attribute("#{attributes_prefix}color-swap")
523 xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
528 def enhance(xmldir, attributes_prefix)
530 rexml_thread_protect {
531 if xmldir.attributes["#{attributes_prefix}enhance"]
532 xmldir.delete_attribute("#{attributes_prefix}enhance")
534 xmldir.add_attribute("#{attributes_prefix}enhance", '1')
539 def change_seektime(xmldir, attributes_prefix, value)
541 rexml_thread_protect {
542 xmldir.add_attribute("#{attributes_prefix}seektime", value)
546 def ask_new_seektime(xmldir, attributes_prefix)
547 value = rexml_thread_protect {
549 xmldir.attributes["#{attributes_prefix}seektime"]
555 dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
557 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
558 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
559 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
563 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
567 dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
568 entry.signal_connect('key-press-event') { |w, event|
569 if event.keyval == Gdk::Keyval::GDK_Return
570 dialog.response(Gtk::Dialog::RESPONSE_OK)
572 elsif event.keyval == Gdk::Keyval::GDK_Escape
573 dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
576 false #- propagate if needed
580 dialog.window_position = Gtk::Window::POS_MOUSE
583 dialog.run { |response|
586 if response == Gtk::Dialog::RESPONSE_OK
588 msg 3, "changing seektime to #{newval}"
589 return { :old => value, :new => newval }
596 def change_pano_amount(xmldir, attributes_prefix, value)
598 rexml_thread_protect {
600 xmldir.delete_attribute("#{attributes_prefix}pano-amount")
602 xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
607 def ask_new_pano_amount(xmldir, attributes_prefix)
608 value = rexml_thread_protect {
610 xmldir.attributes["#{attributes_prefix}pano-amount"]
616 dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
618 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
619 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
620 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
624 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
625 of this panorama image compared to other regular images. For example, if the panorama
626 was taken out of four photos on one row, counting the necessary overlap, the width of
627 this panorama image should probably be roughly three times the width of regular images.
629 With this information, booh will be able to generate panorama thumbnails looking
630 the right 'size', since the height of the thumbnail for this image will be similar
631 to the height of other thumbnails.
634 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)")))).
635 add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
636 add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
637 add(Gtk::Label.new(utf8(_("times the width of other images"))))))
638 spin.signal_connect('value-changed') {
641 dialog.window_position = Gtk::Window::POS_MOUSE
644 spin.value = value.to_f
651 dialog.run { |response|
655 newval = spin.value.to_f
658 if response == Gtk::Dialog::RESPONSE_OK
660 msg 3, "changing panorama amount to #{newval}"
661 return { :old => value, :new => newval }
668 def change_whitebalance(xmlelem, attributes_prefix, value)
670 xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
673 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
675 #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
676 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
677 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
678 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
679 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
680 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
681 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
682 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
683 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
684 $modified_pixbufs[thumbnail_img] ||= {}
685 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
686 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
688 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
689 $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
691 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
694 $modified_pixbufs[thumbnail_img] ||= {}
695 $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
697 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
700 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
701 #- init $modified_pixbufs correctly
702 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
704 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
706 dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
708 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
709 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
710 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
714 _("You can fix the <b>white balance</b> of the image, if your image is too blue
715 or too yellow because the recorder didn't detect the light correctly. Drag the
716 slider below the image to the left for more blue, to the right for more yellow.
720 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
722 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
724 dialog.window_position = Gtk::Window::POS_MOUSE
728 timeout = Gtk.timeout_add(100) {
729 if hs.value != lastval
732 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
738 dialog.run { |response|
739 Gtk.timeout_remove(timeout)
740 if response == Gtk::Dialog::RESPONSE_OK
742 newval = hs.value.to_s
743 msg 3, "changing white balance to #{newval}"
745 return { :old => value, :new => newval }
748 $modified_pixbufs[thumbnail_img] ||= {}
749 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
750 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
758 def change_gammacorrect(xmlelem, attributes_prefix, value)
760 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
763 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
765 #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
766 if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
767 save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
768 xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
769 save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
770 xmlelem.delete_attribute("#{attributes_prefix}white-balance")
771 destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
772 gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
773 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
774 $modified_pixbufs[thumbnail_img] ||= {}
775 $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
776 xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
778 xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
779 $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
781 $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
784 $modified_pixbufs[thumbnail_img] ||= {}
785 $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
787 update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
790 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
791 #- init $modified_pixbufs correctly
792 # update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
794 value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
796 dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
798 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
799 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
800 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
804 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
805 or too bright. Drag the slider below the image.
809 dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
811 dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
813 dialog.window_position = Gtk::Window::POS_MOUSE
817 timeout = Gtk.timeout_add(100) {
818 if hs.value != lastval
821 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
827 dialog.run { |response|
828 Gtk.timeout_remove(timeout)
829 if response == Gtk::Dialog::RESPONSE_OK
831 newval = hs.value.to_s
832 msg 3, "gamma correction to #{newval}"
834 return { :old => value, :new => newval }
837 $modified_pixbufs[thumbnail_img] ||= {}
838 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
839 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
847 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
848 if File.exists?(destfile)
849 File.delete(destfile)
851 #- type can be 'element' or 'subdir'
853 gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
855 gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
859 $max_gen_thumbnail_threads = nil
860 $current_gen_thumbnail_threads = 0
861 $gen_thumbnail_monitor = Monitor.new
863 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
864 if $max_gen_thumbnail_threads.nil?
865 $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
868 push_mousecursor_wait
869 gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
872 $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
877 $gen_thumbnail_monitor.synchronize {
878 if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
879 $current_gen_thumbnail_threads += 1
884 msg 3, "generate thumbnail from new thread"
887 $gen_thumbnail_monitor.synchronize {
888 $current_gen_thumbnail_threads -= 1
892 msg 3, "generate thumbnail from current thread"
897 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
898 distribute_multiple_call = Proc.new { |action, arg|
899 $selected_elements.each_key { |path|
900 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
902 if possible_actions[:can_multiple] && $selected_elements.length > 0
903 UndoHandler.begin_batch
904 $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
905 UndoHandler.end_batch
907 closures[action].call(arg)
909 $selected_elements = {}
912 if optionals.include?('change_image')
913 menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
914 changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
915 changeimg.signal_connect('activate') { closures[:change].call }
916 menu.append(Gtk::SeparatorMenuItem.new)
918 if !possible_actions[:can_multiple] || $selected_elements.length == 0
921 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
922 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
923 view.signal_connect('activate') { closures[:view].call }
925 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
926 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
927 view.signal_connect('activate') { closures[:view].call }
928 menu.append(Gtk::SeparatorMenuItem.new)
931 if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
932 menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
933 exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
934 exif.signal_connect('activate') { show_popup($main_window,
935 utf8(`exif -m '#{fullpath}'`),
936 { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
937 menu.append(Gtk::SeparatorMenuItem.new)
940 menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
941 r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
942 r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
943 menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
944 r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
945 r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
946 if !possible_actions[:can_multiple] || $selected_elements.length == 0
947 menu.append(Gtk::SeparatorMenuItem.new)
948 if !possible_actions[:forbid_left]
949 menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
950 moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
951 moveleft.signal_connect('activate') { closures[:move].call('left') }
952 if !possible_actions[:can_left]
953 moveleft.sensitive = false
956 if !possible_actions[:forbid_right]
957 menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
958 moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
959 moveright.signal_connect('activate') { closures[:move].call('right') }
960 if !possible_actions[:can_right]
961 moveright.sensitive = false
964 if optionals.include?('move_top')
965 menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
966 movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
967 movetop.signal_connect('activate') { closures[:move].call('top') }
968 if !possible_actions[:can_top]
969 movetop.sensitive = false
972 menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
973 moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
974 moveup.signal_connect('activate') { closures[:move].call('up') }
975 if !possible_actions[:can_up]
976 moveup.sensitive = false
978 menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
979 movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
980 movedown.signal_connect('activate') { closures[:move].call('down') }
981 if !possible_actions[:can_down]
982 movedown.sensitive = false
984 if optionals.include?('move_bottom')
985 menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
986 movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
987 movebottom.signal_connect('activate') { closures[:move].call('bottom') }
988 if !possible_actions[:can_bottom]
989 movebottom.sensitive = false
994 if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
995 menu.append(Gtk::SeparatorMenuItem.new)
996 # menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
997 # color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
998 # color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
999 menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
1000 flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
1001 flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
1002 menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
1003 seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
1004 seektime.signal_connect('activate') {
1005 if possible_actions[:can_multiple] && $selected_elements.length > 0
1006 if values = ask_new_seektime(nil, '')
1007 distribute_multiple_call.call(:seektime, values)
1010 closures[:seektime].call
1015 menu.append( Gtk::SeparatorMenuItem.new)
1016 menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
1017 gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
1018 gammacorrect.signal_connect('activate') {
1019 if possible_actions[:can_multiple] && $selected_elements.length > 0
1020 if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
1021 distribute_multiple_call.call(:gammacorrect, values)
1024 closures[:gammacorrect].call
1027 menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
1028 whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
1029 whitebalance.signal_connect('activate') {
1030 if possible_actions[:can_multiple] && $selected_elements.length > 0
1031 if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
1032 distribute_multiple_call.call(:whitebalance, values)
1035 closures[:whitebalance].call
1038 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1039 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
1040 _("Enhance constrast"))))
1042 menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
1044 enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
1045 enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
1046 if type == 'image' && possible_actions[:can_panorama]
1047 menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
1048 panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
1049 panorama.signal_connect('activate') {
1050 if possible_actions[:can_multiple] && $selected_elements.length > 0
1051 if values = ask_new_pano_amount(nil, '')
1052 distribute_multiple_call.call(:pano, values)
1055 distribute_multiple_call.call(:pano)
1059 menu.append( Gtk::SeparatorMenuItem.new)
1060 if optionals.include?('delete')
1061 menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1062 cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1063 if !possible_actions[:can_multiple] || $selected_elements.length == 0
1064 menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1065 paste_item.signal_connect('activate') { closures[:paste].call }
1066 menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1067 clear_item.signal_connect('activate') { $cuts = [] }
1069 paste_item.sensitive = clear_item.sensitive = false
1072 menu.append( Gtk::SeparatorMenuItem.new)
1074 if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1075 menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1076 editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1077 editexternally.signal_connect('activate') {
1078 if check_image_editor
1079 cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1085 menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1086 refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1087 if optionals.include?('delete')
1088 menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1089 delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1092 menu.popup(nil, nil, event.button, event.time)
1095 def delete_current_subalbum
1097 sel = $albums_tv.selection.selected_rows
1098 $xmldir.elements.each { |e|
1099 if e.name == 'image' || e.name == 'video'
1100 e.add_attribute('deleted', 'true')
1103 #- branch if we have a non deleted subalbum
1104 if $xmldir.child_byname_notattr('dir', 'deleted')
1105 $xmldir.delete_attribute('thumbnails-caption')
1106 $xmldir.delete_attribute('thumbnails-captionfile')
1108 $xmldir.add_attribute('deleted', 'true')
1110 while moveup.parent.name == 'dir'
1111 moveup = moveup.parent
1112 if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1113 moveup.add_attribute('deleted', 'true')
1120 save_changes('forced')
1121 populate_subalbums_treeview(false)
1122 $albums_tv.selection.select_path(sel[0])
1128 $current_path = nil #- prevent save_changes from being rerun again
1129 sel = $albums_tv.selection.selected_rows
1130 restore_one = proc { |xmldir|
1131 xmldir.elements.each { |e|
1132 if e.name == 'dir' && e.attributes['deleted']
1135 e.delete_attribute('deleted')
1138 restore_one.call($xmldir)
1139 populate_subalbums_treeview(false)
1140 $albums_tv.selection.select_path(sel[0])
1143 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1146 frame1 = Gtk::Frame.new
1147 fullpath = from_utf8("#{$current_path}/#{filename}")
1149 my_gen_real_thumbnail = proc {
1150 gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1154 pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1155 frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1156 pack_start(img = Gtk::Image.new).
1157 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1158 px, mask = pxb.render_pixmap_and_mask(0)
1159 da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1160 da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1162 frame1.add(img = Gtk::Image.new)
1165 #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1166 if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1167 my_gen_real_thumbnail.call
1169 img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1172 evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1174 tooltips = Gtk::Tooltips.new
1175 tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1176 tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1178 frame2, textview = create_editzone($autotable_sw, 1, img)
1179 textview.buffer.text = caption
1180 textview.set_justification(Gtk::Justification::CENTER)
1182 vbox = Gtk::VBox.new(false, 5)
1183 vbox.pack_start(evtbox, false, false)
1184 vbox.pack_start(frame2, false, false)
1185 autotable.append(vbox, filename)
1187 #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1188 $vbox2widgets[vbox] = { :textview => textview, :image => img }
1190 #- to be able to find widgets by name
1191 $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1193 cleanup_all_thumbnails = proc {
1194 #- remove out of sync images
1195 dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1196 for sizeobj in $images_size
1197 #- cannot use sizeobj because panoramic images will have a larger width
1198 Dir.glob("#{dest_img_base}-*.jpg") do |file|
1206 cleanup_all_thumbnails.call
1207 #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1209 rexml_thread_protect {
1210 $xmldir.delete_attribute('already-generated')
1212 my_gen_real_thumbnail.call
1215 rotate_and_cleanup = proc { |angle|
1216 cleanup_all_thumbnails.call
1217 rexml_thread_protect {
1218 rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1222 move = proc { |direction|
1223 do_method = "move_#{direction}"
1224 undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1226 done = autotable.method(do_method).call(vbox)
1227 textview.grab_focus #- because if moving, focus is stolen
1231 save_undo(_("move %s") % direction,
1233 autotable.method(undo_method).call(vbox)
1234 textview.grab_focus #- because if moving, focus is stolen
1235 autoscroll_if_needed($autotable_sw, img, textview)
1236 $notebook.set_page(1)
1238 autotable.method(do_method).call(vbox)
1239 textview.grab_focus #- because if moving, focus is stolen
1240 autoscroll_if_needed($autotable_sw, img, textview)
1241 $notebook.set_page(1)
1247 color_swap_and_cleanup = proc {
1248 perform_color_swap_and_cleanup = proc {
1249 cleanup_all_thumbnails.call
1250 rexml_thread_protect {
1251 color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1253 my_gen_real_thumbnail.call
1256 perform_color_swap_and_cleanup.call
1258 save_undo(_("color swap"),
1260 perform_color_swap_and_cleanup.call
1262 autoscroll_if_needed($autotable_sw, img, textview)
1263 $notebook.set_page(1)
1265 perform_color_swap_and_cleanup.call
1267 autoscroll_if_needed($autotable_sw, img, textview)
1268 $notebook.set_page(1)
1273 change_seektime_and_cleanup_real = proc { |values|
1274 perform_change_seektime_and_cleanup = proc { |val|
1275 cleanup_all_thumbnails.call
1276 rexml_thread_protect {
1277 change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1279 my_gen_real_thumbnail.call
1281 perform_change_seektime_and_cleanup.call(values[:new])
1283 save_undo(_("specify seektime"),
1285 perform_change_seektime_and_cleanup.call(values[:old])
1287 autoscroll_if_needed($autotable_sw, img, textview)
1288 $notebook.set_page(1)
1290 perform_change_seektime_and_cleanup.call(values[:new])
1292 autoscroll_if_needed($autotable_sw, img, textview)
1293 $notebook.set_page(1)
1298 change_seektime_and_cleanup = proc {
1299 rexml_thread_protect {
1300 if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1301 change_seektime_and_cleanup_real.call(values)
1306 change_pano_amount_and_cleanup_real = proc { |values|
1307 perform_change_pano_amount_and_cleanup = proc { |val|
1308 cleanup_all_thumbnails.call
1309 rexml_thread_protect {
1310 change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1313 perform_change_pano_amount_and_cleanup.call(values[:new])
1315 save_undo(_("change panorama amount"),
1317 perform_change_pano_amount_and_cleanup.call(values[:old])
1319 autoscroll_if_needed($autotable_sw, img, textview)
1320 $notebook.set_page(1)
1322 perform_change_pano_amount_and_cleanup.call(values[:new])
1324 autoscroll_if_needed($autotable_sw, img, textview)
1325 $notebook.set_page(1)
1330 change_pano_amount_and_cleanup = proc {
1331 rexml_thread_protect {
1332 if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1333 change_pano_amount_and_cleanup_real.call(values)
1338 whitebalance_and_cleanup_real = proc { |values|
1339 perform_change_whitebalance_and_cleanup = proc { |val|
1340 cleanup_all_thumbnails.call
1341 rexml_thread_protect {
1342 change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1343 recalc_whitebalance(val, fullpath, thumbnail_img, img,
1344 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1347 perform_change_whitebalance_and_cleanup.call(values[:new])
1349 save_undo(_("fix white balance"),
1351 perform_change_whitebalance_and_cleanup.call(values[:old])
1353 autoscroll_if_needed($autotable_sw, img, textview)
1354 $notebook.set_page(1)
1356 perform_change_whitebalance_and_cleanup.call(values[:new])
1358 autoscroll_if_needed($autotable_sw, img, textview)
1359 $notebook.set_page(1)
1364 whitebalance_and_cleanup = proc {
1365 rexml_thread_protect {
1366 if values = ask_whitebalance(fullpath, thumbnail_img, img,
1367 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1368 whitebalance_and_cleanup_real.call(values)
1373 gammacorrect_and_cleanup_real = proc { |values|
1374 perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1375 cleanup_all_thumbnails.call
1376 rexml_thread_protect {
1377 change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1378 recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1379 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1382 perform_change_gammacorrect_and_cleanup.call(values[:new])
1384 save_undo(_("gamma correction"),
1386 perform_change_gammacorrect_and_cleanup.call(values[:old])
1388 autoscroll_if_needed($autotable_sw, img, textview)
1389 $notebook.set_page(1)
1391 perform_change_gammacorrect_and_cleanup.call(values[:new])
1393 autoscroll_if_needed($autotable_sw, img, textview)
1394 $notebook.set_page(1)
1399 gammacorrect_and_cleanup = Proc.new {
1400 rexml_thread_protect {
1401 if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1402 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1403 gammacorrect_and_cleanup_real.call(values)
1408 enhance_and_cleanup = proc {
1409 perform_enhance_and_cleanup = proc {
1410 cleanup_all_thumbnails.call
1411 rexml_thread_protect {
1412 enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1414 my_gen_real_thumbnail.call
1417 cleanup_all_thumbnails.call
1418 perform_enhance_and_cleanup.call
1420 save_undo(_("enhance"),
1422 perform_enhance_and_cleanup.call
1424 autoscroll_if_needed($autotable_sw, img, textview)
1425 $notebook.set_page(1)
1427 perform_enhance_and_cleanup.call
1429 autoscroll_if_needed($autotable_sw, img, textview)
1430 $notebook.set_page(1)
1435 delete = proc { |isacut|
1436 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 })
1439 perform_delete = proc {
1440 after = autotable.get_next_widget(vbox)
1442 after = autotable.get_previous_widget(vbox)
1444 if $config['deleteondisk'] && !isacut
1445 msg 3, "scheduling for delete: #{fullpath}"
1446 $todelete << fullpath
1448 autotable.remove_widget(vbox)
1450 $vbox2widgets[after][:textview].grab_focus
1451 autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1455 previous_pos = autotable.get_current_number(vbox)
1459 delete_current_subalbum
1461 save_undo(_("delete"),
1463 autotable.reinsert(pos, vbox, filename)
1464 $notebook.set_page(1)
1465 autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1467 msg 3, "removing deletion schedule of: #{fullpath}"
1468 $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
1471 $notebook.set_page(1)
1480 $cuts << { :vbox => vbox, :filename => filename }
1481 $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1486 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1489 autotable.queue_draws << proc {
1490 $vbox2widgets[last[:vbox]][:textview].grab_focus
1491 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1493 save_undo(_("paste"),
1495 cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1496 $notebook.set_page(1)
1499 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1501 $notebook.set_page(1)
1504 $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1509 $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1510 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1511 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1512 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1514 textview.signal_connect('key-press-event') { |w, event|
1517 x, y = autotable.get_current_pos(vbox)
1518 control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1519 shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1520 alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1521 if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1523 if widget_up = autotable.get_widget_at_pos(x, y - 1)
1524 $vbox2widgets[widget_up][:textview].grab_focus
1531 if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1533 if widget_down = autotable.get_widget_at_pos(x, y + 1)
1534 $vbox2widgets[widget_down][:textview].grab_focus
1541 if event.keyval == Gdk::Keyval::GDK_Left
1544 $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1551 rotate_and_cleanup.call(-90)
1554 if event.keyval == Gdk::Keyval::GDK_Right
1555 next_ = autotable.get_next_widget(vbox)
1556 if next_ && autotable.get_current_pos(next_)[0] > x
1558 $vbox2widgets[next_][:textview].grab_focus
1565 rotate_and_cleanup.call(90)
1568 if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1571 if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1572 view_element(filename, { :delete => delete })
1575 if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1578 if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1582 !propagate #- propagate if needed
1585 $ignore_next_release = false
1586 evtbox.signal_connect('button-press-event') { |w, event|
1587 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1588 if event.state & Gdk::Window::BUTTON3_MASK != 0
1589 #- gesture redo: hold right mouse button then click left mouse button
1590 $config['nogestures'] or perform_redo
1591 $ignore_next_release = true
1593 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1595 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1597 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1598 elsif $enhance.active?
1599 enhance_and_cleanup.call
1600 elsif $delete.active?
1604 $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1607 $button1_pressed_autotable = true
1608 elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1609 if event.state & Gdk::Window::BUTTON1_MASK != 0
1610 #- gesture undo: hold left mouse button then click right mouse button
1611 $config['nogestures'] or perform_undo
1612 $ignore_next_release = true
1614 elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1615 view_element(filename, { :delete => delete })
1620 evtbox.signal_connect('button-release-event') { |w, event|
1621 if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1622 if !$ignore_next_release
1623 x, y = autotable.get_current_pos(vbox)
1624 next_ = autotable.get_next_widget(vbox)
1625 popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
1626 { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1627 :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1628 { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1629 :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1630 :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1631 :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1633 $ignore_next_release = false
1634 $gesture_press = nil
1639 #- handle reordering with drag and drop
1640 Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1641 Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1642 vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1643 selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1646 vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1648 #- mouse gesture first (dnd disables button-release-event)
1649 if $gesture_press && $gesture_press[:filename] == filename
1650 if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1651 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1652 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1653 rotate_and_cleanup.call(angle)
1654 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1656 elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1657 msg 3, "gesture delete: click-drag right button to the bottom"
1659 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1664 ctxt.targets.each { |target|
1665 if target.name == 'reorder-elements'
1666 move_dnd = proc { |from,to|
1669 autotable.move(from, to)
1670 save_undo(_("reorder"),
1673 autotable.move(to - 1, from)
1675 autotable.move(to, from + 1)
1677 $notebook.set_page(1)
1679 autotable.move(from, to)
1680 $notebook.set_page(1)
1685 if $multiple_dnd.size == 0
1686 move_dnd.call(selection_data.data.to_i,
1687 autotable.get_current_number(vbox))
1689 UndoHandler.begin_batch
1690 $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1692 #- need to update current position between each call
1693 move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1694 autotable.get_current_number(vbox))
1696 UndoHandler.end_batch
1707 def create_auto_table
1709 $autotable = Gtk::AutoTable.new(5)
1711 $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1712 thumbnails_vb = Gtk::VBox.new(false, 5)
1714 frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1715 $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1716 thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1717 thumbnails_vb.add($autotable)
1719 $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1720 $autotable_sw.add_with_viewport(thumbnails_vb)
1722 #- follows stuff for handling multiple elements selection
1723 press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1725 update_selected = proc {
1726 $autotable.current_order.each { |path|
1727 w = $name2widgets[path][:evtbox].window
1728 xm = w.position[0] + w.size[0]/2
1729 ym = w.position[1] + w.size[1]/2
1730 if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1731 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1732 $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1733 $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1736 if $selected_elements[path] && ! $selected_elements[path][:keep]
1737 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))
1738 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1739 $selected_elements.delete(path)
1744 $autotable.signal_connect('realize') { |w,e|
1745 gc = Gdk::GC.new($autotable.window)
1746 gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1747 gc.function = Gdk::GC::INVERT
1748 #- autoscroll handling for DND and multiple selections
1749 Gtk.timeout_add(100) {
1750 if ! $autotable.window.nil?
1751 w, x, y, mask = $autotable.window.pointer
1752 if mask & Gdk::Window::BUTTON1_MASK != 0
1753 if y < $autotable_sw.vadjustment.value
1755 $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]])
1757 if $button1_pressed_autotable || press_x
1758 scroll_upper($autotable_sw, y)
1761 w, pos_x, pos_y = $autotable.window.pointer
1762 $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]])
1763 update_selected.call
1766 if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1768 $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]])
1770 if $button1_pressed_autotable || press_x
1771 scroll_lower($autotable_sw, y)
1774 w, pos_x, pos_y = $autotable.window.pointer
1775 $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]])
1776 update_selected.call
1781 ! $autotable.window.nil?
1785 $autotable.signal_connect('button-press-event') { |w,e|
1787 if !$button1_pressed_autotable
1790 if e.state & Gdk::Window::SHIFT_MASK == 0
1791 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1792 $selected_elements = {}
1793 $statusbar.push(0, utf8(_("Nothing selected.")))
1795 $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1797 set_mousecursor(Gdk::Cursor::TCROSS)
1801 $autotable.signal_connect('button-release-event') { |w,e|
1803 if $button1_pressed_autotable
1804 #- unselect all only now
1805 $multiple_dnd = $selected_elements.keys
1806 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1807 $selected_elements = {}
1808 $button1_pressed_autotable = false
1811 $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]])
1812 if $selected_elements.length > 0
1813 $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1816 press_x = press_y = pos_x = pos_y = nil
1817 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1821 $autotable.signal_connect('motion-notify-event') { |w,e|
1824 $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]])
1828 $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 update_selected.call
1835 def create_subalbums_page
1837 subalbums_hb = Gtk::HBox.new
1838 $subalbums_vb = Gtk::VBox.new(false, 5)
1839 subalbums_hb.pack_start($subalbums_vb, false, false)
1840 $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1841 $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1842 $subalbums_sw.add_with_viewport(subalbums_hb)
1845 def save_current_file
1851 ios = File.open($filename, "w")
1852 $xmldoc.write(ios, 0)
1854 rescue Iconv::IllegalSequence
1855 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1856 if ! ios.nil? && ! ios.closed?
1859 $xmldoc.xml_decl.encoding = 'UTF-8'
1860 ios = File.open($filename, "w")
1861 $xmldoc.write(ios, 0)
1872 def save_current_file_user
1873 save_tempfilename = $filename
1874 $filename = $orig_filename
1875 if ! save_current_file
1876 show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1877 $filename = save_tempfilename
1881 $generated_outofline = false
1882 $filename = save_tempfilename
1884 msg 3, "performing actual deletion of: " + $todelete.join(', ')
1885 $todelete.each { |f|
1890 def mark_document_as_dirty
1891 $xmldoc.elements.each('//dir') { |elem|
1892 elem.delete_attribute('already-generated')
1896 #- ret: true => ok false => cancel
1897 def ask_save_modifications(msg1, msg2, *options)
1899 options = options.size > 0 ? options[0] : {}
1901 if options[:disallow_cancel]
1902 dialog = Gtk::Dialog.new(msg1,
1904 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1905 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1906 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1908 dialog = Gtk::Dialog.new(msg1,
1910 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1911 [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1912 [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1913 [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1915 dialog.default_response = Gtk::Dialog::RESPONSE_YES
1916 dialog.vbox.add(Gtk::Label.new(msg2))
1917 dialog.window_position = Gtk::Window::POS_CENTER
1920 dialog.run { |response|
1922 if response == Gtk::Dialog::RESPONSE_YES
1923 if ! save_current_file_user
1924 return ask_save_modifications(msg1, msg2, options)
1927 #- if we have generated an album but won't save modifications, we must remove
1928 #- already-generated markers in original file
1929 if $generated_outofline
1931 $xmldoc = REXML::Document.new(File.new($orig_filename))
1932 mark_document_as_dirty
1933 ios = File.open($orig_filename, "w")
1934 $xmldoc.write(ios, 0)
1937 puts "exception: #{$!}"
1941 if response == Gtk::Dialog::RESPONSE_CANCEL
1944 $todelete = [] #- unconditionally clear the list of images/videos to delete
1950 def try_quit(*options)
1951 if ask_save_modifications(utf8(_("Save before quitting?")),
1952 utf8(_("Do you want to save your changes before quitting?")),
1958 def show_popup(parent, msg, *options)
1959 dialog = Gtk::Dialog.new
1960 if options[0] && options[0][:title]
1961 dialog.title = options[0][:title]
1963 dialog.title = utf8(_("Booh message"))
1965 lbl = Gtk::Label.new
1966 if options[0] && options[0][:nomarkup]
1971 if options[0] && options[0][:centered]
1972 lbl.set_justify(Gtk::Justification::CENTER)
1974 if options[0] && options[0][:selectable]
1975 lbl.selectable = true
1977 if options[0] && options[0][:topwidget]
1978 dialog.vbox.add(options[0][:topwidget])
1980 if options[0] && options[0][:scrolled]
1981 sw = Gtk::ScrolledWindow.new(nil, nil)
1982 sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1983 sw.add_with_viewport(lbl)
1985 dialog.set_default_size(500, 600)
1987 dialog.vbox.add(lbl)
1988 dialog.set_default_size(200, 120)
1990 if options[0] && options[0][:okcancel]
1991 dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1993 dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1995 if options[0] && options[0][:pos_centered]
1996 dialog.window_position = Gtk::Window::POS_CENTER
1998 dialog.window_position = Gtk::Window::POS_MOUSE
2001 if options[0] && options[0][:linkurl]
2002 linkbut = Gtk::Button.new('')
2003 linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2004 linkbut.signal_connect('clicked') {
2005 open_url(options[0][:linkurl])
2006 dialog.response(Gtk::Dialog::RESPONSE_OK)
2007 set_mousecursor_normal
2009 linkbut.relief = Gtk::RELIEF_NONE
2010 linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2011 linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2012 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2017 if !options[0] || !options[0][:not_transient]
2018 dialog.transient_for = parent
2019 dialog.run { |response|
2021 if options[0] && options[0][:okcancel]
2022 return response == Gtk::Dialog::RESPONSE_OK
2026 dialog.signal_connect('response') { dialog.destroy }
2030 def set_mainwindow_title(progress)
2031 filename = $orig_filename || $filename
2034 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2036 $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2040 $main_window.title = 'booh - ' + File.basename(filename)
2042 $main_window.title = 'booh'
2047 def backend_wait_message(parent, msg, infopipe_path, mode)
2049 w.set_transient_for(parent)
2052 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2053 vb.pack_start(Gtk::Label.new(msg), false, false)
2055 vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2056 vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2057 if mode != 'one dir scan'
2058 vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2060 if mode == 'web-album'
2061 vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2062 vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2064 vb.pack_start(Gtk::HSeparator.new, false, false)
2066 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2067 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2068 vb.pack_end(bottom, false, false)
2071 update_progression_title_pb1 = proc {
2072 if mode == 'web-album'
2073 set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2074 elsif mode != 'one dir scan'
2075 set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2077 set_mainwindow_title(pb1_1.fraction)
2081 infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2082 refresh_thread = Thread.new {
2083 directories_counter = 0
2084 while line = infopipe.gets
2085 msg 3, "infopipe got data: #{line}"
2086 if line =~ /^directories: (\d+), sizes: (\d+)/
2087 directories = $1.to_f + 1
2089 elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2090 elements = $3.to_f + 1
2091 if mode == 'web-album'
2095 gtk_thread_protect { pb1_1.fraction = 0 }
2096 if mode != 'one dir scan'
2097 newtext = utf8(full_src_dir_to_rel($1, $2))
2098 newtext = '/' if newtext == ''
2099 gtk_thread_protect { pb1_2.text = newtext }
2100 directories_counter += 1
2101 gtk_thread_protect {
2102 pb1_2.fraction = directories_counter / directories
2103 update_progression_title_pb1.call
2106 elsif line =~ /^processing element$/
2107 element_counter += 1
2108 gtk_thread_protect {
2109 pb1_1.fraction = element_counter / elements
2110 update_progression_title_pb1.call
2112 elsif line =~ /^processing size$/
2113 element_counter += 1
2114 gtk_thread_protect {
2115 pb1_1.fraction = element_counter / elements
2116 update_progression_title_pb1.call
2118 elsif line =~ /^finished processing sizes$/
2119 gtk_thread_protect { pb1_1.fraction = 1 }
2120 elsif line =~ /^creating index.html$/
2121 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2122 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2123 directories_counter = 0
2124 elsif line =~ /^index.html: (.+)\|(.+)/
2125 newtext = utf8(full_src_dir_to_rel($1, $2))
2126 newtext = '/' if newtext == ''
2127 gtk_thread_protect { pb2.text = newtext }
2128 directories_counter += 1
2129 gtk_thread_protect {
2130 pb2.fraction = directories_counter / directories
2131 set_mainwindow_title(0.9 + pb2.fraction / 10)
2133 elsif line =~ /^die: (.*)$/
2140 w.signal_connect('delete-event') { w.destroy }
2141 w.signal_connect('destroy') {
2142 Thread.kill(refresh_thread)
2143 gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2146 File.delete(infopipe_path)
2148 set_mainwindow_title(nil)
2150 w.window_position = Gtk::Window::POS_CENTER
2156 def call_backend(cmd, waitmsg, mode, params)
2157 pipe = Tempfile.new("boohpipe")
2158 Thread.critical = true
2161 system("mkfifo #{path}")
2162 Thread.critical = false
2163 cmd += " --info-pipe #{path}"
2164 button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2169 id, exitstatus = Process.waitpid2(pid)
2170 gtk_thread_protect { w8.destroy }
2172 if params[:successmsg]
2173 gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2175 if params[:closure_after]
2176 gtk_thread_protect(¶ms[:closure_after])
2178 elsif exitstatus == 15
2179 #- say nothing, user aborted
2181 gtk_thread_protect { show_popup($main_window,
2182 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2188 button.signal_connect('clicked') {
2189 Process.kill('SIGTERM', pid)
2193 def save_changes(*forced)
2194 if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2198 $xmldir.delete_attribute('already-generated')
2200 propagate_children = proc { |xmldir|
2201 if xmldir.attributes['subdirs-caption']
2202 xmldir.delete_attribute('already-generated')
2204 xmldir.elements.each('dir') { |element|
2205 propagate_children.call(element)
2209 if $xmldir.child_byname_notattr('dir', 'deleted')
2210 new_title = $subalbums_title.buffer.text
2211 if new_title != $xmldir.attributes['subdirs-caption']
2212 parent = $xmldir.parent
2213 if parent.name == 'dir'
2214 parent.delete_attribute('already-generated')
2216 propagate_children.call($xmldir)
2218 $xmldir.add_attribute('subdirs-caption', new_title)
2219 $xmldir.elements.each('dir') { |element|
2220 if !element.attributes['deleted']
2221 path = element.attributes['path']
2222 newtext = $subalbums_edits[path][:editzone].buffer.text
2223 if element.attributes['subdirs-caption']
2224 if element.attributes['subdirs-caption'] != newtext
2225 propagate_children.call(element)
2227 element.add_attribute('subdirs-caption', newtext)
2228 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2230 if element.attributes['thumbnails-caption'] != newtext
2231 element.delete_attribute('already-generated')
2233 element.add_attribute('thumbnails-caption', newtext)
2234 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2240 if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2241 if $xmldir.attributes['thumbnails-caption']
2242 path = $xmldir.attributes['path']
2243 $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2245 elsif $xmldir.attributes['thumbnails-caption']
2246 $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2249 if $xmldir.attributes['thumbnails-caption']
2250 if edit = $subalbums_edits[$xmldir.attributes['path']]
2251 $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2255 #- remove and reinsert elements to reflect new ordering
2258 $xmldir.elements.each { |element|
2259 if element.name == 'image' || element.name == 'video'
2260 saves[element.attributes['filename']] = element.remove
2264 $autotable.current_order.each { |path|
2265 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2266 chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2269 saves.each_key { |path|
2270 chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2271 chld.add_attribute('deleted', 'true')
2275 def sort_by_exif_date
2279 rexml_thread_protect {
2280 $xmldir.elements.each { |element|
2281 if element.name == 'image' || element.name == 'video'
2282 current_order << element.attributes['filename']
2287 #- look for EXIF dates
2290 if current_order.size > 20
2292 w.set_transient_for($main_window)
2294 vb = Gtk::VBox.new(false, 5).set_border_width(5)
2295 vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2296 vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2297 bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2298 b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2299 vb.pack_end(bottom, false, false)
2301 w.signal_connect('delete-event') { w.destroy }
2302 w.window_position = Gtk::Window::POS_CENTER
2306 b.signal_connect('clicked') { aborted = true }
2308 current_order.each { |f|
2310 if entry2type(f) == 'image'
2312 pb.fraction = i.to_f / current_order.size
2313 Gtk.main_iteration while Gtk.events_pending?
2314 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2316 dates[f] = date_time
2329 current_order.each { |f|
2330 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2332 dates[f] = date_time
2338 rexml_thread_protect {
2339 $xmldir.elements.each { |element|
2340 if element.name == 'image' || element.name == 'video'
2341 saves[element.attributes['filename']] = element.remove
2346 neworder = smartsort(current_order, dates)
2348 rexml_thread_protect {
2350 $xmldir.add_element(saves[f].name, saves[f].attributes)
2354 #- let the auto-table reflect new ordering
2358 def remove_all_captions
2361 $autotable.current_order.each { |path|
2362 texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2363 $name2widgets[File.basename(path)][:textview].buffer.text = ''
2365 save_undo(_("remove all captions"),
2367 texts.each_key { |key|
2368 $name2widgets[key][:textview].buffer.text = texts[key]
2370 $notebook.set_page(1)
2372 texts.each_key { |key|
2373 $name2widgets[key][:textview].buffer.text = ''
2375 $notebook.set_page(1)
2381 $selected_elements.each_key { |path|
2382 $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2388 $selected_elements = {}
2392 $undo_tb.sensitive = $undo_mb.sensitive = false
2393 $redo_tb.sensitive = $redo_mb.sensitive = false
2399 $subalbums_vb.children.each { |chld|
2400 $subalbums_vb.remove(chld)
2402 $subalbums = Gtk::Table.new(0, 0, true)
2403 current_y_sub_albums = 0
2405 $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2406 $subalbums_edits = {}
2407 subalbums_counter = 0
2408 subalbums_edits_bypos = {}
2410 add_subalbum = proc { |xmldir, counter|
2411 $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2412 subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2413 if xmldir == $xmldir
2414 thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2415 captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2416 caption = xmldir.attributes['thumbnails-caption']
2417 infotype = 'thumbnails'
2419 thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2420 captionfile, caption = find_subalbum_caption_info(xmldir)
2421 infotype = find_subalbum_info_type(xmldir)
2423 msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2424 hbox = Gtk::HBox.new
2425 hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2427 f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2430 my_gen_real_thumbnail = proc {
2431 gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2434 if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2435 f.add(img = Gtk::Image.new)
2436 my_gen_real_thumbnail.call
2438 f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2440 hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2441 $subalbums.attach(hbox,
2442 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2444 frame, textview = create_editzone($subalbums_sw, 0, img)
2445 textview.buffer.text = caption
2446 $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2447 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2449 change_image = proc {
2450 fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2452 Gtk::FileChooser::ACTION_OPEN,
2454 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2455 fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2456 fc.transient_for = $main_window
2457 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))
2458 f.add(preview_img = Gtk::Image.new)
2460 fc.signal_connect('update-preview') { |w|
2461 if fc.preview_filename
2462 if entry2type(fc.preview_filename) == 'video'
2466 tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2468 fc.preview_widget_active = false
2470 tmpimage = "#{tmpdir}/00000001.jpg"
2472 preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2473 fc.preview_widget_active = true
2474 rescue Gdk::PixbufError
2475 fc.preview_widget_active = false
2477 File.delete(tmpimage)
2484 preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2485 fc.preview_widget_active = true
2486 rescue Gdk::PixbufError
2487 fc.preview_widget_active = false
2492 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2494 old_file = captionfile
2495 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2496 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2497 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2498 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2500 new_file = fc.filename
2501 msg 3, "new captionfile is: #{fc.filename}"
2502 perform_changefile = proc {
2503 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2504 $modified_pixbufs.delete(thumbnail_file)
2505 xmldir.delete_attribute("#{infotype}-rotate")
2506 xmldir.delete_attribute("#{infotype}-color-swap")
2507 xmldir.delete_attribute("#{infotype}-enhance")
2508 xmldir.delete_attribute("#{infotype}-seektime")
2509 my_gen_real_thumbnail.call
2511 perform_changefile.call
2513 save_undo(_("change caption file for sub-album"),
2515 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2516 xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2517 xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2518 xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2519 xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2520 my_gen_real_thumbnail.call
2521 $notebook.set_page(0)
2523 perform_changefile.call
2524 $notebook.set_page(0)
2532 if File.exists?(thumbnail_file)
2533 File.delete(thumbnail_file)
2535 my_gen_real_thumbnail.call
2538 rotate_and_cleanup = proc { |angle|
2539 rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2540 if File.exists?(thumbnail_file)
2541 File.delete(thumbnail_file)
2545 move = proc { |direction|
2548 save_changes('forced')
2549 oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2550 if direction == 'up'
2551 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2552 subalbums_edits_bypos[oldpos - 1][:position] += 1
2554 if direction == 'down'
2555 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2556 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2558 if direction == 'top'
2559 for i in 1 .. oldpos - 1
2560 subalbums_edits_bypos[i][:position] += 1
2562 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2564 if direction == 'bottom'
2565 for i in oldpos + 1 .. subalbums_counter
2566 subalbums_edits_bypos[i][:position] -= 1
2568 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2572 $xmldir.elements.each('dir') { |element|
2573 if (!element.attributes['deleted'])
2574 elems << [ element.attributes['path'], element.remove ]
2577 elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2578 each { |e| $xmldir.add_element(e[1]) }
2579 #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2580 $xmldir.elements.each('descendant::dir') { |elem|
2581 elem.delete_attribute('already-generated')
2584 sel = $albums_tv.selection.selected_rows
2586 populate_subalbums_treeview(false)
2587 $albums_tv.selection.select_path(sel[0])
2590 color_swap_and_cleanup = proc {
2591 perform_color_swap_and_cleanup = proc {
2592 color_swap(xmldir, "#{infotype}-")
2593 my_gen_real_thumbnail.call
2595 perform_color_swap_and_cleanup.call
2597 save_undo(_("color swap"),
2599 perform_color_swap_and_cleanup.call
2600 $notebook.set_page(0)
2602 perform_color_swap_and_cleanup.call
2603 $notebook.set_page(0)
2608 change_seektime_and_cleanup = proc {
2609 if values = ask_new_seektime(xmldir, "#{infotype}-")
2610 perform_change_seektime_and_cleanup = proc { |val|
2611 change_seektime(xmldir, "#{infotype}-", val)
2612 my_gen_real_thumbnail.call
2614 perform_change_seektime_and_cleanup.call(values[:new])
2616 save_undo(_("specify seektime"),
2618 perform_change_seektime_and_cleanup.call(values[:old])
2619 $notebook.set_page(0)
2621 perform_change_seektime_and_cleanup.call(values[:new])
2622 $notebook.set_page(0)
2628 whitebalance_and_cleanup = proc {
2629 if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2630 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2631 perform_change_whitebalance_and_cleanup = proc { |val|
2632 change_whitebalance(xmldir, "#{infotype}-", val)
2633 recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2634 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2635 if File.exists?(thumbnail_file)
2636 File.delete(thumbnail_file)
2639 perform_change_whitebalance_and_cleanup.call(values[:new])
2641 save_undo(_("fix white balance"),
2643 perform_change_whitebalance_and_cleanup.call(values[:old])
2644 $notebook.set_page(0)
2646 perform_change_whitebalance_and_cleanup.call(values[:new])
2647 $notebook.set_page(0)
2653 gammacorrect_and_cleanup = proc {
2654 if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2655 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2656 perform_change_gammacorrect_and_cleanup = proc { |val|
2657 change_gammacorrect(xmldir, "#{infotype}-", val)
2658 recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2659 $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2660 if File.exists?(thumbnail_file)
2661 File.delete(thumbnail_file)
2664 perform_change_gammacorrect_and_cleanup.call(values[:new])
2666 save_undo(_("gamma correction"),
2668 perform_change_gammacorrect_and_cleanup.call(values[:old])
2669 $notebook.set_page(0)
2671 perform_change_gammacorrect_and_cleanup.call(values[:new])
2672 $notebook.set_page(0)
2678 enhance_and_cleanup = proc {
2679 perform_enhance_and_cleanup = proc {
2680 enhance(xmldir, "#{infotype}-")
2681 my_gen_real_thumbnail.call
2684 perform_enhance_and_cleanup.call
2686 save_undo(_("enhance"),
2688 perform_enhance_and_cleanup.call
2689 $notebook.set_page(0)
2691 perform_enhance_and_cleanup.call
2692 $notebook.set_page(0)
2697 evtbox.signal_connect('button-press-event') { |w, event|
2698 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2700 rotate_and_cleanup.call(90)
2702 rotate_and_cleanup.call(-90)
2703 elsif $enhance.active?
2704 enhance_and_cleanup.call
2707 if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2708 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2709 { :forbid_left => true, :forbid_right => true,
2710 :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2711 :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2712 { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2713 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2714 :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2716 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2721 evtbox.signal_connect('button-press-event') { |w, event|
2722 $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2726 evtbox.signal_connect('button-release-event') { |w, event|
2727 if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2728 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2729 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2730 angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2731 msg 3, "gesture rotate: #{angle}"
2732 rotate_and_cleanup.call(angle)
2735 $gesture_press = nil
2738 $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2739 $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2740 current_y_sub_albums += 1
2743 if $xmldir.child_byname_notattr('dir', 'deleted')
2745 frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2746 $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2747 $subalbums_title.set_justification(Gtk::Justification::CENTER)
2748 $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2749 #- this album image/caption
2750 if $xmldir.attributes['thumbnails-caption']
2751 add_subalbum.call($xmldir, 0)
2754 total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2755 $xmldir.elements.each { |element|
2756 if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2757 #- element (image or video) of this album
2758 dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2759 msg 3, "dest_img: #{dest_img}"
2760 add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2761 total[element.name] += 1
2763 if element.name == 'dir' && !element.attributes['deleted']
2764 #- sub-album image/caption
2765 add_subalbum.call(element, subalbums_counter += 1)
2766 total[element.name] += 1
2769 $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2770 total['image'], total['video'], total['dir'] ]))
2771 $subalbums_vb.add($subalbums)
2772 $subalbums_vb.show_all
2774 if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2775 $notebook.get_tab_label($autotable_sw).sensitive = false
2776 $notebook.set_page(0)
2777 $thumbnails_title.buffer.text = ''
2779 $notebook.get_tab_label($autotable_sw).sensitive = true
2780 $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2783 if !$xmldir.child_byname_notattr('dir', 'deleted')
2784 $notebook.get_tab_label($subalbums_sw).sensitive = false
2785 $notebook.set_page(1)
2787 $notebook.get_tab_label($subalbums_sw).sensitive = true
2791 def pixbuf_or_nil(filename)
2793 return Gdk::Pixbuf.new(filename)
2799 def theme_choose(current)
2800 dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2802 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2803 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2804 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2806 model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2807 treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2808 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2809 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2810 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2811 treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2812 treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2813 treeview.signal_connect('button-press-event') { |w, event|
2814 if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2815 dialog.response(Gtk::Dialog::RESPONSE_OK)
2819 dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2821 ([ $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|
2824 iter[0] = File.basename(dir)
2825 iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2826 iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2827 iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2828 if File.basename(dir) == current
2829 treeview.selection.select_iter(iter)
2832 dialog.set_default_size(-1, 500)
2833 dialog.vbox.show_all
2835 dialog.run { |response|
2836 iter = treeview.selection.selected
2838 if response == Gtk::Dialog::RESPONSE_OK && iter
2839 return model.get_value(iter, 0)
2845 def show_password_protections
2846 examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2847 child_iter = $albums_iters[xmldir.attributes['path']]
2848 if xmldir.attributes['password-protect']
2849 child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2850 already_protected = true
2851 elsif already_protected
2852 pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2854 pix = pix.saturate_and_pixelate(1, true)
2860 xmldir.elements.each('dir') { |elem|
2861 if !elem.attributes['deleted']
2862 examine_dir_elem.call(child_iter, elem, already_protected)
2866 examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2869 def populate_subalbums_treeview(select_first)
2873 $subalbums_vb.children.each { |chld|
2874 $subalbums_vb.remove(chld)
2877 source = $xmldoc.root.attributes['source']
2878 msg 3, "source: #{source}"
2880 xmldir = $xmldoc.elements['//dir']
2881 if !xmldir || xmldir.attributes['path'] != source
2882 msg 1, _("Corrupted booh file...")
2886 append_dir_elem = proc { |parent_iter, xmldir|
2887 child_iter = $albums_ts.append(parent_iter)
2888 child_iter[0] = File.basename(xmldir.attributes['path'])
2889 child_iter[1] = xmldir.attributes['path']
2890 $albums_iters[xmldir.attributes['path']] = child_iter
2891 msg 3, "puttin location: #{xmldir.attributes['path']}"
2892 xmldir.elements.each('dir') { |elem|
2893 if !elem.attributes['deleted']
2894 append_dir_elem.call(child_iter, elem)
2898 append_dir_elem.call(nil, xmldir)
2899 show_password_protections
2901 $albums_tv.expand_all
2903 $albums_tv.selection.select_iter($albums_ts.iter_first)
2907 def select_current_theme
2908 select_theme($xmldoc.root.attributes['theme'],
2909 $xmldoc.root.attributes['limit-sizes'],
2910 !$xmldoc.root.attributes['optimize-for-32'].nil?,
2911 $xmldoc.root.attributes['thumbnails-per-row'])
2914 def open_file(filename)
2918 $current_path = nil #- invalidate
2919 $modified_pixbufs = {}
2922 $subalbums_vb.children.each { |chld|
2923 $subalbums_vb.remove(chld)
2926 if !File.exists?(filename)
2927 return utf8(_("File not found."))
2931 $xmldoc = REXML::Document.new(File.new(filename))
2936 if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2937 if entry2type(filename).nil?
2938 return utf8(_("Not a booh file!"))
2940 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."))
2944 if !source = $xmldoc.root.attributes['source']
2945 return utf8(_("Corrupted booh file..."))
2948 if !dest = $xmldoc.root.attributes['destination']
2949 return utf8(_("Corrupted booh file..."))
2952 if !theme = $xmldoc.root.attributes['theme']
2953 return utf8(_("Corrupted booh file..."))
2956 if $xmldoc.root.attributes['version'] < '0.9.0'
2957 msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2958 mark_document_as_dirty
2959 if $xmldoc.root.attributes['version'] < '0.8.4'
2960 msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2961 `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2962 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2963 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2964 if old_dest_dir != new_dest_dir
2965 sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2967 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2968 xmldir.elements.each { |element|
2969 if %w(image video).include?(element.name) && !element.attributes['deleted']
2970 old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2971 new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2972 Dir[old_name + '*'].each { |file|
2973 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2974 file != new_file and sys("mv '#{file}' '#{new_file}'")
2977 if element.name == 'dir' && !element.attributes['deleted']
2978 old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2979 new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2980 old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2984 msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2988 $xmldoc.root.add_attribute('version', $VERSION)
2991 select_current_theme
2993 $filename = filename
2994 set_mainwindow_title(nil)
2995 $default_size['thumbnails'] =~ /(.*)x(.*)/
2996 $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2997 $albums_thumbnail_size =~ /(.*)x(.*)/
2998 $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3000 populate_subalbums_treeview(true)
3002 $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
3006 def open_file_user(filename)
3007 result = open_file(filename)
3009 $config['last-opens'] ||= []
3010 if $config['last-opens'][-1] != utf8(filename)
3011 $config['last-opens'] << utf8(filename)
3013 $orig_filename = $filename
3014 $main_window.title = 'booh - ' + File.basename($orig_filename)
3015 tmp = Tempfile.new("boohtemp")
3016 Thread.critical = true
3017 $filename = tmp.path
3020 ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3021 Thread.critical = false
3023 $tempfiles << $filename << "#{$filename}.backup"
3025 $orig_filename = nil
3031 if !ask_save_modifications(utf8(_("Save this album?")),
3032 utf8(_("Do you want to save the changes to this album?")),
3033 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3036 fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3038 Gtk::FileChooser::ACTION_OPEN,
3040 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3041 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3042 fc.set_current_folder(File.expand_path("~/.booh"))
3043 fc.transient_for = $main_window
3044 fc.preview_widget = previewlabel = Gtk::Label.new.show
3045 fc.signal_connect('update-preview') { |w|
3046 if fc.preview_filename
3048 push_mousecursor_wait(fc)
3049 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3053 xmldoc.elements.each('//*') { |elem|
3054 if elem.name == 'dir'
3056 elsif elem.name == 'image'
3058 elsif elem.name == 'video'
3066 if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3067 fc.preview_widget_active = false
3069 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") %
3070 [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3071 fc.preview_widget_active = true
3077 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3078 push_mousecursor_wait(fc)
3079 msg = open_file_user(fc.filename)
3094 def additional_booh_options
3097 options += "--mproc #{$config['mproc'].to_i} "
3099 options += "--comments-format '#{$config['comments-format']}' "
3100 if $config['transcode-videos']
3101 options += "--transcode-videos '#{$config['transcode-videos']}' "
3106 def ask_multi_languages(value)
3108 spl = value.split(',')
3109 value = [ spl[0..-2], spl[-1] ]
3112 dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3115 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3116 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3118 lbl = Gtk::Label.new
3120 _("You can choose to activate <b>multi-languages</b> support for this web-album
3121 (it will work only if you publish your web-album on an Apache web-server). This will
3122 use the MultiViews feature of Apache; the pages will be served according to the
3123 value of the Accept-Language HTTP header sent by the web browsers, so that people
3124 with different languages preferences will be able to browse your web-album with
3125 navigation in their language (if language is available).
3128 dialog.vbox.add(lbl)
3129 dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3130 add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3131 add(languages = Gtk::Button.new))))
3133 pick_languages = proc {
3134 dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3137 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3138 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3140 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3141 hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3143 SUPPORTED_LANGUAGES.each { |lang|
3144 hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3145 if ! value.nil? && value[0].include?(lang)
3151 dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3152 hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3153 fallback_language = nil
3154 hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3155 fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3156 if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3157 fbl_rb.active = true
3158 fallback_language = SUPPORTED_LANGUAGES[0]
3160 SUPPORTED_LANGUAGES[1..-1].each { |lang|
3161 hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3162 rb.signal_connect('clicked') { fallback_language = lang }
3163 if ! value.nil? && value[1] == lang
3168 dialog2.window_position = Gtk::Window::POS_MOUSE
3172 dialog2.run { |response|
3174 if resp == Gtk::Dialog::RESPONSE_OK
3176 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3177 value[1] = fallback_language
3178 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3185 languages.signal_connect('clicked') {
3188 dialog.window_position = Gtk::Window::POS_MOUSE
3192 rb_yes.active = true
3193 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3195 rb_no.signal_connect('clicked') {
3199 if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3212 dialog.run { |response|
3217 if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3219 return [ true, nil ]
3221 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3230 if !ask_save_modifications(utf8(_("Save this album?")),
3231 utf8(_("Do you want to save the changes to this album?")),
3232 { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3235 dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3237 Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3238 [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3239 [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3241 frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3242 tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3243 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3244 tbl.attach(src = Gtk::Entry.new,
3245 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3246 tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3247 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3248 tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3249 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3250 tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3251 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3252 tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3253 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3254 tbl.attach(dest = Gtk::Entry.new,
3255 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3256 tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3257 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3258 tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3259 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3260 tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3261 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3262 tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3263 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3265 tooltips = Gtk::Tooltips.new
3266 frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3267 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3268 pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3269 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3270 pack_start(sizes = Gtk::HBox.new, false, false, 0))
3271 vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3272 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)
3273 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3274 pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3275 nperpage_model = Gtk::ListStore.new(String, String)
3276 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3277 pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3278 nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3279 nperpagecombo.set_attributes(crt, { :markup => 0 })
3280 iter = nperpage_model.append
3281 iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3283 [ 12, 20, 30, 40, 50 ].each { |v|
3284 iter = nperpage_model.append
3285 iter[0] = iter[1] = v.to_s
3287 nperpagecombo.active = 0
3289 multilanguages_value = nil
3290 vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3291 pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3292 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)
3293 multilanguages.signal_connect('clicked') {
3294 retval = ask_multi_languages(multilanguages_value)
3296 multilanguages_value = retval[1]
3298 if multilanguages_value
3299 ml_label.text = utf8(_("Multi-languages: enabled."))
3301 ml_label.text = utf8(_("Multi-languages: disabled."))
3304 if $config['default-multi-languages']
3305 multilanguages_value = $config['default-multi-languages']
3306 ml_label.text = utf8(_("Multi-languages: enabled."))
3308 ml_label.text = utf8(_("Multi-languages: disabled."))
3311 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3312 pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3313 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)
3314 vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3315 pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3316 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)
3318 src_nb_calculated_for = ''
3320 process_src_nb = proc {
3321 if src.text != src_nb_calculated_for
3322 src_nb_calculated_for = src.text
3324 Thread.kill(src_nb_thread)
3327 if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3328 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3330 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3331 if File.readable?(from_utf8_safe(src_nb_calculated_for))
3332 src_nb_thread = Thread.new {
3333 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3334 total = { 'image' => 0, 'video' => 0, nil => 0 }
3335 `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3336 if File.basename(dir) =~ /^\./
3340 Dir.entries(dir.chomp).each { |file|
3341 total[entry2type(file)] += 1
3343 rescue Errno::EACCES, Errno::ENOENT
3347 gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3351 src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3354 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3360 timeout_src_nb = Gtk.timeout_add(100) {
3364 src_browse.signal_connect('clicked') {
3365 fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3367 Gtk::FileChooser::ACTION_SELECT_FOLDER,
3369 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3370 fc.transient_for = $main_window
3371 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3372 src.text = utf8(fc.filename)
3374 conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3379 dest_browse.signal_connect('clicked') {
3380 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3382 Gtk::FileChooser::ACTION_CREATE_FOLDER,
3384 [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3385 fc.transient_for = $main_window
3386 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3387 dest.text = utf8(fc.filename)
3392 conf_browse.signal_connect('clicked') {
3393 fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3395 Gtk::FileChooser::ACTION_SAVE,
3397 [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3398 fc.transient_for = $main_window
3399 fc.add_shortcut_folder(File.expand_path("~/.booh"))
3400 fc.set_current_folder(File.expand_path("~/.booh"))
3401 if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3402 conf.text = utf8(fc.filename)
3409 recreate_theme_config = proc {
3410 theme_sizes.each { |e| sizes.remove(e[:widget]) }
3412 select_theme(theme_button.label, 'all', optimize432.active?, nil)
3413 $images_size.each { |s|
3414 sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3418 tooltips.set_tip(cb, utf8(s['description']), nil)
3419 theme_sizes << { :widget => cb, :value => s['name'] }
3421 sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3422 tooltips = Gtk::Tooltips.new
3423 tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3424 theme_sizes << { :widget => cb, :value => 'original' }
3427 nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3430 $allowed_N_values.each { |n|
3432 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3434 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3436 tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3440 nperrows << { :widget => rb, :value => n }
3442 nperrowradios.show_all
3444 recreate_theme_config.call
3446 theme_button.signal_connect('clicked') {
3447 if newtheme = theme_choose(theme_button.label)
3448 theme_button.label = newtheme
3449 recreate_theme_config.call
3453 dialog.vbox.add(frame1)
3454 dialog.vbox.add(frame2)
3460 dialog.run { |response|
3461 if response == Gtk::Dialog::RESPONSE_OK
3462 srcdir = from_utf8_safe(src.text)
3463 destdir = from_utf8_safe(dest.text)
3464 confpath = from_utf8_safe(conf.text)
3465 if src.text != '' && srcdir == ''
3466 show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3468 elsif !File.directory?(srcdir)
3469 show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3471 elsif dest.text != '' && destdir == ''
3472 show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3474 elsif destdir != make_dest_filename(destdir)
3475 show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3477 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3478 keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3479 inside it will be permanently removed before creating the web-album!
3480 Are you sure you want to continue?")), { :okcancel => true })
3482 elsif File.exists?(destdir) && !File.directory?(destdir)
3483 show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3485 elsif conf.text == ''
3486 show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3488 elsif conf.text != '' && confpath == ''
3489 show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3491 elsif File.directory?(confpath)
3492 show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3494 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3495 show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3497 system("mkdir '#{destdir}'")
3498 if !File.directory?(destdir)
3499 show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3511 srcdir = from_utf8(src.text)
3512 destdir = from_utf8(dest.text)
3513 configskel = File.expand_path(from_utf8(conf.text))
3514 theme = theme_button.label