#! /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 # # 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 "" $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 if entry.button.has_focus? redraw else entry.button.grab_focus end 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 && ! $allentries[index].button.visible? index += 1 end while $allentries[index] && $allentries[index].button && ! $allentries[index].button.visible? && index > 0 index -= 1 end if index < $allentries.size && $allentries[index] && $allentries[index].button && $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 = "#{options[0][:linkurl]}" 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('(' + char + ')' + 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 execute 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(_("Label name:"))).set_justify(Gtk::Justification::CENTER), 0, 1, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0) table.attach(Gtk::Label.new.set_markup(utf8(_("Amount of pictures:"))).set_justify(Gtk::Justification::CENTER), 1, 2, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0) table.attach(Gtk::Label.new.set_markup(utf8(_("Pictures examples:"))).set_justify(Gtk::Justification::CENTER), 2, 3, 0, 1, Gtk::FILL, Gtk::FILL, 5, 0) table.attach(Gtk::Label.new.set_markup(utf8(_("Action to perform:"))).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("..."), 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(_("(unset)")))) 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(_("to remove")), $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 #- just "executed" return end 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 / 1024 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.to_i * 1024 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(_("Help 1. Open a directory with File/Open; 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 Left/Right keyboard keys, or by clicking on thumbnails. 3. You may associate a label to each thumbnail. Either hit the Delete key to associate the built-in to remove 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 Space key. 4. To help you better view what thumbnails are associated to your labels, you may hide some of them by unchecking the labels checkboxes on the left. 5. Once you're finished reviewing all thumbnails, use File/Execute to execute the desired actions according to associated labels. You can permanently remove (or not) images with the to remove 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(_("Key shortcuts Left/Right: move left and right in images Enter: 'view' current image: for images, display EXIF data; for videos, play it Alt-Left/Right: rotate current image clockwise/counter-clockwise Delete: assign the 'to remove' label on current image Space: clear any label on current image Control-z: undo Control-r: redo Control-Space: 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(_("unlabelled"))) $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(_("to remove"))) $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