[DRE-commits] r3247 - in packages: . booh booh/branches booh/branches/upstream booh/branches/upstream/current booh/branches/upstream/current/bin

lucas at alioth.debian.org lucas at alioth.debian.org
Tue Mar 3 21:48:36 UTC 2009


Author: lucas
Date: 2009-03-03 21:48:36 +0000 (Tue, 03 Mar 2009)
New Revision: 3247

Added:
   packages/booh/
   packages/booh/branches/
   packages/booh/branches/upstream/
   packages/booh/branches/upstream/current/
   packages/booh/branches/upstream/current/bin/
   packages/booh/branches/upstream/current/bin/album2booh
   packages/booh/branches/upstream/current/bin/booh
   packages/booh/branches/upstream/current/bin/booh-backend
   packages/booh/branches/upstream/current/bin/booh-classifier
   packages/booh/branches/upstream/current/bin/booh-fix-whitebalance
   packages/booh/branches/upstream/current/bin/booh-gamma-correction
   packages/booh/branches/upstream/current/bin/webalbum2booh
Log:
[svn-inject] Installing original source of booh

Added: packages/booh/branches/upstream/current/bin/album2booh
===================================================================
--- packages/booh/branches/upstream/current/bin/album2booh	                        (rev 0)
+++ packages/booh/branches/upstream/current/bin/album2booh	2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,170 @@
+#! /usr/bin/ruby
+#
+#                         *  BOOH  *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+# Copyright (c) 2007 Stephane Fillod
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+# 'album' structure:
+# "captions.txt"
+# 	<directory><tab><title>
+# 	<picfilename><tab><title><tab><texte>
+# <directory>".captions.txt"
+# 	<picfilename><tab><title><tab><texte>
+# <directory>/<picfilename>".txt"
+# 	<free text>
+#
+# Since Booh requires UTF-8, you may need to convert caption files:
+# 	$ recode ISO-8859-1..UTF-8 captions.txt
+#
+
+require 'getoptlong'
+require 'gettext'
+include GetText
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+include Booh
+
+#- options
+$options = [
+    [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
+
+    [ '--config',        '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing images and videos within directories with captions") ],
+
+    [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+]
+
+#- default values for some globals 
+$switches = []
+$stdout.sync = true
+
+def usage
+    puts _("Usage: %s [OPTION]...") % File.basename($0)
+    $options.each { |ary|
+        printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
+    }
+end
+
+def handle_options
+    parser = GetoptLong.new
+    parser.set_options(*$options.collect { |ary| ary[0..2] })
+    begin
+        parser.each_option do |name, arg|
+            case name
+            when '--help'
+                usage
+                exit(0)
+
+            when '--config'
+                if File.readable?(arg)
+                    $xmldoc = REXML::Document.new File.new(arg)
+                    $conffile = arg
+                else
+                    die_ _('Config file does not exist or is unreadable.')
+                end
+
+            when '--verbose-level'
+                $verbose_level = arg.to_i
+
+            end
+        end
+    rescue
+        puts $!
+        usage
+        exit(1)
+    end
+
+    if !$xmldoc
+        die_ _("Missing --config parameter.")
+    end
+
+    $source = $xmldoc.root.attributes['source']
+    $dest = $xmldoc.root.attributes['destination']
+end
+
+def utf8_and_entities(string)
+    return utf8(string).gsub('&agrave;', 'à').
+                        gsub('&ccedil;', 'ç').
+                        gsub('&ocirc;',  'ô').
+                        gsub('&eacute;', 'é').
+                        gsub('&ecirc;',  'ê').
+                        gsub('&egrave;', 'è').
+                        gsub('&Egrave;', 'È').
+                        gsub('&icirc;',  'î').
+                        gsub('&lt;',     '<').
+                        gsub('&gt;',     '>').
+                        gsub('&ugrave;', 'ù').
+                        gsub('&quot;',   '"')
+end
+
+def parse_album_captionstxt(filepath)
+    begin
+        contents = File.open(filepath).readlines
+        out = {}
+        out[:legends] = {}
+        for line in contents
+            if line =~ /^(.*)\t(.*)\t(.*)/
+                out[:legends][$1] = $2 + "\n" + $3
+            elsif line =~ /^(.*)\t(.*)/
+                out[:legends][$1] = $2
+            end
+        end
+        return out
+    rescue
+        return nil
+    end
+end
+
+def walk_source_dir
+
+    `find #{$source} -type d`.sort.each { |dir|
+        dir.chomp!
+        msg 2, _("Handling %s from config list...") % dir
+
+        if !infos = parse_album_captionstxt("#{dir}/captions.txt")
+            next
+        end
+
+        #- place xml document on proper node
+        xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
+
+        #if infos.has_key?(:title)
+        #    type = find_subalbum_info_type(xmldir)
+        #    xmldir.add_attribute("#{type}-caption", utf8_and_entities(infos[:title]))
+        #end
+        
+        xmldir.elements.each { |element|
+            if %w(image video).include?(element.name)
+                if infos[:legends].has_key?(element.attributes['filename'])
+                    element.add_attribute('caption', utf8_and_entities(infos[:legends][element.attributes['filename']]))
+                end
+            end
+        }
+    }
+end
+
+
+handle_options
+
+walk_source_dir
+
+ios = File.open("#{$conffile}.merged", "w")
+$xmldoc.write(ios, 0)
+ios.close


Property changes on: packages/booh/branches/upstream/current/bin/album2booh
___________________________________________________________________
Name: svn:executable
   + 

Added: packages/booh/branches/upstream/current/bin/booh
===================================================================
--- packages/booh/branches/upstream/current/bin/booh	                        (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh	2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,4593 @@
+#! /usr/bin/ruby
+#
+#                         *  BOOH  *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'getoptlong'
+require 'tempfile'
+require 'thread'
+
+require 'gtk2'
+require 'booh/libadds'
+require 'booh/GtkAutoTable'
+
+require 'gettext'
+include GetText
+bindtextdomain("booh")
+
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+include Booh
+require 'booh/UndoHandler'
+require 'booh/Synchronizator'
+
+
+#- options
+$options = [
+    [ '--help',          '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
+    [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+    [ '--version',       '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
+]
+
+#- default values for some globals 
+$xmldir = nil
+$modified = false
+$current_cursor = nil
+$ignore_videos = false
+$button1_pressed_autotable = false
+$generated_outofline = false
+
+def usage
+    puts _("Usage: %s [OPTION]...") % File.basename($0)
+    $options.each { |ary|
+        printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
+    }
+end
+
+def handle_options
+    parser = GetoptLong.new
+    parser.set_options(*$options.collect { |ary| ary[0..2] })
+    begin
+        parser.each_option do |name, arg|
+            case name
+            when '--help'
+                usage
+                exit(0)
+
+            when '--version'
+                puts _("Booh version %s
+
+Copyright (c) 2005-2008 Guillaume Cottenceau.
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
+
+                exit(0)
+
+            when '--verbose-level'
+                $verbose_level = arg.to_i
+
+            end
+        end
+    rescue
+        puts $!
+        usage
+        exit(1)
+    end
+end
+
+def read_config
+    $config = {}
+    $config_file = File.expand_path('~/.booh-gui-rc')
+    if File.readable?($config_file)
+        $xmldoc = REXML::Document.new(File.new($config_file))
+        $xmldoc.root.elements.each { |element|
+            txt = element.get_text
+            if txt
+                if txt.value =~ /~~~/ || element.name == 'last-opens'
+                    $config[element.name] = txt.value.split(/~~~/)
+                else
+                    $config[element.name] = txt.value
+                end
+            elsif element.elements.size == 0
+                $config[element.name] = ''
+            else
+                $config[element.name] = {}
+                element.each { |chld|
+                    txt = chld.get_text
+                    $config[element.name][chld.name] = txt ? txt.value : nil
+                }
+            end
+        }
+    end
+    $config['video-viewer'] ||= '/usr/bin/mplayer %f'
+    $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
+    $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
+    $config['comments-format'] ||= '%t'
+    if !FileTest.directory?(File.expand_path('~/.booh'))
+        system("mkdir ~/.booh")
+    end
+    if $config['mproc'].nil?
+        cpus = 0
+        for line in IO.readlines('/proc/cpuinfo') do
+            line =~ /^processor/ and cpus += 1
+        end
+        if cpus > 1
+            $config['mproc'] = cpus
+        end
+    end
+    $config['rotate-set-exif'] ||= 'true'
+    $tempfiles = []
+    $todelete = []
+end
+
+def check_config
+    if !system("which convert >/dev/null 2>/dev/null")
+        show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
+It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
+        exit 1
+    end
+    if !system("which identify >/dev/null 2>/dev/null")
+        show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
+It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
+    end
+    if !system("which exif >/dev/null 2>/dev/null")
+        show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
+    end
+    missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
+    if missing != []
+        show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
+    end
+
+    viewer_binary = $config['video-viewer'].split.first
+    if viewer_binary && !File.executable?(viewer_binary)
+        show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
+You should fix this in Edit/Preferences so that you can view videos.
+
+Problem was: '%s' is not an executable file.
+Hint: don't forget to specify the full path to the executable,
+e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
+    end
+    image_editor_binary = $config['image-editor'].split.first
+    if image_editor_binary && !File.executable?(image_editor_binary)
+        show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
+You should fix this in Edit/Preferences so that you can edit photos externally.
+
+Problem was: '%s' is not an executable file.
+Hint: don't forget to specify the full path to the executable,
+e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
+    end
+    browser_binary = $config['browser'].split.first
+    if browser_binary && !File.executable?(browser_binary)
+        show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
+You should fix this in Edit/Preferences so that you can open URLs.
+
+Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
+    end
+end
+
+def write_config
+    if $config['last-opens'] && $config['last-opens'].size > 10
+        $config['last-opens'] = $config['last-opens'][-10, 10]
+    end
+
+    $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
+    $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
+    $config.each_pair { |key, value|
+        elem = $xmldoc.root.add_element key
+        if value.is_a? Hash
+            $config[key].each_pair { |subkey, subvalue|
+                subelem = elem.add_element subkey
+                subelem.add_text subvalue.to_s
+            }
+        elsif value.is_a? Array
+            elem.add_text value.join('~~~')
+        else
+            if !value
+                elem.remove
+            else
+                elem.add_text value.to_s
+            end
+        end
+    }
+    ios = File.open($config_file, "w")
+    $xmldoc.write(ios, 0)
+    ios.close
+
+    $tempfiles.each { |f|
+        if File.exists?(f)
+            File.delete(f)
+        end
+    }
+end
+
+def set_mousecursor(what, *widget)
+    cursor = what.nil? ? nil : Gdk::Cursor.new(what)
+    if widget[0] && widget[0].window
+        widget[0].window.cursor = cursor
+    end
+    if $main_window && $main_window.window
+        $main_window.window.cursor = cursor
+    end
+    $current_cursor = what
+end
+def set_mousecursor_wait(*widget)
+    gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
+    if Thread.current == Thread.main
+        Gtk.main_iteration while Gtk.events_pending?
+    end
+end
+def set_mousecursor_normal(*widget)
+    gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
+end
+def push_mousecursor_wait(*widget)
+    if $current_cursor != Gdk::Cursor::WATCH
+        $save_cursor = $current_cursor
+        gtk_thread_protect { set_mousecursor_wait(*widget) }
+    end
+end
+def pop_mousecursor(*widget)
+    gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
+end
+
+def current_dest_dir
+    source = $xmldoc.root.attributes['source']
+    dest = $xmldoc.root.attributes['destination']
+    return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
+end
+
+def full_src_dir_to_rel(path, source)
+    return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
+end
+
+def build_full_dest_filename(filename)
+    return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
+end
+
+def save_undo(name, closure, *params)
+    UndoHandler.save_undo(name, closure, [ *params ])
+    $undo_tb.sensitive = $undo_mb.sensitive = true
+    $redo_tb.sensitive = $redo_mb.sensitive = false
+end
+
+def view_element(filename, closures)
+    if entry2type(filename) == 'video'
+        cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
+        msg 2, cmd
+        system(cmd)
+        return
+    end
+
+    w = create_window.set_title(filename)
+
+    msg 3, "filename: #{filename}"
+    dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
+    #- typically this file won't exist in case of videos; try with the largest thumbnail around
+    if !File.exists?(dest_img)
+        if entry2type(filename) == 'video'
+            alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
+            if not alternatives.empty?
+                dest_img = alternatives[-1]
+            end
+        else
+            push_mousecursor_wait
+            gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
+            pop_mousecursor
+            if !File.exists?(dest_img)
+                msg 2, _("Could not generate fullscreen thumbnail!")
+                return
+                end
+        end
+    end
+    evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
+    evt.signal_connect('button-press-event') { |this, event|
+        if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
+            $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
+        end
+        if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
+            menu = Gtk::Menu.new
+            menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
+            delete_item.signal_connect('activate') {
+                w.destroy
+                closures[:delete].call(false)
+            }
+            menu.show_all
+            menu.popup(nil, nil, event.button, event.time)
+        end
+    }
+    evt.signal_connect('button-release-event') { |this, event|
+        if $gesture_press
+            if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
+                msg 3, "gesture delete: click-drag right button to the bottom"
+                w.destroy
+                closures[:delete].call(false)
+                $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
+            end
+        end
+    }
+    tooltips = Gtk::Tooltips.new
+    tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
+
+    w.signal_connect('key-press-event') { |w,event|
+        if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
+            w.destroy
+            closures[:delete].call(false)
+        end
+    }
+
+    bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
+    b.signal_connect('clicked') { w.destroy }
+
+    vb = Gtk::VBox.new
+    vb.pack_start(evt, false, false)
+    vb.pack_end(bottom, false, false)
+
+    w.add(vb)
+    w.signal_connect('delete-event') { w.destroy }
+    w.window_position = Gtk::Window::POS_CENTER
+    w.show_all
+end
+
+def scroll_upper(scrolledwindow, ypos_top)
+    newval = scrolledwindow.vadjustment.value -
+        ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
+    if newval < scrolledwindow.vadjustment.lower
+        newval = scrolledwindow.vadjustment.lower
+    end
+    scrolledwindow.vadjustment.value = newval
+end
+
+def scroll_lower(scrolledwindow, ypos_bottom)
+    newval = scrolledwindow.vadjustment.value +
+        ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
+    if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
+        newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
+    end
+    scrolledwindow.vadjustment.value = newval
+end
+
+def autoscroll_if_needed(scrolledwindow, image, textview)
+    #- autoscroll if cursor or image is not visible, if possible
+    if image && image.window || textview.window
+        ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
+        ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
+        current_miny_visible = scrolledwindow.vadjustment.value
+        current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
+        if ypos_top < current_miny_visible
+            scroll_upper(scrolledwindow, ypos_top)
+        elsif ypos_bottom > current_maxy_visible
+            scroll_lower(scrolledwindow, ypos_bottom)
+        end
+    end
+end
+
+def create_editzone(scrolledwindow, pagenum, image)
+    frame = Gtk::Frame.new
+    frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
+    frame.set_shadow_type(Gtk::SHADOW_IN)
+    textview.signal_connect('key-press-event') { |w, event|
+        textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
+        if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
+            scrolledwindow.signal_emit('key-press-event', event)
+        end
+        if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
+           event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
+            if event.keyval == Gdk::Keyval::GDK_Up
+                if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
+                    scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
+                else
+                    scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
+                end
+            else
+                if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
+                    scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
+                else
+                    scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
+                end
+            end
+        end
+        false  #- propagate
+    }
+
+    candidate_undo_text = nil
+    textview.signal_connect('focus-in-event') { |w, event|
+        textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
+        candidate_undo_text = textview.buffer.text
+        false  #- propagate
+    }
+
+    textview.signal_connect('key-release-event') { |w, event|
+        if candidate_undo_text && candidate_undo_text != textview.buffer.text
+            $modified = true
+            save_undo(_("text edit"),
+                      proc { |text|
+                          save_text = textview.buffer.text
+                          textview.buffer.text = text
+                          textview.grab_focus
+                          $notebook.set_page(pagenum)
+                          proc {
+                              textview.buffer.text = save_text
+                              textview.grab_focus
+                              $notebook.set_page(pagenum)
+                          }
+                      }, candidate_undo_text)
+            candidate_undo_text = nil
+        end
+
+        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)
+            autoscroll_if_needed(scrolledwindow, image, textview)
+        end
+        false  #- propagate
+    }
+
+    return [ frame, textview ]
+end
+
+def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
+
+    if !$modified_pixbufs[thumbnail_img]
+        $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
+    elsif !$modified_pixbufs[thumbnail_img][:orig]
+        $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
+    end
+
+    pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
+
+    #- rotate
+    if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
+        pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
+        msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
+        if pixbuf.height > desired_y
+            pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
+        elsif pixbuf.width < desired_x && pixbuf.height < desired_y
+            pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
+        end
+    end
+
+    #- fix white balance
+    if $modified_pixbufs[thumbnail_img][:whitebalance]
+        pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
+    end
+
+    #- fix gamma correction
+    if $modified_pixbufs[thumbnail_img][:gammacorrect]
+        pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
+    end
+
+    img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
+end
+
+def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+    $modified = true
+
+    #- update rotate attribute
+    new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
+    xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
+
+    if $config['rotate-set-exif'] == 'true'
+        Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
+    end
+
+    $modified_pixbufs[thumbnail_img] ||= {}
+    $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
+    msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
+
+    update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
+end
+
+def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+    $modified = true
+
+    rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+
+    save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
+              proc { |angle|
+                  rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+                  $notebook.set_page(attributes_prefix != '' ? 0 : 1)
+                  proc {
+                      rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+                      $notebook.set_page(0)
+                      $notebook.set_page(attributes_prefix != '' ? 0 : 1)
+                  }
+              }, -angle)
+end
+
+def color_swap(xmldir, attributes_prefix)
+    $modified = true
+    if xmldir.attributes["#{attributes_prefix}color-swap"]
+        xmldir.delete_attribute("#{attributes_prefix}color-swap")
+    else
+        xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
+    end
+end
+
+def enhance(xmldir, attributes_prefix)
+    $modified = true
+    if xmldir.attributes["#{attributes_prefix}enhance"]
+        xmldir.delete_attribute("#{attributes_prefix}enhance")
+    else
+        xmldir.add_attribute("#{attributes_prefix}enhance", '1')
+    end
+end
+
+def change_seektime(xmldir, attributes_prefix, value)
+    $modified = true
+    xmldir.add_attribute("#{attributes_prefix}seektime", value)
+end
+
+def ask_new_seektime(xmldir, attributes_prefix)
+    if xmldir
+        value = xmldir.attributes["#{attributes_prefix}seektime"]
+    else
+        value = ''
+    end
+
+    dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    lbl = Gtk::Label.new
+    lbl.markup = utf8(
+_("Please specify the <b>seek time</b> of the video, to take the thumbnail
+from, in seconds.
+"))
+    dialog.vbox.add(lbl)
+    dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
+    entry.signal_connect('key-press-event') { |w, event|
+        if event.keyval == Gdk::Keyval::GDK_Return
+            dialog.response(Gtk::Dialog::RESPONSE_OK)
+            true
+        elsif event.keyval == Gdk::Keyval::GDK_Escape
+            dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
+            true
+        else
+            false  #- propagate if needed
+        end
+    }
+
+    dialog.window_position = Gtk::Window::POS_MOUSE
+    dialog.show_all
+
+    dialog.run { |response|
+        newval = entry.text
+        dialog.destroy
+        if response == Gtk::Dialog::RESPONSE_OK
+            $modified = true
+            msg 3, "changing seektime to #{newval}"
+            return { :old => value, :new => newval }
+        else
+            return nil
+        end
+    }
+end
+
+def change_pano_amount(xmldir, attributes_prefix, value)
+    $modified = true
+    if value.nil?
+        xmldir.delete_attribute("#{attributes_prefix}pano-amount")
+    else
+        xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
+    end
+end
+
+def ask_new_pano_amount(xmldir, attributes_prefix)
+    if xmldir
+        value = xmldir.attributes["#{attributes_prefix}pano-amount"]
+    else
+        value = nil
+    end
+
+    dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    lbl = Gtk::Label.new
+    lbl.markup = utf8(
+_("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
+of this panorama image compared to other regular images. For example, if the panorama
+was taken out of four photos on one row, counting the necessary overlap, the width of
+this panorama image should probably be roughly three times the width of regular images.
+
+With this information, booh will be able to generate panorama thumbnails looking
+the right 'size', since the height of the thumbnail for this image will be similar
+to the height of other thumbnails.
+"))
+    dialog.vbox.add(lbl)
+    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)")))).
+                                                                         add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
+                                                                         add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
+                                                                         add(Gtk::Label.new(utf8(_("times the width of other images"))))))
+    spin.signal_connect('value-changed') {
+        rb_yes.active = true
+    }
+    dialog.window_position = Gtk::Window::POS_MOUSE
+    dialog.show_all
+    if value
+        spin.value = value.to_f
+        rb_yes.active = true
+        spin.grab_focus
+    else
+        rb_no.active = true
+    end
+
+    dialog.run { |response|
+        if rb_no.active?
+            newval = nil
+        else
+            newval = spin.value.to_f
+        end
+        dialog.destroy
+        if response == Gtk::Dialog::RESPONSE_OK
+            $modified = true
+            msg 3, "changing panorama amount to #{newval}"
+            return { :old => value, :new => newval }
+        else
+            return nil
+        end
+    }
+end
+
+def change_whitebalance(xmlelem, attributes_prefix, value)
+    $modified = true
+    xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
+end
+
+def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+
+    #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
+    if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
+        save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
+        xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
+        save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
+        xmlelem.delete_attribute("#{attributes_prefix}white-balance")
+        destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
+        gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
+                                xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
+        $modified_pixbufs[thumbnail_img] ||= {}
+        $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
+        xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
+        if save_gammacorrect
+            xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
+            $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
+        end
+        $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
+    end
+
+    $modified_pixbufs[thumbnail_img] ||= {}
+    $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
+
+    update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
+end
+
+def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+    #- init $modified_pixbufs correctly
+#    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
+
+    value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
+
+    dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    lbl = Gtk::Label.new
+    lbl.markup = utf8(
+_("You can fix the <b>white balance</b> of the image, if your image is too blue
+or too yellow because the recorder didn't detect the light correctly. Drag the
+slider below the image to the left for more blue, to the right for more yellow.
+"))
+    dialog.vbox.add(lbl)
+    if img_
+        dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
+    end
+    dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
+    
+    dialog.window_position = Gtk::Window::POS_MOUSE
+    dialog.show_all
+
+    lastval = nil
+    timeout = Gtk.timeout_add(100) {
+        if hs.value != lastval
+            lastval = hs.value
+            if img_
+                recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+            end
+        end
+        true
+    }
+
+    dialog.run { |response|
+        Gtk.timeout_remove(timeout)
+        if response == Gtk::Dialog::RESPONSE_OK
+            $modified = true
+            newval = hs.value.to_s
+            msg 3, "changing white balance to #{newval}"
+            dialog.destroy
+            return { :old => value, :new => newval }
+        else
+            if thumbnail_img
+                $modified_pixbufs[thumbnail_img] ||= {}
+                $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
+                $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
+            end
+            dialog.destroy
+            return nil
+        end
+    }
+end
+
+def change_gammacorrect(xmlelem, attributes_prefix, value)
+    $modified = true
+    xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
+end
+
+def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+
+    #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
+    if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
+        save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
+        xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
+        save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
+        xmlelem.delete_attribute("#{attributes_prefix}white-balance")
+        destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
+        gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
+                                xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
+        $modified_pixbufs[thumbnail_img] ||= {}
+        $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
+        xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
+        if save_whitebalance
+            xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
+            $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
+        end
+        $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
+    end
+
+    $modified_pixbufs[thumbnail_img] ||= {}
+    $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
+
+    update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
+end
+
+def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+    #- init $modified_pixbufs correctly
+#    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
+
+    value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
+
+    dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    lbl = Gtk::Label.new
+    lbl.markup = utf8(
+_("You can perform <b>gamma correction</b> of the image, if your image is too dark
+or too bright. Drag the slider below the image.
+"))
+    dialog.vbox.add(lbl)
+    if img_
+        dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
+    end
+    dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
+    
+    dialog.window_position = Gtk::Window::POS_MOUSE
+    dialog.show_all
+
+    lastval = nil
+    timeout = Gtk.timeout_add(100) {
+        if hs.value != lastval
+            lastval = hs.value
+            if img_
+                recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+            end
+        end
+        true
+    }
+
+    dialog.run { |response|
+        Gtk.timeout_remove(timeout)
+        if response == Gtk::Dialog::RESPONSE_OK
+            $modified = true
+            newval = hs.value.to_s
+            msg 3, "gamma correction to #{newval}"
+            dialog.destroy
+            return { :old => value, :new => newval }
+        else
+            if thumbnail_img
+                $modified_pixbufs[thumbnail_img] ||= {}
+                $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
+                $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
+            end
+            dialog.destroy
+            return nil
+        end
+    }
+end
+
+def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
+    if File.exists?(destfile)
+        File.delete(destfile)
+    end
+    #- type can be 'element' or 'subdir'
+    if type == 'element'
+        gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
+    else
+        gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
+    end
+end
+
+def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
+    Thread.new {
+        push_mousecursor_wait
+        gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
+        gtk_thread_protect {
+            img.set(destfile)
+            $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
+        }
+        pop_mousecursor
+    }
+end
+
+def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
+    distribute_multiple_call = Proc.new { |action, arg|
+        $selected_elements.each_key { |path|
+            $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
+        }
+        if possible_actions[:can_multiple] && $selected_elements.length > 0
+            UndoHandler.begin_batch
+            $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
+            UndoHandler.end_batch
+        else
+            closures[action].call(arg)
+        end
+        $selected_elements = {}
+    }
+    menu = Gtk::Menu.new
+    if optionals.include?('change_image')
+        menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
+        changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
+        changeimg.signal_connect('activate') { closures[:change].call }
+        menu.append(Gtk::SeparatorMenuItem.new)
+    end
+    if !possible_actions[:can_multiple] || $selected_elements.length == 0
+        if closures[:view]
+            if type == 'image'
+                menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
+                view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
+                view.signal_connect('activate') { closures[:view].call }
+            else
+                menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
+                view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
+                view.signal_connect('activate') { closures[:view].call }
+                menu.append(Gtk::SeparatorMenuItem.new)
+            end
+        end
+        if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
+            menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
+            exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
+            exif.signal_connect('activate') { show_popup($main_window,
+                                                         utf8(`exif -m '#{fullpath}'`),
+                                                         { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
+            menu.append(Gtk::SeparatorMenuItem.new)
+        end
+    end
+    menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
+    r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
+    r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
+    menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
+    r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
+    r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
+    if !possible_actions[:can_multiple] || $selected_elements.length == 0
+        menu.append(Gtk::SeparatorMenuItem.new)
+        if !possible_actions[:forbid_left]
+            menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
+            moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
+            moveleft.signal_connect('activate') { closures[:move].call('left') }
+            if !possible_actions[:can_left]
+                moveleft.sensitive = false
+            end
+        end
+        if !possible_actions[:forbid_right]
+            menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
+            moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
+            moveright.signal_connect('activate') { closures[:move].call('right') }
+            if !possible_actions[:can_right]
+                moveright.sensitive = false
+            end
+        end
+        if optionals.include?('move_top')
+            menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
+            movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
+            movetop.signal_connect('activate') { closures[:move].call('top') }
+            if !possible_actions[:can_top]
+                movetop.sensitive = false
+            end
+        end
+        menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
+        moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
+        moveup.signal_connect('activate') { closures[:move].call('up') }
+        if !possible_actions[:can_up]
+            moveup.sensitive = false
+        end
+        menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
+        movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
+        movedown.signal_connect('activate') { closures[:move].call('down') }
+        if !possible_actions[:can_down]
+            movedown.sensitive = false
+        end
+        if optionals.include?('move_bottom')
+            menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
+            movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
+            movebottom.signal_connect('activate') { closures[:move].call('bottom') }
+            if !possible_actions[:can_bottom]
+                movebottom.sensitive = false
+            end
+        end
+    end
+    if type == 'video'
+        if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
+            menu.append(Gtk::SeparatorMenuItem.new)
+#            menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
+#            color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
+#            color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
+            menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
+            flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
+            flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
+            menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
+            seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
+            seektime.signal_connect('activate') {
+                if possible_actions[:can_multiple] && $selected_elements.length > 0
+                    if values = ask_new_seektime(nil, '')
+                        distribute_multiple_call.call(:seektime, values)
+                    end
+                else
+                    closures[:seektime].call
+                end
+            }
+        end
+    end
+    menu.append(               Gtk::SeparatorMenuItem.new)
+    menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
+    gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
+    gammacorrect.signal_connect('activate') { 
+        if possible_actions[:can_multiple] && $selected_elements.length > 0
+            if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
+                distribute_multiple_call.call(:gammacorrect, values)
+            end
+        else
+            closures[:gammacorrect].call
+        end
+    }
+    menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
+    whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
+    whitebalance.signal_connect('activate') { 
+        if possible_actions[:can_multiple] && $selected_elements.length > 0
+            if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
+                distribute_multiple_call.call(:whitebalance, values)
+            end
+        else
+            closures[:whitebalance].call
+        end
+    }
+    if !possible_actions[:can_multiple] || $selected_elements.length == 0
+        menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
+                                                                                                             _("Enhance constrast"))))
+    else
+        menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
+    end
+    enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
+    enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
+    if type == 'image' && possible_actions[:can_panorama]
+        menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
+        panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
+        panorama.signal_connect('activate') {
+            if possible_actions[:can_multiple] && $selected_elements.length > 0
+                if values = ask_new_pano_amount(nil, '')
+                    distribute_multiple_call.call(:pano, values)
+                end
+            else
+                distribute_multiple_call.call(:pano)
+            end
+       }
+    end
+    menu.append(               Gtk::SeparatorMenuItem.new)
+    if optionals.include?('delete')
+        menu.append(cut_item     = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
+        cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
+        if !possible_actions[:can_multiple] || $selected_elements.length == 0
+            menu.append(paste_item   = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
+            paste_item.signal_connect('activate') { closures[:paste].call }
+            menu.append(clear_item   = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
+            clear_item.signal_connect('activate') { $cuts = [] }
+            if $cuts.size == 0
+                paste_item.sensitive = clear_item.sensitive = false
+            end
+        end
+        menu.append(               Gtk::SeparatorMenuItem.new)
+    end
+    if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
+        menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
+        editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
+        editexternally.signal_connect('activate') {
+            cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
+            msg 2, cmd
+            system(cmd)
+        }
+    end
+    menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
+    refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
+    if optionals.include?('delete')
+        menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
+        delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
+    end
+    menu.show_all
+    menu.popup(nil, nil, event.button, event.time)
+end
+
+def delete_current_subalbum
+    $modified = true
+    sel = $albums_tv.selection.selected_rows
+    $xmldir.elements.each { |e|
+        if e.name == 'image' || e.name == 'video'
+            e.add_attribute('deleted', 'true')
+        end
+    }
+    #- branch if we have a non deleted subalbum
+    if $xmldir.child_byname_notattr('dir', 'deleted')
+        $xmldir.delete_attribute('thumbnails-caption')
+        $xmldir.delete_attribute('thumbnails-captionfile')
+    else
+        $xmldir.add_attribute('deleted', 'true')
+        moveup = $xmldir
+        while moveup.parent.name == 'dir'
+            moveup = moveup.parent
+            if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
+                moveup.add_attribute('deleted', 'true')
+            else
+                break
+            end
+        end
+        sel[0].up!
+    end
+    save_changes('forced')
+    populate_subalbums_treeview(false)
+    $albums_tv.selection.select_path(sel[0])
+end
+
+def restore_deleted
+    $modified = true
+    save_changes
+    $current_path = nil  #- prevent save_changes from being rerun again
+    sel = $albums_tv.selection.selected_rows
+    restore_one = proc { |xmldir|
+        xmldir.elements.each { |e|
+            if e.name == 'dir' && e.attributes['deleted']
+                restore_one.call(e)
+            end
+            e.delete_attribute('deleted')
+        }
+    }
+    restore_one.call($xmldir)
+    populate_subalbums_treeview(false)
+    $albums_tv.selection.select_path(sel[0])
+end
+
+def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
+
+    img = nil
+    frame1 = Gtk::Frame.new
+    fullpath = from_utf8("#{$current_path}/#{filename}")
+
+    my_gen_real_thumbnail = proc {
+        gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
+    }
+
+    if type == 'video'
+        pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
+        frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
+                                 pack_start(img = Gtk::Image.new).
+                                 pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
+        px, mask = pxb.render_pixmap_and_mask(0)
+        da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
+        da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
+    else
+        frame1.add(img = Gtk::Image.new)
+    end
+
+    #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
+    if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
+        my_gen_real_thumbnail.call
+    else
+        img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
+    end
+
+    evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
+
+    tooltips = Gtk::Tooltips.new
+    tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
+    tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
+
+    frame2, textview = create_editzone($autotable_sw, 1, img)
+    textview.buffer.text = caption
+    textview.set_justification(Gtk::Justification::CENTER)
+
+    vbox = Gtk::VBox.new(false, 5)
+    vbox.pack_start(evtbox, false, false)
+    vbox.pack_start(frame2, false, false)
+    autotable.append(vbox, filename)
+
+    #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
+    $vbox2widgets[vbox] = { :textview => textview, :image => img }
+
+    #- to be able to find widgets by name
+    $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
+
+    cleanup_all_thumbnails = proc {
+        #- remove out of sync images
+        dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
+        for sizeobj in $images_size
+            #- cannot use sizeobj because panoramic images will have a larger width
+            Dir.glob("#{dest_img_base}-*.jpg") do |file|
+                File.delete(file)
+            end
+        end
+
+    }
+
+    refresh = proc {
+        cleanup_all_thumbnails.call
+        #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
+        $modified = true
+        $xmldir.delete_attribute('already-generated')
+        my_gen_real_thumbnail.call
+    }
+ 
+    rotate_and_cleanup = proc { |angle|
+        cleanup_all_thumbnails.call
+        rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
+    }
+
+    move = proc { |direction|
+        do_method = "move_#{direction}"
+        undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
+        perform = proc {
+            done = autotable.method(do_method).call(vbox)
+            textview.grab_focus  #- because if moving, focus is stolen
+            done
+        }
+        if perform.call
+            save_undo(_("move %s") % direction,
+                      proc {
+                          autotable.method(undo_method).call(vbox)
+                          textview.grab_focus  #- because if moving, focus is stolen
+                          autoscroll_if_needed($autotable_sw, img, textview)
+                          $notebook.set_page(1)
+                          proc {
+                              autotable.method(do_method).call(vbox)
+                              textview.grab_focus  #- because if moving, focus is stolen
+                              autoscroll_if_needed($autotable_sw, img, textview)
+                              $notebook.set_page(1)
+                          }
+                      })
+        end
+    }
+
+    color_swap_and_cleanup = proc {
+        perform_color_swap_and_cleanup = proc {
+            cleanup_all_thumbnails.call
+            color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
+            my_gen_real_thumbnail.call
+        }
+
+        perform_color_swap_and_cleanup.call
+
+        save_undo(_("color swap"),
+                  proc {
+                      perform_color_swap_and_cleanup.call
+                      textview.grab_focus
+                      autoscroll_if_needed($autotable_sw, img, textview)
+                      $notebook.set_page(1)
+                      proc {
+                          perform_color_swap_and_cleanup.call
+                          textview.grab_focus
+                          autoscroll_if_needed($autotable_sw, img, textview)
+                          $notebook.set_page(1)
+                      }
+                  })
+    }
+
+    change_seektime_and_cleanup_real = proc { |values|
+        perform_change_seektime_and_cleanup = proc { |val|
+            cleanup_all_thumbnails.call
+            change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
+            my_gen_real_thumbnail.call
+        }
+        perform_change_seektime_and_cleanup.call(values[:new])
+        
+        save_undo(_("specify seektime"),
+                  proc {
+                      perform_change_seektime_and_cleanup.call(values[:old])
+                      textview.grab_focus
+                      autoscroll_if_needed($autotable_sw, img, textview)
+                      $notebook.set_page(1)
+                      proc {
+                          perform_change_seektime_and_cleanup.call(values[:new])
+                          textview.grab_focus
+                          autoscroll_if_needed($autotable_sw, img, textview)
+                          $notebook.set_page(1)
+                      }
+                  })
+    }
+
+    change_seektime_and_cleanup = proc {
+        if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
+            change_seektime_and_cleanup_real.call(values)
+        end
+    }
+
+    change_pano_amount_and_cleanup_real = proc { |values|
+        perform_change_pano_amount_and_cleanup = proc { |val|
+            cleanup_all_thumbnails.call
+            change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
+        }
+        perform_change_pano_amount_and_cleanup.call(values[:new])
+        
+        save_undo(_("change panorama amount"),
+                  proc {
+                      perform_change_pano_amount_and_cleanup.call(values[:old])
+                      textview.grab_focus
+                      autoscroll_if_needed($autotable_sw, img, textview)
+                      $notebook.set_page(1)
+                      proc {
+                          perform_change_pano_amount_and_cleanup.call(values[:new])
+                          textview.grab_focus
+                          autoscroll_if_needed($autotable_sw, img, textview)
+                          $notebook.set_page(1)
+                      }
+                  })
+    }
+
+    change_pano_amount_and_cleanup = proc {
+        if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
+            change_pano_amount_and_cleanup_real.call(values)
+        end
+    }
+
+    whitebalance_and_cleanup_real = proc { |values|
+        perform_change_whitebalance_and_cleanup = proc { |val|
+            cleanup_all_thumbnails.call
+            change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
+            recalc_whitebalance(val, fullpath, thumbnail_img, img,
+                                $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+        }
+        perform_change_whitebalance_and_cleanup.call(values[:new])
+
+        save_undo(_("fix white balance"),
+                  proc {
+                      perform_change_whitebalance_and_cleanup.call(values[:old])
+                      textview.grab_focus
+                      autoscroll_if_needed($autotable_sw, img, textview)
+                      $notebook.set_page(1)
+                      proc {
+                          perform_change_whitebalance_and_cleanup.call(values[:new])
+                          textview.grab_focus
+                          autoscroll_if_needed($autotable_sw, img, textview)
+                          $notebook.set_page(1)
+                      }
+                  })
+    }
+
+    whitebalance_and_cleanup = proc {
+        if values = ask_whitebalance(fullpath, thumbnail_img, img,
+                                     $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+            whitebalance_and_cleanup_real.call(values)
+        end
+    }
+
+    gammacorrect_and_cleanup_real = proc { |values|
+        perform_change_gammacorrect_and_cleanup = Proc.new { |val|
+            cleanup_all_thumbnails.call
+            change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
+            recalc_gammacorrect(val, fullpath, thumbnail_img, img,
+                                $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+        }
+        perform_change_gammacorrect_and_cleanup.call(values[:new])
+        
+        save_undo(_("gamma correction"),
+                  Proc.new {
+                      perform_change_gammacorrect_and_cleanup.call(values[:old])
+                      textview.grab_focus
+                      autoscroll_if_needed($autotable_sw, img, textview)
+                      $notebook.set_page(1)
+                      Proc.new {
+                          perform_change_gammacorrect_and_cleanup.call(values[:new])
+                          textview.grab_focus
+                          autoscroll_if_needed($autotable_sw, img, textview)
+                          $notebook.set_page(1)
+                      }
+                  })
+    }
+    
+    gammacorrect_and_cleanup = Proc.new {
+        if values = ask_gammacorrect(fullpath, thumbnail_img, img,
+                                     $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+            gammacorrect_and_cleanup_real.call(values)
+        end
+    }
+    
+    enhance_and_cleanup = proc {
+        perform_enhance_and_cleanup = proc {
+            cleanup_all_thumbnails.call
+            enhance($xmldir.elements["*[@filename='#{filename}']"], '')
+            my_gen_real_thumbnail.call
+        }
+        
+        cleanup_all_thumbnails.call
+        perform_enhance_and_cleanup.call
+
+        save_undo(_("enhance"),
+                  proc {
+                      perform_enhance_and_cleanup.call
+                      textview.grab_focus
+                      autoscroll_if_needed($autotable_sw, img, textview)
+                      $notebook.set_page(1)
+                      proc {
+                          perform_enhance_and_cleanup.call
+                          textview.grab_focus
+                          autoscroll_if_needed($autotable_sw, img, textview)
+                          $notebook.set_page(1)
+                      }
+                  })
+    }
+
+    delete = proc { |isacut|
+        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 })
+            $modified = true
+            after = nil
+            perform_delete = proc {
+                after = autotable.get_next_widget(vbox)
+                if !after
+                    after = autotable.get_previous_widget(vbox)
+                end
+                if $config['deleteondisk'] && !isacut
+                    msg 3, "scheduling for delete: #{fullpath}"
+                    $todelete << fullpath
+                end
+                autotable.remove_widget(vbox)
+                if after
+                    $vbox2widgets[after][:textview].grab_focus
+                    autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
+                end
+            }
+            
+            previous_pos = autotable.get_current_number(vbox)
+            perform_delete.call
+
+            if !after
+                delete_current_subalbum
+            else
+                save_undo(_("delete"),
+                          proc { |pos|
+                              autotable.reinsert(pos, vbox, filename)
+                              $notebook.set_page(1)
+                              autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
+                              $cuts = []
+                              msg 3, "removing deletion schedule of: #{fullpath}"
+                              $todelete.delete(fullpath)  #- unconditional because deleteondisk option could have been modified
+                              proc {
+                                  perform_delete.call
+                                  $notebook.set_page(1)
+                              }
+                          }, previous_pos)
+            end
+        end
+    }
+
+    cut = proc {
+        delete.call(true)
+        $cuts << { :vbox => vbox, :filename => filename }
+        $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
+    }
+    paste = proc {
+        if $cuts.size > 0
+            $cuts.each { |elem|
+                autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
+            }
+            last = $cuts[-1]
+            autotable.queue_draws << proc {
+                $vbox2widgets[last[:vbox]][:textview].grab_focus
+                autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
+            }
+            save_undo(_("paste"),
+                      proc { |cuts|
+                          cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
+                          $notebook.set_page(1)
+                          proc {
+                              cuts.each { |elem|
+                                  autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
+                              }
+                              $notebook.set_page(1)
+                          }
+                      }, $cuts)
+            $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
+            $cuts = []
+        end
+    }
+
+    $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
+                                 :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
+                                 :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
+                                 :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
+
+    textview.signal_connect('key-press-event') { |w, event|
+        propagate = true
+        if event.state != 0
+            x, y = autotable.get_current_pos(vbox)
+            control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
+            shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
+            alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
+            if event.keyval == Gdk::Keyval::GDK_Up && y > 0
+                if control_pressed
+                    if widget_up = autotable.get_widget_at_pos(x, y - 1)
+                        $vbox2widgets[widget_up][:textview].grab_focus
+                    end
+                end
+                if shift_pressed
+                    move.call('up')
+                end
+            end
+            if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
+                if control_pressed
+                    if widget_down = autotable.get_widget_at_pos(x, y + 1)
+                        $vbox2widgets[widget_down][:textview].grab_focus
+                    end
+                end
+                if shift_pressed
+                    move.call('down')
+                end
+            end
+            if event.keyval == Gdk::Keyval::GDK_Left
+                if x > 0
+                    if control_pressed
+                        $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
+                    end
+                    if shift_pressed
+                        move.call('left')
+                    end
+                end
+                if alt_pressed
+                    rotate_and_cleanup.call(-90)
+                end
+            end
+            if event.keyval == Gdk::Keyval::GDK_Right
+                next_ = autotable.get_next_widget(vbox)
+                if next_ && autotable.get_current_pos(next_)[0] > x
+                    if control_pressed
+                        $vbox2widgets[next_][:textview].grab_focus
+                    end
+                    if shift_pressed
+                        move.call('right')
+                    end
+                end
+                if alt_pressed
+                    rotate_and_cleanup.call(90)
+                end
+            end
+            if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
+                delete.call(false)
+            end
+            if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
+                view_element(filename, { :delete => delete })
+                propagate = false
+            end
+            if event.keyval == Gdk::Keyval::GDK_z && control_pressed
+                perform_undo
+            end
+            if event.keyval == Gdk::Keyval::GDK_r && control_pressed
+                perform_redo
+            end
+        end
+        !propagate  #- propagate if needed
+    }
+
+    $ignore_next_release = false
+    evtbox.signal_connect('button-press-event') { |w, event|
+        if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
+            if event.state & Gdk::Window::BUTTON3_MASK != 0
+                #- gesture redo: hold right mouse button then click left mouse button
+                $config['nogestures'] or perform_redo
+                $ignore_next_release = true
+            else
+                shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
+                if $r90.active?
+                    rotate_and_cleanup.call(shift_or_control ? -90 : 90)
+                elsif $r270.active?
+                    rotate_and_cleanup.call(shift_or_control ? 90 : -90)
+                elsif $enhance.active?
+                    enhance_and_cleanup.call
+                elsif $delete.active?
+                    delete.call(false)
+                else
+                    textview.grab_focus
+                    $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
+                end
+            end
+            $button1_pressed_autotable = true
+        elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
+            if event.state & Gdk::Window::BUTTON1_MASK != 0
+                #- gesture undo: hold left mouse button then click right mouse button
+                $config['nogestures'] or perform_undo
+                $ignore_next_release = true
+            end
+        elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
+            view_element(filename, { :delete => delete })
+        end
+        false   #- propagate
+    }
+
+    evtbox.signal_connect('button-release-event') { |w, event|
+        if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
+            if !$ignore_next_release
+                x, y = autotable.get_current_pos(vbox)
+                next_ = autotable.get_next_widget(vbox)
+                popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
+                                     { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
+                                       :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
+                                     { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
+                                       :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
+                                       :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
+                                       :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
+            end
+            $ignore_next_release = false
+            $gesture_press = nil
+        end
+        false   #- propagate
+    }
+
+    #- handle reordering with drag and drop
+    Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
+    Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
+    vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
+        selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
+    }
+
+    vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
+        done = false
+        #- mouse gesture first (dnd disables button-release-event)
+        if $gesture_press && $gesture_press[:filename] == filename
+            if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
+                angle = x-$gesture_press[:x] > 0 ? 90 : -90
+                msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
+                rotate_and_cleanup.call(angle)
+                $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
+                done = true
+            elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
+                msg 3, "gesture delete: click-drag right button to the bottom"
+                delete.call(false)
+                $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
+                done = true
+            end
+        end
+        if !done
+            ctxt.targets.each { |target|
+                if target.name == 'reorder-elements'
+                    move_dnd = proc { |from,to|
+                        if from != to
+                            $modified = true
+                            autotable.move(from, to)
+                            save_undo(_("reorder"),
+                                      proc { |from, to|
+                                          if to > from
+                                              autotable.move(to - 1, from)
+                                          else
+                                              autotable.move(to, from + 1)
+                                          end
+                                          $notebook.set_page(1)
+                                          proc {
+                                              autotable.move(from, to)
+                                              $notebook.set_page(1)
+                                          }
+                                      }, from, to)
+                        end
+                    }
+                    if $multiple_dnd.size == 0
+                        move_dnd.call(selection_data.data.to_i,
+                                      autotable.get_current_number(vbox))
+                    else
+                        UndoHandler.begin_batch
+                        $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
+                                      each { |path|
+                            #- need to update current position between each call
+                            move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
+                                          autotable.get_current_number(vbox))
+                        }
+                        UndoHandler.end_batch
+                    end
+                    $multiple_dnd = []
+                end
+            }
+        end
+    }
+
+    vbox.show_all
+end
+
+def create_auto_table
+
+    $autotable = Gtk::AutoTable.new(5)
+
+    $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
+    thumbnails_vb = Gtk::VBox.new(false, 5)
+
+    frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
+    $thumbnails_title.set_justification(Gtk::Justification::CENTER)
+    thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
+    thumbnails_vb.add($autotable)
+
+    $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
+    $autotable_sw.add_with_viewport(thumbnails_vb)
+
+    #- follows stuff for handling multiple elements selection
+    press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
+    gc = nil
+    update_selected = proc {
+        $autotable.current_order.each { |path|
+            w = $name2widgets[path][:evtbox].window
+            xm = w.position[0] + w.size[0]/2
+            ym = w.position[1] + w.size[1]/2
+            if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
+                if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
+                    $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
+                    $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
+                end
+            end
+            if $selected_elements[path] && ! $selected_elements[path][:keep]
+                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))
+                    $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
+                    $selected_elements.delete(path)
+                end
+            end
+        }
+    }
+    $autotable.signal_connect('realize') { |w,e|
+        gc = Gdk::GC.new($autotable.window)
+        gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
+        gc.function = Gdk::GC::INVERT
+        #- autoscroll handling for DND and multiple selections
+        Gtk.timeout_add(100) {
+            if ! $autotable.window.nil?
+                w, x, y, mask = $autotable.window.pointer
+                if mask & Gdk::Window::BUTTON1_MASK != 0
+                    if y < $autotable_sw.vadjustment.value
+                        if pos_x
+                            $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]])
+                        end
+                        if $button1_pressed_autotable || press_x
+                            scroll_upper($autotable_sw, y)
+                        end
+                        if not press_x.nil?
+                            w, pos_x, pos_y = $autotable.window.pointer
+                            $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]])
+                            update_selected.call
+                        end
+                    end
+                    if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
+                        if pos_x
+                            $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]])
+                        end
+                        if $button1_pressed_autotable || press_x
+                            scroll_lower($autotable_sw, y)
+                        end
+                        if not press_x.nil?
+                            w, pos_x, pos_y = $autotable.window.pointer
+                            $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]])
+                            update_selected.call
+                        end
+                    end
+                end
+            end
+            ! $autotable.window.nil?
+        }
+    }
+
+    $autotable.signal_connect('button-press-event') { |w,e|
+        if e.button == 1
+            if !$button1_pressed_autotable
+                press_x = e.x
+                press_y = e.y
+                if e.state & Gdk::Window::SHIFT_MASK == 0
+                    $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
+                    $selected_elements = {}
+                    $statusbar.push(0, utf8(_("Nothing selected.")))
+                else
+                    $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
+                end
+                set_mousecursor(Gdk::Cursor::TCROSS)
+            end
+        end
+    }
+    $autotable.signal_connect('button-release-event') { |w,e|
+        if e.button == 1
+            if $button1_pressed_autotable
+                #- unselect all only now
+                $multiple_dnd = $selected_elements.keys
+                $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
+                $selected_elements = {}
+                $button1_pressed_autotable = false
+            else
+                if pos_x
+                    $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]])
+                    if $selected_elements.length > 0
+                        $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
+                    end
+                end
+                press_x = press_y = pos_x = pos_y = nil
+                set_mousecursor(Gdk::Cursor::LEFT_PTR)
+            end
+        end
+    }
+    $autotable.signal_connect('motion-notify-event') { |w,e|
+        if ! press_x.nil?
+            if pos_x
+                $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]])
+            end
+            pos_x = e.x
+            pos_y = e.y
+            $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]])
+            update_selected.call
+        end
+    }
+
+end
+
+def create_subalbums_page
+
+    subalbums_hb = Gtk::HBox.new
+    $subalbums_vb = Gtk::VBox.new(false, 5)
+    subalbums_hb.pack_start($subalbums_vb, false, false)
+    $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
+    $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
+    $subalbums_sw.add_with_viewport(subalbums_hb)
+end
+
+def save_current_file
+    save_changes
+
+    if $filename
+        begin
+            begin
+                ios = File.open($filename, "w")
+                $xmldoc.write(ios, 0)
+                ios.close
+            rescue Iconv::IllegalSequence
+                #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
+                if ! ios.nil? && ! ios.closed?
+                    ios.close
+                end
+                $xmldoc.xml_decl.encoding = 'UTF-8'
+                ios = File.open($filename, "w")
+                $xmldoc.write(ios, 0)
+                ios.close
+            end
+            return true
+        rescue Exception
+            puts $!
+            return false
+        end
+    end
+end
+
+def save_current_file_user
+    save_tempfilename = $filename
+    $filename = $orig_filename
+    if ! save_current_file
+        show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
+        $filename = save_tempfilename
+        return
+    end
+    $modified = false
+    $generated_outofline = false
+    $filename = save_tempfilename
+
+    msg 3, "performing actual deletion of: " + $todelete.join(', ')
+    $todelete.each { |f|
+        File.delete(f)
+    }
+end
+
+def mark_document_as_dirty
+    $xmldoc.elements.each('//dir') { |elem|
+        elem.delete_attribute('already-generated')
+    }
+end
+
+#- ret: true => ok  false => cancel
+def ask_save_modifications(msg1, msg2, *options)
+    ret = true
+    options = options.size > 0 ? options[0] : {}
+    if $modified
+        if options[:disallow_cancel]
+            dialog = Gtk::Dialog.new(msg1,
+                                     $main_window,
+                                     Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                                     [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
+                                     [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
+        else
+            dialog = Gtk::Dialog.new(msg1,
+                                     $main_window,
+                                     Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                                     [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
+                                     [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
+                                     [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
+        end
+        dialog.default_response = Gtk::Dialog::RESPONSE_YES
+        dialog.vbox.add(Gtk::Label.new(msg2))
+        dialog.window_position = Gtk::Window::POS_CENTER
+        dialog.show_all
+        
+        dialog.run { |response|
+            dialog.destroy
+            if response == Gtk::Dialog::RESPONSE_YES
+                if ! save_current_file_user
+                    return ask_save_modifications(msg1, msg2, options)
+                end
+            else
+                #- if we have generated an album but won't save modifications, we must remove 
+                #- already-generated markers in original file
+                if $generated_outofline
+                    begin
+                        $xmldoc = REXML::Document.new File.new($orig_filename)
+                        mark_document_as_dirty
+                        ios = File.open($orig_filename, "w")
+                        $xmldoc.write(ios, 0)
+                        ios.close
+                    rescue Exception
+                        puts "exception: #{$!}"
+                    end
+                end
+            end
+            if response == Gtk::Dialog::RESPONSE_CANCEL
+                ret = false
+            end
+            $todelete = []  #- unconditionally clear the list of images/videos to delete
+        }
+    end
+    return ret
+end
+
+def try_quit(*options)
+    if ask_save_modifications(utf8(_("Save before quitting?")),
+                              utf8(_("Do you want to save your changes before quitting?")),
+                              *options)
+        Gtk.main_quit
+    end
+end
+
+def show_popup(parent, msg, *options)
+    dialog = Gtk::Dialog.new
+    if options[0] && options[0][:title]
+        dialog.title = options[0][:title]
+    else
+        dialog.title = utf8(_("Booh message"))
+    end
+    lbl = Gtk::Label.new
+    if options[0] && options[0][:nomarkup]
+        lbl.text = msg
+    else
+        lbl.markup = msg
+    end
+    if options[0] && options[0][:centered]
+        lbl.set_justify(Gtk::Justification::CENTER)
+    end
+    if options[0] && options[0][:selectable]
+        lbl.selectable = true
+    end
+    if options[0] && options[0][:topwidget]
+        dialog.vbox.add(options[0][:topwidget])
+    end
+    if options[0] && options[0][:scrolled]
+        sw = Gtk::ScrolledWindow.new(nil, nil)
+        sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
+        sw.add_with_viewport(lbl)
+        dialog.vbox.add(sw)
+        dialog.set_default_size(500, 600)
+    else
+        dialog.vbox.add(lbl)
+        dialog.set_default_size(200, 120)
+    end
+    if options[0] && options[0][:okcancel]
+        dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
+    end
+    dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
+
+    if options[0] && options[0][:pos_centered]
+        dialog.window_position = Gtk::Window::POS_CENTER
+    else
+        dialog.window_position = Gtk::Window::POS_MOUSE
+    end
+
+    if options[0] && options[0][:linkurl]
+        linkbut = Gtk::Button.new('')
+        linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
+        linkbut.signal_connect('clicked') {
+            open_url(options[0][:linkurl])
+            dialog.response(Gtk::Dialog::RESPONSE_OK)
+            set_mousecursor_normal
+        }
+        linkbut.relief = Gtk::RELIEF_NONE
+        linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
+        linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
+        dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
+    end
+
+    dialog.show_all
+
+    if !options[0] || !options[0][:not_transient]
+        dialog.transient_for = parent
+        dialog.run { |response|
+            dialog.destroy
+            if options[0] && options[0][:okcancel]
+                return response == Gtk::Dialog::RESPONSE_OK
+            end
+        }
+    else
+        dialog.signal_connect('response') { dialog.destroy }
+    end
+end
+
+def set_mainwindow_title(progress)
+    filename = $orig_filename || $filename
+    if progress
+        if filename
+            $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
+        else
+            $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
+        end
+    else
+        if filename
+            $main_window.title = 'booh - ' + File.basename(filename)
+        else
+            $main_window.title = 'booh'
+        end
+    end
+end
+
+def backend_wait_message(parent, msg, infopipe_path, mode)
+    w = create_window
+    w.set_transient_for(parent)
+    w.modal = true
+
+    vb = Gtk::VBox.new(false, 5).set_border_width(5)
+    vb.pack_start(Gtk::Label.new(msg), false, false)
+
+    vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
+    vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
+    if mode != 'one dir scan'
+        vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
+    end
+    if mode == 'web-album'
+        vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
+        vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
+    end
+    vb.pack_start(Gtk::HSeparator.new, false, false)
+
+    bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
+    b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
+    vb.pack_end(bottom, false, false)
+
+    directories = nil
+    update_progression_title_pb1 = proc {
+        if mode == 'web-album'
+            set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
+        elsif mode != 'one dir scan'
+            set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
+        else
+            set_mainwindow_title(pb1_1.fraction)
+        end
+    }
+
+    infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
+    refresh_thread = Thread.new {
+        directories_counter = 0
+        while line = infopipe.gets
+            if line =~ /^directories: (\d+), sizes: (\d+)/
+                directories = $1.to_f + 1
+                sizes = $2.to_f
+            elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
+                elements = $3.to_f + 1
+                if mode == 'web-album'
+                    elements += sizes
+                end
+                element_counter = 0
+                gtk_thread_protect { pb1_1.fraction = 0 }
+                if mode != 'one dir scan'
+                    newtext = utf8(full_src_dir_to_rel($1, $2))
+                    newtext = '/' if newtext == ''
+                    gtk_thread_protect { pb1_2.text = newtext }
+                    directories_counter += 1
+                    gtk_thread_protect {
+                        pb1_2.fraction = directories_counter / directories
+                        update_progression_title_pb1.call
+                    }
+                end
+            elsif line =~ /^processing element$/
+                element_counter += 1
+                gtk_thread_protect {
+                    pb1_1.fraction = element_counter / elements
+                    update_progression_title_pb1.call
+                }
+            elsif line =~ /^processing size$/
+                element_counter += 1
+                gtk_thread_protect {
+                    pb1_1.fraction = element_counter / elements
+                    update_progression_title_pb1.call
+                }
+            elsif line =~ /^finished processing sizes$/
+                gtk_thread_protect { pb1_1.fraction = 1 }
+            elsif line =~ /^creating index.html$/
+                gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
+                gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
+                directories_counter = 0
+            elsif line =~ /^index.html: (.+)\|(.+)/
+                newtext = utf8(full_src_dir_to_rel($1, $2))
+                newtext = '/' if newtext == ''
+                gtk_thread_protect { pb2.text = newtext }
+                directories_counter += 1
+                gtk_thread_protect {
+                    pb2.fraction = directories_counter / directories
+                    set_mainwindow_title(0.9 + pb2.fraction / 10)
+                }
+            elsif line =~ /^die: (.*)$/
+                $diemsg = $1
+            end
+        end
+    }
+
+    w.add(vb)
+    w.signal_connect('delete-event') { w.destroy }
+    w.signal_connect('destroy') {
+        Thread.kill(refresh_thread)
+        gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
+        if infopipe_path
+            infopipe.close
+            File.delete(infopipe_path)
+        end
+        set_mainwindow_title(nil)
+    }
+    w.window_position = Gtk::Window::POS_CENTER
+    w.show_all
+
+    return [ b, w ]
+end
+
+def call_backend(cmd, waitmsg, mode, params)
+    pipe = Tempfile.new("boohpipe")
+    Thread.critical = true
+    path = pipe.path
+    pipe.close!
+    system("mkfifo #{path}")
+    Thread.critical = false
+    cmd += " --info-pipe #{path}"
+    button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
+    pid = nil
+    Thread.new {
+        msg 2, cmd
+        if pid = fork
+            id, exitstatus = Process.waitpid2(pid)
+            gtk_thread_protect { w8.destroy }
+            if exitstatus == 0
+                if params[:successmsg]
+                    gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
+                end
+                if params[:closure_after]
+                    gtk_thread_protect(&params[:closure_after])
+                end
+            elsif exitstatus == 15
+                #- say nothing, user aborted
+            else
+                gtk_thread_protect { show_popup($main_window,
+                                                utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
+            end
+        else
+            exec(cmd)
+        end
+    }
+    button.signal_connect('clicked') {
+        Process.kill('SIGTERM', pid)
+    }
+end
+
+def save_changes(*forced)
+    if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
+        return
+    end
+
+    $xmldir.delete_attribute('already-generated')
+
+    propagate_children = proc { |xmldir|
+        if xmldir.attributes['subdirs-caption']
+            xmldir.delete_attribute('already-generated')
+        end
+        xmldir.elements.each('dir') { |element|
+            propagate_children.call(element)
+        }
+    }
+
+    if $xmldir.child_byname_notattr('dir', 'deleted')
+        new_title = $subalbums_title.buffer.text
+        if new_title != $xmldir.attributes['subdirs-caption']
+            parent = $xmldir.parent
+            if parent.name == 'dir'
+                parent.delete_attribute('already-generated')
+            end
+            propagate_children.call($xmldir)
+        end
+        $xmldir.add_attribute('subdirs-caption', new_title)
+        $xmldir.elements.each('dir') { |element|
+            if !element.attributes['deleted']
+                path = element.attributes['path']
+                newtext = $subalbums_edits[path][:editzone].buffer.text
+                if element.attributes['subdirs-caption']
+                    if element.attributes['subdirs-caption'] != newtext
+                        propagate_children.call(element)
+                    end
+                    element.add_attribute('subdirs-caption',     newtext)
+                    element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
+                else
+                    if element.attributes['thumbnails-caption'] != newtext
+                        element.delete_attribute('already-generated')
+                    end
+                    element.add_attribute('thumbnails-caption',     newtext)
+                    element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
+                end
+            end
+        }
+    end
+
+    if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
+        if $xmldir.attributes['thumbnails-caption']
+            path = $xmldir.attributes['path']
+            $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
+        end
+    elsif $xmldir.attributes['thumbnails-caption']
+        $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
+    end
+
+    if $xmldir.attributes['thumbnails-caption']
+        if edit = $subalbums_edits[$xmldir.attributes['path']]
+            $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
+        end
+    end
+
+    #- remove and reinsert elements to reflect new ordering
+    saves = {}
+    cpt = 0
+    $xmldir.elements.each { |element|
+        if element.name == 'image' || element.name == 'video'
+            saves[element.attributes['filename']] = element.remove
+            cpt += 1
+        end
+    }
+    $autotable.current_order.each { |path|
+        chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
+        chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
+        saves.delete(path)
+    }
+    saves.each_key { |path|
+        chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
+        chld.add_attribute('deleted', 'true')
+    }
+end
+
+def sort_by_exif_date
+    $modified = true
+    save_changes
+    current_order = []
+    $xmldir.elements.each { |element|
+        if element.name == 'image' || element.name == 'video'
+            current_order << element.attributes['filename']
+        end
+    }
+
+    #- look for EXIF dates
+    dates = {}
+
+    if current_order.size > 20
+        w = create_window
+        w.set_transient_for($main_window)
+        w.modal = true
+        vb = Gtk::VBox.new(false, 5).set_border_width(5)
+        vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
+        vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
+        bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
+        b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
+        vb.pack_end(bottom, false, false)
+        w.add(vb)
+        w.signal_connect('delete-event') { w.destroy }
+        w.window_position = Gtk::Window::POS_CENTER
+        w.show_all
+
+        aborted = false
+        b.signal_connect('clicked') { aborted = true }
+        i = 0
+        current_order.each { |f|
+            i += 1
+            if entry2type(f) == 'image'
+                pb.text = f
+                pb.fraction = i.to_f / current_order.size
+                Gtk.main_iteration while Gtk.events_pending?
+                date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
+                if ! date_time.nil?
+                    dates[f] = date_time
+                end
+            end
+            if aborted
+                break
+            end
+        }
+        w.destroy
+        if aborted
+            return
+        end
+
+    else
+        current_order.each { |f|
+            date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
+            if ! date_time.nil?
+                dates[f] = date_time
+            end
+        }
+    end
+
+    saves = {}
+    $xmldir.elements.each { |element|
+        if element.name == 'image' || element.name == 'video'
+            saves[element.attributes['filename']] = element.remove
+        end
+    }
+
+    neworder = smartsort(current_order, dates)
+
+    neworder.each { |f|
+        $xmldir.add_element(saves[f].name, saves[f].attributes)
+    }
+
+    #- let the auto-table reflect new ordering
+    change_dir
+end
+
+def remove_all_captions
+    $modified = true
+    texts = {}
+    $autotable.current_order.each { |path|
+        texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
+        $name2widgets[File.basename(path)][:textview].buffer.text = ''
+    }
+    save_undo(_("remove all captions"),
+              proc { |texts|
+                  texts.each_key { |key|
+                      $name2widgets[key][:textview].buffer.text = texts[key]
+                  }
+                  $notebook.set_page(1)
+                  proc {
+                      texts.each_key { |key|
+                          $name2widgets[key][:textview].buffer.text = ''
+                      }
+                      $notebook.set_page(1)
+                  }
+              }, texts)
+end
+
+def change_dir
+    $selected_elements.each_key { |path|
+        $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
+    }
+    $autotable.clear
+    $vbox2widgets = {}
+    $name2widgets = {}
+    $name2closures = {}
+    $selected_elements = {}
+    $cuts = []
+    $multiple_dnd = []
+    UndoHandler.cleanup
+    $undo_tb.sensitive = $undo_mb.sensitive = false
+    $redo_tb.sensitive = $redo_mb.sensitive = false
+
+    if !$current_path
+        return
+    end
+
+    $subalbums_vb.children.each { |chld|
+        $subalbums_vb.remove(chld)
+    }
+    $subalbums = Gtk::Table.new(0, 0, true)
+    current_y_sub_albums = 0
+
+    $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
+    $subalbums_edits = {}
+    subalbums_counter = 0
+    subalbums_edits_bypos = {}
+
+    add_subalbum = proc { |xmldir, counter|
+        $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
+        subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
+        if xmldir == $xmldir
+            thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
+            captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
+            caption = xmldir.attributes['thumbnails-caption']
+            infotype = 'thumbnails'
+        else
+            thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
+            captionfile, caption = find_subalbum_caption_info(xmldir)
+            infotype = find_subalbum_info_type(xmldir)
+        end
+        msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
+        hbox = Gtk::HBox.new
+        hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
+        f = Gtk::Frame.new
+        f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
+
+        img = nil
+        my_gen_real_thumbnail = proc {
+            gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
+        }
+
+        if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
+            f.add(img = Gtk::Image.new)
+            my_gen_real_thumbnail.call
+        else
+            f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
+        end
+        hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
+        $subalbums.attach(hbox,
+                          0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
+
+        frame, textview = create_editzone($subalbums_sw, 0, img)
+        textview.buffer.text = caption
+        $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
+                          1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
+
+        change_image = proc {
+            fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
+                                            nil,
+                                            Gtk::FileChooser::ACTION_OPEN,
+                                            nil,
+                                            [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+            fc.set_current_folder(from_utf8(xmldir.attributes['path']))
+            fc.transient_for = $main_window
+            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))
+            f.add(preview_img = Gtk::Image.new)
+            preview.show_all
+            fc.signal_connect('update-preview') { |w|
+                begin
+                    if fc.preview_filename
+                        preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
+                        fc.preview_widget_active = true
+                    end
+                rescue Gdk::PixbufError
+                    fc.preview_widget_active = false
+                end
+            }
+            if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+                $modified = true
+                old_file = captionfile
+                old_rotate = xmldir.attributes["#{infotype}-rotate"]
+                old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
+                old_enhance = xmldir.attributes["#{infotype}-enhance"]
+                old_seektime = xmldir.attributes["#{infotype}-seektime"]
+
+                new_file = fc.filename
+                msg 3, "new captionfile is: #{fc.filename}"
+                perform_changefile = proc {
+                    $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
+                    $modified_pixbufs.delete(thumbnail_file)
+                    xmldir.delete_attribute("#{infotype}-rotate")
+                    xmldir.delete_attribute("#{infotype}-color-swap")
+                    xmldir.delete_attribute("#{infotype}-enhance")
+                    xmldir.delete_attribute("#{infotype}-seektime")
+                    my_gen_real_thumbnail.call
+                }
+                perform_changefile.call
+
+                save_undo(_("change caption file for sub-album"),
+                          proc {
+                              $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
+                              xmldir.add_attribute("#{infotype}-rotate", old_rotate)
+                              xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
+                              xmldir.add_attribute("#{infotype}-enhance", old_enhance)
+                              xmldir.add_attribute("#{infotype}-seektime", old_seektime)
+                              my_gen_real_thumbnail.call
+                              $notebook.set_page(0)
+                              proc {
+                                  perform_changefile.call
+                                  $notebook.set_page(0)
+                              }
+                          })
+            end
+            fc.destroy
+        }
+
+        refresh = proc {
+            if File.exists?(thumbnail_file)
+                File.delete(thumbnail_file)
+            end
+            my_gen_real_thumbnail.call
+        }
+
+        rotate_and_cleanup = proc { |angle|
+            rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
+            if File.exists?(thumbnail_file)
+                File.delete(thumbnail_file)
+            end
+        }
+
+        move = proc { |direction|
+            $modified = true
+
+            save_changes('forced')
+            oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
+            if direction == 'up'
+                $subalbums_edits[xmldir.attributes['path']][:position] -= 1
+                subalbums_edits_bypos[oldpos - 1][:position] += 1
+            end
+            if direction == 'down'
+                $subalbums_edits[xmldir.attributes['path']][:position] += 1
+                subalbums_edits_bypos[oldpos + 1][:position] -= 1
+            end
+            if direction == 'top'
+                for i in 1 .. oldpos - 1
+                    subalbums_edits_bypos[i][:position] += 1
+                end
+                $subalbums_edits[xmldir.attributes['path']][:position] = 1
+            end
+            if direction == 'bottom'
+                for i in oldpos + 1 .. subalbums_counter
+                    subalbums_edits_bypos[i][:position] -= 1
+                end
+                $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
+            end
+
+            elems = []
+            $xmldir.elements.each('dir') { |element|
+                if (!element.attributes['deleted'])
+                    elems << [ element.attributes['path'], element.remove ]
+                end
+            }
+            elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
+                  each { |e| $xmldir.add_element(e[1]) }
+            #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
+            $xmldir.elements.each('descendant::dir') { |elem|
+                elem.delete_attribute('already-generated')
+            }
+
+            sel = $albums_tv.selection.selected_rows
+            change_dir
+            populate_subalbums_treeview(false)
+            $albums_tv.selection.select_path(sel[0])
+        }
+
+        color_swap_and_cleanup = proc {
+            perform_color_swap_and_cleanup = proc {
+                color_swap(xmldir, "#{infotype}-")
+                my_gen_real_thumbnail.call
+            }
+            perform_color_swap_and_cleanup.call
+
+            save_undo(_("color swap"),
+                      proc {
+                          perform_color_swap_and_cleanup.call
+                          $notebook.set_page(0)
+                          proc {
+                              perform_color_swap_and_cleanup.call
+                              $notebook.set_page(0)
+                          }
+                      })
+        }
+
+        change_seektime_and_cleanup = proc {
+            if values = ask_new_seektime(xmldir, "#{infotype}-")
+                perform_change_seektime_and_cleanup = proc { |val|
+                    change_seektime(xmldir, "#{infotype}-", val)
+                    my_gen_real_thumbnail.call
+                }
+                perform_change_seektime_and_cleanup.call(values[:new])
+
+                save_undo(_("specify seektime"),
+                          proc {
+                              perform_change_seektime_and_cleanup.call(values[:old])
+                              $notebook.set_page(0)
+                              proc {
+                                  perform_change_seektime_and_cleanup.call(values[:new])
+                                  $notebook.set_page(0)
+                              }
+                          })
+            end
+        }
+
+        whitebalance_and_cleanup = proc {
+            if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
+                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
+                perform_change_whitebalance_and_cleanup = proc { |val|
+                    change_whitebalance(xmldir, "#{infotype}-", val)
+                    recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
+                                        $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
+                    if File.exists?(thumbnail_file)
+                        File.delete(thumbnail_file)
+                    end
+                }
+                perform_change_whitebalance_and_cleanup.call(values[:new])
+                
+                save_undo(_("fix white balance"),
+                          proc {
+                              perform_change_whitebalance_and_cleanup.call(values[:old])
+                              $notebook.set_page(0)
+                              proc {
+                                  perform_change_whitebalance_and_cleanup.call(values[:new])
+                                  $notebook.set_page(0)
+                              }
+                          })
+            end
+        }
+
+        gammacorrect_and_cleanup = proc {
+            if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
+                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
+                perform_change_gammacorrect_and_cleanup = proc { |val|
+                    change_gammacorrect(xmldir, "#{infotype}-", val)
+                    recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
+                                        $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
+                    if File.exists?(thumbnail_file)
+                        File.delete(thumbnail_file)
+                    end
+                }
+                perform_change_gammacorrect_and_cleanup.call(values[:new])
+                
+                save_undo(_("gamma correction"),
+                          proc {
+                              perform_change_gammacorrect_and_cleanup.call(values[:old])
+                              $notebook.set_page(0)
+                              proc {
+                                  perform_change_gammacorrect_and_cleanup.call(values[:new])
+                                  $notebook.set_page(0)
+                              }
+                          })
+            end
+        }
+
+        enhance_and_cleanup = proc {
+            perform_enhance_and_cleanup = proc {
+                enhance(xmldir, "#{infotype}-")
+                my_gen_real_thumbnail.call
+            }
+            
+            perform_enhance_and_cleanup.call
+            
+            save_undo(_("enhance"),
+                      proc {
+                          perform_enhance_and_cleanup.call
+                          $notebook.set_page(0)
+                          proc {
+                              perform_enhance_and_cleanup.call
+                              $notebook.set_page(0)
+                          }
+                      })
+        }
+
+        evtbox.signal_connect('button-press-event') { |w, event|
+            if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
+                if $r90.active?
+                    rotate_and_cleanup.call(90)
+                elsif $r270.active?
+                    rotate_and_cleanup.call(-90)
+                elsif $enhance.active?
+                    enhance_and_cleanup.call
+                end
+            end
+            if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
+                popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
+                                     { :forbid_left => true, :forbid_right => true,
+                                       :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
+                                       :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
+                                     { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
+                                       :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
+                                       :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
+            end
+            if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
+                change_image.call
+                true   #- handled
+            end
+        }
+        evtbox.signal_connect('button-press-event') { |w, event|
+            $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
+            false
+        }
+
+        evtbox.signal_connect('button-release-event') { |w, event|
+            if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
+                msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
+                if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
+                    angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
+                    msg 3, "gesture rotate: #{angle}"
+                    rotate_and_cleanup.call(angle)
+                end
+            end
+            $gesture_press = nil
+        }
+                
+        $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
+        $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
+        current_y_sub_albums += 1
+    }
+
+    if $xmldir.child_byname_notattr('dir', 'deleted')
+        #- title edition
+        frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
+        $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
+        $subalbums_title.set_justification(Gtk::Justification::CENTER)
+        $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
+        #- this album image/caption
+        if $xmldir.attributes['thumbnails-caption']
+            add_subalbum.call($xmldir, 0)
+        end
+    end
+    total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
+    $xmldir.elements.each { |element|
+        if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
+            #- element (image or video) of this album
+            dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
+            msg 3, "dest_img: #{dest_img}"
+            add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
+            total[element.name] += 1
+        end
+        if element.name == 'dir' && !element.attributes['deleted']
+            #- sub-album image/caption
+            add_subalbum.call(element, subalbums_counter += 1)
+            total[element.name] += 1
+        end
+    }
+    $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
+                                                                                total['image'], total['video'], total['dir'] ]))
+    $subalbums_vb.add($subalbums)
+    $subalbums_vb.show_all
+
+    if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
+        $notebook.get_tab_label($autotable_sw).sensitive = false
+        $notebook.set_page(0)
+        $thumbnails_title.buffer.text = ''
+    else
+        $notebook.get_tab_label($autotable_sw).sensitive = true
+        $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
+    end
+
+    if !$xmldir.child_byname_notattr('dir', 'deleted')
+        $notebook.get_tab_label($subalbums_sw).sensitive = false
+        $notebook.set_page(1)
+    else
+        $notebook.get_tab_label($subalbums_sw).sensitive = true
+    end
+end
+
+def pixbuf_or_nil(filename)
+    begin
+        return Gdk::Pixbuf.new(filename)
+    rescue
+        return nil
+    end
+end
+
+def theme_choose(current)
+    dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
+    treeview = Gtk::TreeView.new(model).set_rules_hint(true)
+    treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
+    treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
+    treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
+    treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
+    treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
+    treeview.signal_connect('button-press-event') { |w, event|
+        if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
+            dialog.response(Gtk::Dialog::RESPONSE_OK)
+        end
+    }
+
+    dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
+
+    ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
+        dir.chomp!
+        iter = model.append
+        iter[0] = File.basename(dir)
+        iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
+        iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
+        iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
+        if File.basename(dir) == current
+            treeview.selection.select_iter(iter)
+        end
+    }
+    dialog.set_default_size(-1, 500)
+    dialog.vbox.show_all
+
+    dialog.run { |response|
+        iter = treeview.selection.selected
+        dialog.destroy
+        if response == Gtk::Dialog::RESPONSE_OK && iter
+            return model.get_value(iter, 0)
+        end
+    }
+    return nil
+end
+
+def show_password_protections
+    examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
+        child_iter = $albums_iters[xmldir.attributes['path']]
+        if xmldir.attributes['password-protect']
+            child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
+            already_protected = true
+        elsif already_protected
+            pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
+            if pix
+                pix = pix.saturate_and_pixelate(1, true)
+            end
+            child_iter[2] = pix
+        else
+            child_iter[2] = nil
+        end
+        xmldir.elements.each('dir') { |elem|
+            if !elem.attributes['deleted']
+                examine_dir_elem.call(child_iter, elem, already_protected)
+            end
+        }
+    }
+    examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
+end
+
+def populate_subalbums_treeview(select_first)
+    $albums_ts.clear
+    $autotable.clear
+    $albums_iters = {}
+    $subalbums_vb.children.each { |chld|
+        $subalbums_vb.remove(chld)
+    }
+
+    source = $xmldoc.root.attributes['source']
+    msg 3, "source: #{source}"
+
+    xmldir = $xmldoc.elements['//dir']
+    if !xmldir || xmldir.attributes['path'] != source
+        msg 1, _("Corrupted booh file...")
+        return
+    end
+
+    append_dir_elem = proc { |parent_iter, xmldir|
+        child_iter = $albums_ts.append(parent_iter)
+        child_iter[0] = File.basename(xmldir.attributes['path'])
+        child_iter[1] = xmldir.attributes['path']
+        $albums_iters[xmldir.attributes['path']] = child_iter
+        msg 3, "puttin location: #{xmldir.attributes['path']}"
+        xmldir.elements.each('dir') { |elem|
+            if !elem.attributes['deleted']
+                append_dir_elem.call(child_iter, elem)
+            end
+        }
+    }
+    append_dir_elem.call(nil, xmldir)
+    show_password_protections
+
+    $albums_tv.expand_all
+    if select_first
+        $albums_tv.selection.select_iter($albums_ts.iter_first)
+    end
+end
+
+def select_current_theme
+    select_theme($xmldoc.root.attributes['theme'],
+                 $xmldoc.root.attributes['limit-sizes'],
+                 !$xmldoc.root.attributes['optimize-for-32'].nil?,
+                 $xmldoc.root.attributes['thumbnails-per-row'])
+end
+
+def open_file(filename)
+
+    $filename = nil
+    $modified = false
+    $current_path = nil   #- invalidate
+    $modified_pixbufs = {}
+    $albums_ts.clear
+    $autotable.clear
+    $subalbums_vb.children.each { |chld|
+        $subalbums_vb.remove(chld)
+    }
+
+    if !File.exists?(filename)
+        return utf8(_("File not found."))
+    end
+
+    begin
+        $xmldoc = REXML::Document.new File.new(filename)
+    rescue Exception
+        $xmldoc = nil
+    end
+
+    if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
+        if entry2type(filename).nil?
+            return utf8(_("Not a booh file!"))
+        else
+            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."))
+        end
+    end
+
+    if !source = $xmldoc.root.attributes['source']
+        return utf8(_("Corrupted booh file..."))
+    end
+
+    if !dest = $xmldoc.root.attributes['destination']
+        return utf8(_("Corrupted booh file..."))
+    end
+
+    if !theme = $xmldoc.root.attributes['theme']
+        return utf8(_("Corrupted booh file..."))
+    end
+
+    if $xmldoc.root.attributes['version'] < '0.9.0'
+        msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
+        mark_document_as_dirty
+        if $xmldoc.root.attributes['version'] < '0.8.4'
+            msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
+            `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
+                old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
+                new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
+                if old_dest_dir != new_dest_dir
+                    sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
+                end
+                if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
+                    xmldir.elements.each { |element|
+                        if %w(image video).include?(element.name) && !element.attributes['deleted']
+                            old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
+                            new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
+                            Dir[old_name + '*'].each { |file|
+                                new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
+                                file != new_file and sys("mv '#{file}' '#{new_file}'")
+                            }
+                        end
+                        if element.name == 'dir' && !element.attributes['deleted']
+                            old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
+                            new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
+                            old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
+                        end
+                    }
+                else
+                    msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
+                end
+            }
+        end
+        $xmldoc.root.add_attribute('version', $VERSION)
+    end
+
+    select_current_theme
+
+    $filename = filename
+    set_mainwindow_title(nil)
+    $default_size['thumbnails'] =~ /(.*)x(.*)/
+    $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
+    $albums_thumbnail_size =~ /(.*)x(.*)/
+    $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
+
+    populate_subalbums_treeview(true)
+
+    $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
+    return nil
+end
+
+def open_file_user(filename)
+    result = open_file(filename)
+    if !result
+        $config['last-opens'] ||= []
+        if $config['last-opens'][-1] != utf8(filename)
+            $config['last-opens'] << utf8(filename)
+        end
+        $orig_filename = $filename
+        $main_window.title = 'booh - ' + File.basename($orig_filename)
+        tmp = Tempfile.new("boohtemp")
+        Thread.critical = true
+        $filename = tmp.path
+        tmp.close!
+        #- for security
+        ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
+        Thread.critical = false
+        ios.close
+        $tempfiles << $filename << "#{$filename}.backup"
+    else
+        $orig_filename = nil
+    end
+    return result
+end
+
+def open_file_popup
+    if !ask_save_modifications(utf8(_("Save this album?")),
+                               utf8(_("Do you want to save the changes to this album?")),
+                               { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
+        return
+    end
+    fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
+                                    nil,
+                                    Gtk::FileChooser::ACTION_OPEN,
+                                    nil,
+                                    [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+    fc.add_shortcut_folder(File.expand_path("~/.booh"))
+    fc.set_current_folder(File.expand_path("~/.booh"))
+    fc.transient_for = $main_window
+    ok = false
+    while !ok
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+            push_mousecursor_wait(fc)
+            msg = open_file_user(fc.filename)
+            pop_mousecursor(fc)
+            if msg
+                show_popup(fc, msg)
+                ok = false
+            else
+                ok = true
+            end
+        else
+            ok = true
+        end
+    end
+    fc.destroy
+end
+
+def additional_booh_options
+    options = ''
+    if $config['mproc']
+        options += "--mproc #{$config['mproc'].to_i} "
+    end
+    options += "--comments-format '#{$config['comments-format']}' "
+    if $config['transcode-videos']
+        options += "--transcode-videos '#{$config['transcode-videos']}' "
+    end
+    return options
+end
+
+def ask_multi_languages(value)
+    if ! value.nil?
+        spl = value.split(',')
+        value = [ spl[0..-2], spl[-1] ]
+    end
+
+    dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
+                             $main_window,
+                             Gtk::Dialog::MODAL,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    lbl = Gtk::Label.new
+    lbl.markup = utf8(
+_("You can choose to activate <b>multi-languages</b> support for this web-album
+(it will work only if you publish your web-album on an Apache web-server). This will
+use the MultiViews feature of Apache; the pages will be served according to the
+value of the Accept-Language HTTP header sent by the web browsers, so that people
+with different languages preferences will be able to browse your web-album with
+navigation in their language (if language is available).
+"))
+
+    dialog.vbox.add(lbl)
+    dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
+                                                                         add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
+                                                                                                     add(languages = Gtk::Button.new))))
+
+    pick_languages = proc {
+        dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
+                                  $main_window,
+                                  Gtk::Dialog::MODAL,
+                                  [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                                  [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+        dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
+        hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
+        cbs = []
+        SUPPORTED_LANGUAGES.each { |lang|
+            hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
+            if ! value.nil? && value[0].include?(lang)
+                cb.active = true
+            end
+            cbs << [ lang, cb ]
+        }
+
+        dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
+        hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
+        fallback_language = nil
+        hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
+        fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
+        if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
+            fbl_rb.active = true
+            fallback_language = SUPPORTED_LANGUAGES[0]
+        end
+        SUPPORTED_LANGUAGES[1..-1].each { |lang|
+            hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
+            rb.signal_connect('clicked') { fallback_language = lang }
+            if ! value.nil? && value[1] == lang
+                rb.active = true
+            end
+        }
+
+        dialog2.window_position = Gtk::Window::POS_MOUSE
+        dialog2.show_all
+
+        resp = nil
+        dialog2.run { |response|
+            resp = response
+            if resp == Gtk::Dialog::RESPONSE_OK
+                value = []
+                value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
+                value[1] = fallback_language
+                languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
+            end
+            dialog2.destroy
+        }
+        resp
+    }
+
+    languages.signal_connect('clicked') {
+        pick_languages.call
+    }
+    dialog.window_position = Gtk::Window::POS_MOUSE
+    if value.nil?
+        rb_no.active = true
+    else
+        rb_yes.active = true
+        languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
+    end
+    rb_no.signal_connect('clicked') {
+        if rb_no.active?
+            languages.hide
+        else
+            if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
+                rb_no.activate
+            else
+                languages.show
+            end
+        end
+    }
+    oldval = value
+    dialog.show_all
+    if rb_no.active?
+        languages.hide
+    end
+
+    dialog.run { |response|
+        if rb_no.active?
+            value = nil
+        end
+        dialog.destroy
+        if response == Gtk::Dialog::RESPONSE_OK && value != oldval
+            if value.nil?
+                return [ true, nil ]
+            else
+                return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
+            end
+        else
+            return [ false ]
+        end
+    }
+end
+
+def new_album
+    if !ask_save_modifications(utf8(_("Save this album?")),
+                               utf8(_("Do you want to save the changes to this album?")),
+                               { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
+        return
+    end
+    dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+    
+    frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
+    tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
+               0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(src = Gtk::Entry.new,
+               1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
+               2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
+               0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
+               1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
+               0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(dest = Gtk::Entry.new,
+               1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
+               2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
+               0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
+               1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
+               2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+
+    tooltips = Gtk::Tooltips.new
+    frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
+                         pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
+                                   pack_start(sizes = Gtk::HBox.new, false, false, 0))
+    vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
+    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)
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
+                                   pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
+    nperpage_model = Gtk::ListStore.new(String, String)
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
+                                   pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
+    nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
+    nperpagecombo.set_attributes(crt, { :markup => 0 })
+    iter = nperpage_model.append
+    iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
+    iter[1] = nil
+    [ 12, 20, 30, 40, 50 ].each { |v|
+        iter = nperpage_model.append
+        iter[0] = iter[1] = v.to_s
+    }
+    nperpagecombo.active = 0
+
+    multilanguages_value = nil
+    vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
+                                        pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
+    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)
+    multilanguages.signal_connect('clicked') {
+        retval = ask_multi_languages(multilanguages_value)
+        if retval[0] 
+            multilanguages_value = retval[1]
+        end
+        if multilanguages_value
+            ml_label.text = utf8(_("Multi-languages: enabled."))
+        else
+            ml_label.text = utf8(_("Multi-languages: disabled."))
+        end
+    }
+    if $config['default-multi-languages']
+        multilanguages_value = $config['default-multi-languages']
+        ml_label.text = utf8(_("Multi-languages: enabled."))
+    else
+        ml_label.text = utf8(_("Multi-languages: disabled."))
+    end
+
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
+                                   pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
+    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)
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
+                                   pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
+    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)
+
+    src_nb_calculated_for = ''
+    src_nb_thread = nil
+    process_src_nb = proc {
+        if src.text != src_nb_calculated_for
+            src_nb_calculated_for = src.text
+            if src_nb_thread
+                Thread.kill(src_nb_thread)
+                src_nb_thread = nil
+            end
+            if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
+                src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
+            else
+                if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
+                    if File.readable?(from_utf8_safe(src_nb_calculated_for))
+                        src_nb_thread = Thread.new {
+                            gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
+                            total = { 'image' => 0, 'video' => 0, nil => 0 }
+                            `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
+                                if File.basename(dir) =~ /^\./
+                                    next
+                                else
+                                    begin
+                                        Dir.entries(dir.chomp).each { |file|
+                                            total[entry2type(file)] += 1
+                                        }
+                                    rescue Errno::EACCES, Errno::ENOENT
+                                    end
+                                end
+                            }
+                            gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
+                            src_nb_thread = nil
+                        }
+                    else
+                        src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
+                    end
+                else
+                    src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
+                end
+            end
+        end
+        true
+    }
+    timeout_src_nb = Gtk.timeout_add(100) {
+        process_src_nb.call
+    }
+
+    src_browse.signal_connect('clicked') {
+        fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
+                                        nil,
+                                        Gtk::FileChooser::ACTION_SELECT_FOLDER,
+                                        nil,
+                                        [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+        fc.transient_for = $main_window
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+            src.text = utf8(fc.filename)
+            process_src_nb.call
+            conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
+        end
+        fc.destroy
+    }
+
+    dest_browse.signal_connect('clicked') {
+        fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
+                                        nil,
+                                        Gtk::FileChooser::ACTION_CREATE_FOLDER,
+                                        nil,
+                                        [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+        fc.transient_for = $main_window
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+            dest.text = utf8(fc.filename)
+        end
+        fc.destroy
+    }
+
+    conf_browse.signal_connect('clicked') {
+        fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
+                                        nil,
+                                        Gtk::FileChooser::ACTION_SAVE,
+                                        nil,
+                                        [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+        fc.transient_for = $main_window
+        fc.add_shortcut_folder(File.expand_path("~/.booh"))
+        fc.set_current_folder(File.expand_path("~/.booh"))
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+            conf.text = utf8(fc.filename)
+        end
+        fc.destroy
+    }
+
+    theme_sizes = []
+    nperrows = []
+    recreate_theme_config = proc {
+        theme_sizes.each { |e| sizes.remove(e[:widget]) }
+        theme_sizes = []
+        select_theme(theme_button.label, 'all', optimize432.active?, nil)
+        $images_size.each { |s|
+            sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
+            if !s['optional']
+                cb.active = true
+            end
+            tooltips.set_tip(cb, utf8(s['description']), nil)
+            theme_sizes << { :widget => cb, :value => s['name'] }
+        }
+        sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
+        tooltips = Gtk::Tooltips.new
+        tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
+        theme_sizes << { :widget => cb, :value => 'original' }
+        sizes.show_all
+
+        nperrows.each { |e| nperrowradios.remove(e[:widget]) }
+        nperrow_group = nil
+        nperrows = []
+        $allowed_N_values.each { |n|
+            if nperrow_group
+                nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
+            else
+                nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
+            end
+            tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
+            if $default_N == n
+                rb.active = true
+            end
+            nperrows << { :widget => rb, :value => n }
+        }
+        nperrowradios.show_all
+    }
+    recreate_theme_config.call
+
+    theme_button.signal_connect('clicked') {
+        if newtheme = theme_choose(theme_button.label)
+            theme_button.label = newtheme
+            recreate_theme_config.call
+        end
+    }
+
+    dialog.vbox.add(frame1)
+    dialog.vbox.add(frame2)
+    dialog.show_all
+
+    keepon = true
+    ok = true
+    while keepon
+        dialog.run { |response|
+            if response == Gtk::Dialog::RESPONSE_OK
+                srcdir = from_utf8_safe(src.text)
+                destdir = from_utf8_safe(dest.text)
+                confpath = from_utf8_safe(conf.text)
+                if src.text != '' && srcdir == ''
+                    show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
+                    src.grab_focus
+                elsif !File.directory?(srcdir)
+                    show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
+                    src.grab_focus
+                elsif dest.text != '' && destdir == ''
+                    show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
+                    dest.grab_focus
+                elsif destdir != make_dest_filename(destdir)
+                    show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
+                    dest.grab_focus
+                elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
+                    keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
+inside it will be permanently removed before creating the web-album!
+Are you sure you want to continue?")), { :okcancel => true })
+                    dest.grab_focus
+                elsif File.exists?(destdir) && !File.directory?(destdir)
+                    show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
+                    dest.grab_focus
+                elsif conf.text == ''
+                    show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
+                    conf.grab_focus
+                elsif conf.text != '' && confpath == ''
+                    show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
+                    conf.grab_focus
+                elsif File.directory?(confpath)
+                    show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
+                    conf.grab_focus
+                elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
+                    show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
+                else
+                    system("mkdir '#{destdir}'")
+                    if !File.directory?(destdir)
+                        show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
+                        dest.grab_focus
+                    else
+                        keepon = false
+                    end
+                end
+            else
+                keepon = ok = false
+            end
+        }
+    end
+    if ok
+        srcdir = from_utf8(src.text)
+        destdir = from_utf8(dest.text)
+        configskel = File.expand_path(from_utf8(conf.text))
+        theme = theme_button.label
+        #- some sort of automatic theme preference
+        $config['default-theme'] = theme
+        $config['default-multi-languages'] = multilanguages_value
+        $config['default-optimize32'] = optimize432.active?.to_s
+        sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
+        nperrow = nperrows.find { |e| e[:widget].active? }[:value]
+        nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
+        opt432 = optimize432.active?
+        madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
+        indexlink = indexlinkentry.text.gsub('\'', '&#39;')
+    end
+    if src_nb_thread
+        Thread.kill(src_nb_thread)
+        gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
+    end
+    dialog.destroy
+    Gtk.timeout_remove(timeout_src_nb)
+
+    if ok
+        call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
+                     "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
+                     (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
+                     (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
+                     "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
+                     utf8(_("Please wait while scanning source directory...")),
+                     'full scan',
+                     { :closure_after => proc {
+                             open_file_user(configskel)
+                             $main_window.urgency_hint = true
+                         } })
+    end
+end
+
+def properties
+    dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+    
+    source = $xmldoc.root.attributes['source']
+    dest = $xmldoc.root.attributes['destination']
+    theme = $xmldoc.root.attributes['theme']
+    opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
+    nperrow = $xmldoc.root.attributes['thumbnails-per-row']
+    nperpage = $xmldoc.root.attributes['thumbnails-per-page']
+    limit_sizes = $xmldoc.root.attributes['limit-sizes']
+    if limit_sizes
+        limit_sizes = limit_sizes.split(/,/)
+    end
+    madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
+    indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
+    save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
+
+    tooltips = Gtk::Tooltips.new
+    frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
+    tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
+               0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
+               1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
+               0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
+               1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
+               0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
+               1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+
+    frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
+                                   pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
+                                   pack_start(sizes = Gtk::HBox.new, false, false, 0))
+    vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
+    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)
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
+                                   pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
+    nperpage_model = Gtk::ListStore.new(String, String)
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
+                                   pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
+    nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
+    nperpagecombo.set_attributes(crt, { :markup => 0 })
+    iter = nperpage_model.append
+    iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
+    iter[1] = nil
+    [ 12, 20, 30, 40, 50 ].each { |v|
+        iter = nperpage_model.append
+        iter[0] = iter[1] = v.to_s
+        if nperpage && nperpage == v.to_s
+            nperpagecombo.active_iter = iter
+        end
+    }
+    if nperpagecombo.active_iter.nil?
+        nperpagecombo.active = 0
+    end
+
+    vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
+                                        pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
+    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)
+    ml_update = proc {
+        if save_multilanguages_value
+            ml_label.text = utf8(_("Multi-languages: enabled."))
+        else
+            ml_label.text = utf8(_("Multi-languages: disabled."))
+        end
+    }
+    ml_update.call
+    multilanguages.signal_connect('clicked') {
+        retval = ask_multi_languages(save_multilanguages_value)
+        if retval[0] 
+            save_multilanguages_value = retval[1]
+        end
+        ml_update.call
+    }
+
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
+                                   pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
+    if indexlink
+        indexlinkentry.text = indexlink
+    end
+    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)
+    vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
+                                   pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
+    if madewith
+        madewithentry.text = madewith
+    end
+    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)
+
+    theme_sizes = []
+    nperrows = []
+    recreate_theme_config = proc {
+        theme_sizes.each { |e| sizes.remove(e[:widget]) }
+        theme_sizes = []
+        select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
+
+        $images_size.each { |s|
+            sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
+            if limit_sizes
+                if limit_sizes.include?(s['name'])
+                    cb.active = true
+                end
+            else
+                if !s['optional']
+                    cb.active = true
+                end
+            end
+            tooltips.set_tip(cb, utf8(s['description']), nil)
+            theme_sizes << { :widget => cb, :value => s['name'] }
+        }
+        sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
+        tooltips = Gtk::Tooltips.new
+        tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
+        if limit_sizes && limit_sizes.include?('original')
+            cb.active = true
+        end
+        theme_sizes << { :widget => cb, :value => 'original' }
+        sizes.show_all
+
+        nperrows.each { |e| nperrowradios.remove(e[:widget]) }
+        nperrow_group = nil
+        nperrows = []
+        $allowed_N_values.each { |n|
+            if nperrow_group
+                nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
+            else
+                nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
+            end
+            tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
+            nperrowradios.add(Gtk::Label.new('  '))
+            if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
+                rb.active = true
+            end
+            nperrows << { :widget => rb, :value => n.to_s }
+        }
+        nperrowradios.show_all
+    }
+    recreate_theme_config.call
+
+    theme_button.signal_connect('clicked') {
+        if newtheme = theme_choose(theme_button.label)
+            limit_sizes = nil
+            nperrow = nil
+            theme_button.label = newtheme
+            recreate_theme_config.call
+        end
+    }
+
+    dialog.vbox.add(frame1)
+    dialog.vbox.add(frame2)
+    dialog.show_all
+
+    keepon = true
+    ok = true
+    while keepon
+        dialog.run { |response|
+            if response == Gtk::Dialog::RESPONSE_OK
+                if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
+                    show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
+                else
+                    keepon = false
+                end
+            else
+                keepon = ok = false
+            end
+        }
+    end
+    save_theme = theme_button.label
+    save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
+    save_opt432 = optimize432.active?
+    save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
+    save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
+    save_madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
+    save_indexlink = indexlinkentry.text.gsub('\'', '&#39;')
+    dialog.destroy
+    
+    if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry || save_multilanguages_value != multilanguages_value)
+        #- some sort of automatic preferences
+        if save_theme != theme
+            $config['default-theme'] = save_theme
+        end
+        if save_multilanguages_value != multilanguages_value
+            $config['default-multi-languages'] = save_multilanguages_value
+        end
+        if save_opt432 != opt432
+            $config['default-optimize32'] = save_opt432.to_s
+        end
+        mark_document_as_dirty
+        save_current_file
+        call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
+                     "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
+                     (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
+                     (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
+                     "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
+                     utf8(_("Please wait while scanning source directory...")),
+                     'full scan',
+                     { :closure_after => proc {
+                             open_file($filename)
+                             $modified = true
+                             $main_window.urgency_hint = true
+                         } })
+    else
+        #- select_theme merges global variables, need to return to current choices
+        select_current_theme
+    end
+end
+
+def merge_current
+    save_current_file
+
+    sel = $albums_tv.selection.selected_rows
+
+    call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
+                 "--verbose-level #{$verbose_level} #{additional_booh_options}",
+                 utf8(_("Please wait while scanning source directory...")),
+                 'one dir scan',
+                 { :closure_after => proc {
+                         open_file($filename)
+                         $albums_tv.selection.select_path(sel[0])
+                         $modified = true
+                         $main_window.urgency_hint = true
+                     } })
+end
+
+def merge_newsubs
+    save_current_file
+
+    sel = $albums_tv.selection.selected_rows
+
+    call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
+                 "--verbose-level #{$verbose_level} #{additional_booh_options}",
+                 utf8(_("Please wait while scanning source directory...")),
+                 'subdirs scan',
+                 { :closure_after => proc {
+                         open_file($filename)
+                         $albums_tv.selection.select_path(sel[0])
+                         $modified = true
+                         $main_window.urgency_hint = true
+                     } })
+end
+
+def merge
+    save_current_file
+
+    theme = $xmldoc.root.attributes['theme']
+    limit_sizes = $xmldoc.root.attributes['limit-sizes']
+    if limit_sizes
+        limit_sizes = "--sizes #{limit_sizes}"
+    end
+    call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
+                 "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
+                 utf8(_("Please wait while scanning source directory...")),
+                 'full scan',
+                 { :closure_after => proc {
+                         open_file($filename)
+                         $modified = true
+                         $main_window.urgency_hint = true
+                     } })
+end
+
+def save_as_do
+    fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
+                                    nil,
+                                    Gtk::FileChooser::ACTION_SAVE,
+                                    nil,
+                                    [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+    fc.transient_for = $main_window
+    fc.add_shortcut_folder(File.expand_path("~/.booh"))
+    fc.set_current_folder(File.expand_path("~/.booh"))
+    fc.filename = $orig_filename
+    if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+        $orig_filename = fc.filename
+        if ! save_current_file_user
+            fc.destroy
+            return save_as_do
+        end
+        $config['last-opens'] ||= []
+        $config['last-opens'] << $orig_filename
+    end
+    fc.destroy
+end
+
+def preferences
+    dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    dialog.vbox.add(notebook = Gtk::Notebook.new)
+    notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
+               0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
+               1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips = Gtk::Tooltips.new
+    tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
+for example: /usr/bin/mplayer %f")), nil)
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
+               0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
+               1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
+for example: /usr/bin/gimp-remote %f")), nil)
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
+               0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
+               1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
+for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
+               0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
+               1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
+    tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
+               0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
+    tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
+               0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting a photo or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
+
+    smp_check.signal_connect('toggled') {
+        smp_hbox.sensitive = smp_check.active?
+    }
+    if $config['mproc']
+        smp_check.active = true
+        smp_spin.value = $config['mproc'].to_i
+    end
+    nogestures_check.active = $config['nogestures']
+    deleteondisk_check.active = $config['deleteondisk']
+
+    notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
+    tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
+               0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+    tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
+               1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nphotos in new albums:"))),
+               0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
+               1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
+               2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for photos and videos in new albums. Use this entry to use something else.")), nil)
+    commentsformat_help.signal_connect('clicked') {
+        show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
+hence you should look at ImageMagick/identify documentation for the most    
+accurate and up-to-date documentation. Last time I checked, documentation
+was:
+
+Print information about the image in a format of your choosing. You can
+include the image filename, type, width, height, Exif data, or other image
+attributes by embedding special format characters:                          
+
+                     %O   page offset
+                     %P   page width and height                             
+                     %b   file size                                         
+                     %c   comment                                           
+                     %d   directory                                         
+                     %e   filename extension                                
+                     %f   filename                                          
+                     %g   page geometry                                     
+                     %h   height                                            
+                     %i   input filename                                    
+                     %k   number of unique colors                           
+                     %l   label                                             
+                     %m   magick                                            
+                     %n   number of scenes                                  
+                     %o   output filename                                   
+                     %p   page number                                       
+                     %q   quantum depth                                     
+                     %r   image class and colorspace                        
+                     %s   scene number                                      
+                     %t   top of filename                                   
+                     %u   unique temporary filename                         
+                     %w   width                                             
+                     %x   x resolution                                      
+                     %y   y resolution                                      
+                     %z   image depth                                       
+                     %@   bounding box                                      
+                     %#   signature                                         
+                     %%   a percent sign                                    
+                                                                            
+For example,                                                                
+                                                                            
+    %m:%f %wx%h
+                                                                            
+displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
+width is 512 and height is 480.                
+                                                                            
+If the first character of string is @, the format is read from a file titled
+by the remaining characters in the string.
+                                                                            
+You can also use the following special formatting syntax to print Exif
+information contained in the file:
+                                                                            
+    %[EXIF:tag]                                                             
+                                                                            
+Where tag can be one of the following:                                      
+                                                                            
+    *  (print all Exif tags, in keyword=data format)                        
+    !  (print all Exif tags, in tag_number data format)                     
+    #hhhh (print data for Exif tag #hhhh)                                   
+    ImageWidth                                                              
+    ImageLength                                                             
+    BitsPerSample                                                           
+    Compression                                                             
+    PhotometricInterpretation                                               
+    FillOrder                                                               
+    DocumentName                                                            
+    ImageDescription                                                        
+    Make                                                                    
+    Model                                                                   
+    StripOffsets                                                            
+    Orientation                                                             
+    SamplesPerPixel                                                         
+    RowsPerStrip                                                            
+    StripByteCounts                                                         
+    XResolution                                                             
+    YResolution                                                             
+    PlanarConfiguration                                                     
+    ResolutionUnit                                                          
+    TransferFunction                                                        
+    Software                                                                
+    DateTime                                                                
+    Artist                                                                  
+    WhitePoint                                                              
+    PrimaryChromaticities                                                   
+    TransferRange                                                           
+    JPEGProc                                                                
+    JPEGInterchangeFormat                                                   
+    JPEGInterchangeFormatLength                                             
+    YCbCrCoefficients                                                       
+    YCbCrSubSampling                                                        
+    YCbCrPositioning                                                        
+    ReferenceBlackWhite                                                     
+    CFARepeatPatternDim                                                     
+    CFAPattern                                                              
+    BatteryLevel                                                            
+    Copyright                                                               
+    ExposureTime                                                            
+    FNumber                                                                 
+    IPTC/NAA                                                                
+    ExifOffset                                                              
+    InterColorProfile                                                       
+    ExposureProgram                                                         
+    SpectralSensitivity                                                     
+    GPSInfo                                                                 
+    ISOSpeedRatings                                                         
+    OECF                                                                    
+    ExifVersion                                                             
+    DateTimeOriginal                                                        
+    DateTimeDigitized                                                       
+    ComponentsConfiguration                                                 
+    CompressedBitsPerPixel                                                  
+    ShutterSpeedValue                                                       
+    ApertureValue                                                           
+    BrightnessValue                                                         
+    ExposureBiasValue                                                       
+    MaxApertureValue                                                        
+    SubjectDistance                                                         
+    MeteringMode                                                            
+    LightSource                                                             
+    Flash                                                                   
+    FocalLength                                                             
+    MakerNote                                                               
+    UserComment                                                             
+    SubSecTime                                                              
+    SubSecTimeOriginal                                                      
+    SubSecTimeDigitized                                                     
+    FlashPixVersion                                                         
+    ColorSpace                                                              
+    ExifImageWidth                                                          
+    ExifImageLength                                                         
+    InteroperabilityOffset                                                  
+    FlashEnergy                                                             
+    SpatialFrequencyResponse                                                
+    FocalPlaneXResolution                                                   
+    FocalPlaneYResolution                                                   
+    FocalPlaneResolutionUnit                                                
+    SubjectLocation                                                         
+    ExposureIndex                                                           
+    SensingMethod                                                           
+    FileSource                                                              
+    SceneType")), { :scrolled => true })
+    }
+    tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
+               0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
+    update_exif_orientation_check.active = $config['rotate-set-exif'] == 'true'
+    tbl.attach(transcode_videos = Gtk::CheckButton.new(utf8(_("Transcode videos"))).set_active(!$config['transcode-videos'].nil?),
+               0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    transcode_videos.active = ! $config['transcode-videos'].nil?
+    tbl.attach(transcode_videos_command = Gtk::Entry.new.set_text($config['transcode-videos'] || 'avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality=6 -o %o %f'),
+               1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(transcode_videos, utf8(_("Whether to transcode videos into the web-album instead of using the original videos directly (can be an interesting disk space saver!). First put the extension of the output video and a colon; then use %f to specify the input and %o the output;
+for example: avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality=6 -o %o %f")), nil)
+    transcode_videos.signal_connect('toggled') {
+        transcode_videos_command.sensitive = transcode_videos.active?
+    }
+    transcode_videos_command.sensitive = transcode_videos.active?
+
+    dialog.vbox.show_all
+    dialog.run { |response|
+        if response == Gtk::Dialog::RESPONSE_OK
+            $config['video-viewer'] = from_utf8(video_viewer_entry.text)
+            $config['image-editor'] = from_utf8(image_editor_entry.text)
+            $config['browser'] = from_utf8(browser_entry.text)
+            if smp_check.active?
+                $config['mproc'] = smp_spin.value.to_i
+            else
+                $config.delete('mproc')
+            end
+            $config['nogestures'] = nogestures_check.active?
+            $config['deleteondisk'] = deleteondisk_check.active?
+
+            $config['convert-enhance'] = from_utf8(enhance_entry.text)
+            $config['comments-format'] = from_utf8(commentsformat_entry.text.gsub(/'/, ''))
+            $config['rotate-set-exif'] = update_exif_orientation_check.active?.to_s
+            if transcode_videos.active?
+                $config['transcode-videos'] = transcode_videos_command.text
+            else
+                $config.delete('transcode-videos')
+            end
+        end
+    }
+    dialog.destroy
+end
+
+def perform_undo
+    if $undo_tb.sensitive?
+        $redo_tb.sensitive = $redo_mb.sensitive = true
+        if not more_undoes = UndoHandler.undo($statusbar)
+            $undo_tb.sensitive = $undo_mb.sensitive = false
+        end
+    end
+end
+
+def perform_redo
+    if $redo_tb.sensitive?
+        $undo_tb.sensitive = $undo_mb.sensitive = true
+        if not more_redoes = UndoHandler.redo($statusbar)
+            $redo_tb.sensitive = $redo_mb.sensitive = false
+        end
+    end
+end
+
+def show_one_click_explanation(intro)
+    show_popup($main_window, utf8(_("<b>One-Click tools.</b>
+
+%s When such a tool is activated
+(<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, <span foreground='darkblue'>Enhance</span> or <span foreground='darkblue'>Delete</span>), clicking
+on a thumbnail will immediately apply the desired action.
+
+Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
+") % intro), { :pos_centered => true })
+end
+
+def create_menu_and_toolbar
+    
+    #- menu
+    mb = Gtk::MenuBar.new
+
+    filemenu = Gtk::MenuItem.new(utf8(_("_File")))
+    filesubmenu = Gtk::Menu.new
+    filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
+    filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
+    filesubmenu.append(            Gtk::SeparatorMenuItem.new)
+    filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
+    filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
+    filesubmenu.append(            Gtk::SeparatorMenuItem.new)
+    tooltips = Gtk::Tooltips.new
+    filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed photos/videos in current subalbum"))).set_sensitive(false))
+    $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
+    tooltips.set_tip($merge_current, utf8(_("Take into account new/removed photos/videos in currently viewed subalbum")), nil)
+    filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
+    $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
+    tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
+    filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed photos/videos"))).set_sensitive(false))
+    $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
+    tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed photos/videos in existing subalbums (anywhere)")), nil)
+    filesubmenu.append(            Gtk::SeparatorMenuItem.new)
+    filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
+    $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
+    tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
+    filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
+    $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
+    filesubmenu.append(            Gtk::SeparatorMenuItem.new)
+    filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
+    tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
+    filesubmenu.append(            Gtk::SeparatorMenuItem.new)
+    filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
+    filemenu.set_submenu(filesubmenu)
+    mb.append(filemenu)
+
+    new.signal_connect('activate') { new_album }
+    open.signal_connect('activate') { open_file_popup }
+    $save.signal_connect('activate') { save_current_file_user }
+    $save_as.signal_connect('activate') { save_as_do }
+    $merge_current.signal_connect('activate') { merge_current }
+    $merge_newsubs.signal_connect('activate') { merge_newsubs }
+    $merge.signal_connect('activate') { merge }
+    $generate.signal_connect('activate') {
+        save_current_file
+        call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
+                     utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
+                     'web-album',
+                     { :successmsg => $xmldoc.root.attributes['multi-languages'] ?
+                         utf8(_("Your web-album is now ready in directory '%s'.
+As multi-languages is activated, you will not be able to view it
+comfortably in your browser though.") % $xmldoc.root.attributes['destination']) :
+                         utf8(_("Your web-album is now ready in directory '%s'.
+Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
+                       :successmsg_linkurl => $xmldoc.root.attributes['multi-languages'] ? $xmldoc.root.attributes['destination'] :
+                                                                                           $xmldoc.root.attributes['destination'] + '/index.html',
+                       :closure_after => proc {
+                             $xmldoc.elements.each('//dir') { |elem|
+                                 $modified ||= elem.attributes['already-generated'].nil?
+                                 elem.add_attribute('already-generated', 'true')
+                             }
+                             UndoHandler.cleanup   #- prevent save_changes to mark current dir as not already generated
+                             $undo_tb.sensitive = $undo_mb.sensitive = false
+                             $redo_tb.sensitive = $redo_mb.sensitive = false
+                             save_current_file
+                             $generated_outofline = true
+                             $main_window.urgency_hint = true
+                         }})
+    }
+    $view_wa.signal_connect('activate') {
+        indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
+        if File.exists?(indexhtml)
+            open_url(indexhtml)
+        else
+            show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
+        end
+    }
+    $properties.signal_connect('activate') { properties }
+
+    quit.signal_connect('activate') { try_quit }
+
+    editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
+    editsubmenu = Gtk::Menu.new
+    editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
+    editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
+    editsubmenu.append(           Gtk::SeparatorMenuItem.new)
+    editsubmenu.append($sort_by_exif_date = Gtk::ImageMenuItem.new(utf8(_("Sort by EXIF date"))).set_sensitive(false))
+    $sort_by_exif_date.image = Gtk::Image.new("#{$FPATH}/images/sort_by_exif_date.png")
+    editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
+    $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
+    tooltips.set_tip($remove_all_captions, utf8(_("Mainly useful when you don't want to type any caption, that will remove default captions made of filenames")), nil)
+    editsubmenu.append(           Gtk::SeparatorMenuItem.new)
+    editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
+    editmenu.set_submenu(editsubmenu)
+    mb.append(editmenu)
+
+    $remove_all_captions.signal_connect('activate') { remove_all_captions }
+    $sort_by_exif_date.signal_connect('activate') { sort_by_exif_date }
+
+    prefs.signal_connect('activate') { preferences }
+    
+    helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
+    helpsubmenu = Gtk::Menu.new
+    helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
+    one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
+    helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
+    speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
+    helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
+    tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
+    helpsubmenu.append(Gtk::SeparatorMenuItem.new)
+    helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
+    helpmenu.set_submenu(helpsubmenu)
+    mb.append(helpmenu)
+
+    one_click.signal_connect('activate') {
+        show_one_click_explanation(_("One-Click tools are available in the toolbar."))
+    }
+    
+    speed.signal_connect('activate') {
+        show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
+
+<span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
+<span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
+<span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
+<span foreground='darkblue'>Control-Enter</span>: for a photo, open larger view; for a video, launch player
+<span foreground='darkblue'>Control-Delete</span>: delete image
+<span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
+<span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
+<span foreground='darkblue'>Control-z</span>: undo
+<span foreground='darkblue'>Control-r</span>: redo
+
+<span size='large' weight='bold'>Mouse gestures:</span>
+
+Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
+for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
+
+<span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
+<span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
+<span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
+<span foreground='darkblue'>Left click, hold left button, right click</span>: undo
+<span foreground='darkblue'>Right click, hold right button, left click</span>: redo
+")), { :pos_centered => true, :not_transient => true })
+    }
+
+    tutos.signal_connect('activate') {
+        open_url('http://booh.org/tutorial')
+    }
+
+    about.signal_connect('activate') { call_about }
+
+
+    #- toolbar
+    tb = Gtk::Toolbar.new
+
+    tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
+    open.label = utf8(_("Open"))  #- to avoid missing gtk2 l10n catalogs
+    open.menu = Gtk::Menu.new
+    open.signal_connect('clicked') { open_file_popup }
+    open.signal_connect('show-menu') {
+        lastopens = Gtk::Menu.new
+        j = 0
+        if $config['last-opens']
+            $config['last-opens'].reverse.each { |e|
+                lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
+                item.signal_connect('activate') {
+                    if ask_save_modifications(utf8(_("Save this album?")),
+                                              utf8(_("Do you want to save the changes to this album?")),
+                                              { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
+                        push_mousecursor_wait
+                        msg = open_file_user(from_utf8(e))
+                        pop_mousecursor
+                        if msg
+                            show_popup($main_window, msg)
+                        end
+                    end
+                }
+                j += 1
+            }
+            lastopens.show_all
+        end
+        open.menu = lastopens
+    }
+
+    tb.insert(-1, Gtk::SeparatorToolItem.new)
+
+    tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
+    $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
+    $r90.label = utf8(_("Rotate"))
+    tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
+    $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
+    $r270.label = utf8(_("Rotate"))
+    tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
+    $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
+    $enhance.label = utf8(_("Enhance"))
+    tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
+    $delete.label = utf8(_("Delete"))  #- to avoid missing gtk2 l10n catalogs
+    tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
+    nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
+    nothing.label = utf8(_("None"))
+
+    tb.insert(-1, Gtk::SeparatorToolItem.new)
+
+    tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
+    tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
+
+
+    $undo_tb.signal_connect('clicked')  { perform_undo }
+    $undo_mb.signal_connect('activate') { perform_undo }
+    $redo_tb.signal_connect('clicked')  { perform_redo }
+    $redo_mb.signal_connect('activate') { perform_redo }
+
+    one_click_explain_try = proc {
+        if !$config['one-click-explained']
+            show_one_click_explanation(_("You have just clicked on a One-Click tool."))
+            $config['one-click-explained'] = true
+        end
+    }
+
+    $r90.signal_connect('toggled') {
+        if $r90.active?
+            set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
+            one_click_explain_try.call
+            $r270.active = false
+            $enhance.active = false
+            $delete.active = false
+            nothing.sensitive = true
+        else
+            if !$r270.active? && !$enhance.active? && !$delete.active?
+                set_mousecursor_normal
+                nothing.sensitive = false
+            else
+                nothing.sensitive = true
+            end
+        end
+        true
+    }
+    $r270.signal_connect('toggled') {
+        if $r270.active?
+            set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
+            one_click_explain_try.call
+            $r90.active = false
+            $enhance.active = false
+            $delete.active = false
+            nothing.sensitive = true
+        else
+            if !$r90.active? && !$enhance.active? && !$delete.active?
+                set_mousecursor_normal
+                nothing.sensitive = false
+            else
+                nothing.sensitive = true
+            end
+        end
+    }
+    $enhance.signal_connect('toggled') {
+        if $enhance.active?
+            set_mousecursor(Gdk::Cursor::SPRAYCAN)
+            one_click_explain_try.call
+            $r90.active = false
+            $r270.active = false
+            $delete.active = false
+            nothing.sensitive = true
+        else
+            if !$r90.active? && !$r270.active? && !$delete.active?
+                set_mousecursor_normal
+                nothing.sensitive = false
+            else
+                nothing.sensitive = true
+            end
+        end
+    }
+    $delete.signal_connect('toggled') {
+        if $delete.active?
+            set_mousecursor(Gdk::Cursor::PIRATE)
+            one_click_explain_try.call
+            $r90.active = false
+            $r270.active = false
+            $enhance.active = false
+            nothing.sensitive = true
+        else
+            if !$r90.active? && !$r270.active? && !$enhance.active?
+                set_mousecursor_normal
+                nothing.sensitive = false
+            else
+                nothing.sensitive = true
+            end
+        end
+    }
+    nothing.signal_connect('clicked') {
+        $r90.active = $r270.active = $enhance.active = $delete.active = false
+        set_mousecursor_normal
+    }
+
+    return [ mb, tb ]
+end
+
+def gtk_thread_protect(&proc)
+    if Thread.current == Thread.main
+        proc.call
+    else
+        $protect_gtk_pending_calls.synchronize {
+            $gtk_pending_calls << proc
+        }
+    end
+end
+
+def gtk_thread_flush
+    #- try to lock. we cannot synchronize blindly because this might be called from
+    #- within the timeout flushing procs. if this is the case, not doing anything
+    #- should be ok since the timeout is already flushing them all.
+    if $protect_gtk_pending_calls.try_lock
+        for closure in $gtk_pending_calls
+            closure.call
+        end
+        $gtk_pending_calls = []
+        $protect_gtk_pending_calls.unlock
+    end
+end
+
+def ask_password_protect
+    value = $xmldir.attributes['password-protect']
+
+    dialog = Gtk::Dialog.new(utf8(_("Password protect this sub-album")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    lbl = Gtk::Label.new
+    lbl.markup = utf8(
+_("You can choose to <b>password protect</b> the sub-album '%s' (only available
+if you plan to publish your web-album with an Apache web-server). This will use
+the .htaccess/.htpasswd feature of Apache (not so strongly crypted password, but
+generally ok for protecting web contents). Users will be prompted with a dialog
+asking for a username and a password, failure to give the correct pair will
+block access.
+") % File.basename($current_path))
+    dialog.vbox.add(lbl)
+    dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("free access")))).
+                                                                         add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("password protect with password file:")))).
+                                                                         add(file = Gtk::Entry.new)))
+    dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0.5, 0.2).add(Gtk::HBox.new.add(bt_help = Gtk::Button.new(utf8(_("help about password file")))).
+                                                                             add(Gtk::Label.new).
+                                                                             add(bt_gen = Gtk::Button.new(utf8(_("generate a password file"))))))
+    dialog.window_position = Gtk::Window::POS_MOUSE
+    dialog.show_all
+    if value.nil?
+        rb_no.active = true
+    else
+        rb_yes.active = true
+        file.text = value
+    end
+
+    bt_help.signal_connect('clicked') {
+        show_popup(dialog, utf8(
+_("Password protection proposed here uses the .htaccess/.htpasswd features
+proposed by Apache. So first, be sure you will publish your web-album on an
+Apache web-server. Second, you will need to have a .htpasswd file accessible
+by Apache somewhere on the web-server disks. The password file you must
+provide in the dialog when choosing to password protect is the full absolute
+path to access this file <b>on the web-server</b> (not on your machine). Note
+that if you use a relative path, it will be considered relative to the
+Document Root of the Apache configuration.")))
+    }
+
+    bt_gen.signal_connect('clicked') {
+        gendialog = Gtk::Dialog.new(utf8(_("Generate a password file")),
+                                    dialog,
+                                    Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                                    [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                                    [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+        
+        lbl = Gtk::Label.new
+        lbl.text = utf8(
+_("I can generate a password file (.htpasswd for Apache) for you. Just type
+the username and password you wish to put in it below and validate."))
+        gendialog.vbox.add(lbl)
+        gendialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(Gtk::Label.new(utf8(_('Username:')))).
+                                                                                add(user = Gtk::Entry.new).
+                                                                                add(Gtk::Label.new(utf8(_('Password:')))).
+                                                                                add(pass = Gtk::Entry.new)))
+        pass.visibility = false
+        gendialog.window_position = Gtk::Window::POS_MOUSE
+        gendialog.show_all
+        gendialog.run { |response|
+            u = user.text
+            p = pass.text
+            gendialog.destroy
+            if response == Gtk::Dialog::RESPONSE_OK
+                def rand_letter
+                    ary = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + [ '.', '/' ]
+                    return ary[rand(ary.length)]
+                end
+                fout = Tempfile.new("htpasswd")
+                fout.write("#{u}:#{p.crypt(rand_letter + rand_letter)}\n")
+                fout.close
+                File.chmod(0644, fout.path)
+                show_popup(dialog, utf8(
+_("The file <b>%s</b> now contains the username and the crypted password. Now
+copy it to a suitable location on the machine hosting the Apache web-server (better not
+below the Document Root), and specify this location in the password protect dialog.") % fout.path), { :selectable => true })
+            end
+        }
+    }
+
+    dialog.run { |response|
+        if rb_no.active?
+            newval = nil
+        else
+            newval = file.text
+        end
+        dialog.destroy
+        if response == Gtk::Dialog::RESPONSE_OK && value != newval
+            $modified = true
+            msg 3, "changing password protection of #{$current_path} to #{newval}"
+            if newval.nil?
+                $xmldir.delete_attribute('password-protect')
+            else
+                $xmldir.add_attribute('password-protect', newval)
+            end
+            save_undo(_("set password protection for %s") % File.basename($current_path),
+                      proc {
+                          if value.nil?
+                              $xmldir.delete_attribute('password-protect')
+                          else
+                              $xmldir.add_attribute('password-protect', value)
+                          end
+                          proc {
+                              if newval.nil?
+                                  $xmldir.delete_attribute('password-protect')
+                              else
+                                  $xmldir.add_attribute('password-protect', newval)
+                              end
+                          }
+                      })
+            show_password_protections
+        end
+    }
+end
+
+def create_main_window
+
+    mb, tb = create_menu_and_toolbar
+
+    $albums_tv = Gtk::TreeView.new
+    $albums_tv.set_size_request(120, -1)
+    $albums_tv.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }))
+    $albums_tv.append_column(tcol = Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, { :text => 0 }))
+    $albums_tv.expander_column = tcol
+    $albums_tv.set_headers_visible(false)
+    $albums_tv.selection.signal_connect('changed') { |w|
+        push_mousecursor_wait
+        save_changes
+	iter = w.selected
+        if !iter
+            msg 3, "no selection"
+        else
+            $current_path = $albums_ts.get_value(iter, 1)
+            change_dir
+        end
+        pop_mousecursor
+    }
+
+#    offset = "0:0"
+#    Gtk.timeout_add(1000) {
+#        puts "trying offset #{offset}"
+#        iter = $albums_ts.get_iter(offset)
+#        if iter
+#            puts "...ok at offset #{offset}"
+#            $current_path = $albums_ts.get_value(iter, 1)
+#            change_dir
+#        end
+#        if offset == "0:0"
+#           offset = "0:1"
+#        else   
+#           offset = "0:0"
+#        end
+#    }
+
+    $albums_tv.signal_connect('button-release-event') { |w, event|
+        if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3 && !$current_path.nil?
+            menu = Gtk::Menu.new
+            menu.append(passprotect = Gtk::ImageMenuItem.new(utf8(_("Password protect"))))
+            passprotect.image = Gtk::Image.new("#{$FPATH}/images/galeon-secure.png")
+            passprotect.signal_connect('activate') { ask_password_protect }
+            menu.append(restore = Gtk::ImageMenuItem.new(utf8(_("Restore deleted photos/videos/subalbums"))))
+            restore.image = Gtk::Image.new("#{$FPATH}/images/restore.png")
+            restore.signal_connect('activate') { restore_deleted }
+            menu.append(Gtk::SeparatorMenuItem.new)
+            menu.append(delete = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
+            delete.signal_connect('activate') {
+                if show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
+                    delete_current_subalbum
+                end
+            }
+            menu.show_all
+            menu.popup(nil, nil, event.button, event.time)
+        end
+    }
+
+    $albums_ts = Gtk::TreeStore.new(String, String, Gdk::Pixbuf)
+    $albums_tv.set_model($albums_ts)
+    $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
+
+    albums_sw = Gtk::ScrolledWindow.new(nil, nil)
+    albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
+    albums_sw.add_with_viewport($albums_tv)
+
+    $notebook = Gtk::Notebook.new
+    create_subalbums_page
+    $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
+    create_auto_table
+    $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
+    $notebook.show_all
+    $notebook.signal_connect('switch-page') { |w, page, num|
+        if num == 0
+            $delete.active = false
+            $delete.sensitive = false
+        else
+            $delete.sensitive = true
+        end
+        if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
+            if num == 0
+                textview.buffer.text = $thumbnails_title.buffer.text
+            else
+                if $notebook.get_tab_label($autotable_sw).sensitive?
+                    $thumbnails_title.buffer.text = textview.buffer.text
+                end
+            end
+        end
+    }
+
+    paned = Gtk::HPaned.new
+    paned.pack1(albums_sw, false, false)
+    paned.pack2($notebook, true, true)
+
+    main_vbox = Gtk::VBox.new(false, 0)
+    main_vbox.pack_start(mb, false, false)
+    main_vbox.pack_start(tb, false, false)
+    main_vbox.pack_start(paned, true, true)
+    main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
+
+    $main_window = create_window
+    $main_window.add(main_vbox)
+    $main_window.signal_connect('delete-event') {
+        try_quit({ :disallow_cancel => true })
+    }
+    $main_window.signal_connect('focus-in-event') {
+        $main_window.urgency_hint = false
+    }
+
+    #- read/save size and position of window
+    if $config['pos-x'] && $config['pos-y']
+        $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
+    else
+        $main_window.window_position = Gtk::Window::POS_CENTER
+    end
+    msg 3, "size: #{$config['width']}x#{$config['height']}"
+    $main_window.set_default_size(($config['width'] || 800).to_i, ($config['height'] || 600).to_i)
+    $main_window.signal_connect('configure-event') {
+        msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
+        x, y = $main_window.window.root_origin
+        width, height = $main_window.window.size
+        $config['pos-x'] = x
+        $config['pos-y'] = y
+        $config['width'] = width
+        $config['height'] = height
+        false
+    }
+
+    $protect_gtk_pending_calls = Mutex.new
+    $gtk_pending_calls = []
+    Gtk.timeout_add(100) {
+        $protect_gtk_pending_calls.synchronize {
+            for closure in $gtk_pending_calls
+                closure.call
+            end
+            $gtk_pending_calls = []
+        }
+        true
+    }
+
+    $statusbar.push(0, utf8(_("Ready.")))
+    $main_window.show_all
+end
+
+
+if str = Gtk.check_version(2, 8, 0)
+    puts "This program requires GTK+ 2.8.0 or later"
+    puts str
+    exit
+end
+
+
+handle_options
+Thread.abort_on_exception = true
+read_config
+
+Gtk.init
+create_main_window
+
+check_config
+
+if ARGV[0]
+    open_file_user(ARGV[0])
+end
+
+Gtk.main
+
+write_config


Property changes on: packages/booh/branches/upstream/current/bin/booh
___________________________________________________________________
Name: svn:executable
   + 

Added: packages/booh/branches/upstream/current/bin/booh-backend
===================================================================
--- packages/booh/branches/upstream/current/bin/booh-backend	                        (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh-backend	2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,1429 @@
+#! /usr/bin/ruby
+#
+#                         *  BOOH  *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'getoptlong'
+require 'gettext'
+require 'gettext/locale'
+include GetText
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+require 'booh/html-merges'
+
+#- bind text domain as soon as possible because some _() functions are called early to build data structures
+bindtextdomain("booh")
+#- save locale for restoring for multi languages
+$default_locale = Locale.get
+
+#- options
+$options = [
+    [ '--help',          '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
+    [ '--version',       '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
+
+    [ '--source',        '-s', GetoptLong::REQUIRED_ARGUMENT, _("Directory which contains original photos/videos as files or subdirs") ],
+    [ '--destination',   '-d', GetoptLong::REQUIRED_ARGUMENT, _("Directory which will contain the web-album; if it already exits, then all existing files and directories inside it will be removed!") ],
+
+    [ '--theme',         '-t', GetoptLong::REQUIRED_ARGUMENT, _("Select HTML theme to use") ],
+    [ '--config',        '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing photos and videos within directories with captions") ],
+    [ '--config-skel',   '-k', GetoptLong::REQUIRED_ARGUMENT, _("Filename where the script will output a config skeleton") ],
+    [ '--merge-config',  '-M', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to merge new/removed photos/videos from --source, and change theme info") ],
+    [ '--merge-config-onedir',  '-O', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, for merging the subdir specified with --dir") ],
+    [ '--merge-config-subdirs', '-U', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, for merging the new subdirs down the subdir specified with --dir") ],
+    [ '--dir',           '-D', GetoptLong::REQUIRED_ARGUMENT, _("Directory for merge with --merge-config-onedir or --merge-config-subdirs") ],
+    [ '--use-config',    '-u', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to change theme info") ],
+    [ '--force',         '-f', GetoptLong::NO_ARGUMENT, _("Force generation of album even if the GUI marked some directories as already generated") ],
+
+    [ '--sizes',         '-S', GetoptLong::REQUIRED_ARGUMENT, _("Specify the list of images sizes to use instead of all specified in the theme (this is a comma-separated list)") ],
+    [ '--multi-languages', '-L', GetoptLong::REQUIRED_ARGUMENT, _("Specify the list of languages to support (uses Apache MultiViews); this is a comma-separated list of supported languages, with last element used as the fallback language; for example: 'fr,eo,en,en'; supported languages: %s") % SUPPORTED_LANGUAGES.join(', ') ],
+    [ '--thumbnails-per-row', '-T', GetoptLong::REQUIRED_ARGUMENT, _("Specify the amount of thumbnails per row in the thumbnails page (if applicable in theme)") ],
+    [ '--thumbnails-per-page', '-p', GetoptLong::REQUIRED_ARGUMENT, _("Specify the amount of thumbnails per page in the thumbnails page, after which split occurs") ],
+    [ '--optimize-for-32', '-o', GetoptLong::NO_ARGUMENT, _("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)") ],
+    [ '--transcode-videos', '-r', GetoptLong::REQUIRED_ARGUMENT, _("Transcode videos with given external program; %f is the placeholder for the input video, %o for the output video; before the external program, the output video extension should be given followed by a colon") ],
+    [ '--index-link',    '-l', GetoptLong::REQUIRED_ARGUMENT, _("Specify the HTML markup to use on the bottom of pages for a small link returning to wherever you see fit in your website (or somewhere else)") ],
+    [ '--made-with',     '-n', GetoptLong::REQUIRED_ARGUMENT, _("Specify the HTML markup to use on the bottom of pages for a small 'made with' message") ],
+    [ '--comments-format','-c', GetoptLong::REQUIRED_ARGUMENT, _("Specify comments format to use for images instead of only filename when creating new albums; use ImageMagick's format") ],
+
+    [ '--mproc',         '-m', GetoptLong::REQUIRED_ARGUMENT, _("Specify the number of processors for multi-processors machines") ],
+
+    [ '--for-gui',       '-g', GetoptLong::NO_ARGUMENT,       _("Do the minimum work to be able to see the album under the GUI (don't generate all thumbnails)") ],
+
+    [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+    [ '--info-pipe',     '-i', GetoptLong::REQUIRED_ARGUMENT, _("Name a file where to write information about what's going on (used by the GUI)") ],
+]
+
+#- default values for some globals 
+$switches = []
+$stdout.sync = true
+$no_identify = false
+$ignore_videos = false
+$forgui = false
+$hardlinks_ok = true
+
+def usage
+    puts _("Usage: %s [OPTION]...") % File.basename($0)
+    $options.each { |ary|
+        printf " %3s, %-18s %s\n", ary[1], ary[0], ary[3]
+    }
+end
+
+def handle_options
+    parser = GetoptLong.new
+    parser.set_options(*$options.collect { |ary| ary[0..2] })
+    begin
+        parser.each_option do |name, arg|
+            case name
+            when '--help'
+                usage
+                exit(0)
+
+            when '--version'
+                puts _("Booh version %s
+
+Copyright (c) 2005-2008 Guillaume Cottenceau.
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
+
+                exit(0)
+
+            when '--source'
+                $source = File.expand_path(arg.sub(%r|/$|, ''))
+                if !File.directory?($source)
+                    die _("Argument to --source must be a directory")
+                end
+            when '--destination'
+                $dest = File.expand_path(arg.sub(%r|/$|, ''))
+                if File.exists?($dest) && !File.directory?($dest)
+                    die _("If --destination exists, it must be a directory")
+                end
+                if $dest != make_dest_filename($dest)
+                    die _("Sorry, destination directory can't contain non simple alphanumeric characters.")
+                end
+#            when '--clean'
+#                system("rm -rf #{$dest}")
+
+            when '--theme'
+                $theme = arg
+            when '--config'
+                arg = File.expand_path(arg)
+                if File.readable?(arg)
+                    $xmldoc = REXML::Document.new File.new(arg)
+                    $mode = 'use_config'
+                else
+                    die _('Config file does not exist or is unreadable.')
+                end
+            when '--config-skel'
+                arg = File.expand_path(arg)
+                if File.exists?(arg)
+                    if File.directory?(arg)
+                        die _("Config skeleton file (%s) already exists and is a directory! Please change the filename.") % arg
+                    else
+                        msg 1, _("Config skeleton file already exists, backuping to %s.backup") % arg
+                        File.rename(arg, "#{arg}.backup")
+                    end
+                end
+                $config_writeto = arg
+                $mode = 'gen_config'
+            when '--merge-config'
+                arg = File.expand_path(arg)
+                if File.readable?(arg)
+                    msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
+                    $xmldoc = REXML::Document.new File.new(arg)
+                    File.rename(arg, "#{arg}.backup")
+                    $config_writeto = arg
+                    $mode = 'merge_config'
+                else
+                    die _('Config file does not exist or is unreadable.')
+                end
+            when '--merge-config-onedir'
+                arg = File.expand_path(arg)
+                if File.readable?(arg)
+                    msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
+                    $xmldoc = REXML::Document.new File.new(arg)
+                    File.rename(arg, "#{arg}.backup")
+                    $config_writeto = arg
+                    $mode = 'merge_config_onedir'
+                else
+                    die _('Config file does not exist or is unreadable.')
+                end
+            when '--merge-config-subdirs'
+                arg = File.expand_path(arg)
+                if File.readable?(arg)
+                    msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
+                    $xmldoc = REXML::Document.new File.new(arg)
+                    File.rename(arg, "#{arg}.backup")
+                    $config_writeto = arg
+                    $mode = 'merge_config_subdirs'
+                else
+                    die _('Config file does not exist or is unreadable.')
+                end
+            when '--dir'
+                arg = File.expand_path(arg)
+                if !File.readable?(arg)
+                    die _('Specified directory to merge with --dir is not readable')
+                else
+                    $onedir = arg
+                end
+            when '--use-config'
+                arg = File.expand_path(arg)
+                if File.readable?(arg)
+                    msg 2, _("Use config notice: backuping current config file to %s.backup") % arg
+                    $xmldoc = REXML::Document.new File.new(arg)
+                    File.rename(arg, "#{arg}.backup")
+                    $config_writeto = arg
+                    $mode = 'use_config_changetheme'
+                else
+                    die _('Config file does not exist or is unreadable.')
+                end
+
+            when '--sizes'
+                $limit_sizes = arg
+
+            when '--multi-languages'
+                parts = arg.split(',')
+                if parts.size == 0 || parts.find_all { |e| ! SUPPORTED_LANGUAGES.include?(e.strip) }.size > 0
+                    die _("--multi-languages: argument must be a comma-separated list of supported languages, with last element used as the fallback language; for example: 'fr,eo,en,en'; supported languages: %s") % SUPPORTED_LANGUAGES.join(', ')
+                end
+                $multi_languages = [ parts[0..-2], parts[-1] ]
+
+            when '--thumbnails-per-row'
+                $N_per_row = arg
+
+            when '--thumbnails-per-page'
+                $N_per_page = arg
+
+            when '--optimize-for-32'
+                $optimize_for_32 = true
+
+            when '--transcode-videos'
+                parts = arg.split(':', 2)
+                if parts.size != 2 || parts[0] =~ / / || arg !~ /%f/ || arg !~ /%o/
+                    die _("--transcode-videos: argument must be the external program for transcoding, and contain %f for the input video, %o for the output video, and before the external program, the output video extension should be given followed by a colon")
+                end
+                $transcode_videos = arg
+
+            when '--made-with'
+                $madewith = arg
+
+            when '--index-link'
+                $indexlink = arg
+
+            when '--comments-format'
+                $commentsformat = arg
+
+            when '--force'
+                $force = true
+
+            when '--mproc'
+                $mproc = arg.to_i
+                $pids = []
+
+            when '--for-gui'
+                $forgui = true
+
+            when '--verbose-level'
+                $verbose_level = arg.to_i
+
+            when '--info-pipe'
+                $info_pipe = File.open(arg, File::WRONLY)
+                $info_pipe.sync = true
+            end
+        end
+    rescue
+        puts $!
+        usage
+        exit(1)
+    end
+
+    if !$source && $xmldoc
+        $source = from_utf8($xmldoc.root.attributes['source']).sub(%r|/$|, '')
+        $dest = from_utf8($xmldoc.root.attributes['destination']).sub(%r|/$|, '')
+        $theme ||= $xmldoc.root.attributes['theme']
+        $limit_sizes ||= $xmldoc.root.attributes['limit-sizes']
+        if $mode == 'use_config' || $mode =~ /^merge_config/
+            $optimize_for_32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
+            $N_per_row = $xmldoc.root.attributes['thumbnails-per-row']
+            languages = $xmldoc.root.attributes['multi-languages']
+            if languages
+                languages = languages.split(',')
+                $multi_languages = [ languages[0..-2], languages[-1] ]
+            end
+            $N_per_page = $xmldoc.root.attributes['thumbnails-per-page']
+            $madewith = $xmldoc.root.attributes['made-with']
+            $indexlink = $xmldoc.root.attributes['index-link']
+        end
+    end
+
+    if $mode == 'merge_config_onedir' && !$onedir
+        die _("Missing --dir for --merge-config-onedir")
+    end
+    if $mode == 'merge_config_subdirs' && !$onedir
+        die _("Missing --dir for --merge-config-subdirs")
+    end
+
+    if !$source
+        usage
+        exit(0)
+    end
+    if !$dest
+        die _("Missing --destination parameter.")
+    end
+    if !$theme
+        $theme = 'simple'
+    end
+
+    select_theme($theme, $limit_sizes, $optimize_for_32, $N_per_row)
+
+    if !$xmldoc
+        $xmldoc = Document.new "<booh/>"
+        $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
+        $xmldoc.root.add_attribute('version', $VERSION)
+        $xmldoc.root.add_attribute('source', $source)
+        $xmldoc.root.add_attribute('destination', $dest)
+        $xmldoc.root.add_attribute('theme', $theme)
+        if $limit_sizes
+            $xmldoc.root.add_attribute('limit-sizes', $limit_sizes)
+        end
+        if $multi_languages
+            $xmldoc.root.add_attribute('multi-languages', $multi_languages[0].join(',') + ',' + $multi_languages[1])
+        end
+        if $optimize_for_32
+            $xmldoc.root.add_attribute('optimize-for-32', 'true')
+        end
+        if $N_per_row
+            $xmldoc.root.add_attribute('thumbnails-per-row', $N_per_row)
+        end
+        if $N_per_page
+            $xmldoc.root.add_attribute('thumbnails-per-page', $N_per_page)
+        end
+        if $madewith
+            $xmldoc.root.add_attribute('made-with', $madewith)
+        end
+        if $indexlink
+            $xmldoc.root.add_attribute('index-link', $indexlink)
+        end
+        $mode = 'gen_config'
+    end
+
+    if $mode == 'merge_config' || $mode == 'use_config_changetheme'
+        $xmldoc.root.add_attribute('theme', $theme)
+        $xmldoc.root.add_attribute('version', $VERSION)
+        if $limit_sizes
+            $xmldoc.root.add_attribute('limit-sizes', $limit_sizes)
+        else
+            $xmldoc.root.delete_attribute('limit-sizes')
+        end
+        if $multi_languages
+            $xmldoc.root.add_attribute('multi-languages', $multi_languages[0].join(',') + ',' + $multi_languages[1])
+        else
+            $xmldoc.root.delete_attribute('multi-languages')
+        end
+
+        if $optimize_for_32
+            $xmldoc.root.add_attribute('optimize-for-32', 'true')
+        else
+            $xmldoc.root.delete_attribute('optimize-for-32')
+        end
+        if $N_per_row
+            $xmldoc.root.add_attribute('thumbnails-per-row', $N_per_row)
+        else
+            $xmldoc.root.delete_attribute('thumbnails-per-row')
+        end
+        if $N_per_page
+            $xmldoc.root.add_attribute('thumbnails-per-page', $N_per_page)
+        else
+            $xmldoc.root.delete_attribute('thumbnails-per-page')
+        end
+        if $madewith
+            $xmldoc.root.add_attribute('made-with', $madewith)
+        else
+            $xmldoc.root.delete_attribute('made-with')
+        end
+        if $indexlink
+            $xmldoc.root.add_attribute('index-link', $indexlink)
+        else
+            $xmldoc.root.delete_attribute('index-link')
+        end
+    end
+
+    if $transcode_videos
+        $xmldoc.root.add_attribute('transcode-videos', $transcode_videos)
+    else
+        $xmldoc.root.delete_attribute('transcode-videos')
+    end
+
+    if $madewith
+        $madewith = $madewith.gsub('%booh', '"http://booh.org/"')
+    end
+    if $multi_languages
+        $htmlsuffix = ''
+    else
+        $htmlsuffix = '.html'
+    end
+end
+
+def read_config
+    $config = {}
+end
+
+def write_config
+end
+
+def info(value)
+    if $info_pipe
+        $info_pipe.puts(value)
+    end
+end
+
+def die(value)
+    if $info_pipe
+        $info_pipe.puts("die: " + value)
+    end
+    die_ value
+end
+
+def check_installation
+    if !system("which convert >/dev/null 2>/dev/null")
+        die _("The program 'convert' is needed. Please install it. 
+It is generally available with the 'ImageMagick' software package.")
+    end
+    if !system("which identify >/dev/null 2>/dev/null")
+        msg 1, _("the program 'identify' is needed to get images sizes and EXIF data. Please install it.
+It is generally available with the 'ImageMagick' software package.")
+        $no_identify = true
+    end
+    missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
+    if missing != []
+        msg 1, _("the following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')
+        $ignore_videos = true
+    end
+end
+
+def replace_line(surround, keyword, line)
+    begin
+        contents = eval "$#{keyword}"
+        line.sub!(/#{surround}#{keyword}#{surround}/, contents)
+    rescue TypeError
+        die _("No '%s' found for substitution") % keyword
+    end
+end
+
+def build_html_skeletons
+    $html_images     = File.open("#{$FPATH}/themes/#{$theme}/skeleton_image.html").readlines
+    $html_thumbnails = File.open("#{$FPATH}/themes/#{$theme}/skeleton_thumbnails.html").readlines
+    $html_index      = File.open("#{$FPATH}/themes/#{$theme}/skeleton_index.html").readlines
+    for line in $html_images + $html_thumbnails + $html_index
+        while line =~ /~~~(\w+)~~~/
+            replace_line('~~~', $1, line)
+        end
+    end
+end
+
+def find_caption_value(xmldir, filename)
+    if cap = xmldir.elements["*[@filename='#{utf8(filename)}']"].attributes['caption']
+        return cap.gsub("\n", '<br/>')
+    else
+        return nil
+    end
+end
+
+def find_captions(xmldir, images)
+    return images.collect { |img| find_caption_value(xmldir, img) }
+end
+
+#- stolen from CVSspam
+def urlencode(text)
+  text.sub(/[^a-zA-Z0-9\-,.*_\/]/) do
+    "%#{sprintf('%2X', $&[0])}"
+  end
+end
+
+def all_images_sizes
+    return $limit_sizes =~ /original/ ? $images_size + [ { 'name' => 'original' } ] : $images_size
+end
+
+def html_reload_to_thumbnails
+    html_reload_to_thumbnails = $preferred_size_reloader.clone
+    html_reload_to_thumbnails.gsub!(/~~theme~~/, $theme)
+    html_reload_to_thumbnails.gsub!(/~~default_size~~/, $default_size['name'])
+    html_reload_to_thumbnails.gsub!(/~~htmlsuffix~~/, $htmlsuffix)
+    html_reload_to_thumbnails.gsub!(/~~all_sizes~~/, all_images_sizes.collect { |s| "\"#{size2js(s['name'])}\"" }.join(', '))
+    size_auto_chooser = '';
+    all_images_sizes.find_all { |s| s.has_key?('optimizedforwidth') }.
+                     sort { |a,b| b['optimizedforwidth'].to_i <=> a['optimizedforwidth'].to_i }.
+                     each { |s| size_auto_chooser += "if (w + 50 > #{s['optimizedforwidth']}) { return 'thumbnails-#{size2js(s['name'])}-0#{$htmlsuffix}'; }\n" }
+    html_reload_to_thumbnails.gsub!(/~~size_auto_chooser~~/, size_auto_chooser)
+    return html_reload_to_thumbnails
+end
+
+def discover_iterations(iterations, line)
+    if line =~ /~~iterate(\d)_open(_max(\d+|N))?~~/
+        for iter in iterations.values
+            if iter['open']
+                iter['open'] = false
+                iter['close_wait'] = $1.to_i
+            end
+        end
+        max = $3 == 'N' ? ($N_per_row || $default_N) : $3
+        iterations[$1.to_i] = { 'open' => true, 'max' => max, 'opening' => '', 'closing' => '' }
+        if $1.to_i == 1
+            line.sub!(/.*/, '~~thumbnails~~')
+        else
+            line.sub!(/.*/, '')
+        end
+    elsif line =~ /~~iterate(\d)_close~~/
+        iterations[$1.to_i]['open']  = false;
+        iterations[$1.to_i]['close'] = true;
+        line.sub!(/.*/, '')
+    else
+        for iter in iterations.values
+            if iter['open']
+                iter['opening'] += line
+                line.sub!(/.*/, '')
+            end
+            if !iter['close'] && iter['close_wait'] && iterations[iter['close_wait']]['close']
+                iter['closing'] += line
+                line.sub!(/.*/, '')
+            end
+        end
+    end
+end
+
+def reset_iterations(iterations)
+    for iter in iterations.values
+        iter['value'] = 0
+    end
+end
+
+def run_iterations(iterations, amount)
+    html = ''
+    should_rerun = false
+    for level in iterations.keys.sort
+        if iterations[level]['value'] == 0
+            html += iterations[level]['opening']
+        elsif level == iterations.keys.max
+            if !iterations[level]['max'] || iterations[level]['max'] && iterations[level]['value'] + amount <= iterations[level]['max'].to_i
+                html += iterations[level]['opening']
+            else
+                should_rerun = true
+            end
+        end
+        iterations[level]['value'] += amount
+        if iterations[level]['max'] && iterations[level]['value'] > iterations[level]['max'].to_i
+            iterations[level]['value'] = 0
+            iterations[level-1]['value'] = 0
+            html += iterations[level-1]['closing']
+        end
+    end
+    if should_rerun
+        return html + run_iterations(iterations, amount)
+    else
+        return html
+    end
+end
+
+def close_iterations(iterations)
+    html = ''
+    for level in iterations.keys.sort.reverse
+        html += iterations[level]['closing']
+    end
+    return html
+end
+
+def img_element(fullpath)
+    if size = get_image_size(fullpath)
+        sizespec = 'width="' + size[:x].to_s + '" height="' + size[:y].to_s + '"'
+    else
+        sizespec = ''
+    end
+    return '<img src="' + File.basename(fullpath) + '" ' + sizespec + ' alt="image"/>'
+end
+
+def size2js(name)
+    return name.gsub(/-/, '')
+end
+
+def substitute_html_sizes(html, sizeobj, type, suffix)
+    sizestrings = []
+    if $images_size.length > 1 || (type == 'image' && $limit_sizes =~ /original/)
+        for sizeobj2 in $images_size
+            sizejs = size2js(sizeobj2['name'])
+            sizen = defer_translation(sizename(sizeobj2['name'], false))
+            if sizeobj != sizeobj2
+                if type == 'thumbnails'
+                    sizestrings << '<a href="thumbnails-' + sizejs + suffix + $htmlsuffix + '" onclick="set_preferred_size(\'' + sizejs + '\')">' + sizen + '</a>'
+                else
+                    sizestrings << '<a id="link' + sizejs + '" onclick="set_preferred_size(\'' + sizejs + '\')">' + sizen + '</a>'
+                end
+            else
+                sizestrings << sizen
+            end
+        end
+        if type == 'image' && $limit_sizes =~ /original/
+            sizestrings << '<a id="linkoriginal" target="newframe">' + defer_translation(sizename('original', false)) + '</a>'
+        end
+    end
+    html.sub!(/~~sizes~~(.+)~~/) { sizestrings.join($1) }
+end
+
+def substitute_navigation(html, xmldir)
+    if xmldir.parent.name == 'dir'
+        nav = ''
+        path = '..'
+        parent = xmldir.parent
+        while parent.name == 'dir'
+            parentcaption = parent.attributes['subdirs-caption'] || File.basename(parent.attributes['path'])
+            nav = "<a href=\"#{path}/index#{$htmlsuffix}\">#{parentcaption}</a> #{defer_translation(N_(" > "))} #{nav}"
+            path += '/..'
+            parent = parent.parent
+        end
+        html.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/) { $1 }
+        html.gsub!(/~~navigation~~/, nav + (xmldir.attributes['subdirs-caption'] || File.basename(xmldir.attributes['path'])))
+    else
+        html.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/, '')
+    end
+end
+
+def substitute_pathtobase(html, xmldir)
+    path = ''
+    location = xmldir
+    while location.parent.name == 'dir'
+        path = '../' + path
+        location = location.parent
+    end
+    html.gsub!(/~~pathtobase~~/, path)
+end
+
+def xmldir2destdir(xmldir)
+    return make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))
+end
+
+def find_previous_album(xmldir)
+    relative_pos = ''
+    begin
+        #- move to previous dir element if exists
+        if prevelem = xmldir.previous_element_byname_notattr('dir', 'deleted')
+            xmldir = prevelem
+            relative_pos += '../' + xmldir2destdir(xmldir) + '/'
+            child = nil
+            #- after having moved to previous dir, we need to go down last subdir until the last one
+            while child = xmldir.elements['dir']
+                while nextchild = child.next_element_byname_notattr('dir', 'deleted')
+                    child = nextchild
+                end
+                relative_pos += xmldir2destdir(child) + '/'
+                xmldir = child
+            end
+        else
+            #- previous dir doesn't exist, move to previous dir element if exists
+            xmldir = xmldir.parent
+            if xmldir.name == 'dir' && !xmldir.attributes['deleted']
+                relative_pos += '../'
+            else
+                return nil
+            end
+        end
+    end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
+    return File.reduce_path(relative_pos)
+end
+
+def find_next_album(xmldir)
+    relative_pos = ''
+    begin
+        #- first child dir element (catches when initial xmldir has both thumbnails and subdirs)
+        if firstchild = xmldir.child_byname_notattr('dir', 'deleted')
+            xmldir = firstchild
+            relative_pos += xmldir2destdir(xmldir) + '/'
+        #- next brother
+        elsif nextbro = xmldir.next_element_byname_notattr('dir', 'deleted')
+            xmldir = nextbro
+            relative_pos += '../' + xmldir2destdir(xmldir) + '/'
+        else
+            #- go up until we have a next brother or we are finished
+            begin
+                xmldir = xmldir.parent
+                relative_pos += '../'
+            end while xmldir && !xmldir.next_element_byname_notattr('dir', 'deleted')
+            if xmldir
+                xmldir = xmldir.next_element_byname('dir')
+                relative_pos += '../' + xmldir2destdir(xmldir) + '/'
+            else
+                return nil
+            end
+        end
+    end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
+    return File.reduce_path(relative_pos)
+end
+
+def find_translation_for_file(file, msg)
+    if $multi_languages
+        if file =~ /\.(\w\w)\.html$/
+            bindtextdomain("booh", { :locale => "#{$1}.UTF-8" })
+            retval = _(msg)
+            Locale.set_current($default_locale)
+            return retval
+        else
+            die "Internal error: cannot find multi language suffix of file '#{file}'"
+        end
+    else
+        return utf8(_(msg))
+    end
+end
+
+def sub_previous_next_album(file, previous_album, next_album, html, previous_album_msg, next_album_msg)
+    if previous_album
+        html.gsub!(/~~previous_album~~/, '<a href="' + previous_album + 'thumbnails' + $htmlsuffix + '">' + previous_album_msg + '</a>')
+        html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/) { $1 }
+    else
+        html.gsub!(/~~previous_album~~/, '')
+        html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/, '')
+    end
+    if next_album
+        html.gsub!(/~~next_album~~/, '<a href="' + next_album + 'thumbnails' + $htmlsuffix + '">' + next_album_msg + '</a>')
+        html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/) { $1 }
+    else
+        html.gsub!(/~~next_album~~/, '')
+        html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/, '')
+    end
+    return html
+end
+
+def save_html(html, base_filename)
+    if html.class == Array
+        html = html.join('')
+    end
+    if $multi_languages
+        for language in ($multi_languages[0] + [ $multi_languages[1] ]).uniq
+            bindtextdomain("booh", { :locale => "#{language}.UTF-8" })
+            ios = File.open("#{base_filename}.#{language}.html", "w")
+            ios.write(html.gsub(/@@(.*?)@@/) { _($1) })
+            ios.close
+            Locale.set_current($default_locale)
+        end
+    else
+        ios = File.open("#{base_filename}.html", "w")
+        ios.write(html.gsub(/@@(.*?)@@/) { utf8(_($1)) })
+        ios.close
+    end
+end
+
+def walk_source_dir
+
+    #- preprocess the path->dir, rexml is very slow with that; we seem to improve speed by 7%
+    optxpath = {}
+    $xmldoc.elements.each('//dir') { |elem|
+        optxpath[elem.attributes['path']] = elem
+    }
+
+    examined_dirs = nil
+    if $mode == 'merge_config_onedir'
+        examined_dirs = [ $onedir ]
+    elsif $mode == 'merge_config_subdirs'
+        examined_dirs = `find '#{$onedir}' -type d -follow`.sort.collect { |v| v.chomp }.delete_if { |v| optxpath.has_key?(utf8(v)) }
+    else
+        examined_dirs = `find '#{$source}' -type d -follow`.sort.collect { |v| v.chomp }
+        if $mode == 'merge_config'
+            $xmldoc.elements.each('//dir') { |elem|
+                if ! examined_dirs.include?(elem.attributes['path'])
+                    msg 2, _("Merging config: removing directory %s from config, isn't on filesystem anymore") % elem.attributes['path']
+                    elem.remove
+                end
+            }
+        end
+    end
+    info("directories: #{examined_dirs.length}, sizes: #{$images_size.length}")
+
+    examined_dirs.each { |dir|
+        if dir =~ /'/
+            die _("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir
+        end
+        if $mode !~ /^use_config/
+            Dir.entries(dir).each { |file|
+                if file =~ /['"\[\]]/
+                    die _("Files can't contain any of the characters ', \", [ or ], sorry: %s") % "#{dir}/#{file}"
+                end
+            }
+        end
+    }
+
+    examined_dirs.each { |dir|
+        if File.basename(dir) =~ /^\./
+            msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
+            next
+        end
+
+        dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
+
+        #- place xml document on proper node if exists, else create
+        xmldir = optxpath[utf8(dir)]
+        if $mode == 'use_config' || $mode == 'use_config_changetheme'
+            if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
+                info("walking: #{dir}|#{$source}, 0 elements")
+                if xmldir && xmldir.attributes['deleted']
+                    system("rm -rf '#{dest_dir}'")
+                end
+                next
+            end
+        else
+            if $mode == 'gen_config' || (($mode == 'merge_config' || $mode == 'merge_config_subdirs') && !xmldir)
+                #- add the <dir..> element if necessary
+                parent = File.dirname(dir)
+                xmldir = $xmldoc.elements["//dir[@path='#{utf8(parent)}']"]
+                if !xmldir
+                    xmldir = $xmldoc.root
+                end
+                #- need to remove the already-generated mark of the parent because of the sub-albums page containing now one more element
+                xmldir.delete_attribute('already-generated')
+                xmldir = optxpath[utf8(dir)] = xmldir.add_element('dir', { 'path' => utf8(dir) })
+            end
+        end
+        xmldir.delete_attribute('already-generated')
+
+        #- read images/videos entries from config or from directories depending on mode
+        entries = []
+        if $mode == 'use_config' || $mode == 'use_config_changetheme'
+            msg 2, _("Handling %s from config list...") % dir
+            xmldir.elements.each { |element|
+                if %w(image video).include?(element.name) && !element.attributes['deleted']
+                    entries << from_utf8(element.attributes['filename'])
+                end
+            }
+        else
+            msg 2, _("Examining %s...") % dir
+            entries = Dir.entries(dir).sort
+            #- populate config in case of gen_config, add new files in case of merge_config
+            for file in entries
+                if file =~ /['"\[\]]/
+                    msg 1, _("Ignoring %s, contains one of forbidden characters: '\"[]") % "#{dir}/#{file}"
+                else
+                    type = entry2type(file)
+                    if type && !xmldir.elements["#{type}[@filename='#{utf8(file)}']"]
+                        #- hack: don't run identify (which is slow) if format only contains default %t
+                        if $commentsformat && type == 'image' && $commentsformat != '%t'
+                            comment = utf8(`identify -format "#{$commentsformat}" '#{dir}/#{file}'`.chomp.sub(/\.$/, ''))
+                        else
+                            comment = utf8cut(file.sub(/\.[^\.]+$/, ''), 18)
+                        end
+                        xmldir.add_element type, { "filename" => utf8(file), "caption" => comment }
+                    end
+                end
+            end
+            if $mode != 'gen_config'
+                #- cleanup removed files from config and reread entries from config to get proper ordering
+                entries = []
+                xmldir.elements.each { |element|
+                    fullpath = "#{dir}/#{from_utf8(element.attributes['filename'])}"
+                    if %w(image video).include?(element.name)
+                        if !File.readable?(fullpath)
+                            msg 1, _("Config merge: removing %s from config; use the backup file to retrieve caption info if this was a mistake") % fullpath
+                            xmldir.delete(element)
+                        elsif !element.attributes['deleted']
+                            entries << from_utf8(element.attributes['filename'])
+                        end
+                    end
+                }
+                #- if there is no more elements here, there is no album here anymore
+                if !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
+                    xmldir.delete_attribute('thumbnails-caption')
+                    xmldir.delete_attribute('thumbnails-captionfile')
+                end
+            end
+        end
+        images = entries.find_all { |e| entry2type(e) == 'image' }
+        msg 3, _("\t%s photos") % images.length
+        videos = entries.find_all { |e| entry2type(e) == 'video' }
+        msg 3, _("\t%s videos") % videos.length
+        info("walking: #{dir}|#{$source}, #{images.length + videos.length} elements")
+
+        system("mkdir -p '#{dest_dir}'")
+
+        #- generate .htaccess file
+        if !$forgui
+            ios = File.open("#{dest_dir}/.htaccess", "w")
+            ios.write("AddCharset UTF-8 .html\n")
+            if auth_user_file = xmldir.attributes['password-protect']
+                msg 3, _("\tgenerating password protection file #{dest_dir}/.htaccess")
+                ios.write("AuthType Basic\nAuthName \"protected area\"\nAuthUserFile #{auth_user_file}\nrequire valid-user\n")
+            end
+            if $multi_languages
+                ios.write("Options Multiviews\n")
+                ios.write("LanguagePriority #{$multi_languages[1]}\n")
+                ios.write("ForceLanguagePriority Prefer Fallback\n")
+                ios.write("DirectoryIndex index\n")
+            end
+            ios.close
+        end
+
+        #- pass through if there are no images and videos
+        if images.size == 0 && videos.size == 0
+            if !$forgui
+                #- cleanup old images/videos, especially if this directory contained images/videos previously.
+                if $mode != 'gen_config'
+                    rightful_images = [ '.htaccess' ]
+                    if xmldir.attributes['thumbnails-caption']
+                        rightful_images << 'thumbnails-thumbnail.jpg'
+                    end
+                    xmldir.elements.each('dir') { |child|
+                        if child.attributes['deleted']
+                            next
+                        end
+                        subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
+                        rightful_images << "thumbnails-#{subdir}.jpg"
+                    }
+                    to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?(File.join(dest_dir, e)) && !rightful_images.include?(e) }
+                    if to_del.size > 0
+                        File.delete(*to_del.collect { |e| File.join(dest_dir, e) })
+                    end
+                end
+                
+                #- copy any resource file that goes with the theme (css, images..)
+                themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
+                                 find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata root CVS).include?(e) }
+                themestuff.each { |entry|
+                    if !File.exists?(File.join(dest_dir, entry))
+                        psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
+                    end
+                }
+
+                #- copy any root-only resource file that goes with the theme (css, images..)
+                if xmldir.parent.name != 'dir'
+                    themestuff_root = Dir.entries("#{$FPATH}/themes/#{$theme}/root").
+                                          find_all { |e| !%w(. .. CVS).include?(e) }
+                    themestuff_root.each { |entry|
+                        if !File.exists?(File.join(dest_dir, entry))
+                            psys("cp '#{$FPATH}/themes/#{$theme}/root/#{entry}' '#{dest_dir}'")
+                        end
+                    }
+                end
+            end
+            next
+        end
+
+        msg 2, _("Outputting in %s...") % dest_dir
+
+        #- populate data structure with sizes from theme
+        for sizeobj in $images_size
+            fullscreen_images ||= {}
+            fullscreen_images[sizeobj['name']] = []
+            thumbnail_images ||= {}
+            thumbnail_images[sizeobj['name']] = []
+            thumbnail_videos ||= {}
+            thumbnail_videos[sizeobj['name']] = []
+        end
+        #- a special dummy size to keep 'references' to thumbnails in case of panorama, because the GUI will use the regular thumbnails
+        thumbnail_images['dont-delete-file-for-gui'] = []
+        if $limit_sizes =~ /original/
+            fullscreen_images['original'] = []
+        end
+
+        images.size >= 1 and msg 3, _("\tcreating photos thumbnails...")
+
+        #- create thumbnails for images
+        images.each { |img|
+            info("processing element")
+            base_dest_img = dest_dir + '/' + make_dest_filename(img.sub(/\.[^\.]+$/, ''))
+            if $forgui
+                thumbnail_dest_img = base_dest_img + "-#{$default_size['thumbnails']}.jpg"
+                gen_thumbnails_element("#{dir}/#{img}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
+            else
+                todo = []
+                elem = xmldir.elements["image[@filename='#{utf8(img)}']"]
+                for sizeobj in $images_size
+                    size_fullscreen = sizeobj['fullscreen']
+                    size_thumbnails = sizeobj['thumbnails']
+                    fullscreen_dest_img = base_dest_img + "-#{size_fullscreen}.jpg"
+                    fullscreen_images[sizeobj['name']] << File.basename(fullscreen_dest_img)
+                    todo << { 'filename' => fullscreen_dest_img, 'size' => size_fullscreen }
+                    if pano = pano_amount(elem)
+                        thumbnail_images['dont-delete-file-for-gui'] << File.basename(base_dest_img + "-#{size_thumbnails}.jpg")
+                        size_thumbnails = size_thumbnails.sub(/(\d+)/) { ($1.to_i * pano).to_i }
+                    end
+                    thumbnail_dest_img = base_dest_img + "-#{size_thumbnails}.jpg"
+                    thumbnail_images[sizeobj['name']] << File.basename(thumbnail_dest_img)
+                    todo << { 'filename' => thumbnail_dest_img,  'size' => size_thumbnails }
+                end
+                gen_thumbnails_element("#{dir}/#{img}", xmldir, true, todo)
+                if $limit_sizes =~ /original/
+                    fullscreen_images['original'] << img
+                end
+                destimg = "#{dest_dir}/#{img}"
+                if $limit_sizes =~ /original/ && !File.exists?(destimg)
+                    if $hardlinks_ok
+                        if ! sys("ln '#{dir}/#{img}' '#{destimg}'")
+                            $hardlinks_ok = false
+                        end
+                    end
+                    if ! $hardlinks_ok
+                        psys("cp '#{dir}/#{img}' '#{destimg}'")
+                    end
+                end
+            end
+        }
+
+        videos.size >= 1 and msg 3, _("\tcreating videos thumbnails...")
+        transcoded_videos = {}
+
+        #- create thumbnails for videos
+        videos.each { |video|
+            info("processing element")
+            if $forgui
+                thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{$default_size['thumbnails']}.jpg"
+                gen_thumbnails_element("#{dir}/#{video}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
+            else
+                todo = []
+                for sizeobj in $images_size
+                    size_thumbnails = sizeobj['thumbnails']
+                    thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{size_thumbnails}.jpg"
+                    thumbnail_videos[sizeobj['name']] << File.basename(thumbnail_dest_img)
+                    todo << { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails }
+                end
+                gen_thumbnails_element("#{dir}/#{video}", xmldir, true, todo)
+
+                if $transcode_videos
+                    parts = $transcode_videos.split(':', 2)
+                    basedestvideo = video.sub(/\.\w+/, '') + '.' + parts[0]
+                    transcoded_videos[video] = basedestvideo
+                    destvideo = "#{dest_dir}/#{basedestvideo}"
+                    if ! File.exists?(destvideo)
+                        psys(parts[1].gsub(/%f/, "'#{dir}/#{video}'").gsub(/%o/, "'#{destvideo}'"))
+                    end
+                else
+                    destvideo = "#{dest_dir}/#{video}"
+                    if ! File.exists?(destvideo)
+                        if $hardlinks_ok
+                            if ! sys("ln '#{dir}/#{video}' '#{destvideo}'")
+                                $hardlinks_ok = false
+                            end
+                        end
+                        if ! $hardlinks_ok
+                            psys("cp '#{dir}/#{video}' '#{destvideo}'")
+                        end
+                    end
+                end
+            end
+        }
+
+        if !$forgui            
+            #- cleanup old images/videos (for when removing elements or sizes)
+            all_elements = fullscreen_images.collect { |e| e[1] }.flatten.
+                     concat(thumbnail_images.collect { |e| e[1] }.flatten).
+                     concat(thumbnail_videos.collect { |e| e[1] }.flatten).
+                     concat($transcode_videos ? transcoded_videos.values : videos).
+                     push('.htaccess')
+            to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?(File.join(dest_dir, e)) && !all_elements.include?(e) && e !~ /^thumbnails-\w+\.jpg/ }
+            if to_del.size > 0
+                msg 3, _("\tcleaning up: #{to_del.join(', ')}")
+                File.delete(*to_del.collect { |e| File.join(dest_dir, e) })
+            end
+
+            #- copy any resource file that goes with the theme (css, images..)
+            themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
+                             find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata root CVS).include?(e) }
+            themestuff.each { |entry|
+                if !File.exists?(File.join(dest_dir, entry))
+                    psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
+                end
+            }
+            
+            #- copy any root-only resource file that goes with the theme (css, images..)
+            if xmldir.parent.name != 'dir'
+                themestuff_root = Dir.entries("#{$FPATH}/themes/#{$theme}/root").
+                                      find_all { |e| !%w(. .. CVS).include?(e) }
+                themestuff_root.each { |entry|
+                    if !File.exists?(File.join(dest_dir, entry))
+                        psys("cp '#{$FPATH}/themes/#{$theme}/root/#{entry}' '#{dest_dir}'")
+                    end
+                }
+            end
+            
+            msg 3, _("\tgenerating HTML pages...")
+            #- fixup max per page
+            if $N_per_page
+                $N_per_page = $N_per_page.to_i / ($N_per_row || $default_N).to_i * ($N_per_row || $default_N).to_i
+            end
+
+            #- generate thumbnails*.html (page with thumbnails)
+            image2thumbnailpage4js = []
+            for sizeobj in $images_size
+                info("processing size")
+                html = $html_thumbnails.collect { |l| l.clone }
+                iterations = {}
+                for i in html
+                    i.sub!(/~~title~~/,
+                           xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
+                    discover_iterations(iterations, i)
+                end
+                all_pages = []
+                html_thumbnails = ''
+                html_thumbnails_nojs = ''
+                counter = 0
+                pagecount = 0
+                reset_iterations(iterations)
+                #- preprocess the @filename->elem, rexml is very slow with that; we dramatically improve this part of the processing
+                optfilename = {}
+                xmldir.elements.each('image') { |elem|
+                    optfilename[elem.attributes['filename']] = elem
+                }
+                for file in entries
+                    type = images.include?(file) ? 'image' : videos.include?(file) ? 'video' : nil
+                    if type
+                        homogeinize_width = 100 / ($N_per_row || $default_N).to_i
+                        if type == 'image' && elem = optfilename[utf8(file)]
+                            if pano = pano_amount(elem)
+                                html_elem = run_iterations(iterations, pano)
+                                counter += count = pano.ceil
+                                html_elem.gsub!(/~~colspan~~/) { "colspan=\"#{count}\"" }
+                                homogeinize_width *= count
+                            else
+                                html_elem = run_iterations(iterations, 1)
+                                counter += 1
+                                html_elem.gsub!(/~~colspan~~/, '')
+                            end
+                        else 
+                            html_elem = run_iterations(iterations, 1)
+                            counter += 1
+                            html_elem.gsub!(/~~colspan~~/, '')
+                        end
+                        html_elem.gsub!(/~~homogeinize_width~~/) { "width=\"#{homogeinize_width}%\"" }
+                        if type == 'image'
+                            index = images.index(file)
+                            html_elem.gsub!(/~~caption_iteration~~/,
+                                            find_caption_value(xmldir, images[index]) || utf8(images[index]))
+                            html_elem.gsub!(/~~ifimage\?~~(.+?)~~fi~~/) { $1 }
+                            html_elem.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/, '')
+                        elsif type == 'video'
+                            index = videos.index(file)
+                            if File.exists?("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}")
+                                html_elem.gsub!(/~~image_iteration~~/,
+                                                '<a href="' + ( $transcode_videos ? transcoded_videos[videos[index]] : videos[index] ) + '">' +
+                                                    img_element("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}") + '</a>')
+                            else
+                                html_elem.gsub!(/~~image_iteration~~/,
+                                                '<a href="' + ( $transcode_videos ? transcoded_videos[videos[index]] : videos[index] ) + '">' +
+                                                    defer_translation(N_("(no preview)")) + '</a>')
+                            end
+                            html_elem.gsub!(/~~caption_iteration~~/, find_caption_value(xmldir, videos[index]) || utf8(videos[index]))
+                            html_elem.gsub!(/~~ifimage\?~~(.+?)~~fi~~/, '')
+                            html_elem.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/) { $1 }
+                        end
+                        html_thumbnails += html_elem
+                        html_thumbnails_nojs += html_elem
+                        if type == 'image'
+                            html_thumbnails.gsub!(/~~image_iteration~~/,
+                                                  '<a href="image-' + size2js(sizeobj['name']) + $htmlsuffix + '#current=' + fullscreen_images[sizeobj['name']][index] +
+                                                      '" name="' + fullscreen_images[sizeobj['name']][index] + '">' +
+                                                      img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
+                            html_thumbnails_nojs.gsub!(/~~image_iteration~~/, 
+                                                       '<a href="' + fullscreen_images[sizeobj['name']][index] + '" name="' + fullscreen_images[sizeobj['name']][index] + '">' +
+                                                       img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
+                            #- remember in which thumbnails page is this element, for image->thumbnail link
+                            if sizeobj == $images_size[0]
+                                image2thumbnailpage4js << pagecount
+                            end
+                        end
+
+                        if counter == $N_per_page
+                            html_thumbnails      += close_iterations(iterations)
+                            html_thumbnails_nojs += close_iterations(iterations)
+                            all_pages << [ html_thumbnails, html_thumbnails_nojs ]
+                            html_thumbnails = ''
+                            html_thumbnails_nojs = ''
+                            counter = 0
+                            reset_iterations(iterations)
+                            pagecount += 1
+                        end
+                    end
+                end
+                if counter > 0
+                    html_thumbnails      += close_iterations(iterations)
+                    html_thumbnails_nojs += close_iterations(iterations)
+                    all_pages << [ html_thumbnails, html_thumbnails_nojs ]
+                end
+                for i in html
+                    i.gsub!(/~~theme~~/, $theme)
+                    i.gsub!(/~~current_size~~/, sizeobj['name'])
+                    i.gsub!(/~~htmlsuffix~~/, $htmlsuffix)
+                    i.gsub!(/~~current_size_js~~/, size2js(sizeobj['name']))
+                    i.gsub!(/~~madewith~~/, $madewith || '')
+                    i.gsub!(/~~indexlink~~/, $indexlink || '')
+                    substitute_navigation(i, xmldir)
+                end
+                html_nojs = html.collect { |l| l.clone }
+                pagecount = 0
+                for page in all_pages
+                    html_thumbnails, html_thumbnails_nojs = page
+                    final_html = html.collect { |l| l.clone }
+                    mstuff = defer_translation(N_("Pages: ")) +
+                             (pagecount > 0 ? "<a href=\"thumbnails-#{size2js(sizeobj['name'])}%nojs-#{pagecount - 1}#{$htmlsuffix}\">" + defer_translation(N_("<- Previous")) + "</a> " : '') +
+                             all_pages.collect_with_index { |p,idx| page == p ? idx + 1 : "<a href=\"thumbnails-#{size2js(sizeobj['name'])}%nojs-#{idx}#{$htmlsuffix}\">#{idx + 1}</a>" }.join(', ') +
+                             (pagecount < all_pages.size - 1 ? " <a href=\"thumbnails-#{size2js(sizeobj['name'])}%nojs-#{pagecount + 1}#{$htmlsuffix}\">" + defer_translation(N_("Next ->")) + "</a> " : '')
+                    for i in final_html
+                        i.sub!(/~~run_slideshow~~/,
+                               images.size <= 1 ? '' : '<a href="image-' + size2js(sizeobj['name']) + $htmlsuffix + '#run_slideshow=1">' + defer_translation(N_("Run slideshow!"))+'</a>')
+                        i.sub!(/~~thumbnails~~/, html_thumbnails)
+                        if all_pages.size == 1
+                            i.gsub!(/~~ifmultiplepages\?~~.*~~fi~~/, '')
+                        else
+                            i.gsub!(/~~ifmultiplepages\?~~(.+?)~~fi~~/) { $1 }
+                            i.gsub!(/~~multiplepagesstuff~~/, mstuff.gsub('%nojs', ''))
+                        end
+                        substitute_html_sizes(i, sizeobj, 'thumbnails', "-#{pagecount}")
+                        substitute_pathtobase(i, xmldir)
+                    end
+                    save_html(final_html, "#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}-#{pagecount}")
+                    final_html_nojs = html_nojs.collect { |l| l.clone }
+                    for i in final_html_nojs
+                        i.sub!(/~~run_slideshow~~/, defer_translation(N_("<i>Click on an image to view it larger</i>")))
+                        i.sub!(/~~thumbnails~~/, html_thumbnails_nojs)
+                        if all_pages.size == 1
+                            i.gsub!(/~~ifmultiplepages\?~~.*~~fi~~/, '')
+                        else
+                            i.gsub!(/~~ifmultiplepages\?~~(.+?)~~fi~~/) { $1 }
+                            i.gsub!(/~~multiplepagesstuff~~/, mstuff.gsub('%nojs', '-nojs'))
+                        end
+                        substitute_html_sizes(i, sizeobj, 'thumbnails', "-nojs-#{pagecount}")
+                        substitute_pathtobase(i, xmldir)
+                    end
+                    save_html(final_html_nojs, "#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}-nojs-#{pagecount}")
+                    pagecount += 1
+                end
+            end
+
+            info("finished processing sizes")
+
+            #- generate "main" thumbnails.html page that will reload to correct size thanks to cookie
+            save_html(html_reload_to_thumbnails, "#{dest_dir}/thumbnails")
+
+            #- generate image.html (page with fullscreen images)
+            if images.size > 0
+                captions4js = find_captions(xmldir, images).collect { |e| e ? '"' + e.gsub('"', '\"') + '"' : '""' }.join(', ')
+                thumbnailspage4js = image2thumbnailpage4js.collect { |e| "\"#{e}\"" }.join(', ')
+                for sizeobj in $images_size
+                    html = $html_images.collect { |l| l.clone }
+                    images4js = fullscreen_images[sizeobj['name']].collect { |e| "\"#{e}\"" }.join(', ')
+                    otherimages4js = ''
+                    othersizes = []
+                    for sizeobj2 in all_images_sizes
+                        if sizeobj != sizeobj2
+                            otherimages4js += "var images_#{size2js(sizeobj2['name'])} = new Array(" + fullscreen_images[sizeobj2['name']].collect { |e| "\"#{e}\"" }.join(', ') + ")\n"
+                            othersizes << "\"#{size2js(sizeobj2['name'])}\""
+                        end
+                    end
+                    for i in html
+                        i.gsub!(/~~images~~/, images4js)
+                        i.gsub!(/~~other_images~~/, otherimages4js)
+                        i.gsub!(/~~thumbnailspages~~/, thumbnailspage4js)
+                        i.gsub!(/~~other_sizes~~/, othersizes.join(', '))
+                        i.gsub!(/~~captions~~/, captions4js)
+                        i.gsub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
+                        i.gsub!(/~~thumbnails~~/, '<a href="thumbnails-' + size2js(sizeobj['name']) + $htmlsuffix + '" id="thumbnails">' + defer_translation(N_('return to thumbnails')) + '</a>')
+                        i.gsub!(/~~theme~~/, $theme)
+                        i.gsub!(/~~current_size~~/, size2js(sizeobj['name']))
+                        i.gsub!(/~~htmlsuffix~~/, $htmlsuffix)
+                        i.gsub!(/~~madewith~~/, $madewith || '')
+                        i.gsub!(/~~indexlink~~/, $indexlink || '')
+                        substitute_html_sizes(i, sizeobj, 'image', '')
+                        substitute_navigation(i, xmldir)
+                        substitute_pathtobase(i, xmldir)
+                    end
+                    save_html(html, "#{dest_dir}/image-#{size2js(sizeobj['name'])}")
+                end
+            end
+        end
+    }
+
+    msg 3, ''
+
+    #- add attributes to <dir..> elements needing so
+    if $mode != 'use_config'
+        msg 3, _("\tfixating configuration file...")
+        $xmldoc.elements.each('//dir') { |element|
+            path = captionpath = element.attributes['path']
+            descendant_element = element.elements['descendant::image'] || element.elements['descendant::video']
+            if !descendant_element
+                msg 3, _("\t\tremoving %s, no element in it") % path
+                element.remove  #- means we have a directory with nothing interesting in it
+            else
+                captionfile = "#{descendant_element.parent.attributes['path']}/#{descendant_element.attributes['filename']}"
+                basename = File.basename(path)
+                if element.elements['dir']
+                    if !element.attributes['subdirs-caption']
+                        element.add_attribute('subdirs-caption', basename)
+                    end
+                    if !element.attributes['subdirs-captionfile']
+                        element.add_attribute('subdirs-captionfile', captionfile)
+                    end
+                end
+                if element.child_byname_notattr('image', 'deleted') || element.child_byname_notattr('video', 'deleted')
+                    if !element.attributes['thumbnails-caption']
+                        element.add_attribute('thumbnails-caption', basename)
+                    end
+                    if !element.attributes['thumbnails-captionfile']
+                        element.add_attribute('thumbnails-captionfile', captionfile)
+                    end
+                end
+            end
+        }
+    end
+
+    #- write down to disk config if necessary
+    if $config_writeto
+        ios = File.open($config_writeto, "w")
+        $xmldoc.write(ios, 0)
+        ios.close
+    end
+
+    if $forgui
+        msg 3, _(" completed necessary stuff for GUI, exiting.")
+        return
+    end
+
+    #- second pass to create index.html files and previous/next links
+    info("creating index.html")
+    msg 3, _("\trescanning directories to generate all 'index.html' files...")
+
+    #- recompute the memoization because elements mights have been removed (the ones with no element in them)
+    optxpath = {}
+    $xmldoc.elements.each('//dir') { |elem|
+        optxpath[elem.attributes['path']] = elem
+    }
+
+    examined_dirs.each { |dir|
+        info("index.html: #{dir}|#{$source}")
+
+        xmldir = optxpath[utf8(dir)]
+        if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
+            next
+        end
+        dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
+
+        previous_album = find_previous_album(xmldir)
+        next_album = find_next_album(xmldir)
+
+        if xmldir.elements['dir']
+            html = $html_index.collect { |l| l.clone }
+            iterations = {}
+            for i in html
+                caption = xmldir.attributes['subdirs-caption']
+                i.gsub!(/~~title~~/, caption)
+                substitute_navigation(i, xmldir)
+                substitute_pathtobase(i, xmldir)
+                discover_iterations(iterations, i)
+            end
+            
+            html_index = ''
+            reset_iterations(iterations)
+            
+            #- deal with "current" album (directs to "thumbnails" page)
+            if xmldir.attributes['thumbnails-caption']
+                thumbnail = "#{dest_dir}/thumbnails-thumbnail.jpg"
+                gen_thumbnails_subdir(from_utf8(xmldir.attributes['thumbnails-captionfile']), xmldir, false,
+                                      [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], 'thumbnails')
+                html_index += run_iterations(iterations, 1)
+                html_index.gsub!(/~~image_iteration~~/, "<a href=\"thumbnails#{$htmlsuffix}\">" + img_element(thumbnail) + '</a>')
+                html_index.gsub!(/~~caption_iteration~~/, xmldir.attributes['thumbnails-caption'])
+            end
+
+            #- deal with sub-albums (direct to subdirs/index.html pages)
+            xmldir.elements.each('dir') { |child|
+                if child.attributes['deleted']
+                    next
+                end
+                subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
+                thumbnail = "#{dest_dir}/thumbnails-#{subdir}.jpg"
+                html_index += run_iterations(iterations, 1)
+                captionfile, caption = find_subalbum_caption_info(child)
+                gen_thumbnails_subdir(captionfile, child, false,
+                                      [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], find_subalbum_info_type(child))
+                html_index.gsub!(/~~caption_iteration~~/, caption)
+                html_index.gsub!(/~~image_iteration~~/, "<a href=\"#{subdir}/index#{$htmlsuffix}\">" + img_element(thumbnail) + '</a>')
+            }
+
+            html_index += close_iterations(iterations)
+
+            for i in html
+                i.gsub!(/~~thumbnails~~/, html_index)
+                i.gsub!(/~~madewith~~/, $madewith || '')
+                i.gsub!(/~~indexlink~~/, $indexlink || '')
+            end
+            
+        else
+            html = html_reload_to_thumbnails
+        end
+
+        save_html(html, "#{dest_dir}/index")
+        if $multi_languages
+            #- in case MultiViews will not work, generate some compat
+            ios = File.open("#{dest_dir}/index.html", "w")
+            ios.write("<html><head><meta http-equiv=\"refresh\" content=\"0.1;url=index.#{$multi_languages[1]}.html\"></head></html>")
+            ios.close
+        end
+
+        #- substitute multiple "return to albums", previous/next correctly
+        #- the following two statements are dramatical optimizations to executing for each substInFile callback
+        dirpresent = xmldir.elements['dir']
+        parentname = xmldir.parent.name
+        if xmldir.child_byname_notattr('image', 'deleted') || xmldir.child_byname_notattr('video', 'deleted')
+            for suffix in [ '', '-nojs' ]
+                for sizeobj in $images_size
+                    Dir.glob("#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}#{suffix}-*.html") do |file|
+                        #- unroll translations, they are costly if rerun for each line of files
+                        rta = find_translation_for_file(file, N_('return to albums'))
+                        pa = find_translation_for_file(file, N_('previous album'))
+                        na = find_translation_for_file(file, N_('next album'))
+                        substInFile(file) { |line|
+                            sub_previous_next_album(file, previous_album, next_album, line, pa, na)
+                            if dirpresent
+                                line.sub!(/~~return_to_albums~~/, '<a href="index' + $htmlsuffix + '">' + rta + '</a>')
+                            else
+                                if parentname == 'dir'
+                                    line.sub!(/~~return_to_albums~~/, '<a href="../index' + $htmlsuffix + '">' + rta + '</a>')
+                                else
+                                    line.sub!(/~~return_to_albums~~/, '')
+                                end
+                            end
+                            line
+                        }
+                    end
+                    if suffix == '' && xmldir.child_byname_notattr('image', 'deleted')
+                        Dir.glob("#{dest_dir}/image-#{size2js(sizeobj['name'])}*.html") do |file|
+                            pa = find_translation_for_file(file, N_('previous album'))
+                            na = find_translation_for_file(file, N_('next album'))
+                            substInFile(file) { |line|
+                                sub_previous_next_album(file, previous_album, next_album, line, pa, na)
+                            }
+                        end
+                    end
+                end
+            end
+        end
+    }
+
+    msg 3, _(" all done.")
+end
+
+handle_options
+read_config
+check_installation
+
+build_html_skeletons
+
+walk_source_dir


Property changes on: packages/booh/branches/upstream/current/bin/booh-backend
___________________________________________________________________
Name: svn:executable
   + 

Added: packages/booh/branches/upstream/current/bin/booh-classifier
===================================================================
--- packages/booh/branches/upstream/current/bin/booh-classifier	                        (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh-classifier	2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,1847 @@
+#! /usr/bin/ruby
+#
+#                         *  BOOH  *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'getoptlong'
+require 'tempfile'
+
+require 'gtk2'
+require 'booh/libadds'
+
+require 'gettext'
+include GetText
+bindtextdomain("booh")
+
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+include Booh
+require 'booh/UndoHandler'
+
+
+#- options
+$options = [
+    [ '--help',          '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
+    [ '--sort-by-exif-date', '-s', GetoptLong::NO_ARGUMENT, _("Sort entries by EXIF date") ],
+    [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+    [ '--version',       '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
+]
+
+$preloader_allowed = false
+
+def usage
+    puts _("Usage: %s [OPTION]...") % File.basename($0)
+    $options.each { |ary|
+        printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
+    }
+end
+
+def handle_options
+    parser = GetoptLong.new
+    parser.set_options(*$options.collect { |ary| ary[0..2] })
+    begin
+        parser.each_option do |name, arg|
+            case name
+            when '--help'
+                usage
+                exit(0)
+
+            when '--sort-by-exif-date'
+                $sort_by_exif_date = true
+
+            when '--verbose-level'
+                $verbose_level = arg.to_i
+
+            when '--version'
+                puts _("Booh version %s
+
+Copyright (c) 2005-2008 Guillaume Cottenceau.
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
+
+                exit(0)
+
+            end
+        end
+    rescue
+        puts $!
+        usage
+        exit(1)
+    end
+end
+
+def startup_memfree
+    if $startup_memfree.nil?
+        meminfo = IO.readlines('/proc/meminfo').join
+        meminfo =~ /MemFree:.*?(\d+)/ or return -1
+        memfree = $1
+        meminfo =~ /Buffers:.*?(\d+)/ and buffers = $1
+        meminfo =~ /Cached:.*?(\d+)/ and cached = $1
+        $startup_memfree = memfree.to_i + buffers.to_i + cached.to_i
+    end
+    return $startup_memfree
+end
+
+def set_cache_memory_use_figure
+    
+    if $config['cache-memory-use'] =~ /memfree_(\d+)/
+        $config['cache-memory-use-figure'] = startup_memfree*$1.to_f/100
+    else
+        $config['cache-memory-use-figure'] = $config['cache-memory-use'].to_i
+    end
+    msg 2, _("Cache memory used: %s kB") % $config['cache-memory-use-figure']
+end
+
+def read_config
+    $config = {}
+    $config_file = File.expand_path('~/.booh-classifier-rc')
+    if File.readable?($config_file)
+        $xmldoc = REXML::Document.new(File.new($config_file))
+        $xmldoc.root.elements.each { |element|
+            txt = element.get_text
+            if txt
+                if txt.value =~ /~~~/
+                    $config[element.name] = txt.value.split(/~~~/)
+                else
+                    $config[element.name] = txt.value
+                end
+            elsif element.elements.size == 0
+                $config[element.name] = ''
+            else
+                $config[element.name] = {}
+                element.each { |chld|
+                    txt = chld.get_text
+                    $config[element.name][chld.name] = txt ? txt.value : nil
+                }
+            end
+        }
+    end
+    $config['video-viewer'] ||= '/usr/bin/mplayer %f'
+    $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
+    $config['preload-distance'] ||= '5'
+    $config['cache-memory-use'] ||= 'memfree_80%'
+    $config['rotate-set-exif'] ||= 'true'
+    $config['thumbnails-height'] ||= '64'
+    set_cache_memory_use_figure
+end
+
+def check_config
+    missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
+    if missing != []
+        show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
+    end
+
+    if !system("which exif >/dev/null 2>/dev/null")
+        show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
+    end
+    viewer_binary = $config['video-viewer'].split.first
+    if viewer_binary && ! File.executable?(viewer_binary)
+        show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
+You should fix this in Edit/Preferences so that you can view videos.
+
+Problem was: '%s' is not an executable file.
+Hint: don't forget to specify the full path to the executable,
+e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
+    end
+    browser_binary = $config['browser'].split.first
+    if browser_binary && ! File.executable?(browser_binary)
+        show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
+You should fix this in Edit/Preferences so that you can open URLs.
+
+Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
+    end
+end
+
+def write_config
+    ios = File.open($config_file, "w")
+    $xmldoc = Document.new "<booh-classifier-rc version='#{$VERSION}'/>"
+    $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
+    $config.each_pair { |key, value|
+        elem = $xmldoc.root.add_element key
+        if value.is_a? Hash
+            $config[key].each_pair { |subkey, subvalue|
+                subelem = elem.add_element subkey
+                subelem.add_text subvalue.to_s
+            }
+        elsif value.is_a? Array
+            elem.add_text value.join('~~~')
+        else
+            if !value
+                elem.remove
+            else
+                elem.add_text value.to_s
+            end
+        end
+    }
+    $xmldoc.write(ios, 0)
+    ios.close
+end
+
+def save_undo(name, closure, *params)
+    UndoHandler.save_undo(name, closure, [ *params ])
+    $undo_mb.sensitive = true
+    $redo_mb.sensitive = false
+end
+
+def get_mem
+    IO.readlines('/proc/self/status').join =~ /VmRSS.*?(\d+)\s*kB/
+    msg 3, "RSS: #{$1}"
+    return $1.to_i
+end
+
+def show_mem(*txt)
+    txt.length > 0 and print txt[0]
+    msg 2, "RSS: #{get_mem}"
+end
+
+class Gdk::Color
+    def darker
+        color = dup
+        color.red = [ color.red - 10000, 0 ].max
+        color.green = [ color.green - 10000, 0 ].max
+        color.blue = [ color.blue - 10000, 0 ].max
+        return color
+    end
+    def lighter
+        color = dup
+        color.red = [ color.red + 10000, 65535 ].min
+        color.green = [ color.green + 10000, 65535 ].min
+        color.blue = [ color.blue + 10000, 65535 ].min
+        return color
+    end
+end
+
+$color_red = Gdk::Color.new(65535, 0, 0)
+$colors = [ Gdk::Color.new(0, 65535, 0),
+            Gdk::Color.new(0, 0, 65535),
+            Gdk::Color.new(65535, 65535, 0),
+            Gdk::Color.new(0, 65535, 65535),
+            Gdk::Color.new(65535, 0, 65535) ]
+
+class Label
+    attr_accessor :color, :name, :button
+    def initialize(name)
+        @name = name
+    end
+end
+
+class Entry
+    @@max_width = nil
+    def Entry.thumbnails_height
+        return $config['thumbnails-height'].to_i
+    end
+
+    attr_accessor :path, :guipath, :type, :angle, :button, :image, :alignment, :removed, :labeled
+
+    def initialize(path, type, guipath)
+        @path = path
+        @type = type
+        @guipath = guipath
+        if @@max_width.nil?
+            @@max_width = $main_window.root_window.size[0] - $labels_vbox.allocation.width - ( $videoborder_pixbuf.width + MainView.borders_thickness) * 2
+        end
+    end
+
+    def pixbuf_full
+        if @pixbuf_full.nil?
+            msg 3, ">>> pixbuf_full #{path}"
+            load_into_pixbuf_full
+        end
+        return @pixbuf_full
+    end
+    def free_pixbuf_full
+        if @pixbuf_full.nil?
+            return false
+        else
+            msg 3, ">>> free_pixbuf_full #{path}"
+            @pixbuf_full = nil
+            return true
+        end
+    end
+    def pixbuf_main(interruptable)
+        width, height = $mainview.window.size 
+        width = MainView.get_usable_width(width)
+        height = MainView.get_usable_height(height)
+        if @pixbuf_main.nil? || width != @width || height != @height
+            msg 3, ">>> pixbuf_main #{path}"
+            @width = width
+            @height = height
+            load_into_pixbuf_full(interruptable)  #- make sure it is loaded
+            if @pixbuf_full.nil?
+                return
+            end
+            if @pixbuf_full.width.to_f / @pixbuf_full.height > width.to_f / height
+                resized_height = @pixbuf_full.height * (width.to_f/@pixbuf_full.width)
+                if @pixbuf_full.width > width || @pixbuf_full.height > resized_height
+                    @pixbuf_main = @pixbuf_full.scale(width, resized_height, Gdk::Pixbuf::INTERP_BILINEAR)
+                else
+                    @pixbuf_main = @pixbuf_full
+                end
+            else
+                resized_width = @pixbuf_full.width * (height.to_f/@pixbuf_full.height)
+                if @pixbuf_full.width > resized_width || @pixbuf_full.height > height
+                    @pixbuf_main = @pixbuf_full.scale(resized_width, height, Gdk::Pixbuf::INTERP_BILINEAR)
+                else
+                    @pixbuf_main = @pixbuf_full
+                end
+            end
+        end
+        return @pixbuf_main
+    end
+    def free_pixbuf_main
+        if @pixbuf_main.nil?
+            return false
+        else
+            msg 3, ">>> free_pixbuf_main #{path}"
+            @pixbuf_main = nil
+            return true
+        end
+    end
+    def pixbuf_thumbnail
+        if @pixbuf_thumbnail.nil?
+            if @pixbuf_main
+                msg 3, ">>> pixbuf_thumbnail from main #{path}"
+                @pixbuf_thumbnail = @pixbuf_main.scale(@pixbuf_main.width * (Entry.thumbnails_height.to_f/@pixbuf_main.height), Entry.thumbnails_height, Gdk::Pixbuf::INTERP_BILINEAR)
+            else
+                msg 3, ">>> pixbuf_thumbnail from file #{path}"
+                @pixbuf_thumbnail = load_into_pixbuf_at_size(false) { |w, h|
+                    if @angle == 0
+                        if h > Entry.thumbnails_height
+                            [ w * Entry.thumbnails_height.to_f/h, Entry.thumbnails_height ]
+                        else
+                            [ w, h ]
+                        end
+                    else
+                        if w > Entry.thumbnails_height
+                            [ Entry.thumbnails_height, h * Entry.thumbnails_height.to_f/w ]
+                        else
+                            [ w, h ]
+                        end
+                    end
+                }
+            end
+        end
+        return @pixbuf_thumbnail
+    end
+    def free_pixbuf_thumbnail
+        if @pixbuf_thumbnail.nil?
+            return false
+        else
+            msg 3, ">>> free_pixbuf_thumbnail #{path}"
+            @pixbuf_thumbnail = nil
+            return true
+        end
+    end
+
+    def outline_color
+        if removed
+            return $color_red
+        elsif labeled
+            return labeled.color
+        else
+            return nil
+        end
+    end
+
+    def show_bg
+        if outline_color.nil?
+            button.modify_bg(Gtk::StateType::NORMAL, nil)
+            button.modify_bg(Gtk::StateType::PRELIGHT, nil)
+            button.modify_bg(Gtk::StateType::ACTIVE, nil)
+        else
+            button.modify_bg(Gtk::StateType::NORMAL, outline_color)
+            button.modify_bg(Gtk::StateType::PRELIGHT, outline_color.lighter)
+            button.modify_bg(Gtk::StateType::ACTIVE, outline_color)
+        end
+    end
+
+    def get_beautified_name
+        if type == 'image'
+            size = get_image_size(path)
+            return _("%s (%sx%s, %s KB)") % [@guipath.gsub(/\.[^.]+$/, ''),
+                                             size[:x],
+                                             size[:y],
+                                             commify(file_size(path)/1024)]
+        else
+            return _("%s (video - %s KB)") % [@guipath.gsub(/\.[^.]+$/, ''),
+                                             commify(file_size(path)/1024)]
+        end
+    end
+
+    private
+    def cleanup_dir(dir)
+        Dir.entries(dir).each { |file| file != '.' && file != '..' and File.delete(File.join(dir, file)) }
+        Dir.delete(dir)
+    end
+
+    def load_into_pixbuf_full(interruptable)
+        if @pixbuf_full.nil?
+            msg 3, ">>> load_into_pixbuf_full #{path}"
+            @pixbuf_full = load_into_pixbuf_at_size(interruptable) { |w, h|
+                if @angle == 0
+                    if w > @@max_width
+                        #- save memory and speedup (+35%) loading 
+                        [ w * (factor = @@max_width.to_f/w), h * factor ]
+                    else
+                        [ w, h ]
+                    end
+                else
+                    if h > @@max_width
+                        [ w * (factor = @@max_width.to_f/h), h * factor ]
+                    else
+                        [ w, h ]
+                    end
+                end
+            }
+        end
+    end
+
+    def load_into_pixbuf_at_size(interruptable, &specify_size)
+        pixbuf = nil
+        if @type == 'video'
+            tmp = Tempfile.new("boohclassifiertemp")
+            dest_dir = tmp.path
+            tmp.close!
+            Dir.mkdir(dest_dir)
+            orig_base = File.basename(path)
+            tmpdir = gen_video_thumbnail(path, false, 0)
+            if tmpdir.nil?
+                return
+            end
+            image_path = "#{tmpdir}/00000001.jpg"
+        else
+            image_path = @path
+        end
+        if @angle.nil?
+            if @type == 'image'
+                @angle = guess_rotate(image_path)
+            else
+                @angle = 0
+            end
+        end
+        begin
+            #- use a pixbuf loader and trigger Gtk.main_iteration on each chunk if needed, to keep the UI responsive even
+            #- if loaded pictures are several MBs large
+            loader = Gdk::PixbufLoader.new
+            loader.signal_connect('size-prepared') { |l, w, h|
+                r = specify_size.call(w, h)
+                #msg 3, "specified sizes: #{r[0]} #{r[1]}"
+                loader.set_size(*specify_size.call(w, h))
+            }
+            id = loader.signal_connect('area-prepared') { pixbuf = loader.pixbuf }
+            if ! loader.load_not_freezing_ui(image_path, interruptable, id)
+                return
+            end
+            loader.close
+            if pixbuf.nil?
+                raise "Loaded pixbuf nil - #{path} #{image_path}"
+            end
+        rescue
+            msg 0, "Cannot load #{image_path}: #{$!}"
+            begin
+                loader.close
+            rescue
+            end
+            return
+        ensure
+            if @type == 'video'
+                File.delete(image_path)
+                Dir.rmdir(tmpdir)
+            end
+        end
+        if pixbuf
+            if @angle != 0
+                msg 3, ">>> load_into_pixbuf_full #{image_path} => rotate #{@angle}"
+                pixbuf = rotate_pixbuf(pixbuf, @angle)
+            end
+        end
+        if @type == 'video'
+            cleanup_dir(dest_dir)
+        end
+        return pixbuf
+    end
+
+    def to_s
+        @path
+    end
+end
+
+$allentries = []
+
+def gc
+    start = Time.now
+    GC.start
+    msg 3, "GC in #{Time.now - start} s"
+end
+
+def free_cache(avoid)
+    i = $allentries.index($mainview.get_shown_entry)
+    return if i.nil?
+    start = Time.now
+    ($allentries.size - 1).downto($config['preload-distance'].to_i + 1) { |j|
+        index = i + j
+        if i + j < $allentries.size && ! avoid.include?(i + j)
+            $allentries[i + j].free_pixbuf_full
+            $allentries[i + j].free_pixbuf_main
+        end
+        if i - j >= 0 && ! avoid.include?(i - j)
+            $allentries[i - j].free_pixbuf_full
+            $allentries[i - j].free_pixbuf_main
+        end
+    }
+    msg 3, "freeing done in #{Time.now - start} s"
+    if get_mem > $config['cache-memory-use-figure'] * 3 / 4
+        gc
+        get_mem
+    end
+end
+
+def run_preloader_real
+    msg 3, "*** >> main preloading triggered..."
+    if $preloader_running
+        msg 3, "*** >>>>>> already running, return <<<<<<<<"
+        return
+    end
+    $preloader_running = true
+    if $mainview.get_shown_entry
+        if get_mem > $config['cache-memory-use-figure']
+            msg 3, "too much RSS, stopping preloading, triggering GC"
+            $preloader_running = false
+            gc
+            get_mem
+            return
+        end
+        if $config['preload-distance'].to_i == 0
+            free_cache([])
+            return
+        end
+        index = $allentries.index($mainview.get_shown_entry)
+        index_right = index
+        index_left = index
+        loaded_right = 0
+        loaded_left = 0
+        right_done = false
+        left_done = false
+        loaded = []
+        while ! right_done || ! left_done
+            if ! right_done
+                index_right += 1
+                while index_right < $allentries.size && ! visible($allentries[index_right])
+                    index_right += 1
+                end
+                if index_right == $allentries.size
+                    right_done = true
+                else
+                    msg 3, "preloading #{$allentries[index_right].path}"
+                    $allentries[index_right].pixbuf_main(false)
+                    loaded << index_right
+                    loaded_right += 1
+                    if loaded_right == $config['preload-distance'].to_i
+                        right_done = true
+                    end
+                end
+            end
+
+            if ! left_done
+                index_left -= 1
+                while index_left >= 0 && ! visible($allentries[index_left])
+                    index_left -= 1
+                end
+                if index_left == -1
+                    left_done = true
+                else
+                    msg 3, "preloading #{$allentries[index_left].path}"
+                    $allentries[index_left].pixbuf_main(false)
+                    loaded << index_left
+                    loaded_left += 1
+                    if loaded_left == $config['preload-distance'].to_i
+                        left_done = true
+                    end
+                end
+            end
+
+            #- in case just loaded another directory
+            if $preloader_force_exit
+                $preloader_running = false
+                $preloader_force_exit = false
+                return
+            end
+            #- in case moved fast
+            if index != $allentries.index($mainview.get_shown_entry)
+                msg 3, "*** >>>> moved already, rerun"
+                $preloader_running = false
+                run_preloader_real
+                return
+            end
+        end
+        free_cache(loaded)
+    end
+    $preloader_running = false
+    msg 3, "*** << main preloading finished"
+end
+
+def run_preloader
+    if ! $preloader_allowed
+        msg 3, "*** preloader not yet allowed"
+        return
+    end
+    Gtk.timeout_add(10) {
+        run_preloader_real
+        false
+    }
+end
+
+class MainView < Gtk::DrawingArea
+
+    @@borders_thickness = 5
+    @@borders_length = 25
+
+    def MainView.borders_thickness
+        return @@borders_thickness
+    end
+
+    def MainView.get_usable_width(available_width)
+        return available_width - ($videoborder_pixbuf.width + @@borders_thickness) * 2
+    end
+
+    def MainView.get_usable_height(available_height)
+        return available_height - @@borders_thickness * 2
+    end
+    
+    def initialize
+        super()
+        signal_connect('expose-event') { draw }
+        signal_connect('configure-event') { update_shown }
+    end
+
+    def try_show_entry(entry)
+        if entry && entry.button
+            entry.button.grab_focus
+        end
+    end
+
+    def set_shown_entry(entry)
+        t1 = Time.now
+        if entry && entry == @entry
+            return
+        end
+        if entry && ! entry.button
+            #- not loaded yet
+            return
+        end
+        @entry = entry
+        redraw
+        run_preloader
+        msg 3, "entry shown in: #{Time.now - t1} s"
+    end
+
+    def get_shown_entry
+        return @entry
+    end
+
+    def show_next_entry(entry)
+        index = $allentries.index(entry)
+        if index < $allentries.size - 1
+            index += 1
+        end
+        while index < $allentries.size - 1 && $allentries[index] && ! $allentries[index].button.visible?
+            index += 1
+        end
+        while $allentries[index] && ! $allentries[index].button.visible? && index > 0
+            index -= 1
+        end
+        if index < $allentries.size && $allentries[index] && $allentries[index].button.visible?
+            try_show_entry($allentries[index])
+        end
+    end
+
+    def redraw
+        @entry and sb_msg(_("Selected %s") % @entry.get_beautified_name)
+        if ! update_shown
+            return
+        end
+        w, h = window.size
+        window.begin_paint(Gdk::Rectangle.new(0, 0, w, h))
+        window.clear
+        draw
+        window.end_paint
+        Gtk.main_iteration while Gtk.events_pending?
+    end
+
+    def update_shown
+        if @entry
+            $interrupt_loading = false
+            pixbuf = @entry.pixbuf_main(true)
+            $interrupt_loading = true
+            if pixbuf
+                @pixbuf = pixbuf
+                width, height = window.size 
+                @xpos = (width - @pixbuf.width)/2
+                @ypos = (height - @pixbuf.height)/2
+                return true
+            else
+                return false
+            end
+        else
+            @pixbuf = nil
+            return true
+        end
+    end
+
+    def draw
+        if @pixbuf
+            window.draw_pixbuf(nil, @pixbuf, 0, 0, @xpos, @ypos, -1, -1, Gdk::RGB::DITHER_NONE, -1, -1)
+            if @entry && @entry.type == 'video'
+                window.draw_borders($videoborder_pixbuf, @xpos - $videoborder_pixbuf.width, @xpos + @pixbuf.width, @ypos, @ypos + @pixbuf.height)
+            end
+            if ! @entry.outline_color.nil?
+                gc = Gdk::GC.new(window)
+                colormap.alloc_color(@entry.outline_color, false, true)
+                gc.set_foreground(@entry.outline_color)
+                if @entry && @entry.type == 'video'
+                    xleft = @xpos - $videoborder_pixbuf.width
+                    xright = @xpos + @pixbuf.width + $videoborder_pixbuf.width
+                else
+                    xleft = @xpos
+                    xright = @xpos + @pixbuf.width
+                end
+                window.draw_polygon(gc, true, [[xleft - @@borders_thickness, @ypos - @@borders_thickness],
+                                               [xright + @@borders_thickness, @ypos - @@borders_thickness],
+                                               [xright + @@borders_thickness, @ypos + @pixbuf.height + @@borders_thickness],
+                                               [xleft - @@borders_thickness, @ypos + @pixbuf.height + @@borders_thickness],
+                                               [xleft - @@borders_thickness, @ypos - 1],
+                                               [xleft - 1, @ypos - 1],
+                                               [xleft - 1, @ypos + @pixbuf.height + 1],
+                                               [xright + 1, @ypos + @pixbuf.height + 1],
+                                               [xright + 1, @ypos - 1],
+                                               [xleft - @@borders_thickness, @ypos - 1]])
+            end
+        end
+    end
+end
+
+def autoscroll_if_needed(button, center)
+    xpos_left = button.allocation.x
+    xpos_right = button.allocation.x + button.allocation.width
+    hadj = $imagesline_sw.hadjustment
+    current_minx_visible = hadj.value
+    current_maxx_visible = hadj.value + hadj.page_size
+    if ! center
+        if xpos_left < current_minx_visible
+            #- autoscroll left
+            newval = hadj.value - (current_minx_visible - xpos_left)
+            hadj.value = newval
+        elsif xpos_right > current_maxx_visible
+            #- autoscroll right
+            newval = hadj.value + (xpos_right - current_maxx_visible)
+            if newval > hadj.upper - hadj.page_size
+                newval = hadj.upper - hadj.page_size
+            end
+            hadj.value = newval
+        end
+    else
+        hadj.value = clamp((xpos_left + xpos_right) / 2 - hadj.page_size / 2, 0, hadj.upper - hadj.page_size)
+    end
+end
+
+def show_popup(parent, msg, *options)
+    dialog = Gtk::Dialog.new
+    if options[0]
+        options = options[0]
+    else
+        options = {}
+    end
+    if options[:title]
+        dialog.title = options[:title]
+    else
+        dialog.title = utf8(_("Booh message"))
+    end
+    lbl = Gtk::Label.new
+    if options[:nomarkup]
+        lbl.text = msg
+    else
+        lbl.markup = msg
+    end
+    if options[:centered]
+        lbl.set_justify(Gtk::Justification::CENTER)
+    end
+    if options[:selectable]
+        lbl.selectable = true
+    end
+    if options[:topwidget]
+        dialog.vbox.add(options[0][:topwidget])
+    end
+    if options[:scrolled]
+        sw = Gtk::ScrolledWindow.new(nil, nil)
+        sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
+        sw.add_with_viewport(lbl)
+        dialog.vbox.add(sw)
+        dialog.set_default_size(500, 600)
+    else
+        dialog.vbox.add(lbl)
+        dialog.set_default_size(200, 120)
+    end
+    if options[:bottomwidget]
+        dialog.vbox.add(options[:bottomwidget])
+    end
+    if options[:okcancel]
+        dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
+    end
+    dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
+
+    if options[:pos_centered]
+        dialog.window_position = Gtk::Window::POS_CENTER
+    else
+        dialog.window_position = Gtk::Window::POS_MOUSE
+    end
+
+    if options[:linkurl]
+        linkbut = Gtk::Button.new('')
+        linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
+        linkbut.signal_connect('clicked') {
+            open_url(options[0][:linkurl] + '/index.html')
+            dialog.response(Gtk::Dialog::RESPONSE_OK)
+            set_mousecursor_normal
+        }
+        linkbut.relief = Gtk::RELIEF_NONE
+        linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
+        linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
+        dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
+    end
+
+    dialog.show_all
+
+    if options[:stuff_connector]
+        options[:stuff_connector].call({ :dialog => dialog })
+    end
+                                        
+    if !options[:not_transient]
+        dialog.transient_for = parent
+        dialog.run { |response|
+            if options[:data_getter]
+                options[:data_getter].call
+            end
+            dialog.destroy
+            if options[:okcancel]
+                return response == Gtk::Dialog::RESPONSE_OK
+            end
+        }
+    else
+        dialog.signal_connect('response') { dialog.destroy }
+    end
+end
+
+def view_entry(entry)
+    if entry.type == 'image'
+        show_popup($main_window,
+                   utf8(`exif -m '#{entry.path}'`),
+                   { :title => utf8(_("EXIF data of %s") % File.basename(entry.path)), :nomarkup => true, :scrolled => true, :not_transient => true })
+    else
+        cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{entry.path}'") + ' &'
+        msg 2, cmd
+        system(cmd)
+    end
+end
+
+def thumbnail_keypressed(entry, event)
+    if event.state & Gdk::Window::MOD1_MASK != 0
+        #- ALT pressed: Alt-Left and Alft-Right rotate
+        if event.keyval == Gdk::Keyval::GDK_Left || event.keyval == Gdk::Keyval::GDK_Right
+            if event.keyval == Gdk::Keyval::GDK_Left
+                entry.angle = (entry.angle - 90) % 360
+            else
+                entry.angle = (entry.angle + 90) % 360
+            end
+            entry.free_pixbuf_full
+            entry.free_pixbuf_main
+            entry.free_pixbuf_thumbnail
+            $mainview.redraw
+            entry.image.pixbuf = entry.pixbuf_thumbnail
+            if $config['rotate-set-exif'] == 'true' && entry.type == 'image'
+                Exif.set_orientation(entry.path, angle_to_exif_orientation(entry.angle))
+            end
+        end
+
+    elsif event.state & Gdk::Window::CONTROL_MASK != 0
+        #- CONTROL pressed: Ctrl-z and Ctrl-r for undo/redo, Ctrl-space for recentre
+        if event.keyval == Gdk::Keyval::GDK_z
+            perform_undo
+        end
+        if event.keyval == Gdk::Keyval::GDK_r
+            perform_redo
+        end
+        if event.keyval == Gdk::Keyval::GDK_space
+            shown = $mainview.get_shown_entry
+            shown and autoscroll_if_needed(shown.button, true)
+        end
+
+    else
+        removed_before = entry.removed
+        label_before = entry.labeled
+
+        if event.keyval == Gdk::Keyval::GDK_Delete
+            entry.removed = true
+            entry.labeled = nil
+            entry.show_bg
+            update_visibility(entry)
+            $mainview.show_next_entry(entry)
+
+            save_undo(_("set for removal"),
+                      proc {
+                          entry.removed = removed_before
+                          entry.labeled = label_before
+                          entry.show_bg
+                          update_visibility(entry)
+                          if entry.button.visible?
+                              $mainview.try_show_entry(entry)
+                          end
+                          proc {
+                              entry.removed = true
+                              entry.labeled = nil
+                              entry.show_bg
+                              update_visibility(entry)
+                              if entry.button.visible?
+                                  $mainview.try_show_entry(entry)
+                              end
+                          }
+                      })
+
+        elsif event.keyval == Gdk::Keyval::GDK_space
+            if entry.labeled
+                msg = _("Cleared label")
+            elsif entry.removed
+                msg = _("Cleared set for removal")
+            end
+            entry.removed = false
+            entry.labeled = nil
+            entry.show_bg
+            $mainview.show_next_entry(entry)
+
+            save_undo(msg,
+                      proc {
+                          entry.removed = removed_before
+                          entry.labeled = label_before
+                          entry.show_bg
+                          $mainview.try_show_entry(entry)
+                          proc {
+                              entry.removed = false
+                              entry.labeled = nil
+                              entry.show_bg
+                              $mainview.try_show_entry(entry)
+                          }
+                      })
+
+        elsif event.keyval == Gdk::Keyval::GDK_Return
+            view_entry(entry)
+
+        else
+            char = [ Gdk::Keyval.to_unicode(event.keyval) ].pack("C*")
+            if char =~ /^[a-zA-z0-9]$/
+                label = $labels[char]
+                
+                if label.nil?
+                    vb = Gtk::VBox.new(false, 0)
+                    vb.pack_start(labelentry = Gtk::Entry.new.set_text(char), false, false)
+                    vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(bt = Gtk::ColorButton.new))
+                    color = bt.color = Gdk::Color.new(16384 + rand(49151), 16384 + rand(49151), 16384 + rand(49151))
+                    bt.signal_connect('color-set') { color = bt.color }
+                    text = nil
+                    labelentry.signal_connect('changed') {  #- cannot add a new label with first letter of an existing label
+                        while $labels.has_key?(labelentry.text[0,1])
+                            labelentry.text = labelentry.text.sub(/./, '')
+                        end
+                    }
+                    if show_popup($main_window,
+                                  utf8(_("You typed the text character '%s', which is not associated with a label.\nType in the full name of the label below to create a new one.")) % char,
+                                  { :okcancel => true, :bottomwidget => vb, :data_getter => proc { text = labelentry.text },
+                                    :stuff_connector => proc { |stuff| labelentry.select_region(0, 0)
+                                                                       labelentry.position = -1
+                                                                       labelentry.signal_connect('activate') { stuff[:dialog].response(Gtk::Dialog::RESPONSE_OK) } } } )
+                        if text.length > 0
+                            char = text[0,1]  #- in case it changed
+                            label = Label.new(text)
+                            label.color = color
+                            $labels[char] = label
+                            $ordered_labels << label
+                            lbl = Gtk::Label.new.set_markup('<b>(' + char + ')</b>' + text[1..-1]).set_justify(Gtk::Justification::CENTER)
+                            $labels_vbox.pack_start(label.button = Gtk::CheckButton.new.add(evt = Gtk::EventBox.new.add(lbl)).show_all)
+                            label.button.active = true
+                            label.button.signal_connect('toggled') { update_all_visibilities }
+                            evt.modify_bg(Gtk::StateType::NORMAL, label.color)
+                            evt.modify_bg(Gtk::StateType::PRELIGHT, label.color.lighter.lighter)
+                            evt.modify_bg(Gtk::StateType::ACTIVE, label.color.lighter)
+                        end
+                    end
+                end
+
+                if label
+                    entry.removed = false
+                    entry.labeled = label
+                    entry.show_bg
+                    update_visibility(entry)
+                    $mainview.show_next_entry(entry)
+
+                    save_undo(_("set label"),
+                              proc {
+                                  entry.removed = removed_before
+                                  entry.labeled = label_before
+                                  entry.show_bg
+                                  update_visibility(entry)
+                                  if entry.button.visible?
+                                      $mainview.try_show_entry(entry)
+                                  end
+                                  proc {
+                                      entry.removed = false
+                                      entry.labeled = label
+                                      entry.show_bg
+                                      update_visibility(entry)
+                                      if entry.button.visible?
+                                          $mainview.try_show_entry(entry)
+                                      end
+                                  }
+                              })
+                end
+            end
+        end
+    end
+end
+
+def sb_msg(msg)
+    $statusbar.pop(0)
+    if msg
+        $statusbar.push(0, utf8(msg))
+    end
+end
+
+def show_entry(entry, i, tips)
+    #- scope entry
+    #msg 3, "showing entry #{entry}"
+    entry.image = Gtk::Image.new(entry.pixbuf_thumbnail)
+    if entry.type == 'video'
+        entry.button = Gtk::Button.new.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false).
+                                           pack_start(entry.image).
+                                           pack_start(da2 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false))
+        da1.signal_connect('realize') { da1.window.set_back_pixmap($videoborder_pixmap, false) }
+        da2.signal_connect('realize') { da2.window.set_back_pixmap($videoborder_pixmap, false) }
+    else
+        entry.button = Gtk::Button.new.add(entry.image)
+    end
+    tips.set_tip(entry.button, entry.get_beautified_name, nil)
+    $imagesline.pack_start(entry.alignment = Gtk::Alignment.new(0.5, 1, 0, 0).add(entry.button).show_all, false, false)
+    entry.button.signal_connect('clicked') {
+        shown = $mainview.get_shown_entry
+        if shown != entry
+            shown and shown.alignment.set(0.5, 1, 0, 0)
+            entry.alignment.set(0.5, 0, 0, 0)
+            autoscroll_if_needed(entry.button, false)
+            $mainview.set_shown_entry(entry)
+        end
+    }
+    entry.button.signal_connect('button-press-event') { |w, event|
+        if entry.type == 'video' && event.event_type == Gdk::Event::BUTTON2_PRESS
+            video_view(entry)
+        end
+    }
+    entry.button.signal_connect('focus-in-event') { entry.button.clicked }
+    entry.button.signal_connect('key-press-event') { |w, e| thumbnail_keypressed(entry, e) }
+    if i == 0
+        entry.button.grab_focus
+    end
+    update_visibility(entry)
+    Gtk.main_iteration while Gtk.events_pending?
+end
+
+def show_entries(allentries)
+    sb_msg(_("Loading images..."))
+    $loading_progressbar.fraction = 0
+    $loading_progressbar.text = utf8(_("Loading... %d%") % 0)
+    $loading_progressbar.show
+    t1 = Time.now
+    total_loaded_files = 0
+    total_loaded_size = 0
+    i = 0
+    tips = Gtk::Tooltips.new
+    while i < allentries.size
+#        printf "%d %s\n", i, __LINE__
+        entry = allentries[i]
+        if i == 0
+            loaded_pixbuf = entry.pixbuf_main(false)
+        else
+            loaded_pixbuf = entry.pixbuf_thumbnail
+        end
+        if $allentries != allentries
+            #- loaded another directory while this one was not yet finished
+            msg 3, "allentries differ, stopping this deprecated load"
+            return
+        end
+
+        if loaded_pixbuf
+            show_entry(entry, i, tips)
+            if $allentries != allentries
+                #- loaded another directory while this one was not yet finished
+                msg 3, "allentries differ, stopping this deprecated load"
+                return
+            end
+
+            total_loaded_size += file_size(entry.path)
+            total_loaded_files += 1
+            i += 1
+            if i > $config['preload-distance'].to_i && i <= $config['preload-distance'].to_i * 2
+                #- when we're at preload distance, beging preloading to preload distance
+                allentries[i - $config['preload-distance'].to_i].pixbuf_main(false)
+            end
+            if i == $config['preload-distance'].to_i * 2 + 1
+                #- when we're after double preload distance, activate normal preloading
+                $preloader_allowed = true
+            end
+            
+        else
+            allentries.delete_at(i)
+        end
+        $loading_progressbar.fraction = i.to_f / allentries.size
+        $loading_progressbar.text = utf8(_("Loading... %d%") % (100 * $loading_progressbar.fraction))
+        if $quit
+            return
+        end
+    end
+    if i <= $config['preload-distance'].to_i * 2
+        #- not yet preloaded correctly
+        $preloader_allowed = true
+        run_preloader
+    end
+    sb_msg(_("%d images of total %s kB loaded in %3.2f seconds.") % [ total_loaded_files, commify(total_loaded_size / 1024), Time.now - t1 ])
+    $loading_progressbar.hide
+    $execute.sensitive = true
+end
+
+def reset_all
+    reset_labels
+    reset_thumbnails
+    $mainview.set_shown_entry(nil)
+    sb_msg(nil)
+    $preloader_allowed = false
+    $execute.sensitive = false
+end
+
+def open_dir(*paths)
+    #- remove visual stuff, so that user will see something is happening
+    reset_all
+    sb_msg(_("Scanning source directory..."))
+    Gtk.main_iteration while Gtk.events_pending?
+
+    for path in paths
+        path = File.expand_path(path.sub(%r|/$|, ''))
+        $workingdir = path
+        entries = []
+        if File.directory?(path)
+            examined_dirs = `find '#{path}' -type d -follow`.sort.collect { |v| v.chomp }
+            #- validate first
+            examined_dirs.each { |dir|
+                if dir =~ /'/
+                    return utf8(_("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir)
+                end
+                Dir.entries(dir).each { |file|
+                    if file =~ /'/ && type = entry2type(file) && type == 'video'
+                        return utf8(_("Videos can't contain a single quote character ('), sorry: %s") % "#{dir}/#{file}")
+                    end
+                }
+            }
+            
+            #- scan for populate second
+            examined_dirs.each { |dir|
+                if File.basename(dir) =~ /^\./
+                    msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
+                    next
+                end
+                entries += Dir.entries(dir).collect { |file| File.join(dir, file) }
+            }
+
+        else
+            entries << path
+        end
+
+        if $sort_by_exif_date
+            dates = {}
+            entries.each { |file|
+                date_time = Exif.datetimeoriginal(file)
+                if ! date_time.nil?
+                    dates[file] = date_time
+                end
+            }
+            entries = smartsort(entries, dates)
+        else
+            entries.sort!
+        end
+        entries.each { |file|
+            type = entry2type(file)
+            if type
+                if File.directory?(path)
+                    $allentries << Entry.new(file, type, file[path.length + 1 .. -1])
+                else
+                    $allentries << Entry.new(file, type, file)
+                end
+            end
+        }
+    end
+    return nil
+end
+
+def open_dir_popup
+    fc = Gtk::FileChooserDialog.new(utf8(_("Specify the directory to work with")),
+                                    nil,
+                                    Gtk::FileChooser::ACTION_SELECT_FOLDER,
+                                    nil,
+                                    [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+    fc.transient_for = $main_window
+    if $workingdir
+        fc.current_folder = $workingdir
+    end
+    ok = false
+    load = false
+    while !ok
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+            msg = open_dir(fc.filename)
+            if msg
+                show_popup(fc, msg)
+                ok = false
+            else
+                ok = true
+                load = true
+            end
+        else
+            ok = true
+        end
+    end
+    fc.destroy
+    if load
+        show_entries($allentries)
+    end
+end
+
+def try_quit(*options)
+    if ! $allentries.detect { |e| e.removed || e.labeled } ||
+       show_popup($main_window,
+                  utf8(_("Are you sure you want to quit?")),
+                  { :okcancel => true })
+        Gtk.main_quit
+        $quit = true
+    end
+end
+
+def execute
+    dialog = Gtk::Dialog.new
+    dialog.title = utf8(_("Booh message"))
+
+    vb1 = Gtk::VBox.new(false, 5)
+    label = Gtk::Label.new.set_markup(utf8(_("You're about to <b>execute</b> actions on the marked images.\nPlease confirm below the actions. You cannot undo this operation!")))
+    vb1.pack_start(label, false, false)
+
+    lastpath = $workingdir
+
+    table = Gtk::Table.new(0, 0, false)
+    table.set_row_spacings(5)
+    table.set_column_spacings(5)
+    table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Label name:</b>"))).set_justify(Gtk::Justification::CENTER), 0, 1, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
+    table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Amount of pictures:</b>"))).set_justify(Gtk::Justification::CENTER), 1, 2, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
+    table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Pictures examples:</b>"))).set_justify(Gtk::Justification::CENTER), 2, 3, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
+    table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Action to perform:</b>"))).set_justify(Gtk::Justification::CENTER), 3, 4, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
+    add_row = proc { |row, name, color, truthproc, normal|
+        table.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(Gtk::EventBox.new.add(Gtk::Label.new.set_markup(name)).modify_bg(Gtk::StateType::NORMAL, color)),
+                     0, 1, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
+        counter = 0
+        examples = Gtk::HBox.new(false, 5)
+        $allentries.each { |entry|
+            if truthproc.call(entry)
+                counter += 1
+                if counter < 4
+                    thumbnail = Gtk::Image.new(entry.pixbuf_thumbnail)
+                    if entry.type == 'video'
+                        thumbnail = Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false).
+                                                  pack_start(thumbnail).
+                                                  pack_start(da2 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false)
+                        da1.signal_connect('realize') { da1.window.set_back_pixmap($videoborder_pixmap, false) }
+                        da2.signal_connect('realize') { da2.window.set_back_pixmap($videoborder_pixmap, false) }
+                    end
+                    examples.pack_start(thumbnail, false, false)
+                elsif counter == 4
+                    examples.pack_start(Gtk::Label.new.set_markup("<b>...</b>"), false, false)
+                end
+            end
+        }
+        table.attach(Gtk::Label.new(counter.to_s).set_justify(Gtk::Justification::CENTER), 1, 2, row, row + 1, 0, 0, 5, 5)
+        table.attach(examples, 2, 3, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
+
+        if counter == 0
+            return {}
+        end
+
+        combostore = Gtk::ListStore.new(Gdk::Pixbuf, String)
+        iter = combostore.append
+        if normal
+            iter[0] = $main_window.render_icon(Gtk::Stock::PASTE, Gtk::IconSize::MENU)
+            iter[1] = utf8(_("Copy to:"))
+            iter = combostore.append
+            iter[0] = $main_window.render_icon(Gtk::Stock::GO_FORWARD, Gtk::IconSize::MENU)
+            iter[1] = utf8(_("Move to:"))
+        else
+            iter[0] = $main_window.render_icon(Gtk::Stock::DELETE, Gtk::IconSize::MENU)
+            iter[1] = utf8(_("Permanently remove"))
+        end
+        iter = combostore.append
+        iter[0] = $main_window.render_icon(Gtk::Stock::MEDIA_STOP, Gtk::IconSize::MENU)
+        iter[1] = utf8(_("Do nothing"))
+        combo = Gtk::ComboBox.new(combostore)
+        combo.active = 0
+        renderer = Gtk::CellRendererPixbuf.new
+        combo.pack_start(renderer, false)
+        combo.set_attributes(renderer, :pixbuf => 0)
+        renderer = Gtk::CellRendererText.new
+        combo.pack_start(renderer, true)
+        combo.set_attributes(renderer, :text => 1)
+
+        if normal
+            pathbutton = Gtk::Button.new.add(pathlabel = Gtk::Label.new.set_markup(utf8(_("<i>(unset)</i>"))))
+            pathbutton.signal_connect('clicked') {
+                fc = Gtk::FileChooserDialog.new(utf8(_("Specify the directory where to move the pictures to")),
+                                                nil,
+                                                Gtk::FileChooser::ACTION_SELECT_FOLDER,
+                                                nil,
+                                                [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+                fc.transient_for = dialog
+                if lastpath
+                    fc.current_folder = lastpath
+                end
+                if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+                    pathlabel.text = fc.filename
+                    pathlabel.set_alignment(0, 0.5)
+                end
+                lastpath = fc.filename
+                fc.destroy
+            }
+            combo.signal_connect('changed') {
+                pathbutton.sensitive = combo.active <= 1
+            }
+            vb = Gtk::VBox.new(false, 5)
+            vb.pack_start(combo, false, false)
+            vb.pack_start(pathbutton, false, false)
+            table.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(vb), 3, 4, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
+            { :combo => combo, :pathlabel => pathlabel }
+        else
+            table.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(combo), 3, 4, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
+            { :combo => combo }
+        end
+    }
+    stuff = {}
+    stuff['toremove'] = add_row.call(1, utf8(_("<i>to remove</i>")), $color_red, proc { |entry| entry.removed }, false)
+    $ordered_labels.each_with_index { |label, row| stuff[label] = add_row.call(row + 2, label.name, label.color, proc { |entry| entry.labeled == label }, true) }
+    vb1.pack_start(sw = Gtk::ScrolledWindow.new(nil, nil).add_with_viewport(table).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC), true, true)
+
+    toremove_amount = $allentries.find_all { |entry| entry.removed }.size
+    toremove_size = commify($allentries.find_all { |entry| entry.removed }.collect { |entry| file_size(entry.path) }.sum / 1024)
+    check_removal = Gtk::CheckButton.new(utf8(_("I have noticed I am about to permanently remove the %d above mentioned pictures (total %s kB).") % [ toremove_amount, toremove_size ]))
+    if toremove_amount > 0
+        vb1.pack_start(check_removal, false, false)
+        stuff['toremove'][:combo].signal_connect('changed') { |widget|
+            check_removal.sensitive = widget.active == 0
+        }
+    end
+
+    dialog.vbox.add(vb1)
+
+    dialog.set_default_size(800, 600)
+    dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
+    dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
+    dialog.window_position = Gtk::Window::POS_MOUSE
+    dialog.transient_for = $main_window
+
+    dialog.show_all
+
+    while true
+        dialog.run { |response|        
+            if response == Gtk::Dialog::RESPONSE_OK
+                if toremove_amount > 0 && ! check_removal.active? && stuff['toremove'][:combo].active == 0
+                    show_popup(dialog, utf8(_("You have not confirmed that you noticed the permanent removal of the pictures marked for deletion.")))
+                    break
+                end
+                problem = false
+                label2entries = {}
+                $labels.values.each { |label| label2entries[label] = [] }
+                $allentries.each { |entry| entry.labeled and label2entries[entry.labeled] << entry }
+                stuff.keys.each { |key|
+                    if key.is_a?(Label) && stuff[key][:combo] && stuff[key][:combo].active <= 1
+                        destination = stuff[key][:pathlabel].text
+                        if destination[0] != ?/
+                            show_popup(dialog, utf8(_("You have not selected a directory where to move/copy %s.") % key.name))
+                            problem = true
+                            break
+                        end
+                        begin
+                            Dir.mkdir(destination)
+                        rescue Errno::EEXIST
+                        end
+                        begin
+                            st = File.stat(destination)
+                        rescue
+                            show_popup(dialog, utf8(_("Directory %s, where to move/copy %s, is not valid or not createable.") % [destination, key.name]))
+                            problem = true
+                            break
+                        end
+                        if ! st.directory? || ! st.writable?
+                            show_popup(dialog, utf8(_("Directory %s, where to move/copy %s, is not valid or not writable.") % [destination, key.name]))
+                            problem = true
+                            break
+                        end
+                        label2entries[key].each { |entry|
+                            begin
+                                File.stat(File.join(destination, File.basename(entry.path)))
+                                show_popup(dialog, utf8(_("Sorry, a file '%s' already exists in directory '%s'.") % [ File.basename(entry.path), destination ]))
+                                problem = true
+                                break
+                            rescue
+                            end
+                        }
+                        if problem
+                            break
+                        end
+                    end
+                }
+                if ! problem
+                    begin
+                        moved = 0
+                        copied = 0
+                        stuff.keys.each { |key|
+                            if key.is_a?(Label) && stuff[key][:combo] && stuff[key][:combo].active <= 1
+                                destination = stuff[key][:pathlabel].text
+                                label2entries[key].each { |entry|
+                                    if stuff[key][:combo].active == 0
+                                        system("cp -dp '#{entry.path}' '#{destination}'") or raise "failed to copy '#{entry.path}'"
+                                        copied += 1
+                                    elsif stuff[key][:combo].active == 1
+                                        system("mv '#{entry.path}' '#{destination}'") or raise "failed to move '#{entry.path}'"
+                                        moved += 1
+                                    end
+                                }
+                            end
+                        }
+                        removed = 0
+                        if stuff['toremove'][:combo] && stuff['toremove'][:combo].active == 0
+                            $allentries.each { |entry|
+                                if entry.removed
+                                    File.delete(entry.path)
+                                    removed += 1
+                                end
+                            }
+                        end
+                    rescue
+                        msg 1, "woops: #{$!}"
+                        show_popup(dialog, utf8(_("Unexpected error: '%s'.") % $!))
+                    end
+                    show_popup(dialog, utf8(_("Successfully moved %d files, copied %d file, and removed %d files.") % [ moved, copied, removed ]))
+                    dialog.destroy
+                    reset_all
+                    return
+                end
+
+            else
+                dialog.destroy
+                return
+            end
+        }
+    end
+end
+
+def visible(entry)
+    if ! entry.button
+        #- not yet loaded
+        return
+    end
+    if entry.labeled
+        if entry.labeled.button.active?
+            return true
+        else
+            return false
+        end
+    elsif entry.removed
+        if $toremove_button.active?
+            return true
+        else
+            return false
+        end
+    else
+        if $unlabelled_button.active?
+            return true
+        else
+            return false
+        end
+    end
+end
+
+def update_visibility(entry)
+    v = visible(entry)
+    if v.nil?
+        return
+    end
+    if v
+        entry.button.show
+    else
+        entry.button.hide
+    end
+end
+        
+def update_all_visibilities_aux
+    $allentries.each { |entry|
+        update_visibility(entry)
+    }
+    shown = $mainview.get_shown_entry
+    shown or return
+    while shown.button && ! shown.button.visible? && shown != $allentries.last
+        shown = $allentries[$allentries.index(shown) + 1]
+    end 
+    if shown.button && shown.button.visible?
+        shown.button.grab_focus
+        return
+    end
+    $allentries.reverse.each { |entry|
+        if entry.button && entry.button.visible?
+            entry.button.grab_focus
+            return
+        end
+    }
+end
+
+def update_all_visibilities
+    update_all_visibilities_aux
+    Gtk.main_iteration while Gtk.events_pending?
+    shown = $mainview.get_shown_entry
+    shown and autoscroll_if_needed(shown.button, false)
+end
+
+
+def preferences
+    dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    tooltips = Gtk::Tooltips.new
+
+    table_y = 0
+
+    dialog.vbox.add(tbl = Gtk::Table.new(0, 0, false))
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;\nfor example: /usr/bin/mplayer %f")), nil)
+
+    table_y += 1
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;\nfor example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
+
+    table_y += 1
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Thumbnails height: ")))),
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(thumbnails_height = Gtk::SpinButton.new(32, 256, 16).set_value($config['thumbnails-height'].to_i)),
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(thumbnails_height, utf8(_("The desired height of the thumbnails in the thumbnails line of the bottom")), nil)
+
+    table_y += 1
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Preloading distance: ")))),
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(preload_distance = Gtk::SpinButton.new(0, 50, 1).set_value($config['preload-distance'].to_i)),
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(preload_distance, utf8(_("Amount of pictures preloaded left and right to the currently shown")), nil)
+
+    table_y += 1
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Cache memory use: ")))),
+               0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(cache_vbox = Gtk::VBox.new(false, 0)),
+               1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    cache_vbox.pack_start(Gtk::HBox.new(false, 0).pack_start(cache_memfree_radio = Gtk::RadioButton.new(''), false, false).
+                                                  pack_start(cache_memfree_spin = Gtk::SpinButton.new(0, 100, 10), false, false).
+                                                  pack_start(cache_memfree_label = Gtk::Label.new(utf8(_("% of free memory"))), false, false), false, false)
+    cache_memfree_spin.signal_connect('value-changed') { cache_memfree_radio.active = true }
+    tooltips.set_tip(cache_memfree_spin, utf8(_("Percentage of free memory (+ buffers/cache) measured at startup")), nil)
+    cache_vbox.pack_start(Gtk::HBox.new(false, 0).pack_start(cache_specify_radio = Gtk::RadioButton.new(cache_memfree_radio, ''), false, false).
+                                                  pack_start(cache_specify_spin = Gtk::SpinButton.new(0, 4000, 50), false, false).
+                                                  pack_start(cache_specify_label = Gtk::Label.new(utf8(_("MB"))).set_sensitive(false), false, false), false, false)
+    cache_specify_spin.signal_connect('value-changed') { cache_specify_radio.active = true }
+    cache_memfree_radio.signal_connect('toggled') {
+        if cache_memfree_radio.active?
+            cache_memfree_label.sensitive = true
+            cache_specify_label.sensitive = false
+        else
+            cache_specify_label.sensitive = true
+            cache_memfree_label.sensitive = false
+        end
+    }
+    tooltips.set_tip(cache_specify_spin, utf8(_("Amount of memory in megabytes")), nil)
+    if $config['cache-memory-use'] =~ /memfree_(\d+)/
+        cache_memfree_spin.value = $1.to_i
+    else
+        cache_specify_spin.value = $config['cache-memory-use'].to_i
+    end
+
+    table_y += 1
+    tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
+               0, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
+    update_exif_orientation_check.active = $config['rotate-set-exif'] == 'true'
+
+    dialog.vbox.show_all
+    dialog.run { |response|
+        if response == Gtk::Dialog::RESPONSE_OK
+            $config['video-viewer'] = from_utf8(video_viewer_entry.text)
+            $config['browser'] = from_utf8(browser_entry.text)
+            $config['thumbnails-height'] = thumbnails_height.value
+            $config['preload-distance'] = preload_distance.value
+            $config['rotate-set-exif'] = update_exif_orientation_check.active?.to_s
+            if cache_memfree_radio.active?
+                $config['cache-memory-use'] = "memfree_#{cache_memfree_spin.value}%"
+            else
+                $config['cache-memory-use'] = cache_specify_spin.value
+            end
+            set_cache_memory_use_figure
+        end
+    }
+    dialog.destroy
+end
+
+def perform_undo
+    if $undo_mb.sensitive?
+        $redo_mb.sensitive = true
+        if not more_undoes = UndoHandler.undo($statusbar)
+            $undo_mb.sensitive = false
+        end
+    end
+end
+
+def perform_redo
+    if $redo_mb.sensitive?
+        $undo_mb.sensitive = true
+        if not more_redoes = UndoHandler.redo($statusbar)
+            $redo_mb.sensitive = false
+        end
+    end
+end
+
+def create_menubar    
+    #- menu
+    mb = Gtk::MenuBar.new
+
+    filemenu = Gtk::MenuItem.new(utf8(_("_File")))
+    filesubmenu = Gtk::Menu.new
+    filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
+    filesubmenu.append(            Gtk::SeparatorMenuItem.new)
+    filesubmenu.append($execute  = Gtk::ImageMenuItem.new(Gtk::Stock::EXECUTE).set_sensitive(false))
+    filesubmenu.append(            Gtk::SeparatorMenuItem.new)
+    filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
+    filemenu.set_submenu(filesubmenu)
+    mb.append(filemenu)
+
+    open.signal_connect('activate') { open_dir_popup }
+    $execute.signal_connect('activate') { execute }
+    quit.signal_connect('activate') { try_quit }
+
+    editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
+    editsubmenu = Gtk::Menu.new
+    editsubmenu.append($undo_mb    = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
+    editsubmenu.append($redo_mb    = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
+    editsubmenu.append(              Gtk::SeparatorMenuItem.new)
+    editsubmenu.append(prefs       = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
+    editmenu.set_submenu(editsubmenu)
+    mb.append(editmenu)
+
+    $undo_mb.signal_connect('activate') { perform_undo }
+    $redo_mb.signal_connect('activate') { perform_redo }
+    prefs.signal_connect('activate') { preferences }
+    
+    helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
+    helpsubmenu = Gtk::Menu.new
+    helpsubmenu.append(howto = Gtk::ImageMenuItem.new(Gtk::Stock::HELP))
+    helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts"))))
+    speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
+    helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
+    tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
+    helpsubmenu.append(Gtk::SeparatorMenuItem.new)
+    helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
+    helpmenu.set_submenu(helpsubmenu)
+    mb.append(helpmenu)
+
+    howto.signal_connect('activate') {
+        show_popup($main_window, utf8(_("<span size='large' weight='bold'>Help</span>
+
+1. Open a directory with <span foreground='darkblue'>File/Open</span>; the classifier will scan it (including subdirectories) and
+show thumbnails for all photos and videos at the bottom.
+
+2. You can then navigate through images with the <span foreground='darkblue'>Left/Right</span> keyboard keys, or by <span foreground='darkblue'>clicking</span>
+on thumbnails.
+
+3. You may associate a <span foreground='darkblue'>label</span> to each thumbnail. Either hit the <span foreground='darkblue'>Delete</span> key to associate
+the built-in <i>to remove</i> label, or hit any alphabetical key to associate a label you define.
+The first time you hit a key without any label associated, a popup will ask for the full
+name of this label, and what color you want. To clear the current label, hit the <span foreground='darkblue'>Space</span> key.
+
+4. To help you better view what thumbnails are associated to your labels, you may <span foreground='darkblue'>hide</span>
+some of them by unchecking the labels checkboxes on the left.
+
+5. Once you're finished reviewing all thumbnails, use <span foreground='darkblue'>File/Execute</span> to execute the desired
+actions according to associated labels. You can permanently remove (or not) images with
+the <i>to remove</i> label, and copy or move images with the labels you defined.
+")), { :pos_centered => true, :not_transient => true })
+    }
+    speed.signal_connect('activate') {
+        show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts</span>
+
+<span foreground='darkblue'>Left/Right</span>: move left and right in images
+<span foreground='darkblue'>Enter</span>: 'view' current image: for images, display EXIF data; for videos, play it
+<span foreground='darkblue'>Alt-Left/Right</span>: rotate current image clockwise/counter-clockwise
+<span foreground='darkblue'>Delete</span>: assign the 'to remove' label on current image
+<span foreground='darkblue'>Space</span>: clear any label on current image
+<span foreground='darkblue'>Control-z</span>: undo
+<span foreground='darkblue'>Control-r</span>: redo
+<span foreground='darkblue'>Control-Space</span>: recenter thumbnails on current item
+
+Any alphabetical key will assign (or popup for) the associated label on current image.
+")), { :pos_centered => true, :not_transient => true })
+    }
+    tutos.signal_connect('activate') { open_url('http://booh.org/tutorial') }
+    about.signal_connect('activate') { call_about }
+
+
+    #- no toolbar, to save height
+
+    return mb
+end
+
+def reset_labels
+    for child in $labels_vbox.children
+        $labels_vbox.remove(child)
+    end
+    $labels_vbox.pack_start(Gtk::Label.new(utf8(_("Labels list:"))).set_justify(Gtk::Justification::CENTER), false, false).show_all
+    $labels = {}
+    $ordered_labels = []
+    lbl = Gtk::Label.new.set_markup(utf8(_("<i>unlabelled</i>")))
+    $labels_vbox.pack_start($unlabelled_button = Gtk::CheckButton.new.add(Gtk::EventBox.new.add(lbl)).show_all)
+    $unlabelled_button.active = true
+    $unlabelled_button.signal_connect('toggled') { update_all_visibilities }
+    lbl = Gtk::Label.new.set_markup(utf8(_("<i>to remove</i>")))
+    $labels_vbox.pack_start($toremove_button = Gtk::CheckButton.new.add(evt = Gtk::EventBox.new.add(lbl)).show_all)
+    $toremove_button.active = true
+    $toremove_button.signal_connect('toggled') { update_all_visibilities }
+    evt.modify_bg(Gtk::StateType::NORMAL, $color_red)
+    evt.modify_bg(Gtk::StateType::PRELIGHT, $color_red.lighter.lighter)
+    evt.modify_bg(Gtk::StateType::ACTIVE, $color_red.lighter)
+end
+
+def reset_thumbnails
+    $allentries = []
+    if $preloader_running
+        $preloader_force_exit = true
+    end
+    for child in $imagesline.children
+        $imagesline.remove(child)
+    end
+    set_imagesline_size_request
+end
+
+def set_imagesline_size_request
+    $imagesline.set_size_request(-1, Gtk::Button.new.size_request[1] + Entry.thumbnails_height + Entry.thumbnails_height/4)
+end
+
+def create_main_window
+
+    $videoborder_pixbuf = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
+    $videoborder_pixmap, = $videoborder_pixbuf.render_pixmap_and_mask(0)
+
+    mb = create_menubar
+
+    main_vbox = Gtk::VBox.new(false, 0)
+    main_vbox.pack_start(mb, false, false)
+    mainview_hbox = Gtk::HBox.new
+    mainview_hbox.pack_start(Gtk::Alignment.new(0.5, 0, 1, 1).add(left_vbox = Gtk::VBox.new(false, 5)), false, true)
+    left_vbox.pack_start(($labels_vbox = Gtk::VBox.new(false, 5)), false, true)
+    left_vbox.pack_end($loading_progressbar = Gtk::ProgressBar.new.set_text(utf8(_("Loading... %d%") % 0)), false, true)
+    mainview_hbox.pack_start($mainview = MainView.new, true, true)
+    main_vbox.pack_start(mainview_hbox, true, true)
+    $imagesline_sw = Gtk::ScrolledWindow.new(nil, nil)
+    $imagesline_sw.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_NEVER)
+    $imagesline_sw.add_with_viewport($imagesline = Gtk::HBox.new(false, 0).show)
+    main_vbox.pack_start($imagesline_sw, false, false)
+    main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
+
+    set_imagesline_size_request
+
+    $main_window = create_window
+    $main_window.add(main_vbox)
+    $main_window.signal_connect('delete-event') {
+        try_quit({ :disallow_cancel => true })
+    }
+
+    #- read/save size and position of window
+    if $config['pos-x'] && $config['pos-y']
+        $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
+    else
+        $main_window.window_position = Gtk::Window::POS_CENTER
+    end
+    msg 3, "size: #{$config['width']}x#{$config['height']}"
+    $main_window.set_default_size(($config['width'] || 800).to_i, ($config['height'] || 600).to_i)
+    $main_window.signal_connect('configure-event') {
+        msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
+        x, y = $main_window.window.root_origin
+        width, height = $main_window.window.size
+        $config['pos-x'] = x
+        $config['pos-y'] = y
+        $config['width'] = width
+        $config['height'] = height
+        false
+    }
+
+    $main_window.show_all
+    $loading_progressbar.hide
+end
+
+
+handle_options
+read_config
+Gtk.init
+
+
+create_main_window
+check_config
+
+if ARGV[0]
+    if msg = open_dir(*ARGV)
+        puts msg
+    else
+        Gtk.idle_add {
+            show_entries($allentries)
+            false
+        }
+    end
+end
+Gtk.main
+
+write_config


Property changes on: packages/booh/branches/upstream/current/bin/booh-classifier
___________________________________________________________________
Name: svn:executable
   + 

Added: packages/booh/branches/upstream/current/bin/booh-fix-whitebalance
===================================================================
--- packages/booh/branches/upstream/current/bin/booh-fix-whitebalance	                        (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh-fix-whitebalance	2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,31 @@
+#!/usr/bin/ruby
+#
+#                         *  BOOH  *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'gtk2'
+require 'booh/libadds'
+
+if ARGV.size != 3
+    puts "Usage: %s orig_file dest_file_jpeg level" % File.basename($0)
+    exit 1
+
+else
+    Gdk::Pixbuf.new(ARGV[0]).whitebalance!(ARGV[2].to_f).save(ARGV[1], "jpeg")
+end


Property changes on: packages/booh/branches/upstream/current/bin/booh-fix-whitebalance
___________________________________________________________________
Name: svn:executable
   + 

Added: packages/booh/branches/upstream/current/bin/booh-gamma-correction
===================================================================
--- packages/booh/branches/upstream/current/bin/booh-gamma-correction	                        (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh-gamma-correction	2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,31 @@
+#!/usr/bin/ruby
+#
+#                         *  BOOH  *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'gtk2'
+require 'booh/libadds'
+
+if ARGV.size != 3
+    puts "Usage: %s orig_file dest_file_jpeg level" % File.basename($0)
+    exit 1
+
+else
+    Gdk::Pixbuf.new(ARGV[0]).gammacorrect!(ARGV[2].to_f).save(ARGV[1], "jpeg")
+end


Property changes on: packages/booh/branches/upstream/current/bin/booh-gamma-correction
___________________________________________________________________
Name: svn:executable
   + 

Added: packages/booh/branches/upstream/current/bin/webalbum2booh
===================================================================
--- packages/booh/branches/upstream/current/bin/webalbum2booh	                        (rev 0)
+++ packages/booh/branches/upstream/current/bin/webalbum2booh	2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,163 @@
+#! /usr/bin/ruby
+#
+#                         *  BOOH  *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'getoptlong'
+require 'gettext'
+include GetText
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+include Booh
+
+#- options
+$options = [
+    [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
+
+    [ '--config',        '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing images and videos within directories with captions") ],
+
+    [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+]
+
+#- default values for some globals 
+$switches = []
+$stdout.sync = true
+
+def usage
+    puts _("Usage: %s [OPTION]...") % File.basename($0)
+    $options.each { |ary|
+        printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
+    }
+end
+
+def handle_options
+    parser = GetoptLong.new
+    parser.set_options(*$options.collect { |ary| ary[0..2] })
+    begin
+        parser.each_option do |name, arg|
+            case name
+            when '--help'
+                usage
+                exit(0)
+
+            when '--config'
+                if File.readable?(arg)
+                    $xmldoc = REXML::Document.new File.new(arg)
+                    $conffile = arg
+                else
+                    die_ _('Config file does not exist or is unreadable.')
+                end
+
+            when '--verbose-level'
+                $verbose_level = arg.to_i
+
+            end
+        end
+    rescue
+        puts $!
+        usage
+        exit(1)
+    end
+
+    if !$xmldoc
+        die_ _("Missing --config parameter.")
+    end
+
+    $source = $xmldoc.root.attributes['source']
+    $dest = $xmldoc.root.attributes['destination']
+end
+
+def utf8_and_entities(string)
+    return utf8(string).gsub('&agrave;', 'à').
+                        gsub('&ccedil;', 'ç').
+                        gsub('&ocirc;',  'ô').
+                        gsub('&eacute;', 'é').
+                        gsub('&ecirc;',  'ê').
+                        gsub('&egrave;', 'è').
+                        gsub('&Egrave;', 'È').
+                        gsub('&icirc;',  'î').
+                        gsub('&lt;',     '<').
+                        gsub('&gt;',     '>').
+                        gsub('&ugrave;', 'ù').
+                        gsub('&quot;',   '"')
+end
+
+def parse_webalbum_indextxt(filepath)
+    begin
+        contents = File.open(filepath).readlines
+        out = {}
+        out[:legends] = {}
+        legend = ''
+        for line in contents
+            if line =~ /^\s*#/
+                next
+            elsif line =~ /^\s*TITLE\s*=\s*(.*)/
+                out[:title] = $1
+            elsif line =~ /^\s*ABSTRACT\s*=\s*(.*)/
+                out[:abstract] = $1
+            elsif line =~ /^\s*IMAGE_LEGEND\s*=\s*(.*)/
+                legend = $1
+            elsif line =~ /^\s*IMAGE_FILE\s*=\s*(.*)/
+                out[:legends][$1] = legend
+            end
+        end
+        return out
+    rescue
+        return nil
+    end
+end
+
+def walk_source_dir
+
+    `find #{$source} -type d`.sort.each { |dir|
+        dir.chomp!
+        msg 2, _("Handling %s from config list...") % dir
+
+        if !infos = parse_webalbum_indextxt("#{dir}/index.txt")
+            next
+        end
+
+        #- place xml document on proper node
+        xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
+
+        if infos.has_key?(:title)
+            type = find_subalbum_info_type(xmldir)
+            xmldir.add_attribute("#{type}-caption", utf8_and_entities(infos[:title]))
+        end
+        
+        xmldir.elements.each { |element|
+            if %w(image video).include?(element.name)
+                if infos[:legends].has_key?(element.attributes['filename'])
+                    element.add_attribute('caption', utf8_and_entities(infos[:legends][element.attributes['filename']]))
+                end
+            end
+        }
+    }
+end
+
+
+handle_options
+
+walk_source_dir
+
+ios = File.open("#{$conffile}.merged", "w")
+$xmldoc.write(ios, 0)
+ios.close


Property changes on: packages/booh/branches/upstream/current/bin/webalbum2booh
___________________________________________________________________
Name: svn:executable
   + 




More information about the Pkg-ruby-extras-commits mailing list