[DRE-commits] r3247 - in packages: . booh booh/branches booh/branches/upstream booh/branches/upstream/current booh/branches/upstream/current/bin
lucas at alioth.debian.org
lucas at alioth.debian.org
Tue Mar 3 21:48:36 UTC 2009
Author: lucas
Date: 2009-03-03 21:48:36 +0000 (Tue, 03 Mar 2009)
New Revision: 3247
Added:
packages/booh/
packages/booh/branches/
packages/booh/branches/upstream/
packages/booh/branches/upstream/current/
packages/booh/branches/upstream/current/bin/
packages/booh/branches/upstream/current/bin/album2booh
packages/booh/branches/upstream/current/bin/booh
packages/booh/branches/upstream/current/bin/booh-backend
packages/booh/branches/upstream/current/bin/booh-classifier
packages/booh/branches/upstream/current/bin/booh-fix-whitebalance
packages/booh/branches/upstream/current/bin/booh-gamma-correction
packages/booh/branches/upstream/current/bin/webalbum2booh
Log:
[svn-inject] Installing original source of booh
Added: packages/booh/branches/upstream/current/bin/album2booh
===================================================================
--- packages/booh/branches/upstream/current/bin/album2booh (rev 0)
+++ packages/booh/branches/upstream/current/bin/album2booh 2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,170 @@
+#! /usr/bin/ruby
+#
+# * BOOH *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+# Copyright (c) 2007 Stephane Fillod
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+# 'album' structure:
+# "captions.txt"
+# <directory><tab><title>
+# <picfilename><tab><title><tab><texte>
+# <directory>".captions.txt"
+# <picfilename><tab><title><tab><texte>
+# <directory>/<picfilename>".txt"
+# <free text>
+#
+# Since Booh requires UTF-8, you may need to convert caption files:
+# $ recode ISO-8859-1..UTF-8 captions.txt
+#
+
+require 'getoptlong'
+require 'gettext'
+include GetText
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+include Booh
+
+#- options
+$options = [
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
+
+ [ '--config', '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing images and videos within directories with captions") ],
+
+ [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+]
+
+#- default values for some globals
+$switches = []
+$stdout.sync = true
+
+def usage
+ puts _("Usage: %s [OPTION]...") % File.basename($0)
+ $options.each { |ary|
+ printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
+ }
+end
+
+def handle_options
+ parser = GetoptLong.new
+ parser.set_options(*$options.collect { |ary| ary[0..2] })
+ begin
+ parser.each_option do |name, arg|
+ case name
+ when '--help'
+ usage
+ exit(0)
+
+ when '--config'
+ if File.readable?(arg)
+ $xmldoc = REXML::Document.new File.new(arg)
+ $conffile = arg
+ else
+ die_ _('Config file does not exist or is unreadable.')
+ end
+
+ when '--verbose-level'
+ $verbose_level = arg.to_i
+
+ end
+ end
+ rescue
+ puts $!
+ usage
+ exit(1)
+ end
+
+ if !$xmldoc
+ die_ _("Missing --config parameter.")
+ end
+
+ $source = $xmldoc.root.attributes['source']
+ $dest = $xmldoc.root.attributes['destination']
+end
+
+def utf8_and_entities(string)
+ return utf8(string).gsub('à', 'à').
+ gsub('ç', 'ç').
+ gsub('ô', 'ô').
+ gsub('é', 'é').
+ gsub('ê', 'ê').
+ gsub('è', 'è').
+ gsub('È', 'È').
+ gsub('î', 'î').
+ gsub('<', '<').
+ gsub('>', '>').
+ gsub('ù', 'ù').
+ gsub('"', '"')
+end
+
+def parse_album_captionstxt(filepath)
+ begin
+ contents = File.open(filepath).readlines
+ out = {}
+ out[:legends] = {}
+ for line in contents
+ if line =~ /^(.*)\t(.*)\t(.*)/
+ out[:legends][$1] = $2 + "\n" + $3
+ elsif line =~ /^(.*)\t(.*)/
+ out[:legends][$1] = $2
+ end
+ end
+ return out
+ rescue
+ return nil
+ end
+end
+
+def walk_source_dir
+
+ `find #{$source} -type d`.sort.each { |dir|
+ dir.chomp!
+ msg 2, _("Handling %s from config list...") % dir
+
+ if !infos = parse_album_captionstxt("#{dir}/captions.txt")
+ next
+ end
+
+ #- place xml document on proper node
+ xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
+
+ #if infos.has_key?(:title)
+ # type = find_subalbum_info_type(xmldir)
+ # xmldir.add_attribute("#{type}-caption", utf8_and_entities(infos[:title]))
+ #end
+
+ xmldir.elements.each { |element|
+ if %w(image video).include?(element.name)
+ if infos[:legends].has_key?(element.attributes['filename'])
+ element.add_attribute('caption', utf8_and_entities(infos[:legends][element.attributes['filename']]))
+ end
+ end
+ }
+ }
+end
+
+
+handle_options
+
+walk_source_dir
+
+ios = File.open("#{$conffile}.merged", "w")
+$xmldoc.write(ios, 0)
+ios.close
Property changes on: packages/booh/branches/upstream/current/bin/album2booh
___________________________________________________________________
Name: svn:executable
+
Added: packages/booh/branches/upstream/current/bin/booh
===================================================================
--- packages/booh/branches/upstream/current/bin/booh (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh 2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,4593 @@
+#! /usr/bin/ruby
+#
+# * BOOH *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'getoptlong'
+require 'tempfile'
+require 'thread'
+
+require 'gtk2'
+require 'booh/libadds'
+require 'booh/GtkAutoTable'
+
+require 'gettext'
+include GetText
+bindtextdomain("booh")
+
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+include Booh
+require 'booh/UndoHandler'
+require 'booh/Synchronizator'
+
+
+#- options
+$options = [
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
+ [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+ [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
+]
+
+#- default values for some globals
+$xmldir = nil
+$modified = false
+$current_cursor = nil
+$ignore_videos = false
+$button1_pressed_autotable = false
+$generated_outofline = false
+
+def usage
+ puts _("Usage: %s [OPTION]...") % File.basename($0)
+ $options.each { |ary|
+ printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
+ }
+end
+
+def handle_options
+ parser = GetoptLong.new
+ parser.set_options(*$options.collect { |ary| ary[0..2] })
+ begin
+ parser.each_option do |name, arg|
+ case name
+ when '--help'
+ usage
+ exit(0)
+
+ when '--version'
+ puts _("Booh version %s
+
+Copyright (c) 2005-2008 Guillaume Cottenceau.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
+
+ exit(0)
+
+ when '--verbose-level'
+ $verbose_level = arg.to_i
+
+ end
+ end
+ rescue
+ puts $!
+ usage
+ exit(1)
+ end
+end
+
+def read_config
+ $config = {}
+ $config_file = File.expand_path('~/.booh-gui-rc')
+ if File.readable?($config_file)
+ $xmldoc = REXML::Document.new(File.new($config_file))
+ $xmldoc.root.elements.each { |element|
+ txt = element.get_text
+ if txt
+ if txt.value =~ /~~~/ || element.name == 'last-opens'
+ $config[element.name] = txt.value.split(/~~~/)
+ else
+ $config[element.name] = txt.value
+ end
+ elsif element.elements.size == 0
+ $config[element.name] = ''
+ else
+ $config[element.name] = {}
+ element.each { |chld|
+ txt = chld.get_text
+ $config[element.name][chld.name] = txt ? txt.value : nil
+ }
+ end
+ }
+ end
+ $config['video-viewer'] ||= '/usr/bin/mplayer %f'
+ $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
+ $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
+ $config['comments-format'] ||= '%t'
+ if !FileTest.directory?(File.expand_path('~/.booh'))
+ system("mkdir ~/.booh")
+ end
+ if $config['mproc'].nil?
+ cpus = 0
+ for line in IO.readlines('/proc/cpuinfo') do
+ line =~ /^processor/ and cpus += 1
+ end
+ if cpus > 1
+ $config['mproc'] = cpus
+ end
+ end
+ $config['rotate-set-exif'] ||= 'true'
+ $tempfiles = []
+ $todelete = []
+end
+
+def check_config
+ if !system("which convert >/dev/null 2>/dev/null")
+ show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
+It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
+ exit 1
+ end
+ if !system("which identify >/dev/null 2>/dev/null")
+ show_popup($main_window, utf8(_("The program 'identify' is needed to get photos sizes and EXIF data. Please install it.
+It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
+ end
+ if !system("which exif >/dev/null 2>/dev/null")
+ show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
+ end
+ missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
+ if missing != []
+ show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
+ end
+
+ viewer_binary = $config['video-viewer'].split.first
+ if viewer_binary && !File.executable?(viewer_binary)
+ show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
+You should fix this in Edit/Preferences so that you can view videos.
+
+Problem was: '%s' is not an executable file.
+Hint: don't forget to specify the full path to the executable,
+e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
+ end
+ image_editor_binary = $config['image-editor'].split.first
+ if image_editor_binary && !File.executable?(image_editor_binary)
+ show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
+You should fix this in Edit/Preferences so that you can edit photos externally.
+
+Problem was: '%s' is not an executable file.
+Hint: don't forget to specify the full path to the executable,
+e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
+ end
+ browser_binary = $config['browser'].split.first
+ if browser_binary && !File.executable?(browser_binary)
+ show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
+You should fix this in Edit/Preferences so that you can open URLs.
+
+Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
+ end
+end
+
+def write_config
+ if $config['last-opens'] && $config['last-opens'].size > 10
+ $config['last-opens'] = $config['last-opens'][-10, 10]
+ end
+
+ $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
+ $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
+ $config.each_pair { |key, value|
+ elem = $xmldoc.root.add_element key
+ if value.is_a? Hash
+ $config[key].each_pair { |subkey, subvalue|
+ subelem = elem.add_element subkey
+ subelem.add_text subvalue.to_s
+ }
+ elsif value.is_a? Array
+ elem.add_text value.join('~~~')
+ else
+ if !value
+ elem.remove
+ else
+ elem.add_text value.to_s
+ end
+ end
+ }
+ ios = File.open($config_file, "w")
+ $xmldoc.write(ios, 0)
+ ios.close
+
+ $tempfiles.each { |f|
+ if File.exists?(f)
+ File.delete(f)
+ end
+ }
+end
+
+def set_mousecursor(what, *widget)
+ cursor = what.nil? ? nil : Gdk::Cursor.new(what)
+ if widget[0] && widget[0].window
+ widget[0].window.cursor = cursor
+ end
+ if $main_window && $main_window.window
+ $main_window.window.cursor = cursor
+ end
+ $current_cursor = what
+end
+def set_mousecursor_wait(*widget)
+ gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
+ if Thread.current == Thread.main
+ Gtk.main_iteration while Gtk.events_pending?
+ end
+end
+def set_mousecursor_normal(*widget)
+ gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
+end
+def push_mousecursor_wait(*widget)
+ if $current_cursor != Gdk::Cursor::WATCH
+ $save_cursor = $current_cursor
+ gtk_thread_protect { set_mousecursor_wait(*widget) }
+ end
+end
+def pop_mousecursor(*widget)
+ gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
+end
+
+def current_dest_dir
+ source = $xmldoc.root.attributes['source']
+ dest = $xmldoc.root.attributes['destination']
+ return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
+end
+
+def full_src_dir_to_rel(path, source)
+ return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
+end
+
+def build_full_dest_filename(filename)
+ return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
+end
+
+def save_undo(name, closure, *params)
+ UndoHandler.save_undo(name, closure, [ *params ])
+ $undo_tb.sensitive = $undo_mb.sensitive = true
+ $redo_tb.sensitive = $redo_mb.sensitive = false
+end
+
+def view_element(filename, closures)
+ if entry2type(filename) == 'video'
+ cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
+ msg 2, cmd
+ system(cmd)
+ return
+ end
+
+ w = create_window.set_title(filename)
+
+ msg 3, "filename: #{filename}"
+ dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
+ #- typically this file won't exist in case of videos; try with the largest thumbnail around
+ if !File.exists?(dest_img)
+ if entry2type(filename) == 'video'
+ alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
+ if not alternatives.empty?
+ dest_img = alternatives[-1]
+ end
+ else
+ push_mousecursor_wait
+ gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
+ pop_mousecursor
+ if !File.exists?(dest_img)
+ msg 2, _("Could not generate fullscreen thumbnail!")
+ return
+ end
+ end
+ end
+ evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
+ evt.signal_connect('button-press-event') { |this, event|
+ if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
+ $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
+ end
+ if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
+ menu = Gtk::Menu.new
+ menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
+ delete_item.signal_connect('activate') {
+ w.destroy
+ closures[:delete].call(false)
+ }
+ menu.show_all
+ menu.popup(nil, nil, event.button, event.time)
+ end
+ }
+ evt.signal_connect('button-release-event') { |this, event|
+ if $gesture_press
+ if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
+ msg 3, "gesture delete: click-drag right button to the bottom"
+ w.destroy
+ closures[:delete].call(false)
+ $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
+ end
+ end
+ }
+ tooltips = Gtk::Tooltips.new
+ tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
+
+ w.signal_connect('key-press-event') { |w,event|
+ if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
+ w.destroy
+ closures[:delete].call(false)
+ end
+ }
+
+ bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
+ b.signal_connect('clicked') { w.destroy }
+
+ vb = Gtk::VBox.new
+ vb.pack_start(evt, false, false)
+ vb.pack_end(bottom, false, false)
+
+ w.add(vb)
+ w.signal_connect('delete-event') { w.destroy }
+ w.window_position = Gtk::Window::POS_CENTER
+ w.show_all
+end
+
+def scroll_upper(scrolledwindow, ypos_top)
+ newval = scrolledwindow.vadjustment.value -
+ ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
+ if newval < scrolledwindow.vadjustment.lower
+ newval = scrolledwindow.vadjustment.lower
+ end
+ scrolledwindow.vadjustment.value = newval
+end
+
+def scroll_lower(scrolledwindow, ypos_bottom)
+ newval = scrolledwindow.vadjustment.value +
+ ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
+ if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
+ newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
+ end
+ scrolledwindow.vadjustment.value = newval
+end
+
+def autoscroll_if_needed(scrolledwindow, image, textview)
+ #- autoscroll if cursor or image is not visible, if possible
+ if image && image.window || textview.window
+ ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
+ ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
+ current_miny_visible = scrolledwindow.vadjustment.value
+ current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
+ if ypos_top < current_miny_visible
+ scroll_upper(scrolledwindow, ypos_top)
+ elsif ypos_bottom > current_maxy_visible
+ scroll_lower(scrolledwindow, ypos_bottom)
+ end
+ end
+end
+
+def create_editzone(scrolledwindow, pagenum, image)
+ frame = Gtk::Frame.new
+ frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
+ frame.set_shadow_type(Gtk::SHADOW_IN)
+ textview.signal_connect('key-press-event') { |w, event|
+ textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
+ if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
+ scrolledwindow.signal_emit('key-press-event', event)
+ end
+ if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
+ event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
+ if event.keyval == Gdk::Keyval::GDK_Up
+ if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
+ scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
+ else
+ scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
+ end
+ else
+ if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
+ scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
+ else
+ scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
+ end
+ end
+ end
+ false #- propagate
+ }
+
+ candidate_undo_text = nil
+ textview.signal_connect('focus-in-event') { |w, event|
+ textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
+ candidate_undo_text = textview.buffer.text
+ false #- propagate
+ }
+
+ textview.signal_connect('key-release-event') { |w, event|
+ if candidate_undo_text && candidate_undo_text != textview.buffer.text
+ $modified = true
+ save_undo(_("text edit"),
+ proc { |text|
+ save_text = textview.buffer.text
+ textview.buffer.text = text
+ textview.grab_focus
+ $notebook.set_page(pagenum)
+ proc {
+ textview.buffer.text = save_text
+ textview.grab_focus
+ $notebook.set_page(pagenum)
+ }
+ }, candidate_undo_text)
+ candidate_undo_text = nil
+ end
+
+ if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
+ autoscroll_if_needed(scrolledwindow, image, textview)
+ end
+ false #- propagate
+ }
+
+ return [ frame, textview ]
+end
+
+def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
+
+ if !$modified_pixbufs[thumbnail_img]
+ $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
+ elsif !$modified_pixbufs[thumbnail_img][:orig]
+ $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
+ end
+
+ pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
+
+ #- rotate
+ if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
+ pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
+ msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
+ if pixbuf.height > desired_y
+ pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
+ elsif pixbuf.width < desired_x && pixbuf.height < desired_y
+ pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
+ end
+ end
+
+ #- fix white balance
+ if $modified_pixbufs[thumbnail_img][:whitebalance]
+ pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
+ end
+
+ #- fix gamma correction
+ if $modified_pixbufs[thumbnail_img][:gammacorrect]
+ pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
+ end
+
+ img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
+end
+
+def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+ $modified = true
+
+ #- update rotate attribute
+ new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
+ xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
+
+ if $config['rotate-set-exif'] == 'true'
+ Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
+ end
+
+ $modified_pixbufs[thumbnail_img] ||= {}
+ $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
+ msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
+
+ update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
+end
+
+def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+ $modified = true
+
+ rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+
+ save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
+ proc { |angle|
+ rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+ $notebook.set_page(attributes_prefix != '' ? 0 : 1)
+ proc {
+ rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
+ $notebook.set_page(0)
+ $notebook.set_page(attributes_prefix != '' ? 0 : 1)
+ }
+ }, -angle)
+end
+
+def color_swap(xmldir, attributes_prefix)
+ $modified = true
+ if xmldir.attributes["#{attributes_prefix}color-swap"]
+ xmldir.delete_attribute("#{attributes_prefix}color-swap")
+ else
+ xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
+ end
+end
+
+def enhance(xmldir, attributes_prefix)
+ $modified = true
+ if xmldir.attributes["#{attributes_prefix}enhance"]
+ xmldir.delete_attribute("#{attributes_prefix}enhance")
+ else
+ xmldir.add_attribute("#{attributes_prefix}enhance", '1')
+ end
+end
+
+def change_seektime(xmldir, attributes_prefix, value)
+ $modified = true
+ xmldir.add_attribute("#{attributes_prefix}seektime", value)
+end
+
+def ask_new_seektime(xmldir, attributes_prefix)
+ if xmldir
+ value = xmldir.attributes["#{attributes_prefix}seektime"]
+ else
+ value = ''
+ end
+
+ dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ lbl = Gtk::Label.new
+ lbl.markup = utf8(
+_("Please specify the <b>seek time</b> of the video, to take the thumbnail
+from, in seconds.
+"))
+ dialog.vbox.add(lbl)
+ dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
+ entry.signal_connect('key-press-event') { |w, event|
+ if event.keyval == Gdk::Keyval::GDK_Return
+ dialog.response(Gtk::Dialog::RESPONSE_OK)
+ true
+ elsif event.keyval == Gdk::Keyval::GDK_Escape
+ dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
+ true
+ else
+ false #- propagate if needed
+ end
+ }
+
+ dialog.window_position = Gtk::Window::POS_MOUSE
+ dialog.show_all
+
+ dialog.run { |response|
+ newval = entry.text
+ dialog.destroy
+ if response == Gtk::Dialog::RESPONSE_OK
+ $modified = true
+ msg 3, "changing seektime to #{newval}"
+ return { :old => value, :new => newval }
+ else
+ return nil
+ end
+ }
+end
+
+def change_pano_amount(xmldir, attributes_prefix, value)
+ $modified = true
+ if value.nil?
+ xmldir.delete_attribute("#{attributes_prefix}pano-amount")
+ else
+ xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
+ end
+end
+
+def ask_new_pano_amount(xmldir, attributes_prefix)
+ if xmldir
+ value = xmldir.attributes["#{attributes_prefix}pano-amount"]
+ else
+ value = nil
+ end
+
+ dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ lbl = Gtk::Label.new
+ lbl.markup = utf8(
+_("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
+of this panorama image compared to other regular images. For example, if the panorama
+was taken out of four photos on one row, counting the necessary overlap, the width of
+this panorama image should probably be roughly three times the width of regular images.
+
+With this information, booh will be able to generate panorama thumbnails looking
+the right 'size', since the height of the thumbnail for this image will be similar
+to the height of other thumbnails.
+"))
+ dialog.vbox.add(lbl)
+ dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
+ add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
+ add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
+ add(Gtk::Label.new(utf8(_("times the width of other images"))))))
+ spin.signal_connect('value-changed') {
+ rb_yes.active = true
+ }
+ dialog.window_position = Gtk::Window::POS_MOUSE
+ dialog.show_all
+ if value
+ spin.value = value.to_f
+ rb_yes.active = true
+ spin.grab_focus
+ else
+ rb_no.active = true
+ end
+
+ dialog.run { |response|
+ if rb_no.active?
+ newval = nil
+ else
+ newval = spin.value.to_f
+ end
+ dialog.destroy
+ if response == Gtk::Dialog::RESPONSE_OK
+ $modified = true
+ msg 3, "changing panorama amount to #{newval}"
+ return { :old => value, :new => newval }
+ else
+ return nil
+ end
+ }
+end
+
+def change_whitebalance(xmlelem, attributes_prefix, value)
+ $modified = true
+ xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
+end
+
+def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+
+ #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
+ if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
+ save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
+ xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
+ save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
+ xmlelem.delete_attribute("#{attributes_prefix}white-balance")
+ destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
+ gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
+ xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
+ $modified_pixbufs[thumbnail_img] ||= {}
+ $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
+ xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
+ if save_gammacorrect
+ xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
+ $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
+ end
+ $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
+ end
+
+ $modified_pixbufs[thumbnail_img] ||= {}
+ $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
+
+ update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
+end
+
+def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+ #- init $modified_pixbufs correctly
+# update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
+
+ value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
+
+ dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ lbl = Gtk::Label.new
+ lbl.markup = utf8(
+_("You can fix the <b>white balance</b> of the image, if your image is too blue
+or too yellow because the recorder didn't detect the light correctly. Drag the
+slider below the image to the left for more blue, to the right for more yellow.
+"))
+ dialog.vbox.add(lbl)
+ if img_
+ dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
+ end
+ dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
+
+ dialog.window_position = Gtk::Window::POS_MOUSE
+ dialog.show_all
+
+ lastval = nil
+ timeout = Gtk.timeout_add(100) {
+ if hs.value != lastval
+ lastval = hs.value
+ if img_
+ recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+ end
+ end
+ true
+ }
+
+ dialog.run { |response|
+ Gtk.timeout_remove(timeout)
+ if response == Gtk::Dialog::RESPONSE_OK
+ $modified = true
+ newval = hs.value.to_s
+ msg 3, "changing white balance to #{newval}"
+ dialog.destroy
+ return { :old => value, :new => newval }
+ else
+ if thumbnail_img
+ $modified_pixbufs[thumbnail_img] ||= {}
+ $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
+ $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
+ end
+ dialog.destroy
+ return nil
+ end
+ }
+end
+
+def change_gammacorrect(xmlelem, attributes_prefix, value)
+ $modified = true
+ xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
+end
+
+def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+
+ #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
+ if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
+ save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
+ xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
+ save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
+ xmlelem.delete_attribute("#{attributes_prefix}white-balance")
+ destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
+ gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
+ xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
+ $modified_pixbufs[thumbnail_img] ||= {}
+ $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
+ xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
+ if save_whitebalance
+ xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
+ $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
+ end
+ $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
+ end
+
+ $modified_pixbufs[thumbnail_img] ||= {}
+ $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
+
+ update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
+end
+
+def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+ #- init $modified_pixbufs correctly
+# update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
+
+ value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
+
+ dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ lbl = Gtk::Label.new
+ lbl.markup = utf8(
+_("You can perform <b>gamma correction</b> of the image, if your image is too dark
+or too bright. Drag the slider below the image.
+"))
+ dialog.vbox.add(lbl)
+ if img_
+ dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
+ end
+ dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
+
+ dialog.window_position = Gtk::Window::POS_MOUSE
+ dialog.show_all
+
+ lastval = nil
+ timeout = Gtk.timeout_add(100) {
+ if hs.value != lastval
+ lastval = hs.value
+ if img_
+ recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
+ end
+ end
+ true
+ }
+
+ dialog.run { |response|
+ Gtk.timeout_remove(timeout)
+ if response == Gtk::Dialog::RESPONSE_OK
+ $modified = true
+ newval = hs.value.to_s
+ msg 3, "gamma correction to #{newval}"
+ dialog.destroy
+ return { :old => value, :new => newval }
+ else
+ if thumbnail_img
+ $modified_pixbufs[thumbnail_img] ||= {}
+ $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
+ $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
+ end
+ dialog.destroy
+ return nil
+ end
+ }
+end
+
+def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
+ if File.exists?(destfile)
+ File.delete(destfile)
+ end
+ #- type can be 'element' or 'subdir'
+ if type == 'element'
+ gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
+ else
+ gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
+ end
+end
+
+def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
+ Thread.new {
+ push_mousecursor_wait
+ gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
+ gtk_thread_protect {
+ img.set(destfile)
+ $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
+ }
+ pop_mousecursor
+ }
+end
+
+def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
+ distribute_multiple_call = Proc.new { |action, arg|
+ $selected_elements.each_key { |path|
+ $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
+ }
+ if possible_actions[:can_multiple] && $selected_elements.length > 0
+ UndoHandler.begin_batch
+ $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
+ UndoHandler.end_batch
+ else
+ closures[action].call(arg)
+ end
+ $selected_elements = {}
+ }
+ menu = Gtk::Menu.new
+ if optionals.include?('change_image')
+ menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
+ changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
+ changeimg.signal_connect('activate') { closures[:change].call }
+ menu.append(Gtk::SeparatorMenuItem.new)
+ end
+ if !possible_actions[:can_multiple] || $selected_elements.length == 0
+ if closures[:view]
+ if type == 'image'
+ menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
+ view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
+ view.signal_connect('activate') { closures[:view].call }
+ else
+ menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
+ view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
+ view.signal_connect('activate') { closures[:view].call }
+ menu.append(Gtk::SeparatorMenuItem.new)
+ end
+ end
+ if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
+ menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
+ exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
+ exif.signal_connect('activate') { show_popup($main_window,
+ utf8(`exif -m '#{fullpath}'`),
+ { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
+ menu.append(Gtk::SeparatorMenuItem.new)
+ end
+ end
+ menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
+ r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
+ r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
+ menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
+ r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
+ r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
+ if !possible_actions[:can_multiple] || $selected_elements.length == 0
+ menu.append(Gtk::SeparatorMenuItem.new)
+ if !possible_actions[:forbid_left]
+ menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
+ moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
+ moveleft.signal_connect('activate') { closures[:move].call('left') }
+ if !possible_actions[:can_left]
+ moveleft.sensitive = false
+ end
+ end
+ if !possible_actions[:forbid_right]
+ menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
+ moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
+ moveright.signal_connect('activate') { closures[:move].call('right') }
+ if !possible_actions[:can_right]
+ moveright.sensitive = false
+ end
+ end
+ if optionals.include?('move_top')
+ menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
+ movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
+ movetop.signal_connect('activate') { closures[:move].call('top') }
+ if !possible_actions[:can_top]
+ movetop.sensitive = false
+ end
+ end
+ menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
+ moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
+ moveup.signal_connect('activate') { closures[:move].call('up') }
+ if !possible_actions[:can_up]
+ moveup.sensitive = false
+ end
+ menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
+ movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
+ movedown.signal_connect('activate') { closures[:move].call('down') }
+ if !possible_actions[:can_down]
+ movedown.sensitive = false
+ end
+ if optionals.include?('move_bottom')
+ menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
+ movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
+ movebottom.signal_connect('activate') { closures[:move].call('bottom') }
+ if !possible_actions[:can_bottom]
+ movebottom.sensitive = false
+ end
+ end
+ end
+ if type == 'video'
+ if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
+ menu.append(Gtk::SeparatorMenuItem.new)
+# menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
+# color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
+# color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
+ menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
+ flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
+ flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
+ menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
+ seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
+ seektime.signal_connect('activate') {
+ if possible_actions[:can_multiple] && $selected_elements.length > 0
+ if values = ask_new_seektime(nil, '')
+ distribute_multiple_call.call(:seektime, values)
+ end
+ else
+ closures[:seektime].call
+ end
+ }
+ end
+ end
+ menu.append( Gtk::SeparatorMenuItem.new)
+ menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
+ gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
+ gammacorrect.signal_connect('activate') {
+ if possible_actions[:can_multiple] && $selected_elements.length > 0
+ if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
+ distribute_multiple_call.call(:gammacorrect, values)
+ end
+ else
+ closures[:gammacorrect].call
+ end
+ }
+ menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
+ whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
+ whitebalance.signal_connect('activate') {
+ if possible_actions[:can_multiple] && $selected_elements.length > 0
+ if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
+ distribute_multiple_call.call(:whitebalance, values)
+ end
+ else
+ closures[:whitebalance].call
+ end
+ }
+ if !possible_actions[:can_multiple] || $selected_elements.length == 0
+ menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
+ _("Enhance constrast"))))
+ else
+ menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
+ end
+ enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
+ enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
+ if type == 'image' && possible_actions[:can_panorama]
+ menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
+ panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
+ panorama.signal_connect('activate') {
+ if possible_actions[:can_multiple] && $selected_elements.length > 0
+ if values = ask_new_pano_amount(nil, '')
+ distribute_multiple_call.call(:pano, values)
+ end
+ else
+ distribute_multiple_call.call(:pano)
+ end
+ }
+ end
+ menu.append( Gtk::SeparatorMenuItem.new)
+ if optionals.include?('delete')
+ menu.append(cut_item = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
+ cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
+ if !possible_actions[:can_multiple] || $selected_elements.length == 0
+ menu.append(paste_item = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
+ paste_item.signal_connect('activate') { closures[:paste].call }
+ menu.append(clear_item = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
+ clear_item.signal_connect('activate') { $cuts = [] }
+ if $cuts.size == 0
+ paste_item.sensitive = clear_item.sensitive = false
+ end
+ end
+ menu.append( Gtk::SeparatorMenuItem.new)
+ end
+ if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
+ menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
+ editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
+ editexternally.signal_connect('activate') {
+ cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
+ msg 2, cmd
+ system(cmd)
+ }
+ end
+ menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
+ refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
+ if optionals.include?('delete')
+ menu.append(delete_item = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
+ delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
+ end
+ menu.show_all
+ menu.popup(nil, nil, event.button, event.time)
+end
+
+def delete_current_subalbum
+ $modified = true
+ sel = $albums_tv.selection.selected_rows
+ $xmldir.elements.each { |e|
+ if e.name == 'image' || e.name == 'video'
+ e.add_attribute('deleted', 'true')
+ end
+ }
+ #- branch if we have a non deleted subalbum
+ if $xmldir.child_byname_notattr('dir', 'deleted')
+ $xmldir.delete_attribute('thumbnails-caption')
+ $xmldir.delete_attribute('thumbnails-captionfile')
+ else
+ $xmldir.add_attribute('deleted', 'true')
+ moveup = $xmldir
+ while moveup.parent.name == 'dir'
+ moveup = moveup.parent
+ if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
+ moveup.add_attribute('deleted', 'true')
+ else
+ break
+ end
+ end
+ sel[0].up!
+ end
+ save_changes('forced')
+ populate_subalbums_treeview(false)
+ $albums_tv.selection.select_path(sel[0])
+end
+
+def restore_deleted
+ $modified = true
+ save_changes
+ $current_path = nil #- prevent save_changes from being rerun again
+ sel = $albums_tv.selection.selected_rows
+ restore_one = proc { |xmldir|
+ xmldir.elements.each { |e|
+ if e.name == 'dir' && e.attributes['deleted']
+ restore_one.call(e)
+ end
+ e.delete_attribute('deleted')
+ }
+ }
+ restore_one.call($xmldir)
+ populate_subalbums_treeview(false)
+ $albums_tv.selection.select_path(sel[0])
+end
+
+def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
+
+ img = nil
+ frame1 = Gtk::Frame.new
+ fullpath = from_utf8("#{$current_path}/#{filename}")
+
+ my_gen_real_thumbnail = proc {
+ gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
+ }
+
+ if type == 'video'
+ pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
+ frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
+ pack_start(img = Gtk::Image.new).
+ pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
+ px, mask = pxb.render_pixmap_and_mask(0)
+ da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
+ da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
+ else
+ frame1.add(img = Gtk::Image.new)
+ end
+
+ #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
+ if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
+ my_gen_real_thumbnail.call
+ else
+ img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
+ end
+
+ evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
+
+ tooltips = Gtk::Tooltips.new
+ tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
+ tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
+
+ frame2, textview = create_editzone($autotable_sw, 1, img)
+ textview.buffer.text = caption
+ textview.set_justification(Gtk::Justification::CENTER)
+
+ vbox = Gtk::VBox.new(false, 5)
+ vbox.pack_start(evtbox, false, false)
+ vbox.pack_start(frame2, false, false)
+ autotable.append(vbox, filename)
+
+ #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
+ $vbox2widgets[vbox] = { :textview => textview, :image => img }
+
+ #- to be able to find widgets by name
+ $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
+
+ cleanup_all_thumbnails = proc {
+ #- remove out of sync images
+ dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
+ for sizeobj in $images_size
+ #- cannot use sizeobj because panoramic images will have a larger width
+ Dir.glob("#{dest_img_base}-*.jpg") do |file|
+ File.delete(file)
+ end
+ end
+
+ }
+
+ refresh = proc {
+ cleanup_all_thumbnails.call
+ #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
+ $modified = true
+ $xmldir.delete_attribute('already-generated')
+ my_gen_real_thumbnail.call
+ }
+
+ rotate_and_cleanup = proc { |angle|
+ cleanup_all_thumbnails.call
+ rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
+ }
+
+ move = proc { |direction|
+ do_method = "move_#{direction}"
+ undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
+ perform = proc {
+ done = autotable.method(do_method).call(vbox)
+ textview.grab_focus #- because if moving, focus is stolen
+ done
+ }
+ if perform.call
+ save_undo(_("move %s") % direction,
+ proc {
+ autotable.method(undo_method).call(vbox)
+ textview.grab_focus #- because if moving, focus is stolen
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ proc {
+ autotable.method(do_method).call(vbox)
+ textview.grab_focus #- because if moving, focus is stolen
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ }
+ })
+ end
+ }
+
+ color_swap_and_cleanup = proc {
+ perform_color_swap_and_cleanup = proc {
+ cleanup_all_thumbnails.call
+ color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
+ my_gen_real_thumbnail.call
+ }
+
+ perform_color_swap_and_cleanup.call
+
+ save_undo(_("color swap"),
+ proc {
+ perform_color_swap_and_cleanup.call
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ proc {
+ perform_color_swap_and_cleanup.call
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ }
+ })
+ }
+
+ change_seektime_and_cleanup_real = proc { |values|
+ perform_change_seektime_and_cleanup = proc { |val|
+ cleanup_all_thumbnails.call
+ change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
+ my_gen_real_thumbnail.call
+ }
+ perform_change_seektime_and_cleanup.call(values[:new])
+
+ save_undo(_("specify seektime"),
+ proc {
+ perform_change_seektime_and_cleanup.call(values[:old])
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ proc {
+ perform_change_seektime_and_cleanup.call(values[:new])
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ }
+ })
+ }
+
+ change_seektime_and_cleanup = proc {
+ if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
+ change_seektime_and_cleanup_real.call(values)
+ end
+ }
+
+ change_pano_amount_and_cleanup_real = proc { |values|
+ perform_change_pano_amount_and_cleanup = proc { |val|
+ cleanup_all_thumbnails.call
+ change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
+ }
+ perform_change_pano_amount_and_cleanup.call(values[:new])
+
+ save_undo(_("change panorama amount"),
+ proc {
+ perform_change_pano_amount_and_cleanup.call(values[:old])
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ proc {
+ perform_change_pano_amount_and_cleanup.call(values[:new])
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ }
+ })
+ }
+
+ change_pano_amount_and_cleanup = proc {
+ if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
+ change_pano_amount_and_cleanup_real.call(values)
+ end
+ }
+
+ whitebalance_and_cleanup_real = proc { |values|
+ perform_change_whitebalance_and_cleanup = proc { |val|
+ cleanup_all_thumbnails.call
+ change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
+ recalc_whitebalance(val, fullpath, thumbnail_img, img,
+ $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+ }
+ perform_change_whitebalance_and_cleanup.call(values[:new])
+
+ save_undo(_("fix white balance"),
+ proc {
+ perform_change_whitebalance_and_cleanup.call(values[:old])
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ proc {
+ perform_change_whitebalance_and_cleanup.call(values[:new])
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ }
+ })
+ }
+
+ whitebalance_and_cleanup = proc {
+ if values = ask_whitebalance(fullpath, thumbnail_img, img,
+ $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+ whitebalance_and_cleanup_real.call(values)
+ end
+ }
+
+ gammacorrect_and_cleanup_real = proc { |values|
+ perform_change_gammacorrect_and_cleanup = Proc.new { |val|
+ cleanup_all_thumbnails.call
+ change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
+ recalc_gammacorrect(val, fullpath, thumbnail_img, img,
+ $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+ }
+ perform_change_gammacorrect_and_cleanup.call(values[:new])
+
+ save_undo(_("gamma correction"),
+ Proc.new {
+ perform_change_gammacorrect_and_cleanup.call(values[:old])
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ Proc.new {
+ perform_change_gammacorrect_and_cleanup.call(values[:new])
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ }
+ })
+ }
+
+ gammacorrect_and_cleanup = Proc.new {
+ if values = ask_gammacorrect(fullpath, thumbnail_img, img,
+ $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+ gammacorrect_and_cleanup_real.call(values)
+ end
+ }
+
+ enhance_and_cleanup = proc {
+ perform_enhance_and_cleanup = proc {
+ cleanup_all_thumbnails.call
+ enhance($xmldir.elements["*[@filename='#{filename}']"], '')
+ my_gen_real_thumbnail.call
+ }
+
+ cleanup_all_thumbnails.call
+ perform_enhance_and_cleanup.call
+
+ save_undo(_("enhance"),
+ proc {
+ perform_enhance_and_cleanup.call
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ proc {
+ perform_enhance_and_cleanup.call
+ textview.grab_focus
+ autoscroll_if_needed($autotable_sw, img, textview)
+ $notebook.set_page(1)
+ }
+ })
+ }
+
+ delete = proc { |isacut|
+ if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
+ $modified = true
+ after = nil
+ perform_delete = proc {
+ after = autotable.get_next_widget(vbox)
+ if !after
+ after = autotable.get_previous_widget(vbox)
+ end
+ if $config['deleteondisk'] && !isacut
+ msg 3, "scheduling for delete: #{fullpath}"
+ $todelete << fullpath
+ end
+ autotable.remove_widget(vbox)
+ if after
+ $vbox2widgets[after][:textview].grab_focus
+ autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
+ end
+ }
+
+ previous_pos = autotable.get_current_number(vbox)
+ perform_delete.call
+
+ if !after
+ delete_current_subalbum
+ else
+ save_undo(_("delete"),
+ proc { |pos|
+ autotable.reinsert(pos, vbox, filename)
+ $notebook.set_page(1)
+ autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
+ $cuts = []
+ msg 3, "removing deletion schedule of: #{fullpath}"
+ $todelete.delete(fullpath) #- unconditional because deleteondisk option could have been modified
+ proc {
+ perform_delete.call
+ $notebook.set_page(1)
+ }
+ }, previous_pos)
+ end
+ end
+ }
+
+ cut = proc {
+ delete.call(true)
+ $cuts << { :vbox => vbox, :filename => filename }
+ $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
+ }
+ paste = proc {
+ if $cuts.size > 0
+ $cuts.each { |elem|
+ autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
+ }
+ last = $cuts[-1]
+ autotable.queue_draws << proc {
+ $vbox2widgets[last[:vbox]][:textview].grab_focus
+ autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
+ }
+ save_undo(_("paste"),
+ proc { |cuts|
+ cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
+ $notebook.set_page(1)
+ proc {
+ cuts.each { |elem|
+ autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
+ }
+ $notebook.set_page(1)
+ }
+ }, $cuts)
+ $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
+ $cuts = []
+ end
+ }
+
+ $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
+ :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
+ :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
+ :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
+
+ textview.signal_connect('key-press-event') { |w, event|
+ propagate = true
+ if event.state != 0
+ x, y = autotable.get_current_pos(vbox)
+ control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
+ shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
+ alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
+ if event.keyval == Gdk::Keyval::GDK_Up && y > 0
+ if control_pressed
+ if widget_up = autotable.get_widget_at_pos(x, y - 1)
+ $vbox2widgets[widget_up][:textview].grab_focus
+ end
+ end
+ if shift_pressed
+ move.call('up')
+ end
+ end
+ if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
+ if control_pressed
+ if widget_down = autotable.get_widget_at_pos(x, y + 1)
+ $vbox2widgets[widget_down][:textview].grab_focus
+ end
+ end
+ if shift_pressed
+ move.call('down')
+ end
+ end
+ if event.keyval == Gdk::Keyval::GDK_Left
+ if x > 0
+ if control_pressed
+ $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
+ end
+ if shift_pressed
+ move.call('left')
+ end
+ end
+ if alt_pressed
+ rotate_and_cleanup.call(-90)
+ end
+ end
+ if event.keyval == Gdk::Keyval::GDK_Right
+ next_ = autotable.get_next_widget(vbox)
+ if next_ && autotable.get_current_pos(next_)[0] > x
+ if control_pressed
+ $vbox2widgets[next_][:textview].grab_focus
+ end
+ if shift_pressed
+ move.call('right')
+ end
+ end
+ if alt_pressed
+ rotate_and_cleanup.call(90)
+ end
+ end
+ if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
+ delete.call(false)
+ end
+ if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
+ view_element(filename, { :delete => delete })
+ propagate = false
+ end
+ if event.keyval == Gdk::Keyval::GDK_z && control_pressed
+ perform_undo
+ end
+ if event.keyval == Gdk::Keyval::GDK_r && control_pressed
+ perform_redo
+ end
+ end
+ !propagate #- propagate if needed
+ }
+
+ $ignore_next_release = false
+ evtbox.signal_connect('button-press-event') { |w, event|
+ if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
+ if event.state & Gdk::Window::BUTTON3_MASK != 0
+ #- gesture redo: hold right mouse button then click left mouse button
+ $config['nogestures'] or perform_redo
+ $ignore_next_release = true
+ else
+ shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
+ if $r90.active?
+ rotate_and_cleanup.call(shift_or_control ? -90 : 90)
+ elsif $r270.active?
+ rotate_and_cleanup.call(shift_or_control ? 90 : -90)
+ elsif $enhance.active?
+ enhance_and_cleanup.call
+ elsif $delete.active?
+ delete.call(false)
+ else
+ textview.grab_focus
+ $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
+ end
+ end
+ $button1_pressed_autotable = true
+ elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
+ if event.state & Gdk::Window::BUTTON1_MASK != 0
+ #- gesture undo: hold left mouse button then click right mouse button
+ $config['nogestures'] or perform_undo
+ $ignore_next_release = true
+ end
+ elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
+ view_element(filename, { :delete => delete })
+ end
+ false #- propagate
+ }
+
+ evtbox.signal_connect('button-release-event') { |w, event|
+ if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
+ if !$ignore_next_release
+ x, y = autotable.get_current_pos(vbox)
+ next_ = autotable.get_next_widget(vbox)
+ popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
+ { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
+ :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
+ { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
+ :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
+ :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
+ :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
+ end
+ $ignore_next_release = false
+ $gesture_press = nil
+ end
+ false #- propagate
+ }
+
+ #- handle reordering with drag and drop
+ Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
+ Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
+ vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
+ selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
+ }
+
+ vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
+ done = false
+ #- mouse gesture first (dnd disables button-release-event)
+ if $gesture_press && $gesture_press[:filename] == filename
+ if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
+ angle = x-$gesture_press[:x] > 0 ? 90 : -90
+ msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
+ rotate_and_cleanup.call(angle)
+ $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
+ done = true
+ elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
+ msg 3, "gesture delete: click-drag right button to the bottom"
+ delete.call(false)
+ $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
+ done = true
+ end
+ end
+ if !done
+ ctxt.targets.each { |target|
+ if target.name == 'reorder-elements'
+ move_dnd = proc { |from,to|
+ if from != to
+ $modified = true
+ autotable.move(from, to)
+ save_undo(_("reorder"),
+ proc { |from, to|
+ if to > from
+ autotable.move(to - 1, from)
+ else
+ autotable.move(to, from + 1)
+ end
+ $notebook.set_page(1)
+ proc {
+ autotable.move(from, to)
+ $notebook.set_page(1)
+ }
+ }, from, to)
+ end
+ }
+ if $multiple_dnd.size == 0
+ move_dnd.call(selection_data.data.to_i,
+ autotable.get_current_number(vbox))
+ else
+ UndoHandler.begin_batch
+ $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
+ each { |path|
+ #- need to update current position between each call
+ move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
+ autotable.get_current_number(vbox))
+ }
+ UndoHandler.end_batch
+ end
+ $multiple_dnd = []
+ end
+ }
+ end
+ }
+
+ vbox.show_all
+end
+
+def create_auto_table
+
+ $autotable = Gtk::AutoTable.new(5)
+
+ $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
+ thumbnails_vb = Gtk::VBox.new(false, 5)
+
+ frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
+ $thumbnails_title.set_justification(Gtk::Justification::CENTER)
+ thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
+ thumbnails_vb.add($autotable)
+
+ $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
+ $autotable_sw.add_with_viewport(thumbnails_vb)
+
+ #- follows stuff for handling multiple elements selection
+ press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
+ gc = nil
+ update_selected = proc {
+ $autotable.current_order.each { |path|
+ w = $name2widgets[path][:evtbox].window
+ xm = w.position[0] + w.size[0]/2
+ ym = w.position[1] + w.size[1]/2
+ if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
+ if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
+ $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
+ $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
+ end
+ end
+ if $selected_elements[path] && ! $selected_elements[path][:keep]
+ if ((xm < press_x && xm < pos_x || xm > pos_x && xm > press_x) || (ym < press_y && ym < pos_y || ym > pos_y && ym > press_y))
+ $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
+ $selected_elements.delete(path)
+ end
+ end
+ }
+ }
+ $autotable.signal_connect('realize') { |w,e|
+ gc = Gdk::GC.new($autotable.window)
+ gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
+ gc.function = Gdk::GC::INVERT
+ #- autoscroll handling for DND and multiple selections
+ Gtk.timeout_add(100) {
+ if ! $autotable.window.nil?
+ w, x, y, mask = $autotable.window.pointer
+ if mask & Gdk::Window::BUTTON1_MASK != 0
+ if y < $autotable_sw.vadjustment.value
+ if pos_x
+ $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
+ end
+ if $button1_pressed_autotable || press_x
+ scroll_upper($autotable_sw, y)
+ end
+ if not press_x.nil?
+ w, pos_x, pos_y = $autotable.window.pointer
+ $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
+ update_selected.call
+ end
+ end
+ if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
+ if pos_x
+ $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
+ end
+ if $button1_pressed_autotable || press_x
+ scroll_lower($autotable_sw, y)
+ end
+ if not press_x.nil?
+ w, pos_x, pos_y = $autotable.window.pointer
+ $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
+ update_selected.call
+ end
+ end
+ end
+ end
+ ! $autotable.window.nil?
+ }
+ }
+
+ $autotable.signal_connect('button-press-event') { |w,e|
+ if e.button == 1
+ if !$button1_pressed_autotable
+ press_x = e.x
+ press_y = e.y
+ if e.state & Gdk::Window::SHIFT_MASK == 0
+ $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
+ $selected_elements = {}
+ $statusbar.push(0, utf8(_("Nothing selected.")))
+ else
+ $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
+ end
+ set_mousecursor(Gdk::Cursor::TCROSS)
+ end
+ end
+ }
+ $autotable.signal_connect('button-release-event') { |w,e|
+ if e.button == 1
+ if $button1_pressed_autotable
+ #- unselect all only now
+ $multiple_dnd = $selected_elements.keys
+ $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
+ $selected_elements = {}
+ $button1_pressed_autotable = false
+ else
+ if pos_x
+ $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
+ if $selected_elements.length > 0
+ $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
+ end
+ end
+ press_x = press_y = pos_x = pos_y = nil
+ set_mousecursor(Gdk::Cursor::LEFT_PTR)
+ end
+ end
+ }
+ $autotable.signal_connect('motion-notify-event') { |w,e|
+ if ! press_x.nil?
+ if pos_x
+ $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
+ end
+ pos_x = e.x
+ pos_y = e.y
+ $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
+ update_selected.call
+ end
+ }
+
+end
+
+def create_subalbums_page
+
+ subalbums_hb = Gtk::HBox.new
+ $subalbums_vb = Gtk::VBox.new(false, 5)
+ subalbums_hb.pack_start($subalbums_vb, false, false)
+ $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
+ $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
+ $subalbums_sw.add_with_viewport(subalbums_hb)
+end
+
+def save_current_file
+ save_changes
+
+ if $filename
+ begin
+ begin
+ ios = File.open($filename, "w")
+ $xmldoc.write(ios, 0)
+ ios.close
+ rescue Iconv::IllegalSequence
+ #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
+ if ! ios.nil? && ! ios.closed?
+ ios.close
+ end
+ $xmldoc.xml_decl.encoding = 'UTF-8'
+ ios = File.open($filename, "w")
+ $xmldoc.write(ios, 0)
+ ios.close
+ end
+ return true
+ rescue Exception
+ puts $!
+ return false
+ end
+ end
+end
+
+def save_current_file_user
+ save_tempfilename = $filename
+ $filename = $orig_filename
+ if ! save_current_file
+ show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
+ $filename = save_tempfilename
+ return
+ end
+ $modified = false
+ $generated_outofline = false
+ $filename = save_tempfilename
+
+ msg 3, "performing actual deletion of: " + $todelete.join(', ')
+ $todelete.each { |f|
+ File.delete(f)
+ }
+end
+
+def mark_document_as_dirty
+ $xmldoc.elements.each('//dir') { |elem|
+ elem.delete_attribute('already-generated')
+ }
+end
+
+#- ret: true => ok false => cancel
+def ask_save_modifications(msg1, msg2, *options)
+ ret = true
+ options = options.size > 0 ? options[0] : {}
+ if $modified
+ if options[:disallow_cancel]
+ dialog = Gtk::Dialog.new(msg1,
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
+ [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
+ else
+ dialog = Gtk::Dialog.new(msg1,
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
+ [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
+ [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
+ end
+ dialog.default_response = Gtk::Dialog::RESPONSE_YES
+ dialog.vbox.add(Gtk::Label.new(msg2))
+ dialog.window_position = Gtk::Window::POS_CENTER
+ dialog.show_all
+
+ dialog.run { |response|
+ dialog.destroy
+ if response == Gtk::Dialog::RESPONSE_YES
+ if ! save_current_file_user
+ return ask_save_modifications(msg1, msg2, options)
+ end
+ else
+ #- if we have generated an album but won't save modifications, we must remove
+ #- already-generated markers in original file
+ if $generated_outofline
+ begin
+ $xmldoc = REXML::Document.new File.new($orig_filename)
+ mark_document_as_dirty
+ ios = File.open($orig_filename, "w")
+ $xmldoc.write(ios, 0)
+ ios.close
+ rescue Exception
+ puts "exception: #{$!}"
+ end
+ end
+ end
+ if response == Gtk::Dialog::RESPONSE_CANCEL
+ ret = false
+ end
+ $todelete = [] #- unconditionally clear the list of images/videos to delete
+ }
+ end
+ return ret
+end
+
+def try_quit(*options)
+ if ask_save_modifications(utf8(_("Save before quitting?")),
+ utf8(_("Do you want to save your changes before quitting?")),
+ *options)
+ Gtk.main_quit
+ end
+end
+
+def show_popup(parent, msg, *options)
+ dialog = Gtk::Dialog.new
+ if options[0] && options[0][:title]
+ dialog.title = options[0][:title]
+ else
+ dialog.title = utf8(_("Booh message"))
+ end
+ lbl = Gtk::Label.new
+ if options[0] && options[0][:nomarkup]
+ lbl.text = msg
+ else
+ lbl.markup = msg
+ end
+ if options[0] && options[0][:centered]
+ lbl.set_justify(Gtk::Justification::CENTER)
+ end
+ if options[0] && options[0][:selectable]
+ lbl.selectable = true
+ end
+ if options[0] && options[0][:topwidget]
+ dialog.vbox.add(options[0][:topwidget])
+ end
+ if options[0] && options[0][:scrolled]
+ sw = Gtk::ScrolledWindow.new(nil, nil)
+ sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
+ sw.add_with_viewport(lbl)
+ dialog.vbox.add(sw)
+ dialog.set_default_size(500, 600)
+ else
+ dialog.vbox.add(lbl)
+ dialog.set_default_size(200, 120)
+ end
+ if options[0] && options[0][:okcancel]
+ dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
+ end
+ dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
+
+ if options[0] && options[0][:pos_centered]
+ dialog.window_position = Gtk::Window::POS_CENTER
+ else
+ dialog.window_position = Gtk::Window::POS_MOUSE
+ end
+
+ if options[0] && options[0][:linkurl]
+ linkbut = Gtk::Button.new('')
+ linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
+ linkbut.signal_connect('clicked') {
+ open_url(options[0][:linkurl])
+ dialog.response(Gtk::Dialog::RESPONSE_OK)
+ set_mousecursor_normal
+ }
+ linkbut.relief = Gtk::RELIEF_NONE
+ linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
+ linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
+ dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
+ end
+
+ dialog.show_all
+
+ if !options[0] || !options[0][:not_transient]
+ dialog.transient_for = parent
+ dialog.run { |response|
+ dialog.destroy
+ if options[0] && options[0][:okcancel]
+ return response == Gtk::Dialog::RESPONSE_OK
+ end
+ }
+ else
+ dialog.signal_connect('response') { dialog.destroy }
+ end
+end
+
+def set_mainwindow_title(progress)
+ filename = $orig_filename || $filename
+ if progress
+ if filename
+ $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
+ else
+ $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
+ end
+ else
+ if filename
+ $main_window.title = 'booh - ' + File.basename(filename)
+ else
+ $main_window.title = 'booh'
+ end
+ end
+end
+
+def backend_wait_message(parent, msg, infopipe_path, mode)
+ w = create_window
+ w.set_transient_for(parent)
+ w.modal = true
+
+ vb = Gtk::VBox.new(false, 5).set_border_width(5)
+ vb.pack_start(Gtk::Label.new(msg), false, false)
+
+ vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
+ vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
+ if mode != 'one dir scan'
+ vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
+ end
+ if mode == 'web-album'
+ vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
+ vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
+ end
+ vb.pack_start(Gtk::HSeparator.new, false, false)
+
+ bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
+ b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
+ vb.pack_end(bottom, false, false)
+
+ directories = nil
+ update_progression_title_pb1 = proc {
+ if mode == 'web-album'
+ set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
+ elsif mode != 'one dir scan'
+ set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
+ else
+ set_mainwindow_title(pb1_1.fraction)
+ end
+ }
+
+ infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
+ refresh_thread = Thread.new {
+ directories_counter = 0
+ while line = infopipe.gets
+ if line =~ /^directories: (\d+), sizes: (\d+)/
+ directories = $1.to_f + 1
+ sizes = $2.to_f
+ elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
+ elements = $3.to_f + 1
+ if mode == 'web-album'
+ elements += sizes
+ end
+ element_counter = 0
+ gtk_thread_protect { pb1_1.fraction = 0 }
+ if mode != 'one dir scan'
+ newtext = utf8(full_src_dir_to_rel($1, $2))
+ newtext = '/' if newtext == ''
+ gtk_thread_protect { pb1_2.text = newtext }
+ directories_counter += 1
+ gtk_thread_protect {
+ pb1_2.fraction = directories_counter / directories
+ update_progression_title_pb1.call
+ }
+ end
+ elsif line =~ /^processing element$/
+ element_counter += 1
+ gtk_thread_protect {
+ pb1_1.fraction = element_counter / elements
+ update_progression_title_pb1.call
+ }
+ elsif line =~ /^processing size$/
+ element_counter += 1
+ gtk_thread_protect {
+ pb1_1.fraction = element_counter / elements
+ update_progression_title_pb1.call
+ }
+ elsif line =~ /^finished processing sizes$/
+ gtk_thread_protect { pb1_1.fraction = 1 }
+ elsif line =~ /^creating index.html$/
+ gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
+ gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
+ directories_counter = 0
+ elsif line =~ /^index.html: (.+)\|(.+)/
+ newtext = utf8(full_src_dir_to_rel($1, $2))
+ newtext = '/' if newtext == ''
+ gtk_thread_protect { pb2.text = newtext }
+ directories_counter += 1
+ gtk_thread_protect {
+ pb2.fraction = directories_counter / directories
+ set_mainwindow_title(0.9 + pb2.fraction / 10)
+ }
+ elsif line =~ /^die: (.*)$/
+ $diemsg = $1
+ end
+ end
+ }
+
+ w.add(vb)
+ w.signal_connect('delete-event') { w.destroy }
+ w.signal_connect('destroy') {
+ Thread.kill(refresh_thread)
+ gtk_thread_flush #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
+ if infopipe_path
+ infopipe.close
+ File.delete(infopipe_path)
+ end
+ set_mainwindow_title(nil)
+ }
+ w.window_position = Gtk::Window::POS_CENTER
+ w.show_all
+
+ return [ b, w ]
+end
+
+def call_backend(cmd, waitmsg, mode, params)
+ pipe = Tempfile.new("boohpipe")
+ Thread.critical = true
+ path = pipe.path
+ pipe.close!
+ system("mkfifo #{path}")
+ Thread.critical = false
+ cmd += " --info-pipe #{path}"
+ button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
+ pid = nil
+ Thread.new {
+ msg 2, cmd
+ if pid = fork
+ id, exitstatus = Process.waitpid2(pid)
+ gtk_thread_protect { w8.destroy }
+ if exitstatus == 0
+ if params[:successmsg]
+ gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
+ end
+ if params[:closure_after]
+ gtk_thread_protect(¶ms[:closure_after])
+ end
+ elsif exitstatus == 15
+ #- say nothing, user aborted
+ else
+ gtk_thread_protect { show_popup($main_window,
+ utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
+ end
+ else
+ exec(cmd)
+ end
+ }
+ button.signal_connect('clicked') {
+ Process.kill('SIGTERM', pid)
+ }
+end
+
+def save_changes(*forced)
+ if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
+ return
+ end
+
+ $xmldir.delete_attribute('already-generated')
+
+ propagate_children = proc { |xmldir|
+ if xmldir.attributes['subdirs-caption']
+ xmldir.delete_attribute('already-generated')
+ end
+ xmldir.elements.each('dir') { |element|
+ propagate_children.call(element)
+ }
+ }
+
+ if $xmldir.child_byname_notattr('dir', 'deleted')
+ new_title = $subalbums_title.buffer.text
+ if new_title != $xmldir.attributes['subdirs-caption']
+ parent = $xmldir.parent
+ if parent.name == 'dir'
+ parent.delete_attribute('already-generated')
+ end
+ propagate_children.call($xmldir)
+ end
+ $xmldir.add_attribute('subdirs-caption', new_title)
+ $xmldir.elements.each('dir') { |element|
+ if !element.attributes['deleted']
+ path = element.attributes['path']
+ newtext = $subalbums_edits[path][:editzone].buffer.text
+ if element.attributes['subdirs-caption']
+ if element.attributes['subdirs-caption'] != newtext
+ propagate_children.call(element)
+ end
+ element.add_attribute('subdirs-caption', newtext)
+ element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
+ else
+ if element.attributes['thumbnails-caption'] != newtext
+ element.delete_attribute('already-generated')
+ end
+ element.add_attribute('thumbnails-caption', newtext)
+ element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
+ end
+ end
+ }
+ end
+
+ if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
+ if $xmldir.attributes['thumbnails-caption']
+ path = $xmldir.attributes['path']
+ $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
+ end
+ elsif $xmldir.attributes['thumbnails-caption']
+ $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
+ end
+
+ if $xmldir.attributes['thumbnails-caption']
+ if edit = $subalbums_edits[$xmldir.attributes['path']]
+ $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
+ end
+ end
+
+ #- remove and reinsert elements to reflect new ordering
+ saves = {}
+ cpt = 0
+ $xmldir.elements.each { |element|
+ if element.name == 'image' || element.name == 'video'
+ saves[element.attributes['filename']] = element.remove
+ cpt += 1
+ end
+ }
+ $autotable.current_order.each { |path|
+ chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
+ chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
+ saves.delete(path)
+ }
+ saves.each_key { |path|
+ chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
+ chld.add_attribute('deleted', 'true')
+ }
+end
+
+def sort_by_exif_date
+ $modified = true
+ save_changes
+ current_order = []
+ $xmldir.elements.each { |element|
+ if element.name == 'image' || element.name == 'video'
+ current_order << element.attributes['filename']
+ end
+ }
+
+ #- look for EXIF dates
+ dates = {}
+
+ if current_order.size > 20
+ w = create_window
+ w.set_transient_for($main_window)
+ w.modal = true
+ vb = Gtk::VBox.new(false, 5).set_border_width(5)
+ vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
+ vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
+ bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
+ b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
+ vb.pack_end(bottom, false, false)
+ w.add(vb)
+ w.signal_connect('delete-event') { w.destroy }
+ w.window_position = Gtk::Window::POS_CENTER
+ w.show_all
+
+ aborted = false
+ b.signal_connect('clicked') { aborted = true }
+ i = 0
+ current_order.each { |f|
+ i += 1
+ if entry2type(f) == 'image'
+ pb.text = f
+ pb.fraction = i.to_f / current_order.size
+ Gtk.main_iteration while Gtk.events_pending?
+ date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
+ if ! date_time.nil?
+ dates[f] = date_time
+ end
+ end
+ if aborted
+ break
+ end
+ }
+ w.destroy
+ if aborted
+ return
+ end
+
+ else
+ current_order.each { |f|
+ date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
+ if ! date_time.nil?
+ dates[f] = date_time
+ end
+ }
+ end
+
+ saves = {}
+ $xmldir.elements.each { |element|
+ if element.name == 'image' || element.name == 'video'
+ saves[element.attributes['filename']] = element.remove
+ end
+ }
+
+ neworder = smartsort(current_order, dates)
+
+ neworder.each { |f|
+ $xmldir.add_element(saves[f].name, saves[f].attributes)
+ }
+
+ #- let the auto-table reflect new ordering
+ change_dir
+end
+
+def remove_all_captions
+ $modified = true
+ texts = {}
+ $autotable.current_order.each { |path|
+ texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
+ $name2widgets[File.basename(path)][:textview].buffer.text = ''
+ }
+ save_undo(_("remove all captions"),
+ proc { |texts|
+ texts.each_key { |key|
+ $name2widgets[key][:textview].buffer.text = texts[key]
+ }
+ $notebook.set_page(1)
+ proc {
+ texts.each_key { |key|
+ $name2widgets[key][:textview].buffer.text = ''
+ }
+ $notebook.set_page(1)
+ }
+ }, texts)
+end
+
+def change_dir
+ $selected_elements.each_key { |path|
+ $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
+ }
+ $autotable.clear
+ $vbox2widgets = {}
+ $name2widgets = {}
+ $name2closures = {}
+ $selected_elements = {}
+ $cuts = []
+ $multiple_dnd = []
+ UndoHandler.cleanup
+ $undo_tb.sensitive = $undo_mb.sensitive = false
+ $redo_tb.sensitive = $redo_mb.sensitive = false
+
+ if !$current_path
+ return
+ end
+
+ $subalbums_vb.children.each { |chld|
+ $subalbums_vb.remove(chld)
+ }
+ $subalbums = Gtk::Table.new(0, 0, true)
+ current_y_sub_albums = 0
+
+ $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
+ $subalbums_edits = {}
+ subalbums_counter = 0
+ subalbums_edits_bypos = {}
+
+ add_subalbum = proc { |xmldir, counter|
+ $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
+ subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
+ if xmldir == $xmldir
+ thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
+ captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
+ caption = xmldir.attributes['thumbnails-caption']
+ infotype = 'thumbnails'
+ else
+ thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
+ captionfile, caption = find_subalbum_caption_info(xmldir)
+ infotype = find_subalbum_info_type(xmldir)
+ end
+ msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
+ hbox = Gtk::HBox.new
+ hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
+ f = Gtk::Frame.new
+ f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
+
+ img = nil
+ my_gen_real_thumbnail = proc {
+ gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
+ }
+
+ if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
+ f.add(img = Gtk::Image.new)
+ my_gen_real_thumbnail.call
+ else
+ f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
+ end
+ hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
+ $subalbums.attach(hbox,
+ 0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
+
+ frame, textview = create_editzone($subalbums_sw, 0, img)
+ textview.buffer.text = caption
+ $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
+ 1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
+
+ change_image = proc {
+ fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
+ nil,
+ Gtk::FileChooser::ACTION_OPEN,
+ nil,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+ fc.set_current_folder(from_utf8(xmldir.attributes['path']))
+ fc.transient_for = $main_window
+ fc.preview_widget = preview = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(f = Gtk::Frame.new.set_shadow_type(Gtk::SHADOW_ETCHED_OUT))
+ f.add(preview_img = Gtk::Image.new)
+ preview.show_all
+ fc.signal_connect('update-preview') { |w|
+ begin
+ if fc.preview_filename
+ preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
+ fc.preview_widget_active = true
+ end
+ rescue Gdk::PixbufError
+ fc.preview_widget_active = false
+ end
+ }
+ if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+ $modified = true
+ old_file = captionfile
+ old_rotate = xmldir.attributes["#{infotype}-rotate"]
+ old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
+ old_enhance = xmldir.attributes["#{infotype}-enhance"]
+ old_seektime = xmldir.attributes["#{infotype}-seektime"]
+
+ new_file = fc.filename
+ msg 3, "new captionfile is: #{fc.filename}"
+ perform_changefile = proc {
+ $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
+ $modified_pixbufs.delete(thumbnail_file)
+ xmldir.delete_attribute("#{infotype}-rotate")
+ xmldir.delete_attribute("#{infotype}-color-swap")
+ xmldir.delete_attribute("#{infotype}-enhance")
+ xmldir.delete_attribute("#{infotype}-seektime")
+ my_gen_real_thumbnail.call
+ }
+ perform_changefile.call
+
+ save_undo(_("change caption file for sub-album"),
+ proc {
+ $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
+ xmldir.add_attribute("#{infotype}-rotate", old_rotate)
+ xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
+ xmldir.add_attribute("#{infotype}-enhance", old_enhance)
+ xmldir.add_attribute("#{infotype}-seektime", old_seektime)
+ my_gen_real_thumbnail.call
+ $notebook.set_page(0)
+ proc {
+ perform_changefile.call
+ $notebook.set_page(0)
+ }
+ })
+ end
+ fc.destroy
+ }
+
+ refresh = proc {
+ if File.exists?(thumbnail_file)
+ File.delete(thumbnail_file)
+ end
+ my_gen_real_thumbnail.call
+ }
+
+ rotate_and_cleanup = proc { |angle|
+ rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
+ if File.exists?(thumbnail_file)
+ File.delete(thumbnail_file)
+ end
+ }
+
+ move = proc { |direction|
+ $modified = true
+
+ save_changes('forced')
+ oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
+ if direction == 'up'
+ $subalbums_edits[xmldir.attributes['path']][:position] -= 1
+ subalbums_edits_bypos[oldpos - 1][:position] += 1
+ end
+ if direction == 'down'
+ $subalbums_edits[xmldir.attributes['path']][:position] += 1
+ subalbums_edits_bypos[oldpos + 1][:position] -= 1
+ end
+ if direction == 'top'
+ for i in 1 .. oldpos - 1
+ subalbums_edits_bypos[i][:position] += 1
+ end
+ $subalbums_edits[xmldir.attributes['path']][:position] = 1
+ end
+ if direction == 'bottom'
+ for i in oldpos + 1 .. subalbums_counter
+ subalbums_edits_bypos[i][:position] -= 1
+ end
+ $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
+ end
+
+ elems = []
+ $xmldir.elements.each('dir') { |element|
+ if (!element.attributes['deleted'])
+ elems << [ element.attributes['path'], element.remove ]
+ end
+ }
+ elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
+ each { |e| $xmldir.add_element(e[1]) }
+ #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
+ $xmldir.elements.each('descendant::dir') { |elem|
+ elem.delete_attribute('already-generated')
+ }
+
+ sel = $albums_tv.selection.selected_rows
+ change_dir
+ populate_subalbums_treeview(false)
+ $albums_tv.selection.select_path(sel[0])
+ }
+
+ color_swap_and_cleanup = proc {
+ perform_color_swap_and_cleanup = proc {
+ color_swap(xmldir, "#{infotype}-")
+ my_gen_real_thumbnail.call
+ }
+ perform_color_swap_and_cleanup.call
+
+ save_undo(_("color swap"),
+ proc {
+ perform_color_swap_and_cleanup.call
+ $notebook.set_page(0)
+ proc {
+ perform_color_swap_and_cleanup.call
+ $notebook.set_page(0)
+ }
+ })
+ }
+
+ change_seektime_and_cleanup = proc {
+ if values = ask_new_seektime(xmldir, "#{infotype}-")
+ perform_change_seektime_and_cleanup = proc { |val|
+ change_seektime(xmldir, "#{infotype}-", val)
+ my_gen_real_thumbnail.call
+ }
+ perform_change_seektime_and_cleanup.call(values[:new])
+
+ save_undo(_("specify seektime"),
+ proc {
+ perform_change_seektime_and_cleanup.call(values[:old])
+ $notebook.set_page(0)
+ proc {
+ perform_change_seektime_and_cleanup.call(values[:new])
+ $notebook.set_page(0)
+ }
+ })
+ end
+ }
+
+ whitebalance_and_cleanup = proc {
+ if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
+ $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
+ perform_change_whitebalance_and_cleanup = proc { |val|
+ change_whitebalance(xmldir, "#{infotype}-", val)
+ recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
+ $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
+ if File.exists?(thumbnail_file)
+ File.delete(thumbnail_file)
+ end
+ }
+ perform_change_whitebalance_and_cleanup.call(values[:new])
+
+ save_undo(_("fix white balance"),
+ proc {
+ perform_change_whitebalance_and_cleanup.call(values[:old])
+ $notebook.set_page(0)
+ proc {
+ perform_change_whitebalance_and_cleanup.call(values[:new])
+ $notebook.set_page(0)
+ }
+ })
+ end
+ }
+
+ gammacorrect_and_cleanup = proc {
+ if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
+ $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
+ perform_change_gammacorrect_and_cleanup = proc { |val|
+ change_gammacorrect(xmldir, "#{infotype}-", val)
+ recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
+ $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
+ if File.exists?(thumbnail_file)
+ File.delete(thumbnail_file)
+ end
+ }
+ perform_change_gammacorrect_and_cleanup.call(values[:new])
+
+ save_undo(_("gamma correction"),
+ proc {
+ perform_change_gammacorrect_and_cleanup.call(values[:old])
+ $notebook.set_page(0)
+ proc {
+ perform_change_gammacorrect_and_cleanup.call(values[:new])
+ $notebook.set_page(0)
+ }
+ })
+ end
+ }
+
+ enhance_and_cleanup = proc {
+ perform_enhance_and_cleanup = proc {
+ enhance(xmldir, "#{infotype}-")
+ my_gen_real_thumbnail.call
+ }
+
+ perform_enhance_and_cleanup.call
+
+ save_undo(_("enhance"),
+ proc {
+ perform_enhance_and_cleanup.call
+ $notebook.set_page(0)
+ proc {
+ perform_enhance_and_cleanup.call
+ $notebook.set_page(0)
+ }
+ })
+ }
+
+ evtbox.signal_connect('button-press-event') { |w, event|
+ if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
+ if $r90.active?
+ rotate_and_cleanup.call(90)
+ elsif $r270.active?
+ rotate_and_cleanup.call(-90)
+ elsif $enhance.active?
+ enhance_and_cleanup.call
+ end
+ end
+ if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
+ popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
+ { :forbid_left => true, :forbid_right => true,
+ :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
+ :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
+ { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
+ :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
+ :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
+ end
+ if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
+ change_image.call
+ true #- handled
+ end
+ }
+ evtbox.signal_connect('button-press-event') { |w, event|
+ $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
+ false
+ }
+
+ evtbox.signal_connect('button-release-event') { |w, event|
+ if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
+ msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
+ if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
+ angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
+ msg 3, "gesture rotate: #{angle}"
+ rotate_and_cleanup.call(angle)
+ end
+ end
+ $gesture_press = nil
+ }
+
+ $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
+ $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
+ current_y_sub_albums += 1
+ }
+
+ if $xmldir.child_byname_notattr('dir', 'deleted')
+ #- title edition
+ frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
+ $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
+ $subalbums_title.set_justification(Gtk::Justification::CENTER)
+ $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
+ #- this album image/caption
+ if $xmldir.attributes['thumbnails-caption']
+ add_subalbum.call($xmldir, 0)
+ end
+ end
+ total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
+ $xmldir.elements.each { |element|
+ if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
+ #- element (image or video) of this album
+ dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
+ msg 3, "dest_img: #{dest_img}"
+ add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
+ total[element.name] += 1
+ end
+ if element.name == 'dir' && !element.attributes['deleted']
+ #- sub-album image/caption
+ add_subalbum.call(element, subalbums_counter += 1)
+ total[element.name] += 1
+ end
+ }
+ $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
+ total['image'], total['video'], total['dir'] ]))
+ $subalbums_vb.add($subalbums)
+ $subalbums_vb.show_all
+
+ if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
+ $notebook.get_tab_label($autotable_sw).sensitive = false
+ $notebook.set_page(0)
+ $thumbnails_title.buffer.text = ''
+ else
+ $notebook.get_tab_label($autotable_sw).sensitive = true
+ $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
+ end
+
+ if !$xmldir.child_byname_notattr('dir', 'deleted')
+ $notebook.get_tab_label($subalbums_sw).sensitive = false
+ $notebook.set_page(1)
+ else
+ $notebook.get_tab_label($subalbums_sw).sensitive = true
+ end
+end
+
+def pixbuf_or_nil(filename)
+ begin
+ return Gdk::Pixbuf.new(filename)
+ rescue
+ return nil
+ end
+end
+
+def theme_choose(current)
+ dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
+ treeview = Gtk::TreeView.new(model).set_rules_hint(true)
+ treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
+ treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
+ treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
+ treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
+ treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
+ treeview.signal_connect('button-press-event') { |w, event|
+ if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
+ dialog.response(Gtk::Dialog::RESPONSE_OK)
+ end
+ }
+
+ dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
+
+ ([ $FPATH + '/themes/simple' ] + (`find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.find_all { |e| e !~ /simple$/ }.sort)).each { |dir|
+ dir.chomp!
+ iter = model.append
+ iter[0] = File.basename(dir)
+ iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
+ iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
+ iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
+ if File.basename(dir) == current
+ treeview.selection.select_iter(iter)
+ end
+ }
+ dialog.set_default_size(-1, 500)
+ dialog.vbox.show_all
+
+ dialog.run { |response|
+ iter = treeview.selection.selected
+ dialog.destroy
+ if response == Gtk::Dialog::RESPONSE_OK && iter
+ return model.get_value(iter, 0)
+ end
+ }
+ return nil
+end
+
+def show_password_protections
+ examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
+ child_iter = $albums_iters[xmldir.attributes['path']]
+ if xmldir.attributes['password-protect']
+ child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
+ already_protected = true
+ elsif already_protected
+ pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
+ if pix
+ pix = pix.saturate_and_pixelate(1, true)
+ end
+ child_iter[2] = pix
+ else
+ child_iter[2] = nil
+ end
+ xmldir.elements.each('dir') { |elem|
+ if !elem.attributes['deleted']
+ examine_dir_elem.call(child_iter, elem, already_protected)
+ end
+ }
+ }
+ examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
+end
+
+def populate_subalbums_treeview(select_first)
+ $albums_ts.clear
+ $autotable.clear
+ $albums_iters = {}
+ $subalbums_vb.children.each { |chld|
+ $subalbums_vb.remove(chld)
+ }
+
+ source = $xmldoc.root.attributes['source']
+ msg 3, "source: #{source}"
+
+ xmldir = $xmldoc.elements['//dir']
+ if !xmldir || xmldir.attributes['path'] != source
+ msg 1, _("Corrupted booh file...")
+ return
+ end
+
+ append_dir_elem = proc { |parent_iter, xmldir|
+ child_iter = $albums_ts.append(parent_iter)
+ child_iter[0] = File.basename(xmldir.attributes['path'])
+ child_iter[1] = xmldir.attributes['path']
+ $albums_iters[xmldir.attributes['path']] = child_iter
+ msg 3, "puttin location: #{xmldir.attributes['path']}"
+ xmldir.elements.each('dir') { |elem|
+ if !elem.attributes['deleted']
+ append_dir_elem.call(child_iter, elem)
+ end
+ }
+ }
+ append_dir_elem.call(nil, xmldir)
+ show_password_protections
+
+ $albums_tv.expand_all
+ if select_first
+ $albums_tv.selection.select_iter($albums_ts.iter_first)
+ end
+end
+
+def select_current_theme
+ select_theme($xmldoc.root.attributes['theme'],
+ $xmldoc.root.attributes['limit-sizes'],
+ !$xmldoc.root.attributes['optimize-for-32'].nil?,
+ $xmldoc.root.attributes['thumbnails-per-row'])
+end
+
+def open_file(filename)
+
+ $filename = nil
+ $modified = false
+ $current_path = nil #- invalidate
+ $modified_pixbufs = {}
+ $albums_ts.clear
+ $autotable.clear
+ $subalbums_vb.children.each { |chld|
+ $subalbums_vb.remove(chld)
+ }
+
+ if !File.exists?(filename)
+ return utf8(_("File not found."))
+ end
+
+ begin
+ $xmldoc = REXML::Document.new File.new(filename)
+ rescue Exception
+ $xmldoc = nil
+ end
+
+ if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
+ if entry2type(filename).nil?
+ return utf8(_("Not a booh file!"))
+ else
+ return utf8(_("Not a booh file!\n\nHint: you cannot import directly a photo or video with File/Open.\nUse File/New to create a new album."))
+ end
+ end
+
+ if !source = $xmldoc.root.attributes['source']
+ return utf8(_("Corrupted booh file..."))
+ end
+
+ if !dest = $xmldoc.root.attributes['destination']
+ return utf8(_("Corrupted booh file..."))
+ end
+
+ if !theme = $xmldoc.root.attributes['theme']
+ return utf8(_("Corrupted booh file..."))
+ end
+
+ if $xmldoc.root.attributes['version'] < '0.9.0'
+ msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
+ mark_document_as_dirty
+ if $xmldoc.root.attributes['version'] < '0.8.4'
+ msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
+ `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
+ old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
+ new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
+ if old_dest_dir != new_dest_dir
+ sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
+ end
+ if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
+ xmldir.elements.each { |element|
+ if %w(image video).include?(element.name) && !element.attributes['deleted']
+ old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
+ new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
+ Dir[old_name + '*'].each { |file|
+ new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
+ file != new_file and sys("mv '#{file}' '#{new_file}'")
+ }
+ end
+ if element.name == 'dir' && !element.attributes['deleted']
+ old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
+ new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
+ old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
+ end
+ }
+ else
+ msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
+ end
+ }
+ end
+ $xmldoc.root.add_attribute('version', $VERSION)
+ end
+
+ select_current_theme
+
+ $filename = filename
+ set_mainwindow_title(nil)
+ $default_size['thumbnails'] =~ /(.*)x(.*)/
+ $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
+ $albums_thumbnail_size =~ /(.*)x(.*)/
+ $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
+
+ populate_subalbums_treeview(true)
+
+ $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
+ return nil
+end
+
+def open_file_user(filename)
+ result = open_file(filename)
+ if !result
+ $config['last-opens'] ||= []
+ if $config['last-opens'][-1] != utf8(filename)
+ $config['last-opens'] << utf8(filename)
+ end
+ $orig_filename = $filename
+ $main_window.title = 'booh - ' + File.basename($orig_filename)
+ tmp = Tempfile.new("boohtemp")
+ Thread.critical = true
+ $filename = tmp.path
+ tmp.close!
+ #- for security
+ ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
+ Thread.critical = false
+ ios.close
+ $tempfiles << $filename << "#{$filename}.backup"
+ else
+ $orig_filename = nil
+ end
+ return result
+end
+
+def open_file_popup
+ if !ask_save_modifications(utf8(_("Save this album?")),
+ utf8(_("Do you want to save the changes to this album?")),
+ { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
+ return
+ end
+ fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
+ nil,
+ Gtk::FileChooser::ACTION_OPEN,
+ nil,
+ [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+ fc.add_shortcut_folder(File.expand_path("~/.booh"))
+ fc.set_current_folder(File.expand_path("~/.booh"))
+ fc.transient_for = $main_window
+ ok = false
+ while !ok
+ if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+ push_mousecursor_wait(fc)
+ msg = open_file_user(fc.filename)
+ pop_mousecursor(fc)
+ if msg
+ show_popup(fc, msg)
+ ok = false
+ else
+ ok = true
+ end
+ else
+ ok = true
+ end
+ end
+ fc.destroy
+end
+
+def additional_booh_options
+ options = ''
+ if $config['mproc']
+ options += "--mproc #{$config['mproc'].to_i} "
+ end
+ options += "--comments-format '#{$config['comments-format']}' "
+ if $config['transcode-videos']
+ options += "--transcode-videos '#{$config['transcode-videos']}' "
+ end
+ return options
+end
+
+def ask_multi_languages(value)
+ if ! value.nil?
+ spl = value.split(',')
+ value = [ spl[0..-2], spl[-1] ]
+ end
+
+ dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
+ $main_window,
+ Gtk::Dialog::MODAL,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ lbl = Gtk::Label.new
+ lbl.markup = utf8(
+_("You can choose to activate <b>multi-languages</b> support for this web-album
+(it will work only if you publish your web-album on an Apache web-server). This will
+use the MultiViews feature of Apache; the pages will be served according to the
+value of the Accept-Language HTTP header sent by the web browsers, so that people
+with different languages preferences will be able to browse your web-album with
+navigation in their language (if language is available).
+"))
+
+ dialog.vbox.add(lbl)
+ dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
+ add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
+ add(languages = Gtk::Button.new))))
+
+ pick_languages = proc {
+ dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
+ $main_window,
+ Gtk::Dialog::MODAL,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
+ hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
+ cbs = []
+ SUPPORTED_LANGUAGES.each { |lang|
+ hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
+ if ! value.nil? && value[0].include?(lang)
+ cb.active = true
+ end
+ cbs << [ lang, cb ]
+ }
+
+ dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
+ hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
+ fallback_language = nil
+ hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
+ fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
+ if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
+ fbl_rb.active = true
+ fallback_language = SUPPORTED_LANGUAGES[0]
+ end
+ SUPPORTED_LANGUAGES[1..-1].each { |lang|
+ hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
+ rb.signal_connect('clicked') { fallback_language = lang }
+ if ! value.nil? && value[1] == lang
+ rb.active = true
+ end
+ }
+
+ dialog2.window_position = Gtk::Window::POS_MOUSE
+ dialog2.show_all
+
+ resp = nil
+ dialog2.run { |response|
+ resp = response
+ if resp == Gtk::Dialog::RESPONSE_OK
+ value = []
+ value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
+ value[1] = fallback_language
+ languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
+ end
+ dialog2.destroy
+ }
+ resp
+ }
+
+ languages.signal_connect('clicked') {
+ pick_languages.call
+ }
+ dialog.window_position = Gtk::Window::POS_MOUSE
+ if value.nil?
+ rb_no.active = true
+ else
+ rb_yes.active = true
+ languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
+ end
+ rb_no.signal_connect('clicked') {
+ if rb_no.active?
+ languages.hide
+ else
+ if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
+ rb_no.activate
+ else
+ languages.show
+ end
+ end
+ }
+ oldval = value
+ dialog.show_all
+ if rb_no.active?
+ languages.hide
+ end
+
+ dialog.run { |response|
+ if rb_no.active?
+ value = nil
+ end
+ dialog.destroy
+ if response == Gtk::Dialog::RESPONSE_OK && value != oldval
+ if value.nil?
+ return [ true, nil ]
+ else
+ return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
+ end
+ else
+ return [ false ]
+ end
+ }
+end
+
+def new_album
+ if !ask_save_modifications(utf8(_("Save this album?")),
+ utf8(_("Do you want to save the changes to this album?")),
+ { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
+ return
+ end
+ dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
+ tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
+ 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(src = Gtk::Entry.new,
+ 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
+ 2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
+ 0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
+ 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
+ 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(dest = Gtk::Entry.new,
+ 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
+ 2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
+ 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
+ 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
+ 2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+
+ tooltips = Gtk::Tooltips.new
+ frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
+ pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
+ pack_start(sizes = Gtk::HBox.new, false, false, 0))
+ vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
+ tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
+ pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
+ nperpage_model = Gtk::ListStore.new(String, String)
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
+ pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
+ nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
+ nperpagecombo.set_attributes(crt, { :markup => 0 })
+ iter = nperpage_model.append
+ iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
+ iter[1] = nil
+ [ 12, 20, 30, 40, 50 ].each { |v|
+ iter = nperpage_model.append
+ iter[0] = iter[1] = v.to_s
+ }
+ nperpagecombo.active = 0
+
+ multilanguages_value = nil
+ vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
+ pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
+ tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
+ multilanguages.signal_connect('clicked') {
+ retval = ask_multi_languages(multilanguages_value)
+ if retval[0]
+ multilanguages_value = retval[1]
+ end
+ if multilanguages_value
+ ml_label.text = utf8(_("Multi-languages: enabled."))
+ else
+ ml_label.text = utf8(_("Multi-languages: disabled."))
+ end
+ }
+ if $config['default-multi-languages']
+ multilanguages_value = $config['default-multi-languages']
+ ml_label.text = utf8(_("Multi-languages: enabled."))
+ else
+ ml_label.text = utf8(_("Multi-languages: disabled."))
+ end
+
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
+ pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
+ tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
+ pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
+ tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
+
+ src_nb_calculated_for = ''
+ src_nb_thread = nil
+ process_src_nb = proc {
+ if src.text != src_nb_calculated_for
+ src_nb_calculated_for = src.text
+ if src_nb_thread
+ Thread.kill(src_nb_thread)
+ src_nb_thread = nil
+ end
+ if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
+ src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
+ else
+ if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
+ if File.readable?(from_utf8_safe(src_nb_calculated_for))
+ src_nb_thread = Thread.new {
+ gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
+ total = { 'image' => 0, 'video' => 0, nil => 0 }
+ `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
+ if File.basename(dir) =~ /^\./
+ next
+ else
+ begin
+ Dir.entries(dir.chomp).each { |file|
+ total[entry2type(file)] += 1
+ }
+ rescue Errno::EACCES, Errno::ENOENT
+ end
+ end
+ }
+ gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
+ src_nb_thread = nil
+ }
+ else
+ src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
+ end
+ else
+ src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
+ end
+ end
+ end
+ true
+ }
+ timeout_src_nb = Gtk.timeout_add(100) {
+ process_src_nb.call
+ }
+
+ src_browse.signal_connect('clicked') {
+ fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
+ nil,
+ Gtk::FileChooser::ACTION_SELECT_FOLDER,
+ nil,
+ [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+ fc.transient_for = $main_window
+ if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+ src.text = utf8(fc.filename)
+ process_src_nb.call
+ conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
+ end
+ fc.destroy
+ }
+
+ dest_browse.signal_connect('clicked') {
+ fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
+ nil,
+ Gtk::FileChooser::ACTION_CREATE_FOLDER,
+ nil,
+ [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+ fc.transient_for = $main_window
+ if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+ dest.text = utf8(fc.filename)
+ end
+ fc.destroy
+ }
+
+ conf_browse.signal_connect('clicked') {
+ fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
+ nil,
+ Gtk::FileChooser::ACTION_SAVE,
+ nil,
+ [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+ fc.transient_for = $main_window
+ fc.add_shortcut_folder(File.expand_path("~/.booh"))
+ fc.set_current_folder(File.expand_path("~/.booh"))
+ if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+ conf.text = utf8(fc.filename)
+ end
+ fc.destroy
+ }
+
+ theme_sizes = []
+ nperrows = []
+ recreate_theme_config = proc {
+ theme_sizes.each { |e| sizes.remove(e[:widget]) }
+ theme_sizes = []
+ select_theme(theme_button.label, 'all', optimize432.active?, nil)
+ $images_size.each { |s|
+ sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
+ if !s['optional']
+ cb.active = true
+ end
+ tooltips.set_tip(cb, utf8(s['description']), nil)
+ theme_sizes << { :widget => cb, :value => s['name'] }
+ }
+ sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
+ tooltips = Gtk::Tooltips.new
+ tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
+ theme_sizes << { :widget => cb, :value => 'original' }
+ sizes.show_all
+
+ nperrows.each { |e| nperrowradios.remove(e[:widget]) }
+ nperrow_group = nil
+ nperrows = []
+ $allowed_N_values.each { |n|
+ if nperrow_group
+ nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
+ else
+ nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
+ end
+ tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
+ if $default_N == n
+ rb.active = true
+ end
+ nperrows << { :widget => rb, :value => n }
+ }
+ nperrowradios.show_all
+ }
+ recreate_theme_config.call
+
+ theme_button.signal_connect('clicked') {
+ if newtheme = theme_choose(theme_button.label)
+ theme_button.label = newtheme
+ recreate_theme_config.call
+ end
+ }
+
+ dialog.vbox.add(frame1)
+ dialog.vbox.add(frame2)
+ dialog.show_all
+
+ keepon = true
+ ok = true
+ while keepon
+ dialog.run { |response|
+ if response == Gtk::Dialog::RESPONSE_OK
+ srcdir = from_utf8_safe(src.text)
+ destdir = from_utf8_safe(dest.text)
+ confpath = from_utf8_safe(conf.text)
+ if src.text != '' && srcdir == ''
+ show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
+ src.grab_focus
+ elsif !File.directory?(srcdir)
+ show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
+ src.grab_focus
+ elsif dest.text != '' && destdir == ''
+ show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
+ dest.grab_focus
+ elsif destdir != make_dest_filename(destdir)
+ show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
+ dest.grab_focus
+ elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
+ keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
+inside it will be permanently removed before creating the web-album!
+Are you sure you want to continue?")), { :okcancel => true })
+ dest.grab_focus
+ elsif File.exists?(destdir) && !File.directory?(destdir)
+ show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
+ dest.grab_focus
+ elsif conf.text == ''
+ show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
+ conf.grab_focus
+ elsif conf.text != '' && confpath == ''
+ show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
+ conf.grab_focus
+ elsif File.directory?(confpath)
+ show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
+ conf.grab_focus
+ elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
+ show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
+ else
+ system("mkdir '#{destdir}'")
+ if !File.directory?(destdir)
+ show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
+ dest.grab_focus
+ else
+ keepon = false
+ end
+ end
+ else
+ keepon = ok = false
+ end
+ }
+ end
+ if ok
+ srcdir = from_utf8(src.text)
+ destdir = from_utf8(dest.text)
+ configskel = File.expand_path(from_utf8(conf.text))
+ theme = theme_button.label
+ #- some sort of automatic theme preference
+ $config['default-theme'] = theme
+ $config['default-multi-languages'] = multilanguages_value
+ $config['default-optimize32'] = optimize432.active?.to_s
+ sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
+ nperrow = nperrows.find { |e| e[:widget].active? }[:value]
+ nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
+ opt432 = optimize432.active?
+ madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
+ indexlink = indexlinkentry.text.gsub('\'', ''')
+ end
+ if src_nb_thread
+ Thread.kill(src_nb_thread)
+ gtk_thread_flush #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
+ end
+ dialog.destroy
+ Gtk.timeout_remove(timeout_src_nb)
+
+ if ok
+ call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
+ "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
+ (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
+ (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
+ "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
+ utf8(_("Please wait while scanning source directory...")),
+ 'full scan',
+ { :closure_after => proc {
+ open_file_user(configskel)
+ $main_window.urgency_hint = true
+ } })
+ end
+end
+
+def properties
+ dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ source = $xmldoc.root.attributes['source']
+ dest = $xmldoc.root.attributes['destination']
+ theme = $xmldoc.root.attributes['theme']
+ opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
+ nperrow = $xmldoc.root.attributes['thumbnails-per-row']
+ nperpage = $xmldoc.root.attributes['thumbnails-per-page']
+ limit_sizes = $xmldoc.root.attributes['limit-sizes']
+ if limit_sizes
+ limit_sizes = limit_sizes.split(/,/)
+ end
+ madewith = ($xmldoc.root.attributes['made-with'] || '').gsub(''', '\'')
+ indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub(''', '\'')
+ save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
+
+ tooltips = Gtk::Tooltips.new
+ frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
+ tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
+ 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
+ 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
+ 0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
+ 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
+ 0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
+ 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+
+ frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
+ pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
+ pack_start(sizes = Gtk::HBox.new, false, false, 0))
+ vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
+ tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)")), nil)
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
+ pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
+ nperpage_model = Gtk::ListStore.new(String, String)
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
+ pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
+ nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
+ nperpagecombo.set_attributes(crt, { :markup => 0 })
+ iter = nperpage_model.append
+ iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
+ iter[1] = nil
+ [ 12, 20, 30, 40, 50 ].each { |v|
+ iter = nperpage_model.append
+ iter[0] = iter[1] = v.to_s
+ if nperpage && nperpage == v.to_s
+ nperpagecombo.active_iter = iter
+ end
+ }
+ if nperpagecombo.active_iter.nil?
+ nperpagecombo.active = 0
+ end
+
+ vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
+ pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
+ tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
+ ml_update = proc {
+ if save_multilanguages_value
+ ml_label.text = utf8(_("Multi-languages: enabled."))
+ else
+ ml_label.text = utf8(_("Multi-languages: disabled."))
+ end
+ }
+ ml_update.call
+ multilanguages.signal_connect('clicked') {
+ retval = ask_multi_languages(save_multilanguages_value)
+ if retval[0]
+ save_multilanguages_value = retval[1]
+ end
+ ml_update.call
+ }
+
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
+ pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
+ if indexlink
+ indexlinkentry.text = indexlink
+ end
+ tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
+ vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
+ pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
+ if madewith
+ madewithentry.text = madewith
+ end
+ tooltips.set_tip(madewithentry, utf8(_('Optional HTML markup to use on pages bottom for a small \'made with\' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!')), nil)
+
+ theme_sizes = []
+ nperrows = []
+ recreate_theme_config = proc {
+ theme_sizes.each { |e| sizes.remove(e[:widget]) }
+ theme_sizes = []
+ select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
+
+ $images_size.each { |s|
+ sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
+ if limit_sizes
+ if limit_sizes.include?(s['name'])
+ cb.active = true
+ end
+ else
+ if !s['optional']
+ cb.active = true
+ end
+ end
+ tooltips.set_tip(cb, utf8(s['description']), nil)
+ theme_sizes << { :widget => cb, :value => s['name'] }
+ }
+ sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
+ tooltips = Gtk::Tooltips.new
+ tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
+ if limit_sizes && limit_sizes.include?('original')
+ cb.active = true
+ end
+ theme_sizes << { :widget => cb, :value => 'original' }
+ sizes.show_all
+
+ nperrows.each { |e| nperrowradios.remove(e[:widget]) }
+ nperrow_group = nil
+ nperrows = []
+ $allowed_N_values.each { |n|
+ if nperrow_group
+ nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
+ else
+ nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
+ end
+ tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
+ nperrowradios.add(Gtk::Label.new(' '))
+ if nperrow && n.to_s == nperrow || !nperrow && $default_N == n
+ rb.active = true
+ end
+ nperrows << { :widget => rb, :value => n.to_s }
+ }
+ nperrowradios.show_all
+ }
+ recreate_theme_config.call
+
+ theme_button.signal_connect('clicked') {
+ if newtheme = theme_choose(theme_button.label)
+ limit_sizes = nil
+ nperrow = nil
+ theme_button.label = newtheme
+ recreate_theme_config.call
+ end
+ }
+
+ dialog.vbox.add(frame1)
+ dialog.vbox.add(frame2)
+ dialog.show_all
+
+ keepon = true
+ ok = true
+ while keepon
+ dialog.run { |response|
+ if response == Gtk::Dialog::RESPONSE_OK
+ if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
+ show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
+ else
+ keepon = false
+ end
+ else
+ keepon = ok = false
+ end
+ }
+ end
+ save_theme = theme_button.label
+ save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
+ save_opt432 = optimize432.active?
+ save_nperrow = nperrows.find { |e| e[:widget].active? }[:value]
+ save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
+ save_madewith = madewithentry.text.gsub('\'', ''') #- because the parameters to booh-backend are between apostrophes
+ save_indexlink = indexlinkentry.text.gsub('\'', ''')
+ dialog.destroy
+
+ if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry || save_multilanguages_value != multilanguages_value)
+ #- some sort of automatic preferences
+ if save_theme != theme
+ $config['default-theme'] = save_theme
+ end
+ if save_multilanguages_value != multilanguages_value
+ $config['default-multi-languages'] = save_multilanguages_value
+ end
+ if save_opt432 != opt432
+ $config['default-optimize32'] = save_opt432.to_s
+ end
+ mark_document_as_dirty
+ save_current_file
+ call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
+ "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
+ (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
+ (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
+ "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
+ utf8(_("Please wait while scanning source directory...")),
+ 'full scan',
+ { :closure_after => proc {
+ open_file($filename)
+ $modified = true
+ $main_window.urgency_hint = true
+ } })
+ else
+ #- select_theme merges global variables, need to return to current choices
+ select_current_theme
+ end
+end
+
+def merge_current
+ save_current_file
+
+ sel = $albums_tv.selection.selected_rows
+
+ call_backend("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
+ "--verbose-level #{$verbose_level} #{additional_booh_options}",
+ utf8(_("Please wait while scanning source directory...")),
+ 'one dir scan',
+ { :closure_after => proc {
+ open_file($filename)
+ $albums_tv.selection.select_path(sel[0])
+ $modified = true
+ $main_window.urgency_hint = true
+ } })
+end
+
+def merge_newsubs
+ save_current_file
+
+ sel = $albums_tv.selection.selected_rows
+
+ call_backend("booh-backend --merge-config-subdirs '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
+ "--verbose-level #{$verbose_level} #{additional_booh_options}",
+ utf8(_("Please wait while scanning source directory...")),
+ 'subdirs scan',
+ { :closure_after => proc {
+ open_file($filename)
+ $albums_tv.selection.select_path(sel[0])
+ $modified = true
+ $main_window.urgency_hint = true
+ } })
+end
+
+def merge
+ save_current_file
+
+ theme = $xmldoc.root.attributes['theme']
+ limit_sizes = $xmldoc.root.attributes['limit-sizes']
+ if limit_sizes
+ limit_sizes = "--sizes #{limit_sizes}"
+ end
+ call_backend("booh-backend --merge-config '#{$filename}' --for-gui " +
+ "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
+ utf8(_("Please wait while scanning source directory...")),
+ 'full scan',
+ { :closure_after => proc {
+ open_file($filename)
+ $modified = true
+ $main_window.urgency_hint = true
+ } })
+end
+
+def save_as_do
+ fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
+ nil,
+ Gtk::FileChooser::ACTION_SAVE,
+ nil,
+ [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+ fc.transient_for = $main_window
+ fc.add_shortcut_folder(File.expand_path("~/.booh"))
+ fc.set_current_folder(File.expand_path("~/.booh"))
+ fc.filename = $orig_filename
+ if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+ $orig_filename = fc.filename
+ if ! save_current_file_user
+ fc.destroy
+ return save_as_do
+ end
+ $config['last-opens'] ||= []
+ $config['last-opens'] << $orig_filename
+ end
+ fc.destroy
+end
+
+def preferences
+ dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ dialog.vbox.add(notebook = Gtk::Notebook.new)
+ notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
+ tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
+ 0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
+ 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips = Gtk::Tooltips.new
+ tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
+for example: /usr/bin/mplayer %f")), nil)
+ tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
+ 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
+ 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
+for example: /usr/bin/gimp-remote %f")), nil)
+ tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
+ 0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
+ 1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
+for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
+ tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
+ 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
+ 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
+ tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
+ 0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
+ tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
+ 0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting a photo or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
+
+ smp_check.signal_connect('toggled') {
+ smp_hbox.sensitive = smp_check.active?
+ }
+ if $config['mproc']
+ smp_check.active = true
+ smp_spin.value = $config['mproc'].to_i
+ end
+ nogestures_check.active = $config['nogestures']
+ deleteondisk_check.active = $config['deleteondisk']
+
+ notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
+ tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
+ 0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
+ tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
+ 1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Label.new.set_markup(utf8(_("Format to use for comments of \nphotos in new albums:"))),
+ 0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(commentsformat_entry = Gtk::Entry.new.set_text($config['comments-format']),
+ 1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(commentsformat_help = Gtk::Button.new(Gtk::Stock::HELP),
+ 2, 3, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(commentsformat_entry, utf8(_("Normally, filenames without extension are used as comments for photos and videos in new albums. Use this entry to use something else.")), nil)
+ commentsformat_help.signal_connect('clicked') {
+ show_popup(dialog, utf8(_("The comments format you specify is actually passed to the 'identify' program,
+hence you should look at ImageMagick/identify documentation for the most
+accurate and up-to-date documentation. Last time I checked, documentation
+was:
+
+Print information about the image in a format of your choosing. You can
+include the image filename, type, width, height, Exif data, or other image
+attributes by embedding special format characters:
+
+ %O page offset
+ %P page width and height
+ %b file size
+ %c comment
+ %d directory
+ %e filename extension
+ %f filename
+ %g page geometry
+ %h height
+ %i input filename
+ %k number of unique colors
+ %l label
+ %m magick
+ %n number of scenes
+ %o output filename
+ %p page number
+ %q quantum depth
+ %r image class and colorspace
+ %s scene number
+ %t top of filename
+ %u unique temporary filename
+ %w width
+ %x x resolution
+ %y y resolution
+ %z image depth
+ %@ bounding box
+ %# signature
+ %% a percent sign
+
+For example,
+
+ %m:%f %wx%h
+
+displays MIFF:bird.miff 512x480 for an image titled bird.miff and whose
+width is 512 and height is 480.
+
+If the first character of string is @, the format is read from a file titled
+by the remaining characters in the string.
+
+You can also use the following special formatting syntax to print Exif
+information contained in the file:
+
+ %[EXIF:tag]
+
+Where tag can be one of the following:
+
+ * (print all Exif tags, in keyword=data format)
+ ! (print all Exif tags, in tag_number data format)
+ #hhhh (print data for Exif tag #hhhh)
+ ImageWidth
+ ImageLength
+ BitsPerSample
+ Compression
+ PhotometricInterpretation
+ FillOrder
+ DocumentName
+ ImageDescription
+ Make
+ Model
+ StripOffsets
+ Orientation
+ SamplesPerPixel
+ RowsPerStrip
+ StripByteCounts
+ XResolution
+ YResolution
+ PlanarConfiguration
+ ResolutionUnit
+ TransferFunction
+ Software
+ DateTime
+ Artist
+ WhitePoint
+ PrimaryChromaticities
+ TransferRange
+ JPEGProc
+ JPEGInterchangeFormat
+ JPEGInterchangeFormatLength
+ YCbCrCoefficients
+ YCbCrSubSampling
+ YCbCrPositioning
+ ReferenceBlackWhite
+ CFARepeatPatternDim
+ CFAPattern
+ BatteryLevel
+ Copyright
+ ExposureTime
+ FNumber
+ IPTC/NAA
+ ExifOffset
+ InterColorProfile
+ ExposureProgram
+ SpectralSensitivity
+ GPSInfo
+ ISOSpeedRatings
+ OECF
+ ExifVersion
+ DateTimeOriginal
+ DateTimeDigitized
+ ComponentsConfiguration
+ CompressedBitsPerPixel
+ ShutterSpeedValue
+ ApertureValue
+ BrightnessValue
+ ExposureBiasValue
+ MaxApertureValue
+ SubjectDistance
+ MeteringMode
+ LightSource
+ Flash
+ FocalLength
+ MakerNote
+ UserComment
+ SubSecTime
+ SubSecTimeOriginal
+ SubSecTimeDigitized
+ FlashPixVersion
+ ColorSpace
+ ExifImageWidth
+ ExifImageLength
+ InteroperabilityOffset
+ FlashEnergy
+ SpatialFrequencyResponse
+ FocalPlaneXResolution
+ FocalPlaneYResolution
+ FocalPlaneResolutionUnit
+ SubjectLocation
+ ExposureIndex
+ SensingMethod
+ FileSource
+ SceneType")), { :scrolled => true })
+ }
+ tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
+ 0, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
+ update_exif_orientation_check.active = $config['rotate-set-exif'] == 'true'
+ tbl.attach(transcode_videos = Gtk::CheckButton.new(utf8(_("Transcode videos"))).set_active(!$config['transcode-videos'].nil?),
+ 0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ transcode_videos.active = ! $config['transcode-videos'].nil?
+ tbl.attach(transcode_videos_command = Gtk::Entry.new.set_text($config['transcode-videos'] || 'avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality=6 -o %o %f'),
+ 1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(transcode_videos, utf8(_("Whether to transcode videos into the web-album instead of using the original videos directly (can be an interesting disk space saver!). First put the extension of the output video and a colon; then use %f to specify the input and %o the output;
+for example: avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality=6 -o %o %f")), nil)
+ transcode_videos.signal_connect('toggled') {
+ transcode_videos_command.sensitive = transcode_videos.active?
+ }
+ transcode_videos_command.sensitive = transcode_videos.active?
+
+ dialog.vbox.show_all
+ dialog.run { |response|
+ if response == Gtk::Dialog::RESPONSE_OK
+ $config['video-viewer'] = from_utf8(video_viewer_entry.text)
+ $config['image-editor'] = from_utf8(image_editor_entry.text)
+ $config['browser'] = from_utf8(browser_entry.text)
+ if smp_check.active?
+ $config['mproc'] = smp_spin.value.to_i
+ else
+ $config.delete('mproc')
+ end
+ $config['nogestures'] = nogestures_check.active?
+ $config['deleteondisk'] = deleteondisk_check.active?
+
+ $config['convert-enhance'] = from_utf8(enhance_entry.text)
+ $config['comments-format'] = from_utf8(commentsformat_entry.text.gsub(/'/, ''))
+ $config['rotate-set-exif'] = update_exif_orientation_check.active?.to_s
+ if transcode_videos.active?
+ $config['transcode-videos'] = transcode_videos_command.text
+ else
+ $config.delete('transcode-videos')
+ end
+ end
+ }
+ dialog.destroy
+end
+
+def perform_undo
+ if $undo_tb.sensitive?
+ $redo_tb.sensitive = $redo_mb.sensitive = true
+ if not more_undoes = UndoHandler.undo($statusbar)
+ $undo_tb.sensitive = $undo_mb.sensitive = false
+ end
+ end
+end
+
+def perform_redo
+ if $redo_tb.sensitive?
+ $undo_tb.sensitive = $undo_mb.sensitive = true
+ if not more_redoes = UndoHandler.redo($statusbar)
+ $redo_tb.sensitive = $redo_mb.sensitive = false
+ end
+ end
+end
+
+def show_one_click_explanation(intro)
+ show_popup($main_window, utf8(_("<b>One-Click tools.</b>
+
+%s When such a tool is activated
+(<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, <span foreground='darkblue'>Enhance</span> or <span foreground='darkblue'>Delete</span>), clicking
+on a thumbnail will immediately apply the desired action.
+
+Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
+") % intro), { :pos_centered => true })
+end
+
+def create_menu_and_toolbar
+
+ #- menu
+ mb = Gtk::MenuBar.new
+
+ filemenu = Gtk::MenuItem.new(utf8(_("_File")))
+ filesubmenu = Gtk::Menu.new
+ filesubmenu.append(new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
+ filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
+ filesubmenu.append( Gtk::SeparatorMenuItem.new)
+ filesubmenu.append($save = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
+ filesubmenu.append($save_as = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
+ filesubmenu.append( Gtk::SeparatorMenuItem.new)
+ tooltips = Gtk::Tooltips.new
+ filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed photos/videos in current subalbum"))).set_sensitive(false))
+ $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
+ tooltips.set_tip($merge_current, utf8(_("Take into account new/removed photos/videos in currently viewed subalbum")), nil)
+ filesubmenu.append($merge_newsubs = Gtk::ImageMenuItem.new(utf8(_("Merge new subalbums (subdirectories) in current subalbum"))).set_sensitive(false))
+ $merge_newsubs.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
+ tooltips.set_tip($merge_newsubs, utf8(_("Take into account new subalbums in currently viewed subalbum (and only here)")), nil)
+ filesubmenu.append($merge = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed photos/videos"))).set_sensitive(false))
+ $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
+ tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed photos/videos in existing subalbums (anywhere)")), nil)
+ filesubmenu.append( Gtk::SeparatorMenuItem.new)
+ filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
+ $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
+ tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
+ filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
+ $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
+ filesubmenu.append( Gtk::SeparatorMenuItem.new)
+ filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
+ tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
+ filesubmenu.append( Gtk::SeparatorMenuItem.new)
+ filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
+ filemenu.set_submenu(filesubmenu)
+ mb.append(filemenu)
+
+ new.signal_connect('activate') { new_album }
+ open.signal_connect('activate') { open_file_popup }
+ $save.signal_connect('activate') { save_current_file_user }
+ $save_as.signal_connect('activate') { save_as_do }
+ $merge_current.signal_connect('activate') { merge_current }
+ $merge_newsubs.signal_connect('activate') { merge_newsubs }
+ $merge.signal_connect('activate') { merge }
+ $generate.signal_connect('activate') {
+ save_current_file
+ call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
+ utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
+ 'web-album',
+ { :successmsg => $xmldoc.root.attributes['multi-languages'] ?
+ utf8(_("Your web-album is now ready in directory '%s'.
+As multi-languages is activated, you will not be able to view it
+comfortably in your browser though.") % $xmldoc.root.attributes['destination']) :
+ utf8(_("Your web-album is now ready in directory '%s'.
+Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
+ :successmsg_linkurl => $xmldoc.root.attributes['multi-languages'] ? $xmldoc.root.attributes['destination'] :
+ $xmldoc.root.attributes['destination'] + '/index.html',
+ :closure_after => proc {
+ $xmldoc.elements.each('//dir') { |elem|
+ $modified ||= elem.attributes['already-generated'].nil?
+ elem.add_attribute('already-generated', 'true')
+ }
+ UndoHandler.cleanup #- prevent save_changes to mark current dir as not already generated
+ $undo_tb.sensitive = $undo_mb.sensitive = false
+ $redo_tb.sensitive = $redo_mb.sensitive = false
+ save_current_file
+ $generated_outofline = true
+ $main_window.urgency_hint = true
+ }})
+ }
+ $view_wa.signal_connect('activate') {
+ indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
+ if File.exists?(indexhtml)
+ open_url(indexhtml)
+ else
+ show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
+ end
+ }
+ $properties.signal_connect('activate') { properties }
+
+ quit.signal_connect('activate') { try_quit }
+
+ editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
+ editsubmenu = Gtk::Menu.new
+ editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
+ editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
+ editsubmenu.append( Gtk::SeparatorMenuItem.new)
+ editsubmenu.append($sort_by_exif_date = Gtk::ImageMenuItem.new(utf8(_("Sort by EXIF date"))).set_sensitive(false))
+ $sort_by_exif_date.image = Gtk::Image.new("#{$FPATH}/images/sort_by_exif_date.png")
+ editsubmenu.append($remove_all_captions = Gtk::ImageMenuItem.new(utf8(_("Remove all captions in this sub-album"))).set_sensitive(false))
+ $remove_all_captions.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-eraser-16.png")
+ tooltips.set_tip($remove_all_captions, utf8(_("Mainly useful when you don't want to type any caption, that will remove default captions made of filenames")), nil)
+ editsubmenu.append( Gtk::SeparatorMenuItem.new)
+ editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
+ editmenu.set_submenu(editsubmenu)
+ mb.append(editmenu)
+
+ $remove_all_captions.signal_connect('activate') { remove_all_captions }
+ $sort_by_exif_date.signal_connect('activate') { sort_by_exif_date }
+
+ prefs.signal_connect('activate') { preferences }
+
+ helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
+ helpsubmenu = Gtk::Menu.new
+ helpsubmenu.append(one_click = Gtk::ImageMenuItem.new(utf8(_("One-click tools"))))
+ one_click.image = Gtk::Image.new("#{$FPATH}/images/stock-tools-16.png")
+ helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts and mouse gestures"))))
+ speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
+ helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
+ tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
+ helpsubmenu.append(Gtk::SeparatorMenuItem.new)
+ helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
+ helpmenu.set_submenu(helpsubmenu)
+ mb.append(helpmenu)
+
+ one_click.signal_connect('activate') {
+ show_one_click_explanation(_("One-Click tools are available in the toolbar."))
+ }
+
+ speed.signal_connect('activate') {
+ show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts:</span>
+
+<span foreground='darkblue'>Tab</span>: go to next image caption and select text (begin typing to erase current text!)
+<span foreground='darkblue'>Shift-Tab</span>: go to previous image caption
+<span foreground='darkblue'>Control-Left/Right/Up/Down</span>: go to specified direction's image caption
+<span foreground='darkblue'>Control-Enter</span>: for a photo, open larger view; for a video, launch player
+<span foreground='darkblue'>Control-Delete</span>: delete image
+<span foreground='darkblue'>Shift-Left/Right/Up/Down</span>: move image left/right/up/down
+<span foreground='darkblue'>Alt-Left/Right</span>: rotate image clockwise/counter-clockwise
+<span foreground='darkblue'>Control-z</span>: undo
+<span foreground='darkblue'>Control-r</span>: redo
+
+<span size='large' weight='bold'>Mouse gestures:</span>
+
+Mouse gestures are 'unusual' mouse movements triggering special actions, and are great
+for speeding up your editions. If bothered, you can disable them from Edit/Preferences.
+
+<span foreground='darkblue'>Left click, drag to the right, release</span>: rotate image clockwise
+<span foreground='darkblue'>Left click, drag to the left, release</span>: rotate image counter-clockwise
+<span foreground='darkblue'>Left click, drag to the bottom, release</span>: remove image
+<span foreground='darkblue'>Left click, hold left button, right click</span>: undo
+<span foreground='darkblue'>Right click, hold right button, left click</span>: redo
+")), { :pos_centered => true, :not_transient => true })
+ }
+
+ tutos.signal_connect('activate') {
+ open_url('http://booh.org/tutorial')
+ }
+
+ about.signal_connect('activate') { call_about }
+
+
+ #- toolbar
+ tb = Gtk::Toolbar.new
+
+ tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
+ open.label = utf8(_("Open")) #- to avoid missing gtk2 l10n catalogs
+ open.menu = Gtk::Menu.new
+ open.signal_connect('clicked') { open_file_popup }
+ open.signal_connect('show-menu') {
+ lastopens = Gtk::Menu.new
+ j = 0
+ if $config['last-opens']
+ $config['last-opens'].reverse.each { |e|
+ lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
+ item.signal_connect('activate') {
+ if ask_save_modifications(utf8(_("Save this album?")),
+ utf8(_("Do you want to save the changes to this album?")),
+ { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
+ push_mousecursor_wait
+ msg = open_file_user(from_utf8(e))
+ pop_mousecursor
+ if msg
+ show_popup($main_window, msg)
+ end
+ end
+ }
+ j += 1
+ }
+ lastopens.show_all
+ end
+ open.menu = lastopens
+ }
+
+ tb.insert(-1, Gtk::SeparatorToolItem.new)
+
+ tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
+ $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
+ $r90.label = utf8(_("Rotate"))
+ tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
+ $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
+ $r270.label = utf8(_("Rotate"))
+ tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
+ $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
+ $enhance.label = utf8(_("Enhance"))
+ tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
+ $delete.label = utf8(_("Delete")) #- to avoid missing gtk2 l10n catalogs
+ tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
+ nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
+ nothing.label = utf8(_("None"))
+
+ tb.insert(-1, Gtk::SeparatorToolItem.new)
+
+ tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
+ tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
+
+
+ $undo_tb.signal_connect('clicked') { perform_undo }
+ $undo_mb.signal_connect('activate') { perform_undo }
+ $redo_tb.signal_connect('clicked') { perform_redo }
+ $redo_mb.signal_connect('activate') { perform_redo }
+
+ one_click_explain_try = proc {
+ if !$config['one-click-explained']
+ show_one_click_explanation(_("You have just clicked on a One-Click tool."))
+ $config['one-click-explained'] = true
+ end
+ }
+
+ $r90.signal_connect('toggled') {
+ if $r90.active?
+ set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
+ one_click_explain_try.call
+ $r270.active = false
+ $enhance.active = false
+ $delete.active = false
+ nothing.sensitive = true
+ else
+ if !$r270.active? && !$enhance.active? && !$delete.active?
+ set_mousecursor_normal
+ nothing.sensitive = false
+ else
+ nothing.sensitive = true
+ end
+ end
+ true
+ }
+ $r270.signal_connect('toggled') {
+ if $r270.active?
+ set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
+ one_click_explain_try.call
+ $r90.active = false
+ $enhance.active = false
+ $delete.active = false
+ nothing.sensitive = true
+ else
+ if !$r90.active? && !$enhance.active? && !$delete.active?
+ set_mousecursor_normal
+ nothing.sensitive = false
+ else
+ nothing.sensitive = true
+ end
+ end
+ }
+ $enhance.signal_connect('toggled') {
+ if $enhance.active?
+ set_mousecursor(Gdk::Cursor::SPRAYCAN)
+ one_click_explain_try.call
+ $r90.active = false
+ $r270.active = false
+ $delete.active = false
+ nothing.sensitive = true
+ else
+ if !$r90.active? && !$r270.active? && !$delete.active?
+ set_mousecursor_normal
+ nothing.sensitive = false
+ else
+ nothing.sensitive = true
+ end
+ end
+ }
+ $delete.signal_connect('toggled') {
+ if $delete.active?
+ set_mousecursor(Gdk::Cursor::PIRATE)
+ one_click_explain_try.call
+ $r90.active = false
+ $r270.active = false
+ $enhance.active = false
+ nothing.sensitive = true
+ else
+ if !$r90.active? && !$r270.active? && !$enhance.active?
+ set_mousecursor_normal
+ nothing.sensitive = false
+ else
+ nothing.sensitive = true
+ end
+ end
+ }
+ nothing.signal_connect('clicked') {
+ $r90.active = $r270.active = $enhance.active = $delete.active = false
+ set_mousecursor_normal
+ }
+
+ return [ mb, tb ]
+end
+
+def gtk_thread_protect(&proc)
+ if Thread.current == Thread.main
+ proc.call
+ else
+ $protect_gtk_pending_calls.synchronize {
+ $gtk_pending_calls << proc
+ }
+ end
+end
+
+def gtk_thread_flush
+ #- try to lock. we cannot synchronize blindly because this might be called from
+ #- within the timeout flushing procs. if this is the case, not doing anything
+ #- should be ok since the timeout is already flushing them all.
+ if $protect_gtk_pending_calls.try_lock
+ for closure in $gtk_pending_calls
+ closure.call
+ end
+ $gtk_pending_calls = []
+ $protect_gtk_pending_calls.unlock
+ end
+end
+
+def ask_password_protect
+ value = $xmldir.attributes['password-protect']
+
+ dialog = Gtk::Dialog.new(utf8(_("Password protect this sub-album")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ lbl = Gtk::Label.new
+ lbl.markup = utf8(
+_("You can choose to <b>password protect</b> the sub-album '%s' (only available
+if you plan to publish your web-album with an Apache web-server). This will use
+the .htaccess/.htpasswd feature of Apache (not so strongly crypted password, but
+generally ok for protecting web contents). Users will be prompted with a dialog
+asking for a username and a password, failure to give the correct pair will
+block access.
+") % File.basename($current_path))
+ dialog.vbox.add(lbl)
+ dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("free access")))).
+ add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("password protect with password file:")))).
+ add(file = Gtk::Entry.new)))
+ dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0.5, 0.2).add(Gtk::HBox.new.add(bt_help = Gtk::Button.new(utf8(_("help about password file")))).
+ add(Gtk::Label.new).
+ add(bt_gen = Gtk::Button.new(utf8(_("generate a password file"))))))
+ dialog.window_position = Gtk::Window::POS_MOUSE
+ dialog.show_all
+ if value.nil?
+ rb_no.active = true
+ else
+ rb_yes.active = true
+ file.text = value
+ end
+
+ bt_help.signal_connect('clicked') {
+ show_popup(dialog, utf8(
+_("Password protection proposed here uses the .htaccess/.htpasswd features
+proposed by Apache. So first, be sure you will publish your web-album on an
+Apache web-server. Second, you will need to have a .htpasswd file accessible
+by Apache somewhere on the web-server disks. The password file you must
+provide in the dialog when choosing to password protect is the full absolute
+path to access this file <b>on the web-server</b> (not on your machine). Note
+that if you use a relative path, it will be considered relative to the
+Document Root of the Apache configuration.")))
+ }
+
+ bt_gen.signal_connect('clicked') {
+ gendialog = Gtk::Dialog.new(utf8(_("Generate a password file")),
+ dialog,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ lbl = Gtk::Label.new
+ lbl.text = utf8(
+_("I can generate a password file (.htpasswd for Apache) for you. Just type
+the username and password you wish to put in it below and validate."))
+ gendialog.vbox.add(lbl)
+ gendialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(Gtk::Label.new(utf8(_('Username:')))).
+ add(user = Gtk::Entry.new).
+ add(Gtk::Label.new(utf8(_('Password:')))).
+ add(pass = Gtk::Entry.new)))
+ pass.visibility = false
+ gendialog.window_position = Gtk::Window::POS_MOUSE
+ gendialog.show_all
+ gendialog.run { |response|
+ u = user.text
+ p = pass.text
+ gendialog.destroy
+ if response == Gtk::Dialog::RESPONSE_OK
+ def rand_letter
+ ary = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + [ '.', '/' ]
+ return ary[rand(ary.length)]
+ end
+ fout = Tempfile.new("htpasswd")
+ fout.write("#{u}:#{p.crypt(rand_letter + rand_letter)}\n")
+ fout.close
+ File.chmod(0644, fout.path)
+ show_popup(dialog, utf8(
+_("The file <b>%s</b> now contains the username and the crypted password. Now
+copy it to a suitable location on the machine hosting the Apache web-server (better not
+below the Document Root), and specify this location in the password protect dialog.") % fout.path), { :selectable => true })
+ end
+ }
+ }
+
+ dialog.run { |response|
+ if rb_no.active?
+ newval = nil
+ else
+ newval = file.text
+ end
+ dialog.destroy
+ if response == Gtk::Dialog::RESPONSE_OK && value != newval
+ $modified = true
+ msg 3, "changing password protection of #{$current_path} to #{newval}"
+ if newval.nil?
+ $xmldir.delete_attribute('password-protect')
+ else
+ $xmldir.add_attribute('password-protect', newval)
+ end
+ save_undo(_("set password protection for %s") % File.basename($current_path),
+ proc {
+ if value.nil?
+ $xmldir.delete_attribute('password-protect')
+ else
+ $xmldir.add_attribute('password-protect', value)
+ end
+ proc {
+ if newval.nil?
+ $xmldir.delete_attribute('password-protect')
+ else
+ $xmldir.add_attribute('password-protect', newval)
+ end
+ }
+ })
+ show_password_protections
+ end
+ }
+end
+
+def create_main_window
+
+ mb, tb = create_menu_and_toolbar
+
+ $albums_tv = Gtk::TreeView.new
+ $albums_tv.set_size_request(120, -1)
+ $albums_tv.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }))
+ $albums_tv.append_column(tcol = Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, { :text => 0 }))
+ $albums_tv.expander_column = tcol
+ $albums_tv.set_headers_visible(false)
+ $albums_tv.selection.signal_connect('changed') { |w|
+ push_mousecursor_wait
+ save_changes
+ iter = w.selected
+ if !iter
+ msg 3, "no selection"
+ else
+ $current_path = $albums_ts.get_value(iter, 1)
+ change_dir
+ end
+ pop_mousecursor
+ }
+
+# offset = "0:0"
+# Gtk.timeout_add(1000) {
+# puts "trying offset #{offset}"
+# iter = $albums_ts.get_iter(offset)
+# if iter
+# puts "...ok at offset #{offset}"
+# $current_path = $albums_ts.get_value(iter, 1)
+# change_dir
+# end
+# if offset == "0:0"
+# offset = "0:1"
+# else
+# offset = "0:0"
+# end
+# }
+
+ $albums_tv.signal_connect('button-release-event') { |w, event|
+ if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3 && !$current_path.nil?
+ menu = Gtk::Menu.new
+ menu.append(passprotect = Gtk::ImageMenuItem.new(utf8(_("Password protect"))))
+ passprotect.image = Gtk::Image.new("#{$FPATH}/images/galeon-secure.png")
+ passprotect.signal_connect('activate') { ask_password_protect }
+ menu.append(restore = Gtk::ImageMenuItem.new(utf8(_("Restore deleted photos/videos/subalbums"))))
+ restore.image = Gtk::Image.new("#{$FPATH}/images/restore.png")
+ restore.signal_connect('activate') { restore_deleted }
+ menu.append(Gtk::SeparatorMenuItem.new)
+ menu.append(delete = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
+ delete.signal_connect('activate') {
+ if show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
+ delete_current_subalbum
+ end
+ }
+ menu.show_all
+ menu.popup(nil, nil, event.button, event.time)
+ end
+ }
+
+ $albums_ts = Gtk::TreeStore.new(String, String, Gdk::Pixbuf)
+ $albums_tv.set_model($albums_ts)
+ $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
+
+ albums_sw = Gtk::ScrolledWindow.new(nil, nil)
+ albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
+ albums_sw.add_with_viewport($albums_tv)
+
+ $notebook = Gtk::Notebook.new
+ create_subalbums_page
+ $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
+ create_auto_table
+ $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
+ $notebook.show_all
+ $notebook.signal_connect('switch-page') { |w, page, num|
+ if num == 0
+ $delete.active = false
+ $delete.sensitive = false
+ else
+ $delete.sensitive = true
+ end
+ if $xmldir && $subalbums_edits[$xmldir.attributes['path']] && textview = $subalbums_edits[$xmldir.attributes['path']][:editzone]
+ if num == 0
+ textview.buffer.text = $thumbnails_title.buffer.text
+ else
+ if $notebook.get_tab_label($autotable_sw).sensitive?
+ $thumbnails_title.buffer.text = textview.buffer.text
+ end
+ end
+ end
+ }
+
+ paned = Gtk::HPaned.new
+ paned.pack1(albums_sw, false, false)
+ paned.pack2($notebook, true, true)
+
+ main_vbox = Gtk::VBox.new(false, 0)
+ main_vbox.pack_start(mb, false, false)
+ main_vbox.pack_start(tb, false, false)
+ main_vbox.pack_start(paned, true, true)
+ main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
+
+ $main_window = create_window
+ $main_window.add(main_vbox)
+ $main_window.signal_connect('delete-event') {
+ try_quit({ :disallow_cancel => true })
+ }
+ $main_window.signal_connect('focus-in-event') {
+ $main_window.urgency_hint = false
+ }
+
+ #- read/save size and position of window
+ if $config['pos-x'] && $config['pos-y']
+ $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
+ else
+ $main_window.window_position = Gtk::Window::POS_CENTER
+ end
+ msg 3, "size: #{$config['width']}x#{$config['height']}"
+ $main_window.set_default_size(($config['width'] || 800).to_i, ($config['height'] || 600).to_i)
+ $main_window.signal_connect('configure-event') {
+ msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
+ x, y = $main_window.window.root_origin
+ width, height = $main_window.window.size
+ $config['pos-x'] = x
+ $config['pos-y'] = y
+ $config['width'] = width
+ $config['height'] = height
+ false
+ }
+
+ $protect_gtk_pending_calls = Mutex.new
+ $gtk_pending_calls = []
+ Gtk.timeout_add(100) {
+ $protect_gtk_pending_calls.synchronize {
+ for closure in $gtk_pending_calls
+ closure.call
+ end
+ $gtk_pending_calls = []
+ }
+ true
+ }
+
+ $statusbar.push(0, utf8(_("Ready.")))
+ $main_window.show_all
+end
+
+
+if str = Gtk.check_version(2, 8, 0)
+ puts "This program requires GTK+ 2.8.0 or later"
+ puts str
+ exit
+end
+
+
+handle_options
+Thread.abort_on_exception = true
+read_config
+
+Gtk.init
+create_main_window
+
+check_config
+
+if ARGV[0]
+ open_file_user(ARGV[0])
+end
+
+Gtk.main
+
+write_config
Property changes on: packages/booh/branches/upstream/current/bin/booh
___________________________________________________________________
Name: svn:executable
+
Added: packages/booh/branches/upstream/current/bin/booh-backend
===================================================================
--- packages/booh/branches/upstream/current/bin/booh-backend (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh-backend 2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,1429 @@
+#! /usr/bin/ruby
+#
+# * BOOH *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'getoptlong'
+require 'gettext'
+require 'gettext/locale'
+include GetText
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+require 'booh/html-merges'
+
+#- bind text domain as soon as possible because some _() functions are called early to build data structures
+bindtextdomain("booh")
+#- save locale for restoring for multi languages
+$default_locale = Locale.get
+
+#- options
+$options = [
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
+ [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
+
+ [ '--source', '-s', GetoptLong::REQUIRED_ARGUMENT, _("Directory which contains original photos/videos as files or subdirs") ],
+ [ '--destination', '-d', GetoptLong::REQUIRED_ARGUMENT, _("Directory which will contain the web-album; if it already exits, then all existing files and directories inside it will be removed!") ],
+
+ [ '--theme', '-t', GetoptLong::REQUIRED_ARGUMENT, _("Select HTML theme to use") ],
+ [ '--config', '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing photos and videos within directories with captions") ],
+ [ '--config-skel', '-k', GetoptLong::REQUIRED_ARGUMENT, _("Filename where the script will output a config skeleton") ],
+ [ '--merge-config', '-M', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to merge new/removed photos/videos from --source, and change theme info") ],
+ [ '--merge-config-onedir', '-O', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, for merging the subdir specified with --dir") ],
+ [ '--merge-config-subdirs', '-U', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, for merging the new subdirs down the subdir specified with --dir") ],
+ [ '--dir', '-D', GetoptLong::REQUIRED_ARGUMENT, _("Directory for merge with --merge-config-onedir or --merge-config-subdirs") ],
+ [ '--use-config', '-u', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing, where to change theme info") ],
+ [ '--force', '-f', GetoptLong::NO_ARGUMENT, _("Force generation of album even if the GUI marked some directories as already generated") ],
+
+ [ '--sizes', '-S', GetoptLong::REQUIRED_ARGUMENT, _("Specify the list of images sizes to use instead of all specified in the theme (this is a comma-separated list)") ],
+ [ '--multi-languages', '-L', GetoptLong::REQUIRED_ARGUMENT, _("Specify the list of languages to support (uses Apache MultiViews); this is a comma-separated list of supported languages, with last element used as the fallback language; for example: 'fr,eo,en,en'; supported languages: %s") % SUPPORTED_LANGUAGES.join(', ') ],
+ [ '--thumbnails-per-row', '-T', GetoptLong::REQUIRED_ARGUMENT, _("Specify the amount of thumbnails per row in the thumbnails page (if applicable in theme)") ],
+ [ '--thumbnails-per-page', '-p', GetoptLong::REQUIRED_ARGUMENT, _("Specify the amount of thumbnails per page in the thumbnails page, after which split occurs") ],
+ [ '--optimize-for-32', '-o', GetoptLong::NO_ARGUMENT, _("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of photos from point-and-shoot cameras - also called compact cameras - is 4/3, whereas photos from SLR cameras - also called reflex cameras - is 3/2)") ],
+ [ '--transcode-videos', '-r', GetoptLong::REQUIRED_ARGUMENT, _("Transcode videos with given external program; %f is the placeholder for the input video, %o for the output video; before the external program, the output video extension should be given followed by a colon") ],
+ [ '--index-link', '-l', GetoptLong::REQUIRED_ARGUMENT, _("Specify the HTML markup to use on the bottom of pages for a small link returning to wherever you see fit in your website (or somewhere else)") ],
+ [ '--made-with', '-n', GetoptLong::REQUIRED_ARGUMENT, _("Specify the HTML markup to use on the bottom of pages for a small 'made with' message") ],
+ [ '--comments-format','-c', GetoptLong::REQUIRED_ARGUMENT, _("Specify comments format to use for images instead of only filename when creating new albums; use ImageMagick's format") ],
+
+ [ '--mproc', '-m', GetoptLong::REQUIRED_ARGUMENT, _("Specify the number of processors for multi-processors machines") ],
+
+ [ '--for-gui', '-g', GetoptLong::NO_ARGUMENT, _("Do the minimum work to be able to see the album under the GUI (don't generate all thumbnails)") ],
+
+ [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+ [ '--info-pipe', '-i', GetoptLong::REQUIRED_ARGUMENT, _("Name a file where to write information about what's going on (used by the GUI)") ],
+]
+
+#- default values for some globals
+$switches = []
+$stdout.sync = true
+$no_identify = false
+$ignore_videos = false
+$forgui = false
+$hardlinks_ok = true
+
+def usage
+ puts _("Usage: %s [OPTION]...") % File.basename($0)
+ $options.each { |ary|
+ printf " %3s, %-18s %s\n", ary[1], ary[0], ary[3]
+ }
+end
+
+def handle_options
+ parser = GetoptLong.new
+ parser.set_options(*$options.collect { |ary| ary[0..2] })
+ begin
+ parser.each_option do |name, arg|
+ case name
+ when '--help'
+ usage
+ exit(0)
+
+ when '--version'
+ puts _("Booh version %s
+
+Copyright (c) 2005-2008 Guillaume Cottenceau.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
+
+ exit(0)
+
+ when '--source'
+ $source = File.expand_path(arg.sub(%r|/$|, ''))
+ if !File.directory?($source)
+ die _("Argument to --source must be a directory")
+ end
+ when '--destination'
+ $dest = File.expand_path(arg.sub(%r|/$|, ''))
+ if File.exists?($dest) && !File.directory?($dest)
+ die _("If --destination exists, it must be a directory")
+ end
+ if $dest != make_dest_filename($dest)
+ die _("Sorry, destination directory can't contain non simple alphanumeric characters.")
+ end
+# when '--clean'
+# system("rm -rf #{$dest}")
+
+ when '--theme'
+ $theme = arg
+ when '--config'
+ arg = File.expand_path(arg)
+ if File.readable?(arg)
+ $xmldoc = REXML::Document.new File.new(arg)
+ $mode = 'use_config'
+ else
+ die _('Config file does not exist or is unreadable.')
+ end
+ when '--config-skel'
+ arg = File.expand_path(arg)
+ if File.exists?(arg)
+ if File.directory?(arg)
+ die _("Config skeleton file (%s) already exists and is a directory! Please change the filename.") % arg
+ else
+ msg 1, _("Config skeleton file already exists, backuping to %s.backup") % arg
+ File.rename(arg, "#{arg}.backup")
+ end
+ end
+ $config_writeto = arg
+ $mode = 'gen_config'
+ when '--merge-config'
+ arg = File.expand_path(arg)
+ if File.readable?(arg)
+ msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
+ $xmldoc = REXML::Document.new File.new(arg)
+ File.rename(arg, "#{arg}.backup")
+ $config_writeto = arg
+ $mode = 'merge_config'
+ else
+ die _('Config file does not exist or is unreadable.')
+ end
+ when '--merge-config-onedir'
+ arg = File.expand_path(arg)
+ if File.readable?(arg)
+ msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
+ $xmldoc = REXML::Document.new File.new(arg)
+ File.rename(arg, "#{arg}.backup")
+ $config_writeto = arg
+ $mode = 'merge_config_onedir'
+ else
+ die _('Config file does not exist or is unreadable.')
+ end
+ when '--merge-config-subdirs'
+ arg = File.expand_path(arg)
+ if File.readable?(arg)
+ msg 2, _("Merge config notice: backuping current config file to %s.backup") % arg
+ $xmldoc = REXML::Document.new File.new(arg)
+ File.rename(arg, "#{arg}.backup")
+ $config_writeto = arg
+ $mode = 'merge_config_subdirs'
+ else
+ die _('Config file does not exist or is unreadable.')
+ end
+ when '--dir'
+ arg = File.expand_path(arg)
+ if !File.readable?(arg)
+ die _('Specified directory to merge with --dir is not readable')
+ else
+ $onedir = arg
+ end
+ when '--use-config'
+ arg = File.expand_path(arg)
+ if File.readable?(arg)
+ msg 2, _("Use config notice: backuping current config file to %s.backup") % arg
+ $xmldoc = REXML::Document.new File.new(arg)
+ File.rename(arg, "#{arg}.backup")
+ $config_writeto = arg
+ $mode = 'use_config_changetheme'
+ else
+ die _('Config file does not exist or is unreadable.')
+ end
+
+ when '--sizes'
+ $limit_sizes = arg
+
+ when '--multi-languages'
+ parts = arg.split(',')
+ if parts.size == 0 || parts.find_all { |e| ! SUPPORTED_LANGUAGES.include?(e.strip) }.size > 0
+ die _("--multi-languages: argument must be a comma-separated list of supported languages, with last element used as the fallback language; for example: 'fr,eo,en,en'; supported languages: %s") % SUPPORTED_LANGUAGES.join(', ')
+ end
+ $multi_languages = [ parts[0..-2], parts[-1] ]
+
+ when '--thumbnails-per-row'
+ $N_per_row = arg
+
+ when '--thumbnails-per-page'
+ $N_per_page = arg
+
+ when '--optimize-for-32'
+ $optimize_for_32 = true
+
+ when '--transcode-videos'
+ parts = arg.split(':', 2)
+ if parts.size != 2 || parts[0] =~ / / || arg !~ /%f/ || arg !~ /%o/
+ die _("--transcode-videos: argument must be the external program for transcoding, and contain %f for the input video, %o for the output video, and before the external program, the output video extension should be given followed by a colon")
+ end
+ $transcode_videos = arg
+
+ when '--made-with'
+ $madewith = arg
+
+ when '--index-link'
+ $indexlink = arg
+
+ when '--comments-format'
+ $commentsformat = arg
+
+ when '--force'
+ $force = true
+
+ when '--mproc'
+ $mproc = arg.to_i
+ $pids = []
+
+ when '--for-gui'
+ $forgui = true
+
+ when '--verbose-level'
+ $verbose_level = arg.to_i
+
+ when '--info-pipe'
+ $info_pipe = File.open(arg, File::WRONLY)
+ $info_pipe.sync = true
+ end
+ end
+ rescue
+ puts $!
+ usage
+ exit(1)
+ end
+
+ if !$source && $xmldoc
+ $source = from_utf8($xmldoc.root.attributes['source']).sub(%r|/$|, '')
+ $dest = from_utf8($xmldoc.root.attributes['destination']).sub(%r|/$|, '')
+ $theme ||= $xmldoc.root.attributes['theme']
+ $limit_sizes ||= $xmldoc.root.attributes['limit-sizes']
+ if $mode == 'use_config' || $mode =~ /^merge_config/
+ $optimize_for_32 = !$xmldoc.root.attributes['optimize-for-32'].nil?
+ $N_per_row = $xmldoc.root.attributes['thumbnails-per-row']
+ languages = $xmldoc.root.attributes['multi-languages']
+ if languages
+ languages = languages.split(',')
+ $multi_languages = [ languages[0..-2], languages[-1] ]
+ end
+ $N_per_page = $xmldoc.root.attributes['thumbnails-per-page']
+ $madewith = $xmldoc.root.attributes['made-with']
+ $indexlink = $xmldoc.root.attributes['index-link']
+ end
+ end
+
+ if $mode == 'merge_config_onedir' && !$onedir
+ die _("Missing --dir for --merge-config-onedir")
+ end
+ if $mode == 'merge_config_subdirs' && !$onedir
+ die _("Missing --dir for --merge-config-subdirs")
+ end
+
+ if !$source
+ usage
+ exit(0)
+ end
+ if !$dest
+ die _("Missing --destination parameter.")
+ end
+ if !$theme
+ $theme = 'simple'
+ end
+
+ select_theme($theme, $limit_sizes, $optimize_for_32, $N_per_row)
+
+ if !$xmldoc
+ $xmldoc = Document.new "<booh/>"
+ $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
+ $xmldoc.root.add_attribute('version', $VERSION)
+ $xmldoc.root.add_attribute('source', $source)
+ $xmldoc.root.add_attribute('destination', $dest)
+ $xmldoc.root.add_attribute('theme', $theme)
+ if $limit_sizes
+ $xmldoc.root.add_attribute('limit-sizes', $limit_sizes)
+ end
+ if $multi_languages
+ $xmldoc.root.add_attribute('multi-languages', $multi_languages[0].join(',') + ',' + $multi_languages[1])
+ end
+ if $optimize_for_32
+ $xmldoc.root.add_attribute('optimize-for-32', 'true')
+ end
+ if $N_per_row
+ $xmldoc.root.add_attribute('thumbnails-per-row', $N_per_row)
+ end
+ if $N_per_page
+ $xmldoc.root.add_attribute('thumbnails-per-page', $N_per_page)
+ end
+ if $madewith
+ $xmldoc.root.add_attribute('made-with', $madewith)
+ end
+ if $indexlink
+ $xmldoc.root.add_attribute('index-link', $indexlink)
+ end
+ $mode = 'gen_config'
+ end
+
+ if $mode == 'merge_config' || $mode == 'use_config_changetheme'
+ $xmldoc.root.add_attribute('theme', $theme)
+ $xmldoc.root.add_attribute('version', $VERSION)
+ if $limit_sizes
+ $xmldoc.root.add_attribute('limit-sizes', $limit_sizes)
+ else
+ $xmldoc.root.delete_attribute('limit-sizes')
+ end
+ if $multi_languages
+ $xmldoc.root.add_attribute('multi-languages', $multi_languages[0].join(',') + ',' + $multi_languages[1])
+ else
+ $xmldoc.root.delete_attribute('multi-languages')
+ end
+
+ if $optimize_for_32
+ $xmldoc.root.add_attribute('optimize-for-32', 'true')
+ else
+ $xmldoc.root.delete_attribute('optimize-for-32')
+ end
+ if $N_per_row
+ $xmldoc.root.add_attribute('thumbnails-per-row', $N_per_row)
+ else
+ $xmldoc.root.delete_attribute('thumbnails-per-row')
+ end
+ if $N_per_page
+ $xmldoc.root.add_attribute('thumbnails-per-page', $N_per_page)
+ else
+ $xmldoc.root.delete_attribute('thumbnails-per-page')
+ end
+ if $madewith
+ $xmldoc.root.add_attribute('made-with', $madewith)
+ else
+ $xmldoc.root.delete_attribute('made-with')
+ end
+ if $indexlink
+ $xmldoc.root.add_attribute('index-link', $indexlink)
+ else
+ $xmldoc.root.delete_attribute('index-link')
+ end
+ end
+
+ if $transcode_videos
+ $xmldoc.root.add_attribute('transcode-videos', $transcode_videos)
+ else
+ $xmldoc.root.delete_attribute('transcode-videos')
+ end
+
+ if $madewith
+ $madewith = $madewith.gsub('%booh', '"http://booh.org/"')
+ end
+ if $multi_languages
+ $htmlsuffix = ''
+ else
+ $htmlsuffix = '.html'
+ end
+end
+
+def read_config
+ $config = {}
+end
+
+def write_config
+end
+
+def info(value)
+ if $info_pipe
+ $info_pipe.puts(value)
+ end
+end
+
+def die(value)
+ if $info_pipe
+ $info_pipe.puts("die: " + value)
+ end
+ die_ value
+end
+
+def check_installation
+ if !system("which convert >/dev/null 2>/dev/null")
+ die _("The program 'convert' is needed. Please install it.
+It is generally available with the 'ImageMagick' software package.")
+ end
+ if !system("which identify >/dev/null 2>/dev/null")
+ msg 1, _("the program 'identify' is needed to get images sizes and EXIF data. Please install it.
+It is generally available with the 'ImageMagick' software package.")
+ $no_identify = true
+ end
+ missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
+ if missing != []
+ msg 1, _("the following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')
+ $ignore_videos = true
+ end
+end
+
+def replace_line(surround, keyword, line)
+ begin
+ contents = eval "$#{keyword}"
+ line.sub!(/#{surround}#{keyword}#{surround}/, contents)
+ rescue TypeError
+ die _("No '%s' found for substitution") % keyword
+ end
+end
+
+def build_html_skeletons
+ $html_images = File.open("#{$FPATH}/themes/#{$theme}/skeleton_image.html").readlines
+ $html_thumbnails = File.open("#{$FPATH}/themes/#{$theme}/skeleton_thumbnails.html").readlines
+ $html_index = File.open("#{$FPATH}/themes/#{$theme}/skeleton_index.html").readlines
+ for line in $html_images + $html_thumbnails + $html_index
+ while line =~ /~~~(\w+)~~~/
+ replace_line('~~~', $1, line)
+ end
+ end
+end
+
+def find_caption_value(xmldir, filename)
+ if cap = xmldir.elements["*[@filename='#{utf8(filename)}']"].attributes['caption']
+ return cap.gsub("\n", '<br/>')
+ else
+ return nil
+ end
+end
+
+def find_captions(xmldir, images)
+ return images.collect { |img| find_caption_value(xmldir, img) }
+end
+
+#- stolen from CVSspam
+def urlencode(text)
+ text.sub(/[^a-zA-Z0-9\-,.*_\/]/) do
+ "%#{sprintf('%2X', $&[0])}"
+ end
+end
+
+def all_images_sizes
+ return $limit_sizes =~ /original/ ? $images_size + [ { 'name' => 'original' } ] : $images_size
+end
+
+def html_reload_to_thumbnails
+ html_reload_to_thumbnails = $preferred_size_reloader.clone
+ html_reload_to_thumbnails.gsub!(/~~theme~~/, $theme)
+ html_reload_to_thumbnails.gsub!(/~~default_size~~/, $default_size['name'])
+ html_reload_to_thumbnails.gsub!(/~~htmlsuffix~~/, $htmlsuffix)
+ html_reload_to_thumbnails.gsub!(/~~all_sizes~~/, all_images_sizes.collect { |s| "\"#{size2js(s['name'])}\"" }.join(', '))
+ size_auto_chooser = '';
+ all_images_sizes.find_all { |s| s.has_key?('optimizedforwidth') }.
+ sort { |a,b| b['optimizedforwidth'].to_i <=> a['optimizedforwidth'].to_i }.
+ each { |s| size_auto_chooser += "if (w + 50 > #{s['optimizedforwidth']}) { return 'thumbnails-#{size2js(s['name'])}-0#{$htmlsuffix}'; }\n" }
+ html_reload_to_thumbnails.gsub!(/~~size_auto_chooser~~/, size_auto_chooser)
+ return html_reload_to_thumbnails
+end
+
+def discover_iterations(iterations, line)
+ if line =~ /~~iterate(\d)_open(_max(\d+|N))?~~/
+ for iter in iterations.values
+ if iter['open']
+ iter['open'] = false
+ iter['close_wait'] = $1.to_i
+ end
+ end
+ max = $3 == 'N' ? ($N_per_row || $default_N) : $3
+ iterations[$1.to_i] = { 'open' => true, 'max' => max, 'opening' => '', 'closing' => '' }
+ if $1.to_i == 1
+ line.sub!(/.*/, '~~thumbnails~~')
+ else
+ line.sub!(/.*/, '')
+ end
+ elsif line =~ /~~iterate(\d)_close~~/
+ iterations[$1.to_i]['open'] = false;
+ iterations[$1.to_i]['close'] = true;
+ line.sub!(/.*/, '')
+ else
+ for iter in iterations.values
+ if iter['open']
+ iter['opening'] += line
+ line.sub!(/.*/, '')
+ end
+ if !iter['close'] && iter['close_wait'] && iterations[iter['close_wait']]['close']
+ iter['closing'] += line
+ line.sub!(/.*/, '')
+ end
+ end
+ end
+end
+
+def reset_iterations(iterations)
+ for iter in iterations.values
+ iter['value'] = 0
+ end
+end
+
+def run_iterations(iterations, amount)
+ html = ''
+ should_rerun = false
+ for level in iterations.keys.sort
+ if iterations[level]['value'] == 0
+ html += iterations[level]['opening']
+ elsif level == iterations.keys.max
+ if !iterations[level]['max'] || iterations[level]['max'] && iterations[level]['value'] + amount <= iterations[level]['max'].to_i
+ html += iterations[level]['opening']
+ else
+ should_rerun = true
+ end
+ end
+ iterations[level]['value'] += amount
+ if iterations[level]['max'] && iterations[level]['value'] > iterations[level]['max'].to_i
+ iterations[level]['value'] = 0
+ iterations[level-1]['value'] = 0
+ html += iterations[level-1]['closing']
+ end
+ end
+ if should_rerun
+ return html + run_iterations(iterations, amount)
+ else
+ return html
+ end
+end
+
+def close_iterations(iterations)
+ html = ''
+ for level in iterations.keys.sort.reverse
+ html += iterations[level]['closing']
+ end
+ return html
+end
+
+def img_element(fullpath)
+ if size = get_image_size(fullpath)
+ sizespec = 'width="' + size[:x].to_s + '" height="' + size[:y].to_s + '"'
+ else
+ sizespec = ''
+ end
+ return '<img src="' + File.basename(fullpath) + '" ' + sizespec + ' alt="image"/>'
+end
+
+def size2js(name)
+ return name.gsub(/-/, '')
+end
+
+def substitute_html_sizes(html, sizeobj, type, suffix)
+ sizestrings = []
+ if $images_size.length > 1 || (type == 'image' && $limit_sizes =~ /original/)
+ for sizeobj2 in $images_size
+ sizejs = size2js(sizeobj2['name'])
+ sizen = defer_translation(sizename(sizeobj2['name'], false))
+ if sizeobj != sizeobj2
+ if type == 'thumbnails'
+ sizestrings << '<a href="thumbnails-' + sizejs + suffix + $htmlsuffix + '" onclick="set_preferred_size(\'' + sizejs + '\')">' + sizen + '</a>'
+ else
+ sizestrings << '<a id="link' + sizejs + '" onclick="set_preferred_size(\'' + sizejs + '\')">' + sizen + '</a>'
+ end
+ else
+ sizestrings << sizen
+ end
+ end
+ if type == 'image' && $limit_sizes =~ /original/
+ sizestrings << '<a id="linkoriginal" target="newframe">' + defer_translation(sizename('original', false)) + '</a>'
+ end
+ end
+ html.sub!(/~~sizes~~(.+)~~/) { sizestrings.join($1) }
+end
+
+def substitute_navigation(html, xmldir)
+ if xmldir.parent.name == 'dir'
+ nav = ''
+ path = '..'
+ parent = xmldir.parent
+ while parent.name == 'dir'
+ parentcaption = parent.attributes['subdirs-caption'] || File.basename(parent.attributes['path'])
+ nav = "<a href=\"#{path}/index#{$htmlsuffix}\">#{parentcaption}</a> #{defer_translation(N_(" > "))} #{nav}"
+ path += '/..'
+ parent = parent.parent
+ end
+ html.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/) { $1 }
+ html.gsub!(/~~navigation~~/, nav + (xmldir.attributes['subdirs-caption'] || File.basename(xmldir.attributes['path'])))
+ else
+ html.gsub!(/~~ifnavigation\?~~(.+?)~~fi~~/, '')
+ end
+end
+
+def substitute_pathtobase(html, xmldir)
+ path = ''
+ location = xmldir
+ while location.parent.name == 'dir'
+ path = '../' + path
+ location = location.parent
+ end
+ html.gsub!(/~~pathtobase~~/, path)
+end
+
+def xmldir2destdir(xmldir)
+ return make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))
+end
+
+def find_previous_album(xmldir)
+ relative_pos = ''
+ begin
+ #- move to previous dir element if exists
+ if prevelem = xmldir.previous_element_byname_notattr('dir', 'deleted')
+ xmldir = prevelem
+ relative_pos += '../' + xmldir2destdir(xmldir) + '/'
+ child = nil
+ #- after having moved to previous dir, we need to go down last subdir until the last one
+ while child = xmldir.elements['dir']
+ while nextchild = child.next_element_byname_notattr('dir', 'deleted')
+ child = nextchild
+ end
+ relative_pos += xmldir2destdir(child) + '/'
+ xmldir = child
+ end
+ else
+ #- previous dir doesn't exist, move to previous dir element if exists
+ xmldir = xmldir.parent
+ if xmldir.name == 'dir' && !xmldir.attributes['deleted']
+ relative_pos += '../'
+ else
+ return nil
+ end
+ end
+ end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
+ return File.reduce_path(relative_pos)
+end
+
+def find_next_album(xmldir)
+ relative_pos = ''
+ begin
+ #- first child dir element (catches when initial xmldir has both thumbnails and subdirs)
+ if firstchild = xmldir.child_byname_notattr('dir', 'deleted')
+ xmldir = firstchild
+ relative_pos += xmldir2destdir(xmldir) + '/'
+ #- next brother
+ elsif nextbro = xmldir.next_element_byname_notattr('dir', 'deleted')
+ xmldir = nextbro
+ relative_pos += '../' + xmldir2destdir(xmldir) + '/'
+ else
+ #- go up until we have a next brother or we are finished
+ begin
+ xmldir = xmldir.parent
+ relative_pos += '../'
+ end while xmldir && !xmldir.next_element_byname_notattr('dir', 'deleted')
+ if xmldir
+ xmldir = xmldir.next_element_byname('dir')
+ relative_pos += '../' + xmldir2destdir(xmldir) + '/'
+ else
+ return nil
+ end
+ end
+ end while !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
+ return File.reduce_path(relative_pos)
+end
+
+def find_translation_for_file(file, msg)
+ if $multi_languages
+ if file =~ /\.(\w\w)\.html$/
+ bindtextdomain("booh", { :locale => "#{$1}.UTF-8" })
+ retval = _(msg)
+ Locale.set_current($default_locale)
+ return retval
+ else
+ die "Internal error: cannot find multi language suffix of file '#{file}'"
+ end
+ else
+ return utf8(_(msg))
+ end
+end
+
+def sub_previous_next_album(file, previous_album, next_album, html, previous_album_msg, next_album_msg)
+ if previous_album
+ html.gsub!(/~~previous_album~~/, '<a href="' + previous_album + 'thumbnails' + $htmlsuffix + '">' + previous_album_msg + '</a>')
+ html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/) { $1 }
+ else
+ html.gsub!(/~~previous_album~~/, '')
+ html.gsub!(/~~ifprevious_album\?~~(.+?)~~fi~~/, '')
+ end
+ if next_album
+ html.gsub!(/~~next_album~~/, '<a href="' + next_album + 'thumbnails' + $htmlsuffix + '">' + next_album_msg + '</a>')
+ html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/) { $1 }
+ else
+ html.gsub!(/~~next_album~~/, '')
+ html.gsub!(/~~ifnext_album\?~~(.+?)~~fi~~/, '')
+ end
+ return html
+end
+
+def save_html(html, base_filename)
+ if html.class == Array
+ html = html.join('')
+ end
+ if $multi_languages
+ for language in ($multi_languages[0] + [ $multi_languages[1] ]).uniq
+ bindtextdomain("booh", { :locale => "#{language}.UTF-8" })
+ ios = File.open("#{base_filename}.#{language}.html", "w")
+ ios.write(html.gsub(/@@(.*?)@@/) { _($1) })
+ ios.close
+ Locale.set_current($default_locale)
+ end
+ else
+ ios = File.open("#{base_filename}.html", "w")
+ ios.write(html.gsub(/@@(.*?)@@/) { utf8(_($1)) })
+ ios.close
+ end
+end
+
+def walk_source_dir
+
+ #- preprocess the path->dir, rexml is very slow with that; we seem to improve speed by 7%
+ optxpath = {}
+ $xmldoc.elements.each('//dir') { |elem|
+ optxpath[elem.attributes['path']] = elem
+ }
+
+ examined_dirs = nil
+ if $mode == 'merge_config_onedir'
+ examined_dirs = [ $onedir ]
+ elsif $mode == 'merge_config_subdirs'
+ examined_dirs = `find '#{$onedir}' -type d -follow`.sort.collect { |v| v.chomp }.delete_if { |v| optxpath.has_key?(utf8(v)) }
+ else
+ examined_dirs = `find '#{$source}' -type d -follow`.sort.collect { |v| v.chomp }
+ if $mode == 'merge_config'
+ $xmldoc.elements.each('//dir') { |elem|
+ if ! examined_dirs.include?(elem.attributes['path'])
+ msg 2, _("Merging config: removing directory %s from config, isn't on filesystem anymore") % elem.attributes['path']
+ elem.remove
+ end
+ }
+ end
+ end
+ info("directories: #{examined_dirs.length}, sizes: #{$images_size.length}")
+
+ examined_dirs.each { |dir|
+ if dir =~ /'/
+ die _("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir
+ end
+ if $mode !~ /^use_config/
+ Dir.entries(dir).each { |file|
+ if file =~ /['"\[\]]/
+ die _("Files can't contain any of the characters ', \", [ or ], sorry: %s") % "#{dir}/#{file}"
+ end
+ }
+ end
+ }
+
+ examined_dirs.each { |dir|
+ if File.basename(dir) =~ /^\./
+ msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
+ next
+ end
+
+ dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
+
+ #- place xml document on proper node if exists, else create
+ xmldir = optxpath[utf8(dir)]
+ if $mode == 'use_config' || $mode == 'use_config_changetheme'
+ if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
+ info("walking: #{dir}|#{$source}, 0 elements")
+ if xmldir && xmldir.attributes['deleted']
+ system("rm -rf '#{dest_dir}'")
+ end
+ next
+ end
+ else
+ if $mode == 'gen_config' || (($mode == 'merge_config' || $mode == 'merge_config_subdirs') && !xmldir)
+ #- add the <dir..> element if necessary
+ parent = File.dirname(dir)
+ xmldir = $xmldoc.elements["//dir[@path='#{utf8(parent)}']"]
+ if !xmldir
+ xmldir = $xmldoc.root
+ end
+ #- need to remove the already-generated mark of the parent because of the sub-albums page containing now one more element
+ xmldir.delete_attribute('already-generated')
+ xmldir = optxpath[utf8(dir)] = xmldir.add_element('dir', { 'path' => utf8(dir) })
+ end
+ end
+ xmldir.delete_attribute('already-generated')
+
+ #- read images/videos entries from config or from directories depending on mode
+ entries = []
+ if $mode == 'use_config' || $mode == 'use_config_changetheme'
+ msg 2, _("Handling %s from config list...") % dir
+ xmldir.elements.each { |element|
+ if %w(image video).include?(element.name) && !element.attributes['deleted']
+ entries << from_utf8(element.attributes['filename'])
+ end
+ }
+ else
+ msg 2, _("Examining %s...") % dir
+ entries = Dir.entries(dir).sort
+ #- populate config in case of gen_config, add new files in case of merge_config
+ for file in entries
+ if file =~ /['"\[\]]/
+ msg 1, _("Ignoring %s, contains one of forbidden characters: '\"[]") % "#{dir}/#{file}"
+ else
+ type = entry2type(file)
+ if type && !xmldir.elements["#{type}[@filename='#{utf8(file)}']"]
+ #- hack: don't run identify (which is slow) if format only contains default %t
+ if $commentsformat && type == 'image' && $commentsformat != '%t'
+ comment = utf8(`identify -format "#{$commentsformat}" '#{dir}/#{file}'`.chomp.sub(/\.$/, ''))
+ else
+ comment = utf8cut(file.sub(/\.[^\.]+$/, ''), 18)
+ end
+ xmldir.add_element type, { "filename" => utf8(file), "caption" => comment }
+ end
+ end
+ end
+ if $mode != 'gen_config'
+ #- cleanup removed files from config and reread entries from config to get proper ordering
+ entries = []
+ xmldir.elements.each { |element|
+ fullpath = "#{dir}/#{from_utf8(element.attributes['filename'])}"
+ if %w(image video).include?(element.name)
+ if !File.readable?(fullpath)
+ msg 1, _("Config merge: removing %s from config; use the backup file to retrieve caption info if this was a mistake") % fullpath
+ xmldir.delete(element)
+ elsif !element.attributes['deleted']
+ entries << from_utf8(element.attributes['filename'])
+ end
+ end
+ }
+ #- if there is no more elements here, there is no album here anymore
+ if !xmldir.child_byname_notattr('image', 'deleted') && !xmldir.child_byname_notattr('video', 'deleted')
+ xmldir.delete_attribute('thumbnails-caption')
+ xmldir.delete_attribute('thumbnails-captionfile')
+ end
+ end
+ end
+ images = entries.find_all { |e| entry2type(e) == 'image' }
+ msg 3, _("\t%s photos") % images.length
+ videos = entries.find_all { |e| entry2type(e) == 'video' }
+ msg 3, _("\t%s videos") % videos.length
+ info("walking: #{dir}|#{$source}, #{images.length + videos.length} elements")
+
+ system("mkdir -p '#{dest_dir}'")
+
+ #- generate .htaccess file
+ if !$forgui
+ ios = File.open("#{dest_dir}/.htaccess", "w")
+ ios.write("AddCharset UTF-8 .html\n")
+ if auth_user_file = xmldir.attributes['password-protect']
+ msg 3, _("\tgenerating password protection file #{dest_dir}/.htaccess")
+ ios.write("AuthType Basic\nAuthName \"protected area\"\nAuthUserFile #{auth_user_file}\nrequire valid-user\n")
+ end
+ if $multi_languages
+ ios.write("Options Multiviews\n")
+ ios.write("LanguagePriority #{$multi_languages[1]}\n")
+ ios.write("ForceLanguagePriority Prefer Fallback\n")
+ ios.write("DirectoryIndex index\n")
+ end
+ ios.close
+ end
+
+ #- pass through if there are no images and videos
+ if images.size == 0 && videos.size == 0
+ if !$forgui
+ #- cleanup old images/videos, especially if this directory contained images/videos previously.
+ if $mode != 'gen_config'
+ rightful_images = [ '.htaccess' ]
+ if xmldir.attributes['thumbnails-caption']
+ rightful_images << 'thumbnails-thumbnail.jpg'
+ end
+ xmldir.elements.each('dir') { |child|
+ if child.attributes['deleted']
+ next
+ end
+ subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
+ rightful_images << "thumbnails-#{subdir}.jpg"
+ }
+ to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?(File.join(dest_dir, e)) && !rightful_images.include?(e) }
+ if to_del.size > 0
+ File.delete(*to_del.collect { |e| File.join(dest_dir, e) })
+ end
+ end
+
+ #- copy any resource file that goes with the theme (css, images..)
+ themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
+ find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata root CVS).include?(e) }
+ themestuff.each { |entry|
+ if !File.exists?(File.join(dest_dir, entry))
+ psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
+ end
+ }
+
+ #- copy any root-only resource file that goes with the theme (css, images..)
+ if xmldir.parent.name != 'dir'
+ themestuff_root = Dir.entries("#{$FPATH}/themes/#{$theme}/root").
+ find_all { |e| !%w(. .. CVS).include?(e) }
+ themestuff_root.each { |entry|
+ if !File.exists?(File.join(dest_dir, entry))
+ psys("cp '#{$FPATH}/themes/#{$theme}/root/#{entry}' '#{dest_dir}'")
+ end
+ }
+ end
+ end
+ next
+ end
+
+ msg 2, _("Outputting in %s...") % dest_dir
+
+ #- populate data structure with sizes from theme
+ for sizeobj in $images_size
+ fullscreen_images ||= {}
+ fullscreen_images[sizeobj['name']] = []
+ thumbnail_images ||= {}
+ thumbnail_images[sizeobj['name']] = []
+ thumbnail_videos ||= {}
+ thumbnail_videos[sizeobj['name']] = []
+ end
+ #- a special dummy size to keep 'references' to thumbnails in case of panorama, because the GUI will use the regular thumbnails
+ thumbnail_images['dont-delete-file-for-gui'] = []
+ if $limit_sizes =~ /original/
+ fullscreen_images['original'] = []
+ end
+
+ images.size >= 1 and msg 3, _("\tcreating photos thumbnails...")
+
+ #- create thumbnails for images
+ images.each { |img|
+ info("processing element")
+ base_dest_img = dest_dir + '/' + make_dest_filename(img.sub(/\.[^\.]+$/, ''))
+ if $forgui
+ thumbnail_dest_img = base_dest_img + "-#{$default_size['thumbnails']}.jpg"
+ gen_thumbnails_element("#{dir}/#{img}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
+ else
+ todo = []
+ elem = xmldir.elements["image[@filename='#{utf8(img)}']"]
+ for sizeobj in $images_size
+ size_fullscreen = sizeobj['fullscreen']
+ size_thumbnails = sizeobj['thumbnails']
+ fullscreen_dest_img = base_dest_img + "-#{size_fullscreen}.jpg"
+ fullscreen_images[sizeobj['name']] << File.basename(fullscreen_dest_img)
+ todo << { 'filename' => fullscreen_dest_img, 'size' => size_fullscreen }
+ if pano = pano_amount(elem)
+ thumbnail_images['dont-delete-file-for-gui'] << File.basename(base_dest_img + "-#{size_thumbnails}.jpg")
+ size_thumbnails = size_thumbnails.sub(/(\d+)/) { ($1.to_i * pano).to_i }
+ end
+ thumbnail_dest_img = base_dest_img + "-#{size_thumbnails}.jpg"
+ thumbnail_images[sizeobj['name']] << File.basename(thumbnail_dest_img)
+ todo << { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails }
+ end
+ gen_thumbnails_element("#{dir}/#{img}", xmldir, true, todo)
+ if $limit_sizes =~ /original/
+ fullscreen_images['original'] << img
+ end
+ destimg = "#{dest_dir}/#{img}"
+ if $limit_sizes =~ /original/ && !File.exists?(destimg)
+ if $hardlinks_ok
+ if ! sys("ln '#{dir}/#{img}' '#{destimg}'")
+ $hardlinks_ok = false
+ end
+ end
+ if ! $hardlinks_ok
+ psys("cp '#{dir}/#{img}' '#{destimg}'")
+ end
+ end
+ end
+ }
+
+ videos.size >= 1 and msg 3, _("\tcreating videos thumbnails...")
+ transcoded_videos = {}
+
+ #- create thumbnails for videos
+ videos.each { |video|
+ info("processing element")
+ if $forgui
+ thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{$default_size['thumbnails']}.jpg"
+ gen_thumbnails_element("#{dir}/#{video}", xmldir, true, [ { 'filename' => thumbnail_dest_img, 'size' => $default_size['thumbnails'] } ])
+ else
+ todo = []
+ for sizeobj in $images_size
+ size_thumbnails = sizeobj['thumbnails']
+ thumbnail_dest_img = dest_dir + '/' + make_dest_filename(video.sub(/\.[^\.]+$/, '')) + "-#{size_thumbnails}.jpg"
+ thumbnail_videos[sizeobj['name']] << File.basename(thumbnail_dest_img)
+ todo << { 'filename' => thumbnail_dest_img, 'size' => size_thumbnails }
+ end
+ gen_thumbnails_element("#{dir}/#{video}", xmldir, true, todo)
+
+ if $transcode_videos
+ parts = $transcode_videos.split(':', 2)
+ basedestvideo = video.sub(/\.\w+/, '') + '.' + parts[0]
+ transcoded_videos[video] = basedestvideo
+ destvideo = "#{dest_dir}/#{basedestvideo}"
+ if ! File.exists?(destvideo)
+ psys(parts[1].gsub(/%f/, "'#{dir}/#{video}'").gsub(/%o/, "'#{destvideo}'"))
+ end
+ else
+ destvideo = "#{dest_dir}/#{video}"
+ if ! File.exists?(destvideo)
+ if $hardlinks_ok
+ if ! sys("ln '#{dir}/#{video}' '#{destvideo}'")
+ $hardlinks_ok = false
+ end
+ end
+ if ! $hardlinks_ok
+ psys("cp '#{dir}/#{video}' '#{destvideo}'")
+ end
+ end
+ end
+ end
+ }
+
+ if !$forgui
+ #- cleanup old images/videos (for when removing elements or sizes)
+ all_elements = fullscreen_images.collect { |e| e[1] }.flatten.
+ concat(thumbnail_images.collect { |e| e[1] }.flatten).
+ concat(thumbnail_videos.collect { |e| e[1] }.flatten).
+ concat($transcode_videos ? transcoded_videos.values : videos).
+ push('.htaccess')
+ to_del = Dir.entries(dest_dir).find_all { |e| !File.directory?(File.join(dest_dir, e)) && !all_elements.include?(e) && e !~ /^thumbnails-\w+\.jpg/ }
+ if to_del.size > 0
+ msg 3, _("\tcleaning up: #{to_del.join(', ')}")
+ File.delete(*to_del.collect { |e| File.join(dest_dir, e) })
+ end
+
+ #- copy any resource file that goes with the theme (css, images..)
+ themestuff = Dir.entries("#{$FPATH}/themes/#{$theme}").
+ find_all { |e| !%w(. .. skeleton_image.html skeleton_thumbnails.html skeleton_index.html metadata root CVS).include?(e) }
+ themestuff.each { |entry|
+ if !File.exists?(File.join(dest_dir, entry))
+ psys("cp '#{$FPATH}/themes/#{$theme}/#{entry}' '#{dest_dir}'")
+ end
+ }
+
+ #- copy any root-only resource file that goes with the theme (css, images..)
+ if xmldir.parent.name != 'dir'
+ themestuff_root = Dir.entries("#{$FPATH}/themes/#{$theme}/root").
+ find_all { |e| !%w(. .. CVS).include?(e) }
+ themestuff_root.each { |entry|
+ if !File.exists?(File.join(dest_dir, entry))
+ psys("cp '#{$FPATH}/themes/#{$theme}/root/#{entry}' '#{dest_dir}'")
+ end
+ }
+ end
+
+ msg 3, _("\tgenerating HTML pages...")
+ #- fixup max per page
+ if $N_per_page
+ $N_per_page = $N_per_page.to_i / ($N_per_row || $default_N).to_i * ($N_per_row || $default_N).to_i
+ end
+
+ #- generate thumbnails*.html (page with thumbnails)
+ image2thumbnailpage4js = []
+ for sizeobj in $images_size
+ info("processing size")
+ html = $html_thumbnails.collect { |l| l.clone }
+ iterations = {}
+ for i in html
+ i.sub!(/~~title~~/,
+ xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
+ discover_iterations(iterations, i)
+ end
+ all_pages = []
+ html_thumbnails = ''
+ html_thumbnails_nojs = ''
+ counter = 0
+ pagecount = 0
+ reset_iterations(iterations)
+ #- preprocess the @filename->elem, rexml is very slow with that; we dramatically improve this part of the processing
+ optfilename = {}
+ xmldir.elements.each('image') { |elem|
+ optfilename[elem.attributes['filename']] = elem
+ }
+ for file in entries
+ type = images.include?(file) ? 'image' : videos.include?(file) ? 'video' : nil
+ if type
+ homogeinize_width = 100 / ($N_per_row || $default_N).to_i
+ if type == 'image' && elem = optfilename[utf8(file)]
+ if pano = pano_amount(elem)
+ html_elem = run_iterations(iterations, pano)
+ counter += count = pano.ceil
+ html_elem.gsub!(/~~colspan~~/) { "colspan=\"#{count}\"" }
+ homogeinize_width *= count
+ else
+ html_elem = run_iterations(iterations, 1)
+ counter += 1
+ html_elem.gsub!(/~~colspan~~/, '')
+ end
+ else
+ html_elem = run_iterations(iterations, 1)
+ counter += 1
+ html_elem.gsub!(/~~colspan~~/, '')
+ end
+ html_elem.gsub!(/~~homogeinize_width~~/) { "width=\"#{homogeinize_width}%\"" }
+ if type == 'image'
+ index = images.index(file)
+ html_elem.gsub!(/~~caption_iteration~~/,
+ find_caption_value(xmldir, images[index]) || utf8(images[index]))
+ html_elem.gsub!(/~~ifimage\?~~(.+?)~~fi~~/) { $1 }
+ html_elem.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/, '')
+ elsif type == 'video'
+ index = videos.index(file)
+ if File.exists?("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}")
+ html_elem.gsub!(/~~image_iteration~~/,
+ '<a href="' + ( $transcode_videos ? transcoded_videos[videos[index]] : videos[index] ) + '">' +
+ img_element("#{dest_dir}/#{thumbnail_videos[sizeobj['name']][index]}") + '</a>')
+ else
+ html_elem.gsub!(/~~image_iteration~~/,
+ '<a href="' + ( $transcode_videos ? transcoded_videos[videos[index]] : videos[index] ) + '">' +
+ defer_translation(N_("(no preview)")) + '</a>')
+ end
+ html_elem.gsub!(/~~caption_iteration~~/, find_caption_value(xmldir, videos[index]) || utf8(videos[index]))
+ html_elem.gsub!(/~~ifimage\?~~(.+?)~~fi~~/, '')
+ html_elem.gsub!(/~~ifvideo\?~~(.+?)~~fi~~/) { $1 }
+ end
+ html_thumbnails += html_elem
+ html_thumbnails_nojs += html_elem
+ if type == 'image'
+ html_thumbnails.gsub!(/~~image_iteration~~/,
+ '<a href="image-' + size2js(sizeobj['name']) + $htmlsuffix + '#current=' + fullscreen_images[sizeobj['name']][index] +
+ '" name="' + fullscreen_images[sizeobj['name']][index] + '">' +
+ img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
+ html_thumbnails_nojs.gsub!(/~~image_iteration~~/,
+ '<a href="' + fullscreen_images[sizeobj['name']][index] + '" name="' + fullscreen_images[sizeobj['name']][index] + '">' +
+ img_element("#{dest_dir}/#{thumbnail_images[sizeobj['name']][index]}") + '</a>')
+ #- remember in which thumbnails page is this element, for image->thumbnail link
+ if sizeobj == $images_size[0]
+ image2thumbnailpage4js << pagecount
+ end
+ end
+
+ if counter == $N_per_page
+ html_thumbnails += close_iterations(iterations)
+ html_thumbnails_nojs += close_iterations(iterations)
+ all_pages << [ html_thumbnails, html_thumbnails_nojs ]
+ html_thumbnails = ''
+ html_thumbnails_nojs = ''
+ counter = 0
+ reset_iterations(iterations)
+ pagecount += 1
+ end
+ end
+ end
+ if counter > 0
+ html_thumbnails += close_iterations(iterations)
+ html_thumbnails_nojs += close_iterations(iterations)
+ all_pages << [ html_thumbnails, html_thumbnails_nojs ]
+ end
+ for i in html
+ i.gsub!(/~~theme~~/, $theme)
+ i.gsub!(/~~current_size~~/, sizeobj['name'])
+ i.gsub!(/~~htmlsuffix~~/, $htmlsuffix)
+ i.gsub!(/~~current_size_js~~/, size2js(sizeobj['name']))
+ i.gsub!(/~~madewith~~/, $madewith || '')
+ i.gsub!(/~~indexlink~~/, $indexlink || '')
+ substitute_navigation(i, xmldir)
+ end
+ html_nojs = html.collect { |l| l.clone }
+ pagecount = 0
+ for page in all_pages
+ html_thumbnails, html_thumbnails_nojs = page
+ final_html = html.collect { |l| l.clone }
+ mstuff = defer_translation(N_("Pages: ")) +
+ (pagecount > 0 ? "<a href=\"thumbnails-#{size2js(sizeobj['name'])}%nojs-#{pagecount - 1}#{$htmlsuffix}\">" + defer_translation(N_("<- Previous")) + "</a> " : '') +
+ all_pages.collect_with_index { |p,idx| page == p ? idx + 1 : "<a href=\"thumbnails-#{size2js(sizeobj['name'])}%nojs-#{idx}#{$htmlsuffix}\">#{idx + 1}</a>" }.join(', ') +
+ (pagecount < all_pages.size - 1 ? " <a href=\"thumbnails-#{size2js(sizeobj['name'])}%nojs-#{pagecount + 1}#{$htmlsuffix}\">" + defer_translation(N_("Next ->")) + "</a> " : '')
+ for i in final_html
+ i.sub!(/~~run_slideshow~~/,
+ images.size <= 1 ? '' : '<a href="image-' + size2js(sizeobj['name']) + $htmlsuffix + '#run_slideshow=1">' + defer_translation(N_("Run slideshow!"))+'</a>')
+ i.sub!(/~~thumbnails~~/, html_thumbnails)
+ if all_pages.size == 1
+ i.gsub!(/~~ifmultiplepages\?~~.*~~fi~~/, '')
+ else
+ i.gsub!(/~~ifmultiplepages\?~~(.+?)~~fi~~/) { $1 }
+ i.gsub!(/~~multiplepagesstuff~~/, mstuff.gsub('%nojs', ''))
+ end
+ substitute_html_sizes(i, sizeobj, 'thumbnails', "-#{pagecount}")
+ substitute_pathtobase(i, xmldir)
+ end
+ save_html(final_html, "#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}-#{pagecount}")
+ final_html_nojs = html_nojs.collect { |l| l.clone }
+ for i in final_html_nojs
+ i.sub!(/~~run_slideshow~~/, defer_translation(N_("<i>Click on an image to view it larger</i>")))
+ i.sub!(/~~thumbnails~~/, html_thumbnails_nojs)
+ if all_pages.size == 1
+ i.gsub!(/~~ifmultiplepages\?~~.*~~fi~~/, '')
+ else
+ i.gsub!(/~~ifmultiplepages\?~~(.+?)~~fi~~/) { $1 }
+ i.gsub!(/~~multiplepagesstuff~~/, mstuff.gsub('%nojs', '-nojs'))
+ end
+ substitute_html_sizes(i, sizeobj, 'thumbnails', "-nojs-#{pagecount}")
+ substitute_pathtobase(i, xmldir)
+ end
+ save_html(final_html_nojs, "#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}-nojs-#{pagecount}")
+ pagecount += 1
+ end
+ end
+
+ info("finished processing sizes")
+
+ #- generate "main" thumbnails.html page that will reload to correct size thanks to cookie
+ save_html(html_reload_to_thumbnails, "#{dest_dir}/thumbnails")
+
+ #- generate image.html (page with fullscreen images)
+ if images.size > 0
+ captions4js = find_captions(xmldir, images).collect { |e| e ? '"' + e.gsub('"', '\"') + '"' : '""' }.join(', ')
+ thumbnailspage4js = image2thumbnailpage4js.collect { |e| "\"#{e}\"" }.join(', ')
+ for sizeobj in $images_size
+ html = $html_images.collect { |l| l.clone }
+ images4js = fullscreen_images[sizeobj['name']].collect { |e| "\"#{e}\"" }.join(', ')
+ otherimages4js = ''
+ othersizes = []
+ for sizeobj2 in all_images_sizes
+ if sizeobj != sizeobj2
+ otherimages4js += "var images_#{size2js(sizeobj2['name'])} = new Array(" + fullscreen_images[sizeobj2['name']].collect { |e| "\"#{e}\"" }.join(', ') + ")\n"
+ othersizes << "\"#{size2js(sizeobj2['name'])}\""
+ end
+ end
+ for i in html
+ i.gsub!(/~~images~~/, images4js)
+ i.gsub!(/~~other_images~~/, otherimages4js)
+ i.gsub!(/~~thumbnailspages~~/, thumbnailspage4js)
+ i.gsub!(/~~other_sizes~~/, othersizes.join(', '))
+ i.gsub!(/~~captions~~/, captions4js)
+ i.gsub!(/~~title~~/, xmldir.attributes['thumbnails-caption'] || utf8(File.basename(dir)))
+ i.gsub!(/~~thumbnails~~/, '<a href="thumbnails-' + size2js(sizeobj['name']) + $htmlsuffix + '" id="thumbnails">' + defer_translation(N_('return to thumbnails')) + '</a>')
+ i.gsub!(/~~theme~~/, $theme)
+ i.gsub!(/~~current_size~~/, size2js(sizeobj['name']))
+ i.gsub!(/~~htmlsuffix~~/, $htmlsuffix)
+ i.gsub!(/~~madewith~~/, $madewith || '')
+ i.gsub!(/~~indexlink~~/, $indexlink || '')
+ substitute_html_sizes(i, sizeobj, 'image', '')
+ substitute_navigation(i, xmldir)
+ substitute_pathtobase(i, xmldir)
+ end
+ save_html(html, "#{dest_dir}/image-#{size2js(sizeobj['name'])}")
+ end
+ end
+ end
+ }
+
+ msg 3, ''
+
+ #- add attributes to <dir..> elements needing so
+ if $mode != 'use_config'
+ msg 3, _("\tfixating configuration file...")
+ $xmldoc.elements.each('//dir') { |element|
+ path = captionpath = element.attributes['path']
+ descendant_element = element.elements['descendant::image'] || element.elements['descendant::video']
+ if !descendant_element
+ msg 3, _("\t\tremoving %s, no element in it") % path
+ element.remove #- means we have a directory with nothing interesting in it
+ else
+ captionfile = "#{descendant_element.parent.attributes['path']}/#{descendant_element.attributes['filename']}"
+ basename = File.basename(path)
+ if element.elements['dir']
+ if !element.attributes['subdirs-caption']
+ element.add_attribute('subdirs-caption', basename)
+ end
+ if !element.attributes['subdirs-captionfile']
+ element.add_attribute('subdirs-captionfile', captionfile)
+ end
+ end
+ if element.child_byname_notattr('image', 'deleted') || element.child_byname_notattr('video', 'deleted')
+ if !element.attributes['thumbnails-caption']
+ element.add_attribute('thumbnails-caption', basename)
+ end
+ if !element.attributes['thumbnails-captionfile']
+ element.add_attribute('thumbnails-captionfile', captionfile)
+ end
+ end
+ end
+ }
+ end
+
+ #- write down to disk config if necessary
+ if $config_writeto
+ ios = File.open($config_writeto, "w")
+ $xmldoc.write(ios, 0)
+ ios.close
+ end
+
+ if $forgui
+ msg 3, _(" completed necessary stuff for GUI, exiting.")
+ return
+ end
+
+ #- second pass to create index.html files and previous/next links
+ info("creating index.html")
+ msg 3, _("\trescanning directories to generate all 'index.html' files...")
+
+ #- recompute the memoization because elements mights have been removed (the ones with no element in them)
+ optxpath = {}
+ $xmldoc.elements.each('//dir') { |elem|
+ optxpath[elem.attributes['path']] = elem
+ }
+
+ examined_dirs.each { |dir|
+ info("index.html: #{dir}|#{$source}")
+
+ xmldir = optxpath[utf8(dir)]
+ if !xmldir || (xmldir.attributes['already-generated'] && !$force) || xmldir.attributes['deleted']
+ next
+ end
+ dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote($source)}/, $dest))
+
+ previous_album = find_previous_album(xmldir)
+ next_album = find_next_album(xmldir)
+
+ if xmldir.elements['dir']
+ html = $html_index.collect { |l| l.clone }
+ iterations = {}
+ for i in html
+ caption = xmldir.attributes['subdirs-caption']
+ i.gsub!(/~~title~~/, caption)
+ substitute_navigation(i, xmldir)
+ substitute_pathtobase(i, xmldir)
+ discover_iterations(iterations, i)
+ end
+
+ html_index = ''
+ reset_iterations(iterations)
+
+ #- deal with "current" album (directs to "thumbnails" page)
+ if xmldir.attributes['thumbnails-caption']
+ thumbnail = "#{dest_dir}/thumbnails-thumbnail.jpg"
+ gen_thumbnails_subdir(from_utf8(xmldir.attributes['thumbnails-captionfile']), xmldir, false,
+ [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], 'thumbnails')
+ html_index += run_iterations(iterations, 1)
+ html_index.gsub!(/~~image_iteration~~/, "<a href=\"thumbnails#{$htmlsuffix}\">" + img_element(thumbnail) + '</a>')
+ html_index.gsub!(/~~caption_iteration~~/, xmldir.attributes['thumbnails-caption'])
+ end
+
+ #- deal with sub-albums (direct to subdirs/index.html pages)
+ xmldir.elements.each('dir') { |child|
+ if child.attributes['deleted']
+ next
+ end
+ subdir = make_dest_filename(from_utf8(File.basename(child.attributes['path'])))
+ thumbnail = "#{dest_dir}/thumbnails-#{subdir}.jpg"
+ html_index += run_iterations(iterations, 1)
+ captionfile, caption = find_subalbum_caption_info(child)
+ gen_thumbnails_subdir(captionfile, child, false,
+ [ { 'filename' => thumbnail, 'size' => $albums_thumbnail_size } ], find_subalbum_info_type(child))
+ html_index.gsub!(/~~caption_iteration~~/, caption)
+ html_index.gsub!(/~~image_iteration~~/, "<a href=\"#{subdir}/index#{$htmlsuffix}\">" + img_element(thumbnail) + '</a>')
+ }
+
+ html_index += close_iterations(iterations)
+
+ for i in html
+ i.gsub!(/~~thumbnails~~/, html_index)
+ i.gsub!(/~~madewith~~/, $madewith || '')
+ i.gsub!(/~~indexlink~~/, $indexlink || '')
+ end
+
+ else
+ html = html_reload_to_thumbnails
+ end
+
+ save_html(html, "#{dest_dir}/index")
+ if $multi_languages
+ #- in case MultiViews will not work, generate some compat
+ ios = File.open("#{dest_dir}/index.html", "w")
+ ios.write("<html><head><meta http-equiv=\"refresh\" content=\"0.1;url=index.#{$multi_languages[1]}.html\"></head></html>")
+ ios.close
+ end
+
+ #- substitute multiple "return to albums", previous/next correctly
+ #- the following two statements are dramatical optimizations to executing for each substInFile callback
+ dirpresent = xmldir.elements['dir']
+ parentname = xmldir.parent.name
+ if xmldir.child_byname_notattr('image', 'deleted') || xmldir.child_byname_notattr('video', 'deleted')
+ for suffix in [ '', '-nojs' ]
+ for sizeobj in $images_size
+ Dir.glob("#{dest_dir}/thumbnails-#{size2js(sizeobj['name'])}#{suffix}-*.html") do |file|
+ #- unroll translations, they are costly if rerun for each line of files
+ rta = find_translation_for_file(file, N_('return to albums'))
+ pa = find_translation_for_file(file, N_('previous album'))
+ na = find_translation_for_file(file, N_('next album'))
+ substInFile(file) { |line|
+ sub_previous_next_album(file, previous_album, next_album, line, pa, na)
+ if dirpresent
+ line.sub!(/~~return_to_albums~~/, '<a href="index' + $htmlsuffix + '">' + rta + '</a>')
+ else
+ if parentname == 'dir'
+ line.sub!(/~~return_to_albums~~/, '<a href="../index' + $htmlsuffix + '">' + rta + '</a>')
+ else
+ line.sub!(/~~return_to_albums~~/, '')
+ end
+ end
+ line
+ }
+ end
+ if suffix == '' && xmldir.child_byname_notattr('image', 'deleted')
+ Dir.glob("#{dest_dir}/image-#{size2js(sizeobj['name'])}*.html") do |file|
+ pa = find_translation_for_file(file, N_('previous album'))
+ na = find_translation_for_file(file, N_('next album'))
+ substInFile(file) { |line|
+ sub_previous_next_album(file, previous_album, next_album, line, pa, na)
+ }
+ end
+ end
+ end
+ end
+ end
+ }
+
+ msg 3, _(" all done.")
+end
+
+handle_options
+read_config
+check_installation
+
+build_html_skeletons
+
+walk_source_dir
Property changes on: packages/booh/branches/upstream/current/bin/booh-backend
___________________________________________________________________
Name: svn:executable
+
Added: packages/booh/branches/upstream/current/bin/booh-classifier
===================================================================
--- packages/booh/branches/upstream/current/bin/booh-classifier (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh-classifier 2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,1847 @@
+#! /usr/bin/ruby
+#
+# * BOOH *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'getoptlong'
+require 'tempfile'
+
+require 'gtk2'
+require 'booh/libadds'
+
+require 'gettext'
+include GetText
+bindtextdomain("booh")
+
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+include Booh
+require 'booh/UndoHandler'
+
+
+#- options
+$options = [
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
+ [ '--sort-by-exif-date', '-s', GetoptLong::NO_ARGUMENT, _("Sort entries by EXIF date") ],
+ [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+ [ '--version', '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
+]
+
+$preloader_allowed = false
+
+def usage
+ puts _("Usage: %s [OPTION]...") % File.basename($0)
+ $options.each { |ary|
+ printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
+ }
+end
+
+def handle_options
+ parser = GetoptLong.new
+ parser.set_options(*$options.collect { |ary| ary[0..2] })
+ begin
+ parser.each_option do |name, arg|
+ case name
+ when '--help'
+ usage
+ exit(0)
+
+ when '--sort-by-exif-date'
+ $sort_by_exif_date = true
+
+ when '--verbose-level'
+ $verbose_level = arg.to_i
+
+ when '--version'
+ puts _("Booh version %s
+
+Copyright (c) 2005-2008 Guillaume Cottenceau.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
+
+ exit(0)
+
+ end
+ end
+ rescue
+ puts $!
+ usage
+ exit(1)
+ end
+end
+
+def startup_memfree
+ if $startup_memfree.nil?
+ meminfo = IO.readlines('/proc/meminfo').join
+ meminfo =~ /MemFree:.*?(\d+)/ or return -1
+ memfree = $1
+ meminfo =~ /Buffers:.*?(\d+)/ and buffers = $1
+ meminfo =~ /Cached:.*?(\d+)/ and cached = $1
+ $startup_memfree = memfree.to_i + buffers.to_i + cached.to_i
+ end
+ return $startup_memfree
+end
+
+def set_cache_memory_use_figure
+
+ if $config['cache-memory-use'] =~ /memfree_(\d+)/
+ $config['cache-memory-use-figure'] = startup_memfree*$1.to_f/100
+ else
+ $config['cache-memory-use-figure'] = $config['cache-memory-use'].to_i
+ end
+ msg 2, _("Cache memory used: %s kB") % $config['cache-memory-use-figure']
+end
+
+def read_config
+ $config = {}
+ $config_file = File.expand_path('~/.booh-classifier-rc')
+ if File.readable?($config_file)
+ $xmldoc = REXML::Document.new(File.new($config_file))
+ $xmldoc.root.elements.each { |element|
+ txt = element.get_text
+ if txt
+ if txt.value =~ /~~~/
+ $config[element.name] = txt.value.split(/~~~/)
+ else
+ $config[element.name] = txt.value
+ end
+ elsif element.elements.size == 0
+ $config[element.name] = ''
+ else
+ $config[element.name] = {}
+ element.each { |chld|
+ txt = chld.get_text
+ $config[element.name][chld.name] = txt ? txt.value : nil
+ }
+ end
+ }
+ end
+ $config['video-viewer'] ||= '/usr/bin/mplayer %f'
+ $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
+ $config['preload-distance'] ||= '5'
+ $config['cache-memory-use'] ||= 'memfree_80%'
+ $config['rotate-set-exif'] ||= 'true'
+ $config['thumbnails-height'] ||= '64'
+ set_cache_memory_use_figure
+end
+
+def check_config
+ missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
+ if missing != []
+ show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
+ end
+
+ if !system("which exif >/dev/null 2>/dev/null")
+ show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
+ end
+ viewer_binary = $config['video-viewer'].split.first
+ if viewer_binary && ! File.executable?(viewer_binary)
+ show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
+You should fix this in Edit/Preferences so that you can view videos.
+
+Problem was: '%s' is not an executable file.
+Hint: don't forget to specify the full path to the executable,
+e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
+ end
+ browser_binary = $config['browser'].split.first
+ if browser_binary && ! File.executable?(browser_binary)
+ show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
+You should fix this in Edit/Preferences so that you can open URLs.
+
+Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
+ end
+end
+
+def write_config
+ ios = File.open($config_file, "w")
+ $xmldoc = Document.new "<booh-classifier-rc version='#{$VERSION}'/>"
+ $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
+ $config.each_pair { |key, value|
+ elem = $xmldoc.root.add_element key
+ if value.is_a? Hash
+ $config[key].each_pair { |subkey, subvalue|
+ subelem = elem.add_element subkey
+ subelem.add_text subvalue.to_s
+ }
+ elsif value.is_a? Array
+ elem.add_text value.join('~~~')
+ else
+ if !value
+ elem.remove
+ else
+ elem.add_text value.to_s
+ end
+ end
+ }
+ $xmldoc.write(ios, 0)
+ ios.close
+end
+
+def save_undo(name, closure, *params)
+ UndoHandler.save_undo(name, closure, [ *params ])
+ $undo_mb.sensitive = true
+ $redo_mb.sensitive = false
+end
+
+def get_mem
+ IO.readlines('/proc/self/status').join =~ /VmRSS.*?(\d+)\s*kB/
+ msg 3, "RSS: #{$1}"
+ return $1.to_i
+end
+
+def show_mem(*txt)
+ txt.length > 0 and print txt[0]
+ msg 2, "RSS: #{get_mem}"
+end
+
+class Gdk::Color
+ def darker
+ color = dup
+ color.red = [ color.red - 10000, 0 ].max
+ color.green = [ color.green - 10000, 0 ].max
+ color.blue = [ color.blue - 10000, 0 ].max
+ return color
+ end
+ def lighter
+ color = dup
+ color.red = [ color.red + 10000, 65535 ].min
+ color.green = [ color.green + 10000, 65535 ].min
+ color.blue = [ color.blue + 10000, 65535 ].min
+ return color
+ end
+end
+
+$color_red = Gdk::Color.new(65535, 0, 0)
+$colors = [ Gdk::Color.new(0, 65535, 0),
+ Gdk::Color.new(0, 0, 65535),
+ Gdk::Color.new(65535, 65535, 0),
+ Gdk::Color.new(0, 65535, 65535),
+ Gdk::Color.new(65535, 0, 65535) ]
+
+class Label
+ attr_accessor :color, :name, :button
+ def initialize(name)
+ @name = name
+ end
+end
+
+class Entry
+ @@max_width = nil
+ def Entry.thumbnails_height
+ return $config['thumbnails-height'].to_i
+ end
+
+ attr_accessor :path, :guipath, :type, :angle, :button, :image, :alignment, :removed, :labeled
+
+ def initialize(path, type, guipath)
+ @path = path
+ @type = type
+ @guipath = guipath
+ if @@max_width.nil?
+ @@max_width = $main_window.root_window.size[0] - $labels_vbox.allocation.width - ( $videoborder_pixbuf.width + MainView.borders_thickness) * 2
+ end
+ end
+
+ def pixbuf_full
+ if @pixbuf_full.nil?
+ msg 3, ">>> pixbuf_full #{path}"
+ load_into_pixbuf_full
+ end
+ return @pixbuf_full
+ end
+ def free_pixbuf_full
+ if @pixbuf_full.nil?
+ return false
+ else
+ msg 3, ">>> free_pixbuf_full #{path}"
+ @pixbuf_full = nil
+ return true
+ end
+ end
+ def pixbuf_main(interruptable)
+ width, height = $mainview.window.size
+ width = MainView.get_usable_width(width)
+ height = MainView.get_usable_height(height)
+ if @pixbuf_main.nil? || width != @width || height != @height
+ msg 3, ">>> pixbuf_main #{path}"
+ @width = width
+ @height = height
+ load_into_pixbuf_full(interruptable) #- make sure it is loaded
+ if @pixbuf_full.nil?
+ return
+ end
+ if @pixbuf_full.width.to_f / @pixbuf_full.height > width.to_f / height
+ resized_height = @pixbuf_full.height * (width.to_f/@pixbuf_full.width)
+ if @pixbuf_full.width > width || @pixbuf_full.height > resized_height
+ @pixbuf_main = @pixbuf_full.scale(width, resized_height, Gdk::Pixbuf::INTERP_BILINEAR)
+ else
+ @pixbuf_main = @pixbuf_full
+ end
+ else
+ resized_width = @pixbuf_full.width * (height.to_f/@pixbuf_full.height)
+ if @pixbuf_full.width > resized_width || @pixbuf_full.height > height
+ @pixbuf_main = @pixbuf_full.scale(resized_width, height, Gdk::Pixbuf::INTERP_BILINEAR)
+ else
+ @pixbuf_main = @pixbuf_full
+ end
+ end
+ end
+ return @pixbuf_main
+ end
+ def free_pixbuf_main
+ if @pixbuf_main.nil?
+ return false
+ else
+ msg 3, ">>> free_pixbuf_main #{path}"
+ @pixbuf_main = nil
+ return true
+ end
+ end
+ def pixbuf_thumbnail
+ if @pixbuf_thumbnail.nil?
+ if @pixbuf_main
+ msg 3, ">>> pixbuf_thumbnail from main #{path}"
+ @pixbuf_thumbnail = @pixbuf_main.scale(@pixbuf_main.width * (Entry.thumbnails_height.to_f/@pixbuf_main.height), Entry.thumbnails_height, Gdk::Pixbuf::INTERP_BILINEAR)
+ else
+ msg 3, ">>> pixbuf_thumbnail from file #{path}"
+ @pixbuf_thumbnail = load_into_pixbuf_at_size(false) { |w, h|
+ if @angle == 0
+ if h > Entry.thumbnails_height
+ [ w * Entry.thumbnails_height.to_f/h, Entry.thumbnails_height ]
+ else
+ [ w, h ]
+ end
+ else
+ if w > Entry.thumbnails_height
+ [ Entry.thumbnails_height, h * Entry.thumbnails_height.to_f/w ]
+ else
+ [ w, h ]
+ end
+ end
+ }
+ end
+ end
+ return @pixbuf_thumbnail
+ end
+ def free_pixbuf_thumbnail
+ if @pixbuf_thumbnail.nil?
+ return false
+ else
+ msg 3, ">>> free_pixbuf_thumbnail #{path}"
+ @pixbuf_thumbnail = nil
+ return true
+ end
+ end
+
+ def outline_color
+ if removed
+ return $color_red
+ elsif labeled
+ return labeled.color
+ else
+ return nil
+ end
+ end
+
+ def show_bg
+ if outline_color.nil?
+ button.modify_bg(Gtk::StateType::NORMAL, nil)
+ button.modify_bg(Gtk::StateType::PRELIGHT, nil)
+ button.modify_bg(Gtk::StateType::ACTIVE, nil)
+ else
+ button.modify_bg(Gtk::StateType::NORMAL, outline_color)
+ button.modify_bg(Gtk::StateType::PRELIGHT, outline_color.lighter)
+ button.modify_bg(Gtk::StateType::ACTIVE, outline_color)
+ end
+ end
+
+ def get_beautified_name
+ if type == 'image'
+ size = get_image_size(path)
+ return _("%s (%sx%s, %s KB)") % [@guipath.gsub(/\.[^.]+$/, ''),
+ size[:x],
+ size[:y],
+ commify(file_size(path)/1024)]
+ else
+ return _("%s (video - %s KB)") % [@guipath.gsub(/\.[^.]+$/, ''),
+ commify(file_size(path)/1024)]
+ end
+ end
+
+ private
+ def cleanup_dir(dir)
+ Dir.entries(dir).each { |file| file != '.' && file != '..' and File.delete(File.join(dir, file)) }
+ Dir.delete(dir)
+ end
+
+ def load_into_pixbuf_full(interruptable)
+ if @pixbuf_full.nil?
+ msg 3, ">>> load_into_pixbuf_full #{path}"
+ @pixbuf_full = load_into_pixbuf_at_size(interruptable) { |w, h|
+ if @angle == 0
+ if w > @@max_width
+ #- save memory and speedup (+35%) loading
+ [ w * (factor = @@max_width.to_f/w), h * factor ]
+ else
+ [ w, h ]
+ end
+ else
+ if h > @@max_width
+ [ w * (factor = @@max_width.to_f/h), h * factor ]
+ else
+ [ w, h ]
+ end
+ end
+ }
+ end
+ end
+
+ def load_into_pixbuf_at_size(interruptable, &specify_size)
+ pixbuf = nil
+ if @type == 'video'
+ tmp = Tempfile.new("boohclassifiertemp")
+ dest_dir = tmp.path
+ tmp.close!
+ Dir.mkdir(dest_dir)
+ orig_base = File.basename(path)
+ tmpdir = gen_video_thumbnail(path, false, 0)
+ if tmpdir.nil?
+ return
+ end
+ image_path = "#{tmpdir}/00000001.jpg"
+ else
+ image_path = @path
+ end
+ if @angle.nil?
+ if @type == 'image'
+ @angle = guess_rotate(image_path)
+ else
+ @angle = 0
+ end
+ end
+ begin
+ #- use a pixbuf loader and trigger Gtk.main_iteration on each chunk if needed, to keep the UI responsive even
+ #- if loaded pictures are several MBs large
+ loader = Gdk::PixbufLoader.new
+ loader.signal_connect('size-prepared') { |l, w, h|
+ r = specify_size.call(w, h)
+ #msg 3, "specified sizes: #{r[0]} #{r[1]}"
+ loader.set_size(*specify_size.call(w, h))
+ }
+ id = loader.signal_connect('area-prepared') { pixbuf = loader.pixbuf }
+ if ! loader.load_not_freezing_ui(image_path, interruptable, id)
+ return
+ end
+ loader.close
+ if pixbuf.nil?
+ raise "Loaded pixbuf nil - #{path} #{image_path}"
+ end
+ rescue
+ msg 0, "Cannot load #{image_path}: #{$!}"
+ begin
+ loader.close
+ rescue
+ end
+ return
+ ensure
+ if @type == 'video'
+ File.delete(image_path)
+ Dir.rmdir(tmpdir)
+ end
+ end
+ if pixbuf
+ if @angle != 0
+ msg 3, ">>> load_into_pixbuf_full #{image_path} => rotate #{@angle}"
+ pixbuf = rotate_pixbuf(pixbuf, @angle)
+ end
+ end
+ if @type == 'video'
+ cleanup_dir(dest_dir)
+ end
+ return pixbuf
+ end
+
+ def to_s
+ @path
+ end
+end
+
+$allentries = []
+
+def gc
+ start = Time.now
+ GC.start
+ msg 3, "GC in #{Time.now - start} s"
+end
+
+def free_cache(avoid)
+ i = $allentries.index($mainview.get_shown_entry)
+ return if i.nil?
+ start = Time.now
+ ($allentries.size - 1).downto($config['preload-distance'].to_i + 1) { |j|
+ index = i + j
+ if i + j < $allentries.size && ! avoid.include?(i + j)
+ $allentries[i + j].free_pixbuf_full
+ $allentries[i + j].free_pixbuf_main
+ end
+ if i - j >= 0 && ! avoid.include?(i - j)
+ $allentries[i - j].free_pixbuf_full
+ $allentries[i - j].free_pixbuf_main
+ end
+ }
+ msg 3, "freeing done in #{Time.now - start} s"
+ if get_mem > $config['cache-memory-use-figure'] * 3 / 4
+ gc
+ get_mem
+ end
+end
+
+def run_preloader_real
+ msg 3, "*** >> main preloading triggered..."
+ if $preloader_running
+ msg 3, "*** >>>>>> already running, return <<<<<<<<"
+ return
+ end
+ $preloader_running = true
+ if $mainview.get_shown_entry
+ if get_mem > $config['cache-memory-use-figure']
+ msg 3, "too much RSS, stopping preloading, triggering GC"
+ $preloader_running = false
+ gc
+ get_mem
+ return
+ end
+ if $config['preload-distance'].to_i == 0
+ free_cache([])
+ return
+ end
+ index = $allentries.index($mainview.get_shown_entry)
+ index_right = index
+ index_left = index
+ loaded_right = 0
+ loaded_left = 0
+ right_done = false
+ left_done = false
+ loaded = []
+ while ! right_done || ! left_done
+ if ! right_done
+ index_right += 1
+ while index_right < $allentries.size && ! visible($allentries[index_right])
+ index_right += 1
+ end
+ if index_right == $allentries.size
+ right_done = true
+ else
+ msg 3, "preloading #{$allentries[index_right].path}"
+ $allentries[index_right].pixbuf_main(false)
+ loaded << index_right
+ loaded_right += 1
+ if loaded_right == $config['preload-distance'].to_i
+ right_done = true
+ end
+ end
+ end
+
+ if ! left_done
+ index_left -= 1
+ while index_left >= 0 && ! visible($allentries[index_left])
+ index_left -= 1
+ end
+ if index_left == -1
+ left_done = true
+ else
+ msg 3, "preloading #{$allentries[index_left].path}"
+ $allentries[index_left].pixbuf_main(false)
+ loaded << index_left
+ loaded_left += 1
+ if loaded_left == $config['preload-distance'].to_i
+ left_done = true
+ end
+ end
+ end
+
+ #- in case just loaded another directory
+ if $preloader_force_exit
+ $preloader_running = false
+ $preloader_force_exit = false
+ return
+ end
+ #- in case moved fast
+ if index != $allentries.index($mainview.get_shown_entry)
+ msg 3, "*** >>>> moved already, rerun"
+ $preloader_running = false
+ run_preloader_real
+ return
+ end
+ end
+ free_cache(loaded)
+ end
+ $preloader_running = false
+ msg 3, "*** << main preloading finished"
+end
+
+def run_preloader
+ if ! $preloader_allowed
+ msg 3, "*** preloader not yet allowed"
+ return
+ end
+ Gtk.timeout_add(10) {
+ run_preloader_real
+ false
+ }
+end
+
+class MainView < Gtk::DrawingArea
+
+ @@borders_thickness = 5
+ @@borders_length = 25
+
+ def MainView.borders_thickness
+ return @@borders_thickness
+ end
+
+ def MainView.get_usable_width(available_width)
+ return available_width - ($videoborder_pixbuf.width + @@borders_thickness) * 2
+ end
+
+ def MainView.get_usable_height(available_height)
+ return available_height - @@borders_thickness * 2
+ end
+
+ def initialize
+ super()
+ signal_connect('expose-event') { draw }
+ signal_connect('configure-event') { update_shown }
+ end
+
+ def try_show_entry(entry)
+ if entry && entry.button
+ entry.button.grab_focus
+ end
+ end
+
+ def set_shown_entry(entry)
+ t1 = Time.now
+ if entry && entry == @entry
+ return
+ end
+ if entry && ! entry.button
+ #- not loaded yet
+ return
+ end
+ @entry = entry
+ redraw
+ run_preloader
+ msg 3, "entry shown in: #{Time.now - t1} s"
+ end
+
+ def get_shown_entry
+ return @entry
+ end
+
+ def show_next_entry(entry)
+ index = $allentries.index(entry)
+ if index < $allentries.size - 1
+ index += 1
+ end
+ while index < $allentries.size - 1 && $allentries[index] && ! $allentries[index].button.visible?
+ index += 1
+ end
+ while $allentries[index] && ! $allentries[index].button.visible? && index > 0
+ index -= 1
+ end
+ if index < $allentries.size && $allentries[index] && $allentries[index].button.visible?
+ try_show_entry($allentries[index])
+ end
+ end
+
+ def redraw
+ @entry and sb_msg(_("Selected %s") % @entry.get_beautified_name)
+ if ! update_shown
+ return
+ end
+ w, h = window.size
+ window.begin_paint(Gdk::Rectangle.new(0, 0, w, h))
+ window.clear
+ draw
+ window.end_paint
+ Gtk.main_iteration while Gtk.events_pending?
+ end
+
+ def update_shown
+ if @entry
+ $interrupt_loading = false
+ pixbuf = @entry.pixbuf_main(true)
+ $interrupt_loading = true
+ if pixbuf
+ @pixbuf = pixbuf
+ width, height = window.size
+ @xpos = (width - @pixbuf.width)/2
+ @ypos = (height - @pixbuf.height)/2
+ return true
+ else
+ return false
+ end
+ else
+ @pixbuf = nil
+ return true
+ end
+ end
+
+ def draw
+ if @pixbuf
+ window.draw_pixbuf(nil, @pixbuf, 0, 0, @xpos, @ypos, -1, -1, Gdk::RGB::DITHER_NONE, -1, -1)
+ if @entry && @entry.type == 'video'
+ window.draw_borders($videoborder_pixbuf, @xpos - $videoborder_pixbuf.width, @xpos + @pixbuf.width, @ypos, @ypos + @pixbuf.height)
+ end
+ if ! @entry.outline_color.nil?
+ gc = Gdk::GC.new(window)
+ colormap.alloc_color(@entry.outline_color, false, true)
+ gc.set_foreground(@entry.outline_color)
+ if @entry && @entry.type == 'video'
+ xleft = @xpos - $videoborder_pixbuf.width
+ xright = @xpos + @pixbuf.width + $videoborder_pixbuf.width
+ else
+ xleft = @xpos
+ xright = @xpos + @pixbuf.width
+ end
+ window.draw_polygon(gc, true, [[xleft - @@borders_thickness, @ypos - @@borders_thickness],
+ [xright + @@borders_thickness, @ypos - @@borders_thickness],
+ [xright + @@borders_thickness, @ypos + @pixbuf.height + @@borders_thickness],
+ [xleft - @@borders_thickness, @ypos + @pixbuf.height + @@borders_thickness],
+ [xleft - @@borders_thickness, @ypos - 1],
+ [xleft - 1, @ypos - 1],
+ [xleft - 1, @ypos + @pixbuf.height + 1],
+ [xright + 1, @ypos + @pixbuf.height + 1],
+ [xright + 1, @ypos - 1],
+ [xleft - @@borders_thickness, @ypos - 1]])
+ end
+ end
+ end
+end
+
+def autoscroll_if_needed(button, center)
+ xpos_left = button.allocation.x
+ xpos_right = button.allocation.x + button.allocation.width
+ hadj = $imagesline_sw.hadjustment
+ current_minx_visible = hadj.value
+ current_maxx_visible = hadj.value + hadj.page_size
+ if ! center
+ if xpos_left < current_minx_visible
+ #- autoscroll left
+ newval = hadj.value - (current_minx_visible - xpos_left)
+ hadj.value = newval
+ elsif xpos_right > current_maxx_visible
+ #- autoscroll right
+ newval = hadj.value + (xpos_right - current_maxx_visible)
+ if newval > hadj.upper - hadj.page_size
+ newval = hadj.upper - hadj.page_size
+ end
+ hadj.value = newval
+ end
+ else
+ hadj.value = clamp((xpos_left + xpos_right) / 2 - hadj.page_size / 2, 0, hadj.upper - hadj.page_size)
+ end
+end
+
+def show_popup(parent, msg, *options)
+ dialog = Gtk::Dialog.new
+ if options[0]
+ options = options[0]
+ else
+ options = {}
+ end
+ if options[:title]
+ dialog.title = options[:title]
+ else
+ dialog.title = utf8(_("Booh message"))
+ end
+ lbl = Gtk::Label.new
+ if options[:nomarkup]
+ lbl.text = msg
+ else
+ lbl.markup = msg
+ end
+ if options[:centered]
+ lbl.set_justify(Gtk::Justification::CENTER)
+ end
+ if options[:selectable]
+ lbl.selectable = true
+ end
+ if options[:topwidget]
+ dialog.vbox.add(options[0][:topwidget])
+ end
+ if options[:scrolled]
+ sw = Gtk::ScrolledWindow.new(nil, nil)
+ sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
+ sw.add_with_viewport(lbl)
+ dialog.vbox.add(sw)
+ dialog.set_default_size(500, 600)
+ else
+ dialog.vbox.add(lbl)
+ dialog.set_default_size(200, 120)
+ end
+ if options[:bottomwidget]
+ dialog.vbox.add(options[:bottomwidget])
+ end
+ if options[:okcancel]
+ dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
+ end
+ dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
+
+ if options[:pos_centered]
+ dialog.window_position = Gtk::Window::POS_CENTER
+ else
+ dialog.window_position = Gtk::Window::POS_MOUSE
+ end
+
+ if options[:linkurl]
+ linkbut = Gtk::Button.new('')
+ linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
+ linkbut.signal_connect('clicked') {
+ open_url(options[0][:linkurl] + '/index.html')
+ dialog.response(Gtk::Dialog::RESPONSE_OK)
+ set_mousecursor_normal
+ }
+ linkbut.relief = Gtk::RELIEF_NONE
+ linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
+ linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
+ dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
+ end
+
+ dialog.show_all
+
+ if options[:stuff_connector]
+ options[:stuff_connector].call({ :dialog => dialog })
+ end
+
+ if !options[:not_transient]
+ dialog.transient_for = parent
+ dialog.run { |response|
+ if options[:data_getter]
+ options[:data_getter].call
+ end
+ dialog.destroy
+ if options[:okcancel]
+ return response == Gtk::Dialog::RESPONSE_OK
+ end
+ }
+ else
+ dialog.signal_connect('response') { dialog.destroy }
+ end
+end
+
+def view_entry(entry)
+ if entry.type == 'image'
+ show_popup($main_window,
+ utf8(`exif -m '#{entry.path}'`),
+ { :title => utf8(_("EXIF data of %s") % File.basename(entry.path)), :nomarkup => true, :scrolled => true, :not_transient => true })
+ else
+ cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{entry.path}'") + ' &'
+ msg 2, cmd
+ system(cmd)
+ end
+end
+
+def thumbnail_keypressed(entry, event)
+ if event.state & Gdk::Window::MOD1_MASK != 0
+ #- ALT pressed: Alt-Left and Alft-Right rotate
+ if event.keyval == Gdk::Keyval::GDK_Left || event.keyval == Gdk::Keyval::GDK_Right
+ if event.keyval == Gdk::Keyval::GDK_Left
+ entry.angle = (entry.angle - 90) % 360
+ else
+ entry.angle = (entry.angle + 90) % 360
+ end
+ entry.free_pixbuf_full
+ entry.free_pixbuf_main
+ entry.free_pixbuf_thumbnail
+ $mainview.redraw
+ entry.image.pixbuf = entry.pixbuf_thumbnail
+ if $config['rotate-set-exif'] == 'true' && entry.type == 'image'
+ Exif.set_orientation(entry.path, angle_to_exif_orientation(entry.angle))
+ end
+ end
+
+ elsif event.state & Gdk::Window::CONTROL_MASK != 0
+ #- CONTROL pressed: Ctrl-z and Ctrl-r for undo/redo, Ctrl-space for recentre
+ if event.keyval == Gdk::Keyval::GDK_z
+ perform_undo
+ end
+ if event.keyval == Gdk::Keyval::GDK_r
+ perform_redo
+ end
+ if event.keyval == Gdk::Keyval::GDK_space
+ shown = $mainview.get_shown_entry
+ shown and autoscroll_if_needed(shown.button, true)
+ end
+
+ else
+ removed_before = entry.removed
+ label_before = entry.labeled
+
+ if event.keyval == Gdk::Keyval::GDK_Delete
+ entry.removed = true
+ entry.labeled = nil
+ entry.show_bg
+ update_visibility(entry)
+ $mainview.show_next_entry(entry)
+
+ save_undo(_("set for removal"),
+ proc {
+ entry.removed = removed_before
+ entry.labeled = label_before
+ entry.show_bg
+ update_visibility(entry)
+ if entry.button.visible?
+ $mainview.try_show_entry(entry)
+ end
+ proc {
+ entry.removed = true
+ entry.labeled = nil
+ entry.show_bg
+ update_visibility(entry)
+ if entry.button.visible?
+ $mainview.try_show_entry(entry)
+ end
+ }
+ })
+
+ elsif event.keyval == Gdk::Keyval::GDK_space
+ if entry.labeled
+ msg = _("Cleared label")
+ elsif entry.removed
+ msg = _("Cleared set for removal")
+ end
+ entry.removed = false
+ entry.labeled = nil
+ entry.show_bg
+ $mainview.show_next_entry(entry)
+
+ save_undo(msg,
+ proc {
+ entry.removed = removed_before
+ entry.labeled = label_before
+ entry.show_bg
+ $mainview.try_show_entry(entry)
+ proc {
+ entry.removed = false
+ entry.labeled = nil
+ entry.show_bg
+ $mainview.try_show_entry(entry)
+ }
+ })
+
+ elsif event.keyval == Gdk::Keyval::GDK_Return
+ view_entry(entry)
+
+ else
+ char = [ Gdk::Keyval.to_unicode(event.keyval) ].pack("C*")
+ if char =~ /^[a-zA-z0-9]$/
+ label = $labels[char]
+
+ if label.nil?
+ vb = Gtk::VBox.new(false, 0)
+ vb.pack_start(labelentry = Gtk::Entry.new.set_text(char), false, false)
+ vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(bt = Gtk::ColorButton.new))
+ color = bt.color = Gdk::Color.new(16384 + rand(49151), 16384 + rand(49151), 16384 + rand(49151))
+ bt.signal_connect('color-set') { color = bt.color }
+ text = nil
+ labelentry.signal_connect('changed') { #- cannot add a new label with first letter of an existing label
+ while $labels.has_key?(labelentry.text[0,1])
+ labelentry.text = labelentry.text.sub(/./, '')
+ end
+ }
+ if show_popup($main_window,
+ utf8(_("You typed the text character '%s', which is not associated with a label.\nType in the full name of the label below to create a new one.")) % char,
+ { :okcancel => true, :bottomwidget => vb, :data_getter => proc { text = labelentry.text },
+ :stuff_connector => proc { |stuff| labelentry.select_region(0, 0)
+ labelentry.position = -1
+ labelentry.signal_connect('activate') { stuff[:dialog].response(Gtk::Dialog::RESPONSE_OK) } } } )
+ if text.length > 0
+ char = text[0,1] #- in case it changed
+ label = Label.new(text)
+ label.color = color
+ $labels[char] = label
+ $ordered_labels << label
+ lbl = Gtk::Label.new.set_markup('<b>(' + char + ')</b>' + text[1..-1]).set_justify(Gtk::Justification::CENTER)
+ $labels_vbox.pack_start(label.button = Gtk::CheckButton.new.add(evt = Gtk::EventBox.new.add(lbl)).show_all)
+ label.button.active = true
+ label.button.signal_connect('toggled') { update_all_visibilities }
+ evt.modify_bg(Gtk::StateType::NORMAL, label.color)
+ evt.modify_bg(Gtk::StateType::PRELIGHT, label.color.lighter.lighter)
+ evt.modify_bg(Gtk::StateType::ACTIVE, label.color.lighter)
+ end
+ end
+ end
+
+ if label
+ entry.removed = false
+ entry.labeled = label
+ entry.show_bg
+ update_visibility(entry)
+ $mainview.show_next_entry(entry)
+
+ save_undo(_("set label"),
+ proc {
+ entry.removed = removed_before
+ entry.labeled = label_before
+ entry.show_bg
+ update_visibility(entry)
+ if entry.button.visible?
+ $mainview.try_show_entry(entry)
+ end
+ proc {
+ entry.removed = false
+ entry.labeled = label
+ entry.show_bg
+ update_visibility(entry)
+ if entry.button.visible?
+ $mainview.try_show_entry(entry)
+ end
+ }
+ })
+ end
+ end
+ end
+ end
+end
+
+def sb_msg(msg)
+ $statusbar.pop(0)
+ if msg
+ $statusbar.push(0, utf8(msg))
+ end
+end
+
+def show_entry(entry, i, tips)
+ #- scope entry
+ #msg 3, "showing entry #{entry}"
+ entry.image = Gtk::Image.new(entry.pixbuf_thumbnail)
+ if entry.type == 'video'
+ entry.button = Gtk::Button.new.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false).
+ pack_start(entry.image).
+ pack_start(da2 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false))
+ da1.signal_connect('realize') { da1.window.set_back_pixmap($videoborder_pixmap, false) }
+ da2.signal_connect('realize') { da2.window.set_back_pixmap($videoborder_pixmap, false) }
+ else
+ entry.button = Gtk::Button.new.add(entry.image)
+ end
+ tips.set_tip(entry.button, entry.get_beautified_name, nil)
+ $imagesline.pack_start(entry.alignment = Gtk::Alignment.new(0.5, 1, 0, 0).add(entry.button).show_all, false, false)
+ entry.button.signal_connect('clicked') {
+ shown = $mainview.get_shown_entry
+ if shown != entry
+ shown and shown.alignment.set(0.5, 1, 0, 0)
+ entry.alignment.set(0.5, 0, 0, 0)
+ autoscroll_if_needed(entry.button, false)
+ $mainview.set_shown_entry(entry)
+ end
+ }
+ entry.button.signal_connect('button-press-event') { |w, event|
+ if entry.type == 'video' && event.event_type == Gdk::Event::BUTTON2_PRESS
+ video_view(entry)
+ end
+ }
+ entry.button.signal_connect('focus-in-event') { entry.button.clicked }
+ entry.button.signal_connect('key-press-event') { |w, e| thumbnail_keypressed(entry, e) }
+ if i == 0
+ entry.button.grab_focus
+ end
+ update_visibility(entry)
+ Gtk.main_iteration while Gtk.events_pending?
+end
+
+def show_entries(allentries)
+ sb_msg(_("Loading images..."))
+ $loading_progressbar.fraction = 0
+ $loading_progressbar.text = utf8(_("Loading... %d%") % 0)
+ $loading_progressbar.show
+ t1 = Time.now
+ total_loaded_files = 0
+ total_loaded_size = 0
+ i = 0
+ tips = Gtk::Tooltips.new
+ while i < allentries.size
+# printf "%d %s\n", i, __LINE__
+ entry = allentries[i]
+ if i == 0
+ loaded_pixbuf = entry.pixbuf_main(false)
+ else
+ loaded_pixbuf = entry.pixbuf_thumbnail
+ end
+ if $allentries != allentries
+ #- loaded another directory while this one was not yet finished
+ msg 3, "allentries differ, stopping this deprecated load"
+ return
+ end
+
+ if loaded_pixbuf
+ show_entry(entry, i, tips)
+ if $allentries != allentries
+ #- loaded another directory while this one was not yet finished
+ msg 3, "allentries differ, stopping this deprecated load"
+ return
+ end
+
+ total_loaded_size += file_size(entry.path)
+ total_loaded_files += 1
+ i += 1
+ if i > $config['preload-distance'].to_i && i <= $config['preload-distance'].to_i * 2
+ #- when we're at preload distance, beging preloading to preload distance
+ allentries[i - $config['preload-distance'].to_i].pixbuf_main(false)
+ end
+ if i == $config['preload-distance'].to_i * 2 + 1
+ #- when we're after double preload distance, activate normal preloading
+ $preloader_allowed = true
+ end
+
+ else
+ allentries.delete_at(i)
+ end
+ $loading_progressbar.fraction = i.to_f / allentries.size
+ $loading_progressbar.text = utf8(_("Loading... %d%") % (100 * $loading_progressbar.fraction))
+ if $quit
+ return
+ end
+ end
+ if i <= $config['preload-distance'].to_i * 2
+ #- not yet preloaded correctly
+ $preloader_allowed = true
+ run_preloader
+ end
+ sb_msg(_("%d images of total %s kB loaded in %3.2f seconds.") % [ total_loaded_files, commify(total_loaded_size / 1024), Time.now - t1 ])
+ $loading_progressbar.hide
+ $execute.sensitive = true
+end
+
+def reset_all
+ reset_labels
+ reset_thumbnails
+ $mainview.set_shown_entry(nil)
+ sb_msg(nil)
+ $preloader_allowed = false
+ $execute.sensitive = false
+end
+
+def open_dir(*paths)
+ #- remove visual stuff, so that user will see something is happening
+ reset_all
+ sb_msg(_("Scanning source directory..."))
+ Gtk.main_iteration while Gtk.events_pending?
+
+ for path in paths
+ path = File.expand_path(path.sub(%r|/$|, ''))
+ $workingdir = path
+ entries = []
+ if File.directory?(path)
+ examined_dirs = `find '#{path}' -type d -follow`.sort.collect { |v| v.chomp }
+ #- validate first
+ examined_dirs.each { |dir|
+ if dir =~ /'/
+ return utf8(_("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % dir)
+ end
+ Dir.entries(dir).each { |file|
+ if file =~ /'/ && type = entry2type(file) && type == 'video'
+ return utf8(_("Videos can't contain a single quote character ('), sorry: %s") % "#{dir}/#{file}")
+ end
+ }
+ }
+
+ #- scan for populate second
+ examined_dirs.each { |dir|
+ if File.basename(dir) =~ /^\./
+ msg 1, _("Ignoring directory %s, begins with a dot (indicating a hidden directory)") % dir
+ next
+ end
+ entries += Dir.entries(dir).collect { |file| File.join(dir, file) }
+ }
+
+ else
+ entries << path
+ end
+
+ if $sort_by_exif_date
+ dates = {}
+ entries.each { |file|
+ date_time = Exif.datetimeoriginal(file)
+ if ! date_time.nil?
+ dates[file] = date_time
+ end
+ }
+ entries = smartsort(entries, dates)
+ else
+ entries.sort!
+ end
+ entries.each { |file|
+ type = entry2type(file)
+ if type
+ if File.directory?(path)
+ $allentries << Entry.new(file, type, file[path.length + 1 .. -1])
+ else
+ $allentries << Entry.new(file, type, file)
+ end
+ end
+ }
+ end
+ return nil
+end
+
+def open_dir_popup
+ fc = Gtk::FileChooserDialog.new(utf8(_("Specify the directory to work with")),
+ nil,
+ Gtk::FileChooser::ACTION_SELECT_FOLDER,
+ nil,
+ [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+ fc.transient_for = $main_window
+ if $workingdir
+ fc.current_folder = $workingdir
+ end
+ ok = false
+ load = false
+ while !ok
+ if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+ msg = open_dir(fc.filename)
+ if msg
+ show_popup(fc, msg)
+ ok = false
+ else
+ ok = true
+ load = true
+ end
+ else
+ ok = true
+ end
+ end
+ fc.destroy
+ if load
+ show_entries($allentries)
+ end
+end
+
+def try_quit(*options)
+ if ! $allentries.detect { |e| e.removed || e.labeled } ||
+ show_popup($main_window,
+ utf8(_("Are you sure you want to quit?")),
+ { :okcancel => true })
+ Gtk.main_quit
+ $quit = true
+ end
+end
+
+def execute
+ dialog = Gtk::Dialog.new
+ dialog.title = utf8(_("Booh message"))
+
+ vb1 = Gtk::VBox.new(false, 5)
+ label = Gtk::Label.new.set_markup(utf8(_("You're about to <b>execute</b> actions on the marked images.\nPlease confirm below the actions. You cannot undo this operation!")))
+ vb1.pack_start(label, false, false)
+
+ lastpath = $workingdir
+
+ table = Gtk::Table.new(0, 0, false)
+ table.set_row_spacings(5)
+ table.set_column_spacings(5)
+ table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Label name:</b>"))).set_justify(Gtk::Justification::CENTER), 0, 1, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
+ table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Amount of pictures:</b>"))).set_justify(Gtk::Justification::CENTER), 1, 2, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
+ table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Pictures examples:</b>"))).set_justify(Gtk::Justification::CENTER), 2, 3, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
+ table.attach(Gtk::Label.new.set_markup(utf8(_("<b>Action to perform:</b>"))).set_justify(Gtk::Justification::CENTER), 3, 4, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0)
+ add_row = proc { |row, name, color, truthproc, normal|
+ table.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(Gtk::EventBox.new.add(Gtk::Label.new.set_markup(name)).modify_bg(Gtk::StateType::NORMAL, color)),
+ 0, 1, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
+ counter = 0
+ examples = Gtk::HBox.new(false, 5)
+ $allentries.each { |entry|
+ if truthproc.call(entry)
+ counter += 1
+ if counter < 4
+ thumbnail = Gtk::Image.new(entry.pixbuf_thumbnail)
+ if entry.type == 'video'
+ thumbnail = Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false).
+ pack_start(thumbnail).
+ pack_start(da2 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false)
+ da1.signal_connect('realize') { da1.window.set_back_pixmap($videoborder_pixmap, false) }
+ da2.signal_connect('realize') { da2.window.set_back_pixmap($videoborder_pixmap, false) }
+ end
+ examples.pack_start(thumbnail, false, false)
+ elsif counter == 4
+ examples.pack_start(Gtk::Label.new.set_markup("<b>...</b>"), false, false)
+ end
+ end
+ }
+ table.attach(Gtk::Label.new(counter.to_s).set_justify(Gtk::Justification::CENTER), 1, 2, row, row + 1, 0, 0, 5, 5)
+ table.attach(examples, 2, 3, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
+
+ if counter == 0
+ return {}
+ end
+
+ combostore = Gtk::ListStore.new(Gdk::Pixbuf, String)
+ iter = combostore.append
+ if normal
+ iter[0] = $main_window.render_icon(Gtk::Stock::PASTE, Gtk::IconSize::MENU)
+ iter[1] = utf8(_("Copy to:"))
+ iter = combostore.append
+ iter[0] = $main_window.render_icon(Gtk::Stock::GO_FORWARD, Gtk::IconSize::MENU)
+ iter[1] = utf8(_("Move to:"))
+ else
+ iter[0] = $main_window.render_icon(Gtk::Stock::DELETE, Gtk::IconSize::MENU)
+ iter[1] = utf8(_("Permanently remove"))
+ end
+ iter = combostore.append
+ iter[0] = $main_window.render_icon(Gtk::Stock::MEDIA_STOP, Gtk::IconSize::MENU)
+ iter[1] = utf8(_("Do nothing"))
+ combo = Gtk::ComboBox.new(combostore)
+ combo.active = 0
+ renderer = Gtk::CellRendererPixbuf.new
+ combo.pack_start(renderer, false)
+ combo.set_attributes(renderer, :pixbuf => 0)
+ renderer = Gtk::CellRendererText.new
+ combo.pack_start(renderer, true)
+ combo.set_attributes(renderer, :text => 1)
+
+ if normal
+ pathbutton = Gtk::Button.new.add(pathlabel = Gtk::Label.new.set_markup(utf8(_("<i>(unset)</i>"))))
+ pathbutton.signal_connect('clicked') {
+ fc = Gtk::FileChooserDialog.new(utf8(_("Specify the directory where to move the pictures to")),
+ nil,
+ Gtk::FileChooser::ACTION_SELECT_FOLDER,
+ nil,
+ [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+ fc.transient_for = dialog
+ if lastpath
+ fc.current_folder = lastpath
+ end
+ if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+ pathlabel.text = fc.filename
+ pathlabel.set_alignment(0, 0.5)
+ end
+ lastpath = fc.filename
+ fc.destroy
+ }
+ combo.signal_connect('changed') {
+ pathbutton.sensitive = combo.active <= 1
+ }
+ vb = Gtk::VBox.new(false, 5)
+ vb.pack_start(combo, false, false)
+ vb.pack_start(pathbutton, false, false)
+ table.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(vb), 3, 4, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
+ { :combo => combo, :pathlabel => pathlabel }
+ else
+ table.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(combo), 3, 4, row, row + 1, Gtk::FILL, Gtk::FILL, 5, 5)
+ { :combo => combo }
+ end
+ }
+ stuff = {}
+ stuff['toremove'] = add_row.call(1, utf8(_("<i>to remove</i>")), $color_red, proc { |entry| entry.removed }, false)
+ $ordered_labels.each_with_index { |label, row| stuff[label] = add_row.call(row + 2, label.name, label.color, proc { |entry| entry.labeled == label }, true) }
+ vb1.pack_start(sw = Gtk::ScrolledWindow.new(nil, nil).add_with_viewport(table).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC), true, true)
+
+ toremove_amount = $allentries.find_all { |entry| entry.removed }.size
+ toremove_size = commify($allentries.find_all { |entry| entry.removed }.collect { |entry| file_size(entry.path) }.sum / 1024)
+ check_removal = Gtk::CheckButton.new(utf8(_("I have noticed I am about to permanently remove the %d above mentioned pictures (total %s kB).") % [ toremove_amount, toremove_size ]))
+ if toremove_amount > 0
+ vb1.pack_start(check_removal, false, false)
+ stuff['toremove'][:combo].signal_connect('changed') { |widget|
+ check_removal.sensitive = widget.active == 0
+ }
+ end
+
+ dialog.vbox.add(vb1)
+
+ dialog.set_default_size(800, 600)
+ dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
+ dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
+ dialog.window_position = Gtk::Window::POS_MOUSE
+ dialog.transient_for = $main_window
+
+ dialog.show_all
+
+ while true
+ dialog.run { |response|
+ if response == Gtk::Dialog::RESPONSE_OK
+ if toremove_amount > 0 && ! check_removal.active? && stuff['toremove'][:combo].active == 0
+ show_popup(dialog, utf8(_("You have not confirmed that you noticed the permanent removal of the pictures marked for deletion.")))
+ break
+ end
+ problem = false
+ label2entries = {}
+ $labels.values.each { |label| label2entries[label] = [] }
+ $allentries.each { |entry| entry.labeled and label2entries[entry.labeled] << entry }
+ stuff.keys.each { |key|
+ if key.is_a?(Label) && stuff[key][:combo] && stuff[key][:combo].active <= 1
+ destination = stuff[key][:pathlabel].text
+ if destination[0] != ?/
+ show_popup(dialog, utf8(_("You have not selected a directory where to move/copy %s.") % key.name))
+ problem = true
+ break
+ end
+ begin
+ Dir.mkdir(destination)
+ rescue Errno::EEXIST
+ end
+ begin
+ st = File.stat(destination)
+ rescue
+ show_popup(dialog, utf8(_("Directory %s, where to move/copy %s, is not valid or not createable.") % [destination, key.name]))
+ problem = true
+ break
+ end
+ if ! st.directory? || ! st.writable?
+ show_popup(dialog, utf8(_("Directory %s, where to move/copy %s, is not valid or not writable.") % [destination, key.name]))
+ problem = true
+ break
+ end
+ label2entries[key].each { |entry|
+ begin
+ File.stat(File.join(destination, File.basename(entry.path)))
+ show_popup(dialog, utf8(_("Sorry, a file '%s' already exists in directory '%s'.") % [ File.basename(entry.path), destination ]))
+ problem = true
+ break
+ rescue
+ end
+ }
+ if problem
+ break
+ end
+ end
+ }
+ if ! problem
+ begin
+ moved = 0
+ copied = 0
+ stuff.keys.each { |key|
+ if key.is_a?(Label) && stuff[key][:combo] && stuff[key][:combo].active <= 1
+ destination = stuff[key][:pathlabel].text
+ label2entries[key].each { |entry|
+ if stuff[key][:combo].active == 0
+ system("cp -dp '#{entry.path}' '#{destination}'") or raise "failed to copy '#{entry.path}'"
+ copied += 1
+ elsif stuff[key][:combo].active == 1
+ system("mv '#{entry.path}' '#{destination}'") or raise "failed to move '#{entry.path}'"
+ moved += 1
+ end
+ }
+ end
+ }
+ removed = 0
+ if stuff['toremove'][:combo] && stuff['toremove'][:combo].active == 0
+ $allentries.each { |entry|
+ if entry.removed
+ File.delete(entry.path)
+ removed += 1
+ end
+ }
+ end
+ rescue
+ msg 1, "woops: #{$!}"
+ show_popup(dialog, utf8(_("Unexpected error: '%s'.") % $!))
+ end
+ show_popup(dialog, utf8(_("Successfully moved %d files, copied %d file, and removed %d files.") % [ moved, copied, removed ]))
+ dialog.destroy
+ reset_all
+ return
+ end
+
+ else
+ dialog.destroy
+ return
+ end
+ }
+ end
+end
+
+def visible(entry)
+ if ! entry.button
+ #- not yet loaded
+ return
+ end
+ if entry.labeled
+ if entry.labeled.button.active?
+ return true
+ else
+ return false
+ end
+ elsif entry.removed
+ if $toremove_button.active?
+ return true
+ else
+ return false
+ end
+ else
+ if $unlabelled_button.active?
+ return true
+ else
+ return false
+ end
+ end
+end
+
+def update_visibility(entry)
+ v = visible(entry)
+ if v.nil?
+ return
+ end
+ if v
+ entry.button.show
+ else
+ entry.button.hide
+ end
+end
+
+def update_all_visibilities_aux
+ $allentries.each { |entry|
+ update_visibility(entry)
+ }
+ shown = $mainview.get_shown_entry
+ shown or return
+ while shown.button && ! shown.button.visible? && shown != $allentries.last
+ shown = $allentries[$allentries.index(shown) + 1]
+ end
+ if shown.button && shown.button.visible?
+ shown.button.grab_focus
+ return
+ end
+ $allentries.reverse.each { |entry|
+ if entry.button && entry.button.visible?
+ entry.button.grab_focus
+ return
+ end
+ }
+end
+
+def update_all_visibilities
+ update_all_visibilities_aux
+ Gtk.main_iteration while Gtk.events_pending?
+ shown = $mainview.get_shown_entry
+ shown and autoscroll_if_needed(shown.button, false)
+end
+
+
+def preferences
+ dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
+ $main_window,
+ Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+ [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+ tooltips = Gtk::Tooltips.new
+
+ table_y = 0
+
+ dialog.vbox.add(tbl = Gtk::Table.new(0, 0, false))
+ tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
+ 0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
+ 1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;\nfor example: /usr/bin/mplayer %f")), nil)
+
+ table_y += 1
+ tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
+ 0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
+ 1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;\nfor example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
+
+ table_y += 1
+ tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Thumbnails height: ")))),
+ 0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(thumbnails_height = Gtk::SpinButton.new(32, 256, 16).set_value($config['thumbnails-height'].to_i)),
+ 1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(thumbnails_height, utf8(_("The desired height of the thumbnails in the thumbnails line of the bottom")), nil)
+
+ table_y += 1
+ tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Preloading distance: ")))),
+ 0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(preload_distance = Gtk::SpinButton.new(0, 50, 1).set_value($config['preload-distance'].to_i)),
+ 1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(preload_distance, utf8(_("Amount of pictures preloaded left and right to the currently shown")), nil)
+
+ table_y += 1
+ tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Cache memory use: ")))),
+ 0, 1, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(cache_vbox = Gtk::VBox.new(false, 0)),
+ 1, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ cache_vbox.pack_start(Gtk::HBox.new(false, 0).pack_start(cache_memfree_radio = Gtk::RadioButton.new(''), false, false).
+ pack_start(cache_memfree_spin = Gtk::SpinButton.new(0, 100, 10), false, false).
+ pack_start(cache_memfree_label = Gtk::Label.new(utf8(_("% of free memory"))), false, false), false, false)
+ cache_memfree_spin.signal_connect('value-changed') { cache_memfree_radio.active = true }
+ tooltips.set_tip(cache_memfree_spin, utf8(_("Percentage of free memory (+ buffers/cache) measured at startup")), nil)
+ cache_vbox.pack_start(Gtk::HBox.new(false, 0).pack_start(cache_specify_radio = Gtk::RadioButton.new(cache_memfree_radio, ''), false, false).
+ pack_start(cache_specify_spin = Gtk::SpinButton.new(0, 4000, 50), false, false).
+ pack_start(cache_specify_label = Gtk::Label.new(utf8(_("MB"))).set_sensitive(false), false, false), false, false)
+ cache_specify_spin.signal_connect('value-changed') { cache_specify_radio.active = true }
+ cache_memfree_radio.signal_connect('toggled') {
+ if cache_memfree_radio.active?
+ cache_memfree_label.sensitive = true
+ cache_specify_label.sensitive = false
+ else
+ cache_specify_label.sensitive = true
+ cache_memfree_label.sensitive = false
+ end
+ }
+ tooltips.set_tip(cache_specify_spin, utf8(_("Amount of memory in megabytes")), nil)
+ if $config['cache-memory-use'] =~ /memfree_(\d+)/
+ cache_memfree_spin.value = $1.to_i
+ else
+ cache_specify_spin.value = $config['cache-memory-use'].to_i
+ end
+
+ table_y += 1
+ tbl.attach(update_exif_orientation_check = Gtk::CheckButton.new(utf8(_("Update file's EXIF orientation when rotating a picture"))),
+ 0, 2, table_y, table_y + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+ tooltips.set_tip(update_exif_orientation_check, utf8(_("When rotating a picture (Alt-Right/Left), also update EXIF orientation in the file itself")), nil)
+ update_exif_orientation_check.active = $config['rotate-set-exif'] == 'true'
+
+ dialog.vbox.show_all
+ dialog.run { |response|
+ if response == Gtk::Dialog::RESPONSE_OK
+ $config['video-viewer'] = from_utf8(video_viewer_entry.text)
+ $config['browser'] = from_utf8(browser_entry.text)
+ $config['thumbnails-height'] = thumbnails_height.value
+ $config['preload-distance'] = preload_distance.value
+ $config['rotate-set-exif'] = update_exif_orientation_check.active?.to_s
+ if cache_memfree_radio.active?
+ $config['cache-memory-use'] = "memfree_#{cache_memfree_spin.value}%"
+ else
+ $config['cache-memory-use'] = cache_specify_spin.value
+ end
+ set_cache_memory_use_figure
+ end
+ }
+ dialog.destroy
+end
+
+def perform_undo
+ if $undo_mb.sensitive?
+ $redo_mb.sensitive = true
+ if not more_undoes = UndoHandler.undo($statusbar)
+ $undo_mb.sensitive = false
+ end
+ end
+end
+
+def perform_redo
+ if $redo_mb.sensitive?
+ $undo_mb.sensitive = true
+ if not more_redoes = UndoHandler.redo($statusbar)
+ $redo_mb.sensitive = false
+ end
+ end
+end
+
+def create_menubar
+ #- menu
+ mb = Gtk::MenuBar.new
+
+ filemenu = Gtk::MenuItem.new(utf8(_("_File")))
+ filesubmenu = Gtk::Menu.new
+ filesubmenu.append(open = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
+ filesubmenu.append( Gtk::SeparatorMenuItem.new)
+ filesubmenu.append($execute = Gtk::ImageMenuItem.new(Gtk::Stock::EXECUTE).set_sensitive(false))
+ filesubmenu.append( Gtk::SeparatorMenuItem.new)
+ filesubmenu.append(quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
+ filemenu.set_submenu(filesubmenu)
+ mb.append(filemenu)
+
+ open.signal_connect('activate') { open_dir_popup }
+ $execute.signal_connect('activate') { execute }
+ quit.signal_connect('activate') { try_quit }
+
+ editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
+ editsubmenu = Gtk::Menu.new
+ editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
+ editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
+ editsubmenu.append( Gtk::SeparatorMenuItem.new)
+ editsubmenu.append(prefs = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
+ editmenu.set_submenu(editsubmenu)
+ mb.append(editmenu)
+
+ $undo_mb.signal_connect('activate') { perform_undo }
+ $redo_mb.signal_connect('activate') { perform_redo }
+ prefs.signal_connect('activate') { preferences }
+
+ helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
+ helpsubmenu = Gtk::Menu.new
+ helpsubmenu.append(howto = Gtk::ImageMenuItem.new(Gtk::Stock::HELP))
+ helpsubmenu.append(speed = Gtk::ImageMenuItem.new(utf8(_("Speedup: key shortcuts"))))
+ speed.image = Gtk::Image.new("#{$FPATH}/images/stock-info-16.png")
+ helpsubmenu.append(tutos = Gtk::ImageMenuItem.new(utf8(_("Online tutorials (opens a web-browser)"))))
+ tutos.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
+ helpsubmenu.append(Gtk::SeparatorMenuItem.new)
+ helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
+ helpmenu.set_submenu(helpsubmenu)
+ mb.append(helpmenu)
+
+ howto.signal_connect('activate') {
+ show_popup($main_window, utf8(_("<span size='large' weight='bold'>Help</span>
+
+1. Open a directory with <span foreground='darkblue'>File/Open</span>; the classifier will scan it (including subdirectories) and
+show thumbnails for all photos and videos at the bottom.
+
+2. You can then navigate through images with the <span foreground='darkblue'>Left/Right</span> keyboard keys, or by <span foreground='darkblue'>clicking</span>
+on thumbnails.
+
+3. You may associate a <span foreground='darkblue'>label</span> to each thumbnail. Either hit the <span foreground='darkblue'>Delete</span> key to associate
+the built-in <i>to remove</i> label, or hit any alphabetical key to associate a label you define.
+The first time you hit a key without any label associated, a popup will ask for the full
+name of this label, and what color you want. To clear the current label, hit the <span foreground='darkblue'>Space</span> key.
+
+4. To help you better view what thumbnails are associated to your labels, you may <span foreground='darkblue'>hide</span>
+some of them by unchecking the labels checkboxes on the left.
+
+5. Once you're finished reviewing all thumbnails, use <span foreground='darkblue'>File/Execute</span> to execute the desired
+actions according to associated labels. You can permanently remove (or not) images with
+the <i>to remove</i> label, and copy or move images with the labels you defined.
+")), { :pos_centered => true, :not_transient => true })
+ }
+ speed.signal_connect('activate') {
+ show_popup($main_window, utf8(_("<span size='large' weight='bold'>Key shortcuts</span>
+
+<span foreground='darkblue'>Left/Right</span>: move left and right in images
+<span foreground='darkblue'>Enter</span>: 'view' current image: for images, display EXIF data; for videos, play it
+<span foreground='darkblue'>Alt-Left/Right</span>: rotate current image clockwise/counter-clockwise
+<span foreground='darkblue'>Delete</span>: assign the 'to remove' label on current image
+<span foreground='darkblue'>Space</span>: clear any label on current image
+<span foreground='darkblue'>Control-z</span>: undo
+<span foreground='darkblue'>Control-r</span>: redo
+<span foreground='darkblue'>Control-Space</span>: recenter thumbnails on current item
+
+Any alphabetical key will assign (or popup for) the associated label on current image.
+")), { :pos_centered => true, :not_transient => true })
+ }
+ tutos.signal_connect('activate') { open_url('http://booh.org/tutorial') }
+ about.signal_connect('activate') { call_about }
+
+
+ #- no toolbar, to save height
+
+ return mb
+end
+
+def reset_labels
+ for child in $labels_vbox.children
+ $labels_vbox.remove(child)
+ end
+ $labels_vbox.pack_start(Gtk::Label.new(utf8(_("Labels list:"))).set_justify(Gtk::Justification::CENTER), false, false).show_all
+ $labels = {}
+ $ordered_labels = []
+ lbl = Gtk::Label.new.set_markup(utf8(_("<i>unlabelled</i>")))
+ $labels_vbox.pack_start($unlabelled_button = Gtk::CheckButton.new.add(Gtk::EventBox.new.add(lbl)).show_all)
+ $unlabelled_button.active = true
+ $unlabelled_button.signal_connect('toggled') { update_all_visibilities }
+ lbl = Gtk::Label.new.set_markup(utf8(_("<i>to remove</i>")))
+ $labels_vbox.pack_start($toremove_button = Gtk::CheckButton.new.add(evt = Gtk::EventBox.new.add(lbl)).show_all)
+ $toremove_button.active = true
+ $toremove_button.signal_connect('toggled') { update_all_visibilities }
+ evt.modify_bg(Gtk::StateType::NORMAL, $color_red)
+ evt.modify_bg(Gtk::StateType::PRELIGHT, $color_red.lighter.lighter)
+ evt.modify_bg(Gtk::StateType::ACTIVE, $color_red.lighter)
+end
+
+def reset_thumbnails
+ $allentries = []
+ if $preloader_running
+ $preloader_force_exit = true
+ end
+ for child in $imagesline.children
+ $imagesline.remove(child)
+ end
+ set_imagesline_size_request
+end
+
+def set_imagesline_size_request
+ $imagesline.set_size_request(-1, Gtk::Button.new.size_request[1] + Entry.thumbnails_height + Entry.thumbnails_height/4)
+end
+
+def create_main_window
+
+ $videoborder_pixbuf = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
+ $videoborder_pixmap, = $videoborder_pixbuf.render_pixmap_and_mask(0)
+
+ mb = create_menubar
+
+ main_vbox = Gtk::VBox.new(false, 0)
+ main_vbox.pack_start(mb, false, false)
+ mainview_hbox = Gtk::HBox.new
+ mainview_hbox.pack_start(Gtk::Alignment.new(0.5, 0, 1, 1).add(left_vbox = Gtk::VBox.new(false, 5)), false, true)
+ left_vbox.pack_start(($labels_vbox = Gtk::VBox.new(false, 5)), false, true)
+ left_vbox.pack_end($loading_progressbar = Gtk::ProgressBar.new.set_text(utf8(_("Loading... %d%") % 0)), false, true)
+ mainview_hbox.pack_start($mainview = MainView.new, true, true)
+ main_vbox.pack_start(mainview_hbox, true, true)
+ $imagesline_sw = Gtk::ScrolledWindow.new(nil, nil)
+ $imagesline_sw.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_NEVER)
+ $imagesline_sw.add_with_viewport($imagesline = Gtk::HBox.new(false, 0).show)
+ main_vbox.pack_start($imagesline_sw, false, false)
+ main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
+
+ set_imagesline_size_request
+
+ $main_window = create_window
+ $main_window.add(main_vbox)
+ $main_window.signal_connect('delete-event') {
+ try_quit({ :disallow_cancel => true })
+ }
+
+ #- read/save size and position of window
+ if $config['pos-x'] && $config['pos-y']
+ $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
+ else
+ $main_window.window_position = Gtk::Window::POS_CENTER
+ end
+ msg 3, "size: #{$config['width']}x#{$config['height']}"
+ $main_window.set_default_size(($config['width'] || 800).to_i, ($config['height'] || 600).to_i)
+ $main_window.signal_connect('configure-event') {
+ msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
+ x, y = $main_window.window.root_origin
+ width, height = $main_window.window.size
+ $config['pos-x'] = x
+ $config['pos-y'] = y
+ $config['width'] = width
+ $config['height'] = height
+ false
+ }
+
+ $main_window.show_all
+ $loading_progressbar.hide
+end
+
+
+handle_options
+read_config
+Gtk.init
+
+
+create_main_window
+check_config
+
+if ARGV[0]
+ if msg = open_dir(*ARGV)
+ puts msg
+ else
+ Gtk.idle_add {
+ show_entries($allentries)
+ false
+ }
+ end
+end
+Gtk.main
+
+write_config
Property changes on: packages/booh/branches/upstream/current/bin/booh-classifier
___________________________________________________________________
Name: svn:executable
+
Added: packages/booh/branches/upstream/current/bin/booh-fix-whitebalance
===================================================================
--- packages/booh/branches/upstream/current/bin/booh-fix-whitebalance (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh-fix-whitebalance 2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,31 @@
+#!/usr/bin/ruby
+#
+# * BOOH *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'gtk2'
+require 'booh/libadds'
+
+if ARGV.size != 3
+ puts "Usage: %s orig_file dest_file_jpeg level" % File.basename($0)
+ exit 1
+
+else
+ Gdk::Pixbuf.new(ARGV[0]).whitebalance!(ARGV[2].to_f).save(ARGV[1], "jpeg")
+end
Property changes on: packages/booh/branches/upstream/current/bin/booh-fix-whitebalance
___________________________________________________________________
Name: svn:executable
+
Added: packages/booh/branches/upstream/current/bin/booh-gamma-correction
===================================================================
--- packages/booh/branches/upstream/current/bin/booh-gamma-correction (rev 0)
+++ packages/booh/branches/upstream/current/bin/booh-gamma-correction 2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,31 @@
+#!/usr/bin/ruby
+#
+# * BOOH *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'gtk2'
+require 'booh/libadds'
+
+if ARGV.size != 3
+ puts "Usage: %s orig_file dest_file_jpeg level" % File.basename($0)
+ exit 1
+
+else
+ Gdk::Pixbuf.new(ARGV[0]).gammacorrect!(ARGV[2].to_f).save(ARGV[1], "jpeg")
+end
Property changes on: packages/booh/branches/upstream/current/bin/booh-gamma-correction
___________________________________________________________________
Name: svn:executable
+
Added: packages/booh/branches/upstream/current/bin/webalbum2booh
===================================================================
--- packages/booh/branches/upstream/current/bin/webalbum2booh (rev 0)
+++ packages/booh/branches/upstream/current/bin/webalbum2booh 2009-03-03 21:48:36 UTC (rev 3247)
@@ -0,0 +1,163 @@
+#! /usr/bin/ruby
+#
+# * BOOH *
+#
+# A.k.a 'Best web-album Of the world, Or your money back, Humerus'.
+#
+# The acronyn sucks, however this is a tribute to Dragon Ball by
+# Akira Toriyama, where the last enemy beaten by heroes of Dragon
+# Ball is named "Boo". But there was already a free software project
+# called Boo, so this one will be it "Booh". Or whatever.
+#
+#
+# Copyright (c) 2004-2006 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+#
+# This software may be freely redistributed under the terms of the GNU
+# public license version 2.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+require 'getoptlong'
+require 'gettext'
+include GetText
+require 'booh/rexml/document'
+include REXML
+
+require 'booh/booh-lib'
+include Booh
+
+#- options
+$options = [
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
+
+ [ '--config', '-C', GetoptLong::REQUIRED_ARGUMENT, _("File containing config listing images and videos within directories with captions") ],
+
+ [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+]
+
+#- default values for some globals
+$switches = []
+$stdout.sync = true
+
+def usage
+ puts _("Usage: %s [OPTION]...") % File.basename($0)
+ $options.each { |ary|
+ printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
+ }
+end
+
+def handle_options
+ parser = GetoptLong.new
+ parser.set_options(*$options.collect { |ary| ary[0..2] })
+ begin
+ parser.each_option do |name, arg|
+ case name
+ when '--help'
+ usage
+ exit(0)
+
+ when '--config'
+ if File.readable?(arg)
+ $xmldoc = REXML::Document.new File.new(arg)
+ $conffile = arg
+ else
+ die_ _('Config file does not exist or is unreadable.')
+ end
+
+ when '--verbose-level'
+ $verbose_level = arg.to_i
+
+ end
+ end
+ rescue
+ puts $!
+ usage
+ exit(1)
+ end
+
+ if !$xmldoc
+ die_ _("Missing --config parameter.")
+ end
+
+ $source = $xmldoc.root.attributes['source']
+ $dest = $xmldoc.root.attributes['destination']
+end
+
+def utf8_and_entities(string)
+ return utf8(string).gsub('à', 'Ã ').
+ gsub('ç', 'ç').
+ gsub('ô', 'ô').
+ gsub('é', 'é').
+ gsub('ê', 'ê').
+ gsub('è', 'è').
+ gsub('È', 'Ã').
+ gsub('î', 'î').
+ gsub('<', '<').
+ gsub('>', '>').
+ gsub('ù', 'ù').
+ gsub('"', '"')
+end
+
+def parse_webalbum_indextxt(filepath)
+ begin
+ contents = File.open(filepath).readlines
+ out = {}
+ out[:legends] = {}
+ legend = ''
+ for line in contents
+ if line =~ /^\s*#/
+ next
+ elsif line =~ /^\s*TITLE\s*=\s*(.*)/
+ out[:title] = $1
+ elsif line =~ /^\s*ABSTRACT\s*=\s*(.*)/
+ out[:abstract] = $1
+ elsif line =~ /^\s*IMAGE_LEGEND\s*=\s*(.*)/
+ legend = $1
+ elsif line =~ /^\s*IMAGE_FILE\s*=\s*(.*)/
+ out[:legends][$1] = legend
+ end
+ end
+ return out
+ rescue
+ return nil
+ end
+end
+
+def walk_source_dir
+
+ `find #{$source} -type d`.sort.each { |dir|
+ dir.chomp!
+ msg 2, _("Handling %s from config list...") % dir
+
+ if !infos = parse_webalbum_indextxt("#{dir}/index.txt")
+ next
+ end
+
+ #- place xml document on proper node
+ xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
+
+ if infos.has_key?(:title)
+ type = find_subalbum_info_type(xmldir)
+ xmldir.add_attribute("#{type}-caption", utf8_and_entities(infos[:title]))
+ end
+
+ xmldir.elements.each { |element|
+ if %w(image video).include?(element.name)
+ if infos[:legends].has_key?(element.attributes['filename'])
+ element.add_attribute('caption', utf8_and_entities(infos[:legends][element.attributes['filename']]))
+ end
+ end
+ }
+ }
+end
+
+
+handle_options
+
+walk_source_dir
+
+ios = File.open("#{$conffile}.merged", "w")
+$xmldoc.write(ios, 0)
+ios.close
Property changes on: packages/booh/branches/upstream/current/bin/webalbum2booh
___________________________________________________________________
Name: svn:executable
+
More information about the Pkg-ruby-extras-commits
mailing list