workaround progression pipe bug
[booh] / bin / booh
old mode 100755 (executable)
new mode 100644 (file)
index 4127984..5766217
--- a/bin/booh
+++ b/bin/booh
@@ -10,7 +10,7 @@
 # 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>
+# Copyright (c) 2004-2013 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.
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
+begin
+    require 'rubygems'
+rescue LoadError
+end
+
 require 'getoptlong'
 require 'tempfile'
 require 'thread'
@@ -31,20 +36,19 @@ require 'gettext'
 include GetText
 bindtextdomain("booh")
 
-require 'booh/rexml/document'
+require '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") ],
-
+    [ '--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 
@@ -72,6 +76,15 @@ def handle_options
                 usage
                 exit(0)
 
+            when '--version'
+                puts _("Booh version %s
+
+Copyright (c) 2005-2013 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
 
@@ -84,42 +97,56 @@ def handle_options
     end
 end
 
+def count_cpus
+    cpus = 0
+    for line in IO.readlines('/proc/cpuinfo') do
+        line =~ /^processor/ and cpus += 1
+    end
+    return cpus
+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(/~~~/)
+        begin
+            xmldoc = REXML::Document.new(File.new($config_file))
+        rescue
+            #- encoding unsupported anymore? file edited manually? ignore then
+            msg 1, "Ignoring #{$config_file}, failed to parse it: #{$!}"
+        end
+        if xmldoc
+            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] = txt.value
+                    $config[element.name] = {}
+                    element.each { |chld|
+                        txt = chld.get_text
+                        $config[element.name][chld.name] = txt ? txt.value : nil
+                    }
                 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
     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['video-viewer'] ||= '/usr/bin/mplayer %f || /usr/bin/vlc %f'
+    $config['image-editor'] ||= '/usr/bin/gimp-remote %f || /usr/bin/gimp %f'
+    $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f || /usr/bin/firefox -remote 'openURL(%f,new-window)' || /usr/bin/firefox %f"
+    $config['use-mp4'] ||= "true"
+    $config['mp4-generator'] ||= "/usr/bin/ffmpeg -i %f -b 800k -ar 22050 -ab 32k %o"
     $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
+        cpus = count_cpus
         if cpus > 1
             $config['mproc'] = cpus
         end
@@ -129,6 +156,29 @@ def read_config
     $todelete = []
 end
 
+def check_config_preferences_dep
+    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
+
+    mp4_generator_binary = $config['use-mp4'] == 'true' && $config['mp4-generator'].split.first
+    if mp4_generator_binary && !File.executable?(mp4_generator_binary)
+        show_popup($main_window, utf8(_("The configured .mp4 generator seems to be unavailable.
+You should fix this in Edit/Preferences so that you can have working
+embedded flash 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/ffmpeg' is correct but 'ffmpeg' only is not.") % mp4_generator_binary), { :pos_centered => true, :not_transient => true })
+    end
+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.
@@ -136,7 +186,7 @@ It is generally available with the 'ImageMagick' software package.")), { :pos_ce
         exit 1
     end
     if !system("which identify >/dev/null 2>/dev/null")
-        show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
+        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")
@@ -147,30 +197,29 @@ It is generally available with the 'ImageMagick' software package.")), { :pos_ce
         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.
+    check_config_preferences_dep
 
-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 })
+    cpus = count_cpus
+
+    if $config['cpus'] && cpus > $config['cpus'].to_i
+        show_popup($main_window, utf8(_("It seems you now have more CPUs available than last time booh was run.
+You should probably increase the amount of CPUs configured in Edit/Preferences,
+so that web-albums are generated as fast as possible on this computer.")), { :pos_centered => true, :not_transient => true })
     end
-    image_editor_binary = $config['image-editor'].split.first
-    if image_editor_binary && !File.executable?(image_editor_binary)
+    $config['cpus'] = cpus
+end
+
+def check_image_editor
+    if last_failed_binary = check_multi_binaries($config['image-editor'])
         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 images externally.
+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 })
+e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
+        return false
+    else
+        return true
     end
 end
 
@@ -179,11 +228,10 @@ def write_config
         $config['last-opens'] = $config['last-opens'][-10, 10]
     end
 
-    ios = File.open($config_file, "w")
-    $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
-    $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
+    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
+        elem = xmldoc.root.add_element key
         if value.is_a? Hash
             $config[key].each_pair { |subkey, subvalue|
                 subelem = elem.add_element subkey
@@ -199,7 +247,8 @@ def write_config
             end
         end
     }
-    $xmldoc.write(ios, 0)
+    ios = File.open($config_file, "w")
+    xmldoc.write(ios)
     ios.close
 
     $tempfiles.each { |f|
@@ -266,7 +315,7 @@ def view_element(filename, closures)
         return
     end
 
-    w = Gtk::Window.new.set_title(filename)
+    w = create_window.set_title(filename)
 
     msg 3, "filename: #{filename}"
     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
@@ -287,7 +336,13 @@ def view_element(filename, closures)
                 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)))
+    aspect = utf8(_("Aspect: unknown"))
+    size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
+    if size
+        aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
+    end
+    vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
+    evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(vbox).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
     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 }
@@ -446,9 +501,9 @@ def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
         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)
+            pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, :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)
+            pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), :bilinear)
         end
     end
 
@@ -472,7 +527,8 @@ def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x
     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'
+    #- change exif orientation if configured so (but forget in case of thumbnails caption)
+    if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
         Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
     end
 
@@ -502,33 +558,41 @@ 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
+    rexml_thread_protect {
+        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
+    rexml_thread_protect {
+        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)
+    rexml_thread_protect {
+        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
+    value = rexml_thread_protect {
+        if xmldir
+            xmldir.attributes["#{attributes_prefix}seektime"]
+        else
+            ''
+        end
+    }
 
     dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
                              $main_window,
@@ -542,7 +606,7 @@ _("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))
+    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)
@@ -573,19 +637,23 @@ 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
+    rexml_thread_protect {
+        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
+    value = rexml_thread_protect {
+        if xmldir
+            xmldir.attributes["#{attributes_prefix}pano-amount"]
+        else
+            nil
+        end
+    }
 
     dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
                              $main_window,
@@ -601,7 +669,8 @@ was taken out of four photos on one row, counting the necessary overlap, the wid
 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'.
+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)")))).
@@ -685,7 +754,7 @@ def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desi
     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 your camera didn't detect the light correctly. Drag the
+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)
@@ -829,8 +898,15 @@ def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
     end
 end
 
+$max_gen_thumbnail_threads = nil
+$current_gen_thumbnail_threads = 0
+$gen_thumbnail_monitor = Monitor.new
+
 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
-    Thread.new {
+    if $max_gen_thumbnail_threads.nil?
+        $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
+    end
+    genproc = Proc.new { 
         push_mousecursor_wait
         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
         gtk_thread_protect {
@@ -839,6 +915,25 @@ def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
         }
         pop_mousecursor
     }
+    usethread = false
+    $gen_thumbnail_monitor.synchronize {
+        if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
+            $current_gen_thumbnail_threads += 1
+            usethread = true
+        end
+    }
+    if usethread
+        msg 3, "generate thumbnail from new thread"
+        Thread.new {
+            genproc.call
+            $gen_thumbnail_monitor.synchronize {
+                $current_gen_thumbnail_threads -= 1
+            }
+        }
+    else
+        msg 3, "generate thumbnail from current thread"
+        genproc.call
+    end
 end
 
 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
@@ -983,8 +1078,8 @@ def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_pr
         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"))))
+        menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
+                                                                                                                                      _("Enhance constrast"))))
     else
         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
     end
@@ -1022,9 +1117,11 @@ def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_pr
         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)
+            if check_image_editor
+                cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
+                msg 2, cmd
+                system(cmd)
+            end
         }
     end
     menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
@@ -1096,7 +1193,7 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
 
     if type == 'video'
-        pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
+        pxb = GdkPixbuf::Pixbuf.new(:file => "#{$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))
@@ -1139,10 +1236,9 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
         #- remove out of sync images
         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
         for sizeobj in $images_size
-            for file in [ "#{dest_img_base}-#{sizeobj['fullscreen']}.jpg", "#{dest_img_base}-#{sizeobj['thumbnails']}.jpg" ]
-                if File.exists?(file)
-                    File.delete(file)
-                end
+            #- cannot use sizeobj because panoramic images will have a larger width
+            Dir.glob("#{dest_img_base}-*.jpg") do |file|
+                File.delete(file)
             end
         end
 
@@ -1152,13 +1248,17 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
         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')
+        rexml_thread_protect {
+            $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])
+        rexml_thread_protect {
+            rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
+        }
     }
 
     move = proc { |direction|
@@ -1189,7 +1289,9 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     color_swap_and_cleanup = proc {
         perform_color_swap_and_cleanup = proc {
             cleanup_all_thumbnails.call
-            color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
+            rexml_thread_protect {
+                color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
+            }
             my_gen_real_thumbnail.call
         }
 
@@ -1213,7 +1315,9 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     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)
+            rexml_thread_protect {
+                change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
+            }
             my_gen_real_thumbnail.call
         }
         perform_change_seektime_and_cleanup.call(values[:new])
@@ -1234,15 +1338,19 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
 
     change_seektime_and_cleanup = proc {
-        if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
-            change_seektime_and_cleanup_real.call(values)
-        end
+        rexml_thread_protect {
+            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)
+            rexml_thread_protect {
+                change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
+            }
         }
         perform_change_pano_amount_and_cleanup.call(values[:new])
         
@@ -1262,17 +1370,21 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
 
     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
+        rexml_thread_protect {
+            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], '')
+            rexml_thread_protect {
+                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])
 
@@ -1292,18 +1404,22 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
 
     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
+        rexml_thread_protect {
+            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], '')
+            rexml_thread_protect {
+                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])
         
@@ -1323,16 +1439,20 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
     
     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
+        rexml_thread_protect {
+            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}']"], '')
+            rexml_thread_protect {
+                enhance($xmldir.elements["*[@filename='#{filename}']"], '')
+            }
             my_gen_real_thumbnail.call
         }
         
@@ -1544,7 +1664,7 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
             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}']"], '',
+                popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $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,
@@ -1652,7 +1772,9 @@ def create_auto_table
             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)
+                    if $name2widgets[path][:img].pixbuf
+                        $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
+                    end
                 end
             end
             if $selected_elements[path] && ! $selected_elements[path][:keep]
@@ -1771,7 +1893,7 @@ def save_current_file
         begin
             begin
                 ios = File.open($filename, "w")
-                $xmldoc.write(ios, 0)
+                $xmldoc.write(ios)
                 ios.close
             rescue Iconv::IllegalSequence
                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
@@ -1780,7 +1902,7 @@ def save_current_file
                 end
                 $xmldoc.xml_decl.encoding = 'UTF-8'
                 ios = File.open($filename, "w")
-                $xmldoc.write(ios, 0)
+                $xmldoc.write(ios)
                 ios.close
             end
             return true
@@ -1805,8 +1927,13 @@ def save_current_file_user
 
     msg 3, "performing actual deletion of: " + $todelete.join(', ')
     $todelete.each { |f|
-        File.delete(f)
+        begin
+            File.delete(f)
+        rescue
+            puts "Failed to delete #{f}: #{$!}"
+        end
     }
+    $todelete = []
 end
 
 def mark_document_as_dirty
@@ -1850,10 +1977,10 @@ def ask_save_modifications(msg1, msg2, *options)
                 #- already-generated markers in original file
                 if $generated_outofline
                     begin
-                        $xmldoc = REXML::Document.new File.new($orig_filename)
+                        $xmldoc = REXML::Document.new(File.new($orig_filename))
                         mark_document_as_dirty
                         ios = File.open($orig_filename, "w")
-                        $xmldoc.write(ios, 0)
+                        $xmldoc.write(ios)
                         ios.close
                     rescue Exception
                         puts "exception: #{$!}"
@@ -1863,7 +1990,6 @@ def ask_save_modifications(msg1, msg2, *options)
             if response == Gtk::Dialog::RESPONSE_CANCEL
                 ret = false
             end
-            $todelete = []  #- unconditionally clear the list of images/videos to delete
         }
     end
     return ret
@@ -1896,9 +2022,6 @@ def show_popup(parent, msg, *options)
     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)
@@ -1924,7 +2047,7 @@ def show_popup(parent, msg, *options)
         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')
+            open_url(options[0][:linkurl])
             dialog.response(Gtk::Dialog::RESPONSE_OK)
             set_mousecursor_normal
         }
@@ -1949,8 +2072,25 @@ def show_popup(parent, msg, *options)
     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 = Gtk::Window.new
+    w = create_window
     w.set_transient_for(parent)
     w.modal = true
 
@@ -1958,7 +2098,7 @@ def backend_wait_message(parent, msg, infopipe_path, mode)
     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 images and videos..."))), false, false)
+    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
@@ -1972,10 +2112,24 @@ def backend_wait_message(parent, msg, infopipe_path, mode)
     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
+        #- immediately stops if trying to read before file is written from backend.. simple dirty solution for the moment
+        sleep 1 
         while line = infopipe.gets
+            msg 3, "infopipe got data: #{line}"
             if line =~ /^directories: (\d+), sizes: (\d+)/
                 directories = $1.to_f + 1
                 sizes = $2.to_f
@@ -1991,14 +2145,23 @@ def backend_wait_message(parent, msg, infopipe_path, mode)
                     newtext = '/' if newtext == ''
                     gtk_thread_protect { pb1_2.text = newtext }
                     directories_counter += 1
-                    gtk_thread_protect { pb1_2.fraction = directories_counter / directories }
+                    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 }
+                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 }
+                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$/
@@ -2010,7 +2173,10 @@ def backend_wait_message(parent, msg, infopipe_path, mode)
                 newtext = '/' if newtext == ''
                 gtk_thread_protect { pb2.text = newtext }
                 directories_counter += 1
-                gtk_thread_protect { pb2.fraction = directories_counter / directories }
+                gtk_thread_protect {
+                    pb2.fraction = directories_counter / directories
+                    set_mainwindow_title(0.9 + pb2.fraction / 10)
+                }
             elsif line =~ /^die: (.*)$/
                 $diemsg = $1
             end
@@ -2026,6 +2192,7 @@ def backend_wait_message(parent, msg, infopipe_path, mode)
             infopipe.close
             File.delete(infopipe_path)
         end
+        set_mainwindow_title(nil)
     }
     w.window_position = Gtk::Window::POS_CENTER
     w.show_all
@@ -2035,10 +2202,11 @@ end
 
 def call_backend(cmd, waitmsg, mode, params)
     pipe = Tempfile.new("boohpipe")
+    path = pipe.path
     pipe.close!
-    system("mkfifo #{pipe.path}")
-    cmd += " --info-pipe #{pipe.path}"
-    button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
+    system("mkfifo #{path}")
+    cmd += " --info-pipe #{path}"
+    button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
     pid = nil
     Thread.new {
         msg 2, cmd
@@ -2056,7 +2224,9 @@ def call_backend(cmd, waitmsg, mode, params)
                 #- say nothing, user aborted
             else
                 gtk_thread_protect { show_popup($main_window,
-                                                utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
+                                                utf8($diemsg ? _("Unexpected internal error, sorry:\n\n%s") % $diemsg :
+                                                               _("Unexpected internal error, sorry.\nCheck console for error message."))) }
+                $diemsg = nil
             end
         else
             exec(cmd)
@@ -2153,17 +2323,19 @@ 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
+    rexml_thread_protect {
+        $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 = Gtk::Window.new
+        w = create_window
         w.set_transient_for($main_window)
         w.modal = true
         vb = Gtk::VBox.new(false, 5).set_border_width(5)
@@ -2189,6 +2361,8 @@ def sort_by_exif_date
                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
                 if ! date_time.nil?
                     dates[f] = date_time
+                elsif f =~ /(20\d{2}).?(\d{2}).?(\d{2}).(\d{2}).?(\d{2}).?(\d{2})/
+                    dates[f] = "#$1:#$2:#$3 #$4:#$5:#$6"
                 end
             end
             if aborted
@@ -2205,21 +2379,27 @@ def sort_by_exif_date
             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
             if ! date_time.nil?
                 dates[f] = date_time
+            elsif f =~ /(20\d{2}).?(\d{2}).?(\d{2}).(\d{2}).?(\d{2}).?(\d{2})/
+                dates[f] = "#$1:#$2:#$3 #$4:#$5:#$6"
             end
         }
     end
 
     saves = {}
-    $xmldir.elements.each { |element|
-        if element.name == 'image' || element.name == 'video'
-            saves[element.attributes['filename']] = element.remove
-        end
+    rexml_thread_protect {
+        $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)
+    rexml_thread_protect {
+        neworder.each { |f|
+            $xmldir.add_element(saves[f].name, saves[f].attributes)
+        }
     }
 
     #- let the auto-table reflect new ordering
@@ -2273,7 +2453,7 @@ def change_dir
     $subalbums = Gtk::Table.new(0, 0, true)
     current_y_sub_albums = 0
 
-    $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
+    $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
     $subalbums_edits = {}
     subalbums_counter = 0
     subalbums_edits_bypos = {}
@@ -2329,13 +2509,36 @@ def change_dir
             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
+                if fc.preview_filename
+                    if entry2type(fc.preview_filename) == 'video'
+                        image_path = nil
+                        tmpdir = nil
+                        begin
+                            tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
+                            if tmpdir.nil?
+                                fc.preview_widget_active = false
+                            else
+                                tmpimage = "#{tmpdir}/00000001.jpg"
+                                begin
+                                    preview_img.pixbuf = GdkPixbuf::Pixbuf.new(:file => tmpimage, :width => 240,
+                                                                               :height => 180)
+                                    fc.preview_widget_active = true
+                                rescue Gdk::PixbufError
+                                    fc.preview_widget_active = false
+                                ensure
+                                    File.delete(tmpimage)
+                                    Dir.rmdir(tmpdir)
+                                end
+                            end
+                        end
+                    else
+                        begin
+                            preview_img.pixbuf = rotate_pixbuf(GdkPixbuf::Pixbuf.new(:file => fc.preview_filename, :width => 240, :height => 180), guess_rotate(fc.preview_filename))
+                            fc.preview_widget_active = true
+                        rescue Gdk::PixbufError
+                            fc.preview_widget_active = false
+                        end
                     end
-                rescue Gdk::PixbufError
-                    fc.preview_widget_active = false
                 end
             }
             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
@@ -2592,7 +2795,7 @@ def change_dir
     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.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
@@ -2615,7 +2818,7 @@ def change_dir
             total[element.name] += 1
         end
     }
-    $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
+    $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
@@ -2639,7 +2842,7 @@ end
 
 def pixbuf_or_nil(filename)
     begin
-        return Gdk::Pixbuf.new(filename)
+        return GdkPixbuf::Pixbuf.new(:file => filename)
     rescue
         return nil
     end
@@ -2665,9 +2868,9 @@ def theme_choose(current)
         end
     }
 
-    dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
+    dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
 
-    `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
+    ([ $FPATH + '/themes/gradient' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.split("\n").find_all { |e| e !~ /\bgradient\b/ }.sort)).each { |dir|
         dir.chomp!
         iter = model.append
         iter[0] = File.basename(dir)
@@ -2678,9 +2881,9 @@ def theme_choose(current)
             treeview.selection.select_iter(iter)
         end
     }
-
-    dialog.set_default_size(700, 400)
+    dialog.set_default_size(-1, 500)
     dialog.vbox.show_all
+
     dialog.run { |response|
         iter = treeview.selection.selected
         dialog.destroy
@@ -2777,7 +2980,7 @@ def open_file(filename)
     end
 
     begin
-        $xmldoc = REXML::Document.new File.new(filename)
+        $xmldoc = REXML::Document.new(File.new(filename))
     rescue Exception
         $xmldoc = nil
     end
@@ -2786,7 +2989,7 @@ def open_file(filename)
         if entry2type(filename).nil?
             return utf8(_("Not a booh file!"))
         else
-            return utf8(_("Not a booh file!\n\nHint: you cannot import directly an image or video with File/Open.\nUse File/New to create a new album."))
+            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
 
@@ -2802,12 +3005,12 @@ def open_file(filename)
         return utf8(_("Corrupted booh file..."))
     end
 
-    if $xmldoc.root.attributes['version'] < '0.8.99.2'
+    if $xmldoc.root.attributes['version'] < $VERSION
         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|
+            `find '#{source}' -type d -follow`.split("\n").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
@@ -2840,6 +3043,7 @@ def open_file(filename)
     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(.*)/
@@ -2847,7 +3051,7 @@ def open_file(filename)
 
     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
+    $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $upload.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
     return nil
 end
 
@@ -2859,10 +3063,12 @@ def open_file_user(filename)
             $config['last-opens'] << utf8(filename)
         end
         $orig_filename = $filename
+        $main_window.title = 'booh - ' + File.basename($orig_filename)
         tmp = Tempfile.new("boohtemp")
+        $filename = tmp.path
         tmp.close!
         #- for security
-        ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
+        ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
         ios.close
         $tempfiles << $filename << "#{$filename}.backup"
     else
@@ -2885,9 +3091,40 @@ def open_file_popup
     fc.add_shortcut_folder(File.expand_path("~/.booh"))
     fc.set_current_folder(File.expand_path("~/.booh"))
     fc.transient_for = $main_window
+    fc.preview_widget = previewlabel = Gtk::Label.new.show
+    fc.signal_connect('update-preview') { |w|
+        if fc.preview_filename
+            begin
+                push_mousecursor_wait(fc)
+                xmldoc = REXML::Document.new(File.new(fc.preview_filename))
+                subalbums = 0
+                images = 0
+                videos = 0
+                xmldoc.elements.each('//*') { |elem|
+                    if elem.name == 'dir'
+                        subalbums += 1
+                    elsif elem.name == 'image'
+                        images += 1
+                    elsif elem.name == 'video'
+                        videos += 1
+                    end
+                }
+            rescue Exception
+            ensure
+                pop_mousecursor(fc)
+            end
+            if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
+                fc.preview_widget_active = false
+            else
+                previewlabel.markup = utf8(_("<i>Source:</i> %s\n<i>Destination:</i> %s\n<i>Subalbums:</i> %s\n<i>Images:</i> %s\n<i>Videos:</i> %s") %
+                                           [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
+                fc.preview_widget_active = true
+            end
+        end
+    }
     ok = false
     while !ok
-        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
             push_mousecursor_wait(fc)
             msg = open_file_user(fc.filename)
             pop_mousecursor(fc)
@@ -2909,10 +3146,139 @@ def additional_booh_options
     if $config['mproc']
         options += "--mproc #{$config['mproc'].to_i} "
     end
-    options += "--comments-format '#{$config['comments-format']}'"
+    options += "--comments-format '#{$config['comments-format']}' "
+    if $config['transcode-videos']
+        options += "--transcode-videos '#{$config['transcode-videos']}' "
+    end
+    if $config['use-mp4'] == 'true'
+        options += "--mp4-generator '#{$config['mp4-generator']}' "
+    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?")),
@@ -2926,13 +3292,13 @@ def new_album
                              [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 images/videos: "))),
+    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 images/videos down this directory:</i></span> "))),
+    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)
@@ -2952,11 +3318,11 @@ def new_album
     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))
+                         pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'gradient'), 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"))))
-    tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
+    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)
@@ -2972,45 +3338,98 @@ def new_album
         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('made with <a href=%booh>booh</a>!'), true, true, 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)
+    vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
+    vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
+    tooltips.set_tip(quotehtml, utf8(_("If checked, text using markup special characters such as '<grin>' will be shown properly; if unchecked, markup such as '<a href..' links will be interpreted by the browser properly")), nil)
 
     src_nb_calculated_for = ''
-    src_nb_thread = nil
+    src_nb_process = 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
+            if src_nb_process
+                begin
+                    Process.kill(9, src_nb_process)
+                rescue Errno::ESRCH
+                    #- process doesn't exist anymore - race condition
+                end
             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
+                        rd, wr = IO.pipe
+                        if src_nb_process
+                            while src_nb_process
+                                msg 3, "sleeping for completion of previous process"
+                                sleep 0.05
+                            end
+                            gtk_thread_flush  #- flush to avoid race condition in src_nb markup update
+                        end
+                        src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
+                        total = { 'image' => 0, 'video' => 0, nil => 0 }
+                        if src_nb_process = fork
+                            msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
+                            #- parent
+                            wr.close
+                            Thread.new {
+                                rd.readlines.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
+                                }
+                                rd.close
+                                msg 3, "ripping #{src_nb_process}"
+                                dummy, exitstatus = Process.waitpid2(src_nb_process)
+                                if exitstatus == 0
+                                    gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
                                 end
+                                src_nb_process = nil
                             }
-                            gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
-                            src_nb_thread = nil
-                        }
+                            
+                        else
+                            #- child
+                            rd.close
+                            wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
+                            Process.exit!(0)  #- _exit
+                        end                       
                     else
                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
                     end
@@ -3026,13 +3445,13 @@ def new_album
     }
 
     src_browse.signal_connect('clicked') {
-        fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
+        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
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
             src.text = utf8(fc.filename)
             process_src_nb.call
             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
@@ -3047,7 +3466,7 @@ def new_album
                                         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
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
             dest.text = utf8(fc.filename)
         end
         fc.destroy
@@ -3062,7 +3481,7 @@ def new_album
         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
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
             conf.text = utf8(fc.filename)
         end
         fc.destroy
@@ -3084,7 +3503,7 @@ def new_album
         }
         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
         tooltips = Gtk::Tooltips.new
-        tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
+        tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
         theme_sizes << { :widget => cb, :value => 'original' }
         sizes.show_all
 
@@ -3127,10 +3546,10 @@ def new_album
                 destdir = from_utf8_safe(dest.text)
                 confpath = from_utf8_safe(conf.text)
                 if src.text != '' && srcdir == ''
-                    show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
+                    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 images/videos doesn't exist. Please check your input.")))
+                    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.")))
@@ -3139,7 +3558,9 @@ def new_album
                     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. Are you sure you want to continue?")), { :okcancel => true })
+                    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.")))
@@ -3176,15 +3597,29 @@ def new_album
         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
+        $config['default-addthis'] = addthis.active?.to_s
+        $config['default-quotehtml'] = quotehtml.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('"', '&quot;').gsub('\'', '&#39;')
-        indexlink = indexlinkentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
+        madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
+        indexlink = indexlinkentry.text.gsub('\'', '&#39;')
+        athis = addthis.active?
+        qhtml = quotehtml.active?
     end
-    if src_nb_thread
-        Thread.kill(src_nb_thread)
+    if src_nb_process
+        begin
+            Process.kill(9, src_nb_process)
+            while src_nb_process
+                msg 3, "sleeping for completion of previous process"
+                sleep 0.05
+            end
+        rescue Errno::ESRCH
+            #- process doesn't exist
+        end
         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
@@ -3194,10 +3629,15 @@ def new_album
         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} " : '') +
-                     "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
+                     (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
+                     "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' " +
+                     "#{athis ? '--addthis' : ''} #{qhtml ? '--quote-html' : ''} #{additional_booh_options}",
                      utf8(_("Please wait while scanning source directory...")),
                      'full scan',
-                     { :closure_after => proc { open_file_user(configskel) } })
+                     { :closure_after => proc {
+                             open_file_user(configskel)
+                             $main_window.urgency_hint = true
+                         } })
     end
 end
 
@@ -3220,10 +3660,13 @@ def properties
     end
     madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
     indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
+    athis = !$xmldoc.root.attributes['addthis'].nil?
+    qhtml = !$xmldoc.root.attributes['quote-html'].nil?
+    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 images/videos: "))),
+    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)
@@ -3242,7 +3685,7 @@ def properties
     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 pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
+    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)
@@ -3264,6 +3707,25 @@ def properties
         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
@@ -3276,6 +3738,9 @@ def properties
         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)
+    vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active(athis))
+    vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active(qhtml))
+    tooltips.set_tip(quotehtml, utf8(_("If checked, text using markup special characters such as '<grin>' will be shown properly; if unchecked, markup such as '<a href..' links will be interpreted by the browser properly")), nil)
 
     theme_sizes = []
     nperrows = []
@@ -3300,7 +3765,7 @@ def properties
         }
         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
         tooltips = Gtk::Tooltips.new
-        tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
+        tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
         if limit_sizes && limit_sizes.include?('original')
             cb.active = true
         end
@@ -3360,26 +3825,43 @@ def properties
     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('"', '&quot;').gsub('\'', '&#39;')
-    save_indexlink = indexlinkentry.text.gsub('"', '&quot;').gsub('\'', '&#39;')
+    save_madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
+    save_indexlink = indexlinkentry.text.gsub('\'', '&#39;')
+    save_addthis = addthis.active?
+    save_quotehtml = quotehtml.active?
     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)
+    if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlink || save_multilanguages_value != multilanguages_value || save_quotehtml != qhtml || save_addthis != athis)
+        #- some sort of automatic preferences
         if save_theme != theme
-            #- some sort of automatic theme preference
             $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
+        if save_addthis != athis
+            $config['default-addthis'] = save_addthis.to_s
+        end
+        if save_quotehtml != qhtml
+            $config['default-quotehtml'] = save_quotehtml.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_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
+                     (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
+                     "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' " +
+                     "#{save_addthis ? '--addthis' : ''} #{save_quotehtml ? '--quote-html' : ''} #{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
@@ -3400,6 +3882,7 @@ def merge_current
                          open_file($filename)
                          $albums_tv.selection.select_path(sel[0])
                          $modified = true
+                         $main_window.urgency_hint = true
                      } })
 end
 
@@ -3416,6 +3899,7 @@ def merge_newsubs
                          open_file($filename)
                          $albums_tv.selection.select_path(sel[0])
                          $modified = true
+                         $main_window.urgency_hint = true
                      } })
 end
 
@@ -3434,6 +3918,7 @@ def merge
                  { :closure_after => proc {
                          open_file($filename)
                          $modified = true
+                         $main_window.urgency_hint = true
                      } })
 end
 
@@ -3447,7 +3932,7 @@ def save_as_do
     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
+    if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
         $orig_filename = fc.filename
         if ! save_current_file_user
             fc.destroy
@@ -3466,45 +3951,66 @@ def preferences
                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
 
+    table_counter = 0
     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)
+               0, 1, table_counter, table_counter + 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)
+               1, 2, table_counter, table_counter + 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)
+
+    table_counter += 1
     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)
+               0, 1, table_counter, table_counter + 1, 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)
+               1, 2, table_counter, table_counter + 1, 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)
+
+    table_counter += 1
     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)
+               0, 1, table_counter, table_counter + 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, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               1, 2, table_counter, table_counter + 1, 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)
+
+    table_counter += 1
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(mp4_check = Gtk::CheckButton.new(utf8(_("Use this .mp4 generator for videos:")))),
+               0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(mp4_generator_entry = Gtk::Entry.new.set_text($config['mp4-generator']).set_sensitive(false)),
+               1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(mp4_generator_entry, utf8(_("Use %f to specify the input filename, %o the output filename;
+for example: /usr/bin/ffmpeg -i %f -b ${i}k -ar 22050 -ab 32k %o")), nil)
+
+    table_counter += 1
     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)
+               0, 1, table_counter, table_counter + 1, 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)
+               1, 2, table_counter, table_counter + 1, 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)
+
+    table_counter += 1
     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
-               0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 2, table_counter, table_counter + 1, 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 images/videos as well"))),
-               0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
-    tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting an image 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)
 
+    table_counter += 1
+    tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
+               0, 2, table_counter, table_counter + 1, 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)
+
+    mp4_check.signal_connect('toggled') {
+        mp4_generator_entry.sensitive = mp4_check.active?
+    }
+    if $config['use-mp4'] == 'true'
+        mp4_check.active = true
+    end
     smp_check.signal_connect('toggled') {
-        if smp_check.active?
-            smp_hbox.sensitive = true
-        else
-            smp_hbox.sensitive = false
-        end
+        smp_hbox.sensitive = smp_check.active?
     }
     if $config['mproc']
         smp_check.active = true
@@ -3518,13 +4024,13 @@ for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/b
                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 \nimages in new albums:"))),
+    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 images and videos in new albums. Use this entry to use something else for images.")), nil)
+    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    
@@ -3667,11 +4173,21 @@ Where tag can be one of the following:
     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|
@@ -3679,6 +4195,12 @@ Where tag can be one of the following:
             $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 mp4_check.active?
+                $config['use-mp4'] = 'true'
+                $config['mp4-generator'] = from_utf8(mp4_generator_entry.text)
+            else
+                $config['use-mp4'] = 'false'
+            end
             if smp_check.active?
                 $config['mproc'] = smp_spin.value.to_i
             else
@@ -3690,9 +4212,16 @@ Where tag can be one of the following:
             $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
+
+    check_config_preferences_dep
 end
 
 def perform_undo
@@ -3724,6 +4253,173 @@ Click the <span foreground='darkblue'>None</span> icon when you're finished with
 ") % intro), { :pos_centered => true })
 end
 
+def perform_remote_synchronization(url, detail_label, progressbar_window, dialog, read, write)
+    begin
+        gtk_thread_protect {
+            detail_label.set_markup("<i>" + utf8(_("Logging into remote site...")) + "</i>")
+        }
+        lftp_additionals = $config['lftp-additionals']
+        if lftp_additionals
+            msg 3, "Adding lftp additionals:\n" + lftp_additionals
+            write.puts(lftp_additionals)
+        else
+            msg 3, "No lftp additionals"
+        end
+        write.puts("set net:max-retries 1")
+        write.puts("set cmd:fail-exit true")
+        write.puts("open " + url)
+        write.puts("lcd " + File.dirname($xmldoc.root.attributes['destination']))
+        write.puts("ls")  #- force connection and fail on exit, in order to detect problems with host or path
+        write.puts("echo __ls_EOF_1234567890abcdefghijk")  #- detect end
+
+        ok_to_mirror = false
+        while line = read.gets
+            msg 3, "received from lftp (login stage): #{line}"
+            if line == "__ls_EOF_1234567890abcdefghijk\n"
+                ok_to_mirror = true
+                break
+            end
+        end
+        if ! ok_to_mirror
+            gtk_thread_protect {
+                progressbar_window.destroy
+                show_popup(dialog, utf8(_("Failed to connect to specified URL, please check your input.")), { :pos_centered => true })
+            }
+            return
+        end
+
+        msg 3, "lftp login and ls ok, mirroring..."
+        gtk_thread_protect {
+            detail_label.set_markup("<i>" + utf8(_("Mirroring data...")) + "</i>")
+        }
+        mirrored_successfully = false
+        write.puts("set net:max-retries 5")
+        write.puts("mirror -R " + File.basename($xmldoc.root.attributes['destination']))
+        write.puts("echo __finished___ls_EOF_1234567890abcdefghijk")  #- detect end
+        while line = read.gets
+            msg 3, "received from lftp (mirror stage): #{line}"
+            if line == "__finished___ls_EOF_1234567890abcdefghijk\n"
+                mirrored_successfully = true
+                break
+            end
+        end
+
+        write.close
+        read.close
+
+        gtk_thread_protect {
+            progressbar_window.destroy
+            $main_window.urgency_hint = true
+            if mirrored_successfully
+                show_popup(dialog, utf8(_("Successfully mirrored into remote repository.")), { :pos_centered => true })
+            else
+                show_popup(dialog, utf8(_("Failed to mirror into remote repository.")), { :pos_centered => true })
+            end
+        }
+
+    rescue
+        msg 3, "failed lftp dialog: #{$!}"
+    end
+end
+
+def remote_synchronization
+
+    remote_synchro = Gtk::Dialog.new(utf8(_("Upload web-album")),
+                                     $main_window,
+                                     Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                                     [Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_OK])
+    remote_synchro.vbox.add(Gtk::Label.new.set_markup(utf8(_("<b>Upload web-album.</b>
+
+Mirror web-album into remote repository (lftp URL):
+<i>The destination directory '%s' will be created/updated there.</i>") % File.basename($xmldoc.root.attributes['destination']))).set_alignment(0, 0))
+    remote_synchro.vbox.add(Gtk::HBox.new(false, 0).pack_start(repo = Gtk::Entry.new.set_text($xmldoc.root.attributes['remote_synchronization_url'] || ''), true, true).
+                                                    pack_start(mirror = Gtk::Button.new(utf8(_("Upload"))).set_image(Gtk::Image.new("#{$FPATH}/images/stock-upload-16.png")), false, false))
+
+    mirror.signal_connect('clicked') {
+        if $xmldoc.root.attributes['remote_synchronization_url'] != repo.text
+            $modified = true
+            $xmldoc.root.add_attribute('remote_synchronization_url', repo.text)
+        end
+        w = create_window
+        w.set_transient_for(remote_synchro)
+        w.modal = true
+        vb = Gtk::VBox.new(false, 5).set_border_width(5)
+        vb.pack_start(Gtk::Label.new(utf8(_("Please wait, mirroring..."))), false, false)
+        vb.pack_start(detail = Gtk::Label.new.set_markup("<i>" + utf8(_("Initialization...")) + "</i>"), 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)
+        refresh_thread = Thread.new {
+            while true
+                gtk_thread_protect { pb.pulse }
+                sleep 0.5
+            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
+        }
+        w.window_position = Gtk::Window::POS_CENTER
+
+        pid = nil
+        cmd = 'lftp'
+        rd1, wr1 = IO.pipe
+        rd2, wr2 = IO.pipe
+
+        if ! pid = fork
+            rd2.close
+            wr1.close
+            $stdin.reopen(rd1)
+            $stdout.reopen(wr2)
+            $stderr.reopen(wr2)
+            begin
+                exec(cmd)
+            rescue
+                Process.exit!(66)  #- _exit
+            end
+        end
+
+        rd1.close
+        wr2.close
+
+        remote_synchronization_thread = Thread.new {
+            perform_remote_synchronization(repo.text, detail, w, remote_synchro, rd2, wr1)
+            begin
+                Process.kill('SIGTERM', pid)
+            rescue
+            end
+        }
+
+        b.signal_connect('clicked') {
+            Thread.kill(remote_synchronization_thread)
+            begin
+                Process.kill('SIGTERM', pid)
+            rescue
+                #- race condition (process just died)
+            end
+            w.destroy
+        }
+        w.show_all
+
+        Thread.new {
+            id, exitstatus = Process.waitpid2(pid)
+            if exitstatus >> 8 == 66
+                gtk_thread_protect {
+                    w.destroy
+                    show_popup(remote_synchro, utf8(_("Failed to execute 'lftp' program.")), { :pos_centered => true })
+                }
+            end
+        }
+    }
+
+    remote_synchro.window_position = Gtk::Window::POS_CENTER
+    remote_synchro.show_all
+    remote_synchro.run { remote_synchro.destroy }
+end
+
 def create_menu_and_toolbar
     
     #- menu
@@ -3738,21 +4434,26 @@ def create_menu_and_toolbar
     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 images/videos in current subalbum"))).set_sensitive(false))
+    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 images/videos in currently viewed subalbum")), nil)
+    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 images/videos"))).set_sensitive(false))
+    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 images/videos in existing subalbums (anywhere)")), nil)
+    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($extend   = Gtk::ImageMenuItem.new(utf8(_("Extend album..."))).set_sensitive(false))
+    $extend.image = Gtk::Image.new("#{$FPATH}/images/stock-scale-16.png")
     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($upload = Gtk::ImageMenuItem.new(utf8(_("Upload web-album"))).set_sensitive(false))
+    $upload.image = Gtk::Image.new("#{$FPATH}/images/stock-upload-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)
@@ -3768,14 +4469,20 @@ def create_menu_and_toolbar
     $merge_current.signal_connect('activate') { merge_current }
     $merge_newsubs.signal_connect('activate') { merge_newsubs }
     $merge.signal_connect('activate') { merge }
+    $extend.signal_connect('activate') { extend_ }
     $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 => 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['destination'],
+                     { :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
+locally 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?
@@ -3786,6 +4493,7 @@ Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
                              $redo_tb.sensitive = $redo_mb.sensitive = false
                              save_current_file
                              $generated_outofline = true
+                             $main_window.urgency_hint = true
                          }})
     }
     $view_wa.signal_connect('activate') {
@@ -3796,6 +4504,18 @@ Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
             show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
         end
     }
+    $upload.signal_connect('activate') {
+        indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
+        if File.exists?(indexhtml)
+            if !system("which lftp >/dev/null 2>/dev/null")
+                show_popup($main_window, utf8(_("The program 'lftp' is needed to upload web-albums. Please install it.")), { :pos_centered => true })
+            else
+                remote_synchronization
+            end
+        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 }
@@ -3843,7 +4563,7 @@ Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
 <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 an image, open larger view; for a video, launch player
+<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
@@ -3864,7 +4584,7 @@ for speeding up your editions. If bothered, you can disable them from Edit/Prefe
     }
 
     tutos.signal_connect('activate') {
-        open_url('http://booh.org/tutorial.html')
+        open_url('http://booh.org/tutorial')
     }
 
     about.signal_connect('activate') { call_about }
@@ -4018,23 +4738,24 @@ def gtk_thread_protect(&proc)
     if Thread.current == Thread.main
         proc.call
     else
-        $protect_gtk_pending_calls.synchronize {
+        $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 = nil
+    continue = true
+    begin
+        $gtk_pending_calls.synchronize {
+            closure = $gtk_pending_calls.shift
+            continue = $gtk_pending_calls.size > 0
+        }
+        if closure
             closure.call
         end
-        $gtk_pending_calls = []
-        $protect_gtk_pending_calls.unlock
-    end
+    end while continue
 end
 
 def ask_password_protect
@@ -4158,6 +4879,140 @@ below the Document Root), and specify this location in the password protect dial
     }
 end
 
+def extend_
+    if ! ask_save_modifications(utf8(_("Save modifications?")),
+                                utf8(_("You need to save or discard your changes before extending the album.")),
+                                { :no => Gtk::Stock::DISCARD })
+        return
+    end
+
+    #- handle discard
+    $xmldoc = REXML::Document.new(File.new($orig_filename))
+    $modified = false
+
+    dialog = Gtk::Dialog.new(utf8(_("Extend the 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(
+_("If you want this album to be part of a larger album, you can choose to
+<b>extend</b> it. This album will become a sub-album of the larger album.
+A new directory will be created in source and destination directories with
+the specified name, and everything currently in there will be moved down.
+A typical use case is a ski album, with <span foreground='darkblue'>Tignes</span> and <span foreground='darkblue'>Courchevel</span> subalbums;
+if you want to extend it to a vacations album, then you may input the
+name <span foreground='darkblue'>Ski</span>; after the album is extended, then you may create other
+directories at the same level as <span foreground='darkblue'>Ski</span> in the source directory, such as
+<span foreground='darkblue'>Summers</span> and <span foreground='darkblue'>Christmas</span> and arrange related photos/videos in there."))
+    dialog.vbox.add(lbl)
+    dialog.vbox.add(Gtk::Label.new)
+
+    new_tv = Gtk::TreeView.new.set_size_request(-1, 150)
+    new_tv.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, { :text => 0 }))
+    new_tv.set_headers_visible(false)
+    new_tv.selection.mode = Gtk::SELECTION_NONE
+    new_tv.set_model(new_ts = Gtk::TreeStore.new(String))
+    topdir = $xmldoc.elements['//dir']
+    append_dir_elem = proc { |parent_iter, xmldir|
+        child_iter = new_ts.append(parent_iter)
+        child_iter[0] = File.basename(xmldir.attributes['path'])
+        xmldir.elements.each('dir') { |elem|
+            if !elem.attributes['deleted']
+                append_dir_elem.call(child_iter, elem)
+            end
+        }
+        child_iter
+    }
+    top = new_ts.append(nil)
+    top[0] = File.basename(topdir.attributes['path'])
+    new_top = append_dir_elem.call(top, topdir)
+    new_tv.expand_all
+    new_sw = Gtk::ScrolledWindow.new(nil, nil)
+    new_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
+    new_sw.add(new_tv)
+    dialog.vbox.add(Gtk::HBox.new.pack_start(Gtk::Label.new(utf8(_("New layout: "))), false, false, 0).pack_start(new_sw, true, true, 0))
+    dialog.vbox.add(Gtk::Label.new)
+
+    dialog.vbox.add(Gtk::HBox.new.pack_start(Gtk::Label.new(utf8(_("Directory name:"))), false, false, 0).pack_start(e = Gtk::Entry.new.set_text(File.basename($xmldoc.root.attributes['source'])), true, true, 0))
+#    dialog.window_position = Gtk::Window::POS_MOUSE
+    new_top[0] = e.text
+    e.signal_connect('changed') {
+        new_top[0] = e.text
+    }
+    dialog.show_all
+    e.grab_focus
+
+    keepon = true
+    while keepon
+        dialog.run { |response|
+            perform = proc {
+                name = e.text
+                if name =~ /'/
+                    show_popup(dialog, utf8(_("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % name))
+                    return true
+                end
+                name = from_utf8(name)
+                source = from_utf8($xmldoc.root.attributes['source'])
+                dest = from_utf8($xmldoc.root.attributes['destination'])
+                if File.exists?("#{source}/#{name}")
+                    show_popup(dialog, utf8(_("%s already exists.") % "#{source}/#{name}"))
+                    return true
+                end
+                if ! FileTest.writable?(source)
+                    show_popup(dialog, utf8(_("No write access to '%s'.") % source))
+                    return true
+                end
+                if File.exists?("#{dest}/#{name}")
+                    show_popup(dialog, utf8(_("%s already exists.") % "#{dest}/#{name}"))
+                    return true
+                end
+                tomove1 = Dir.entries(source) - [ '.', '..' ]
+                tomove2 = Dir.entries(dest) - [ '.', '..' ]
+                begin
+                    Dir.mkdir("#{source}/#{name}")
+                    Dir.mkdir("#{dest}/#{name}")
+                rescue Exception
+                    show_popup(dialog, utf8(_("Erroneous name.")))
+                    puts $!
+                    return true
+                end
+                tomove1.each { |file|
+                    sys("mv '#{source}/#{file}' '#{source}/#{name}'")
+                }
+                tomove2.each { |file|
+                    sys("mv '#{dest}/#{file}' '#{dest}/#{name}'")
+                }
+
+                substmatch = /^#{Regexp.quote($xmldoc.root.attributes['source'])}/
+                substrepl = $xmldoc.root.attributes['source'] + '/' + utf8(name)
+                substelems = [ 'path', 'subdirs-captionfile', 'thumbnails-captionfile' ]
+                $xmldoc.elements.each('//dir') { |elem|
+                    substelems.each { |elname|
+                        if el = elem.attributes[elname]
+                            elem.add_attribute(elname, el.sub(substmatch, substrepl))
+                        end
+                    }
+                }
+
+                $xmldoc.root.add_element('dir', { 'path' => utf8(source) }).add_element($xmldoc.root.elements[1].remove)
+                save_current_file_user
+                open_file($orig_filename)
+                return false
+            }
+
+            if response == Gtk::Dialog::RESPONSE_OK
+                keepon = perform.call
+            else
+                keepon = false
+            end
+        }
+    end
+    dialog.destroy
+end
+
 def create_main_window
 
     mb, tb = create_menu_and_toolbar
@@ -4181,29 +5036,13 @@ def create_main_window
         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 images/videos/subalbums"))))
+            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)
@@ -4218,7 +5057,7 @@ def create_main_window
         end
     }
 
-    $albums_ts = Gtk::TreeStore.new(String, String, Gdk::Pixbuf)
+    $albums_ts = Gtk::TreeStore.new(String, String, GdkPixbuf::Pixbuf)
     $albums_tv.set_model($albums_ts)
     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
 
@@ -4260,11 +5099,14 @@ def create_main_window
     main_vbox.pack_start(paned, true, true)
     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
 
-    $main_window = Gtk::Window.new
+    $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']
@@ -4273,7 +5115,7 @@ def create_main_window
         $main_window.window_position = Gtk::Window::POS_CENTER
     end
     msg 3, "size: #{$config['width']}x#{$config['height']}"
-    $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
+    $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
@@ -4285,15 +5127,10 @@ def create_main_window
         false
     }
 
-    $protect_gtk_pending_calls = Mutex.new
     $gtk_pending_calls = []
+    $gtk_pending_calls.extend(MonitorMixin)
     Gtk.timeout_add(100) {
-        $protect_gtk_pending_calls.synchronize {
-            for closure in $gtk_pending_calls
-                closure.call
-            end
-            $gtk_pending_calls = []
-        }
+        gtk_thread_flush
         true
     }
 
@@ -4302,66 +5139,46 @@ def create_main_window
 end
 
 
-handle_options
-
-
-#- rexml sanity check (bugs in ruby 1.8.4, ruby 1.8.6)
-xmldoc = Document.new("<test/>")
-xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, "UTF-8")
-content = ['61c3a927223c3e26'].pack("H*")  #- is some UTF-8 text but just to make sure my editor won't magically convert..
-xmldoc.root.add_attribute('attr', content)
-out = []
-xmldoc.write(out, 0)
-
-xmldoc = REXML::Document.new(out.join)
-sanity1 = xmldoc.root.attributes['attr']
-out = []
-xmldoc.write(out, 0)
-
-xmldoc = REXML::Document.new(out.join)
-sanity2 = xmldoc.root.attributes['attr']
-out = []
-xmldoc.write(out, 0)
-
-msg 3, "REXML sanity outcome: sanity1=#{sanity1}, sanity2=#{sanity2}"
-
-if sanity1 != sanity2
-    puts _("REXML sanity check failed (this is normal with unpatched ruby-1.8.4 or ruby-1.8.6, which ship a broken REXML). For safeness, won't proceed.")
-    exit 1
+if str = Gtk.check_version(2, 8, 0)
+    puts "This program requires GTK+ 2.8.0 or later"
+    puts str
+    exit
 end
 
+handle_options
 
-#- rexml sanity check again (bug in ruby 1.8.6-p111)
-xmldoc = Document.new "<booh/>"
-xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, 'UTF-8')
-elem = xmldoc.root.add_element('elem')
-elem.add_text('cdata')
-tmp = Tempfile.new("sanitytemp")
-tmp.close!
-ios = File.open(sanity_filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
-xmldoc.write(ios, 0)
-ios.close
-
-xmldoc = Document.new(File.new(sanity_filename))
-File.delete(sanity_filename)
-if xmldoc.root.elements['elem'].get_text.value != 'cdata'
-    puts _("REXML sanity check failed (this is normal with unpatched ruby-1.8.6-p111, which ships a broken REXML). For safeness, won't proceed.")
-    puts "Notice for bug reporting: REXML has version: #{REXML::VERSION} at date: #{REXML::DATE}."
-    exit 1
+binding_version = Gtk::BINDING_VERSION
+msg 3, "binding version: " + binding_version.join('.')
+if binding_version == [ 0, 15, 0 ]
+    puts "It seems that we're running ruby-gtk2 0.15.0; this version is known to crash; please upgrade or downgrade."
+    exit
+end
+if binding_version == [ 0, 17, 0 ]
+    puts "It seems that we're running ruby-gtk2 0.17.0; this version is known to have a serious memory leak; please upgrade or downgrade."
+    exit
+end
+if binding_version == [ 0, 18, 0 ]
+    puts "It seems that we're running ruby-gtk2 0.18.0; this version will crash due to missing Gdk::GC; please upgrade or downgrade."
+    exit
+end
+ruby_version = RUBY_VERSION.split('.').collect { |v| v.to_i }
+if binding_version[0] <= 0 && binding_version[1] <= 16 && ruby_version[0] >= 1 && ruby_version[1] >= 8 && ruby_version[2] >= 7
+    puts "It seems that we're running ruby-gtk2 <= 0.16.0 with ruby >= 1.8.7; this combination is known to crash; please upgrade or downgrade some."
+    exit
 end
-
 
 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