require 'getoptlong'
require 'tempfile'
-require 'thread'
require 'gtk2'
require 'booh/libadds'
[ '--sort-by-exif-date', '-s', GetoptLong::NO_ARGUMENT, _("Sort entries by EXIF date") ],
]
+$preloader_allowed = false
+
def usage
puts _("Usage: %s [OPTION]...") % File.basename($0)
$options.each { |ary|
if @@max_width.nil?
@@max_width = $main_window.root_window.size[0] - $labels_vbox.allocation.width - ( $videoborder_pixbuf.width + MainView.borders_thickness) * 2
end
- @protect_cleanup = Mutex.new
end
def pixbuf_full
- @protect_cleanup.synchronize {
- if @pixbuf_full.nil?
- msg 3, ">>> pixbuf_full #{path}"
- load_into_pixbuf_full
- end
- return @pixbuf_full
- }
+ if @pixbuf_full.nil?
+ msg 3, ">>> pixbuf_full #{path}"
+ load_into_pixbuf_full
+ end
+ return @pixbuf_full
end
def free_pixbuf_full
- @protect_cleanup.synchronize {
+ if @pixbuf_full.nil?
+ return false
+ else
+ msg 3, ">>> free_pixbuf_full #{path}"
+ @pixbuf_full = nil
+ return true
+ end
+ end
+ def pixbuf_main
+ 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 #- make sure it is loaded
if @pixbuf_full.nil?
- return false
- else
- msg 3, ">>> free_pixbuf_full #{path}"
- @pixbuf_full = nil
- return true
+ return
end
- }
- end
- def pixbuf_main(width, height)
- @protect_cleanup.synchronize {
- if @pixbuf_main.nil? || width != @width || height != @height
- msg 3, ">>> pixbuf_main #{path}"
- @width = width
- @height = height
- load_into_pixbuf_full #- make sure it is loaded
- 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
+ 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
- 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
+ @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
- return @pixbuf_main
- }
+ end
+ return @pixbuf_main
end
def free_pixbuf_main
- @protect_cleanup.synchronize {
- if @pixbuf_main.nil?
- return false
- else
- msg 3, ">>> free_pixbuf_main #{path}"
- @pixbuf_main = nil
- return true
- end
- }
+ if @pixbuf_main.nil?
+ return false
+ else
+ msg 3, ">>> free_pixbuf_main #{path}"
+ @pixbuf_main = nil
+ return true
+ end
end
def pixbuf_thumbnail
- return @protect_cleanup.synchronize {
- if @pixbuf_thumbnail.nil?
- msg 3, ">>> pixbuf_thumbnail #{path}"
- load_into_pixbuf_full #- make sure it is loaded
- @pixbuf_thumbnail = @pixbuf_full.scale(@pixbuf_full.width * (@@thumbnails_height.to_f/@pixbuf_full.height), @@thumbnails_height, Gdk::Pixbuf::INTERP_BILINEAR)
+ if @pixbuf_thumbnail.nil?
+ if @pixbuf_main
+ msg 3, ">>> pixbuf_thumbnail from main #{path}"
+ @pixbuf_thumbnail = @pixbuf_main.scale(@pixbuf_main.width * (@@thumbnails_height.to_f/@pixbuf_main.height), @@thumbnails_height, Gdk::Pixbuf::INTERP_BILINEAR)
+ else
+ msg 3, ">>> pixbuf_thumbnail from file #{path}"
+ @pixbuf_thumbnail = load_into_pixbuf_at_size { |w, h|
+ if @angle == 0
+ if h > @@thumbnails_height
+ [ w * @@thumbnails_height.to_f/h, @@thumbnails_height ]
+ else
+ [ w, h ]
+ end
+ else
+ if w > @@thumbnails_height
+ [ @@thumbnails_height, h * @@thumbnails_height.to_f/w ]
+ else
+ [ w, h ]
+ end
+ end
+ }
end
- return @pixbuf_thumbnail
- }
+ end
+ return @pixbuf_thumbnail
end
def free_pixbuf_thumbnail
- @protect_cleanup.synchronize {
- if @pixbuf_thumbnail.nil?
- return false
- else
- msg 3, ">>> free_pixbuf_thumbnail #{path}"
- @pixbuf_thumbnail = nil
- return true
- end
- }
+ if @pixbuf_thumbnail.nil?
+ return false
+ else
+ msg 3, ">>> free_pixbuf_thumbnail #{path}"
+ @pixbuf_thumbnail = nil
+ return true
+ end
end
def outline_color
def load_into_pixbuf_full
if @pixbuf_full.nil?
msg 3, ">>> load_into_pixbuf_full #{path}"
- if @type == 'video'
- tmp = Tempfile.new("boohclassifiertemp")
- tmp.close!
- Dir.mkdir(dest_dir = tmp.path)
- 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
- begin
- #- use a pixbuf loader to allow the loading thread to be interrupted by the main thread more often, 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|
- maxdim = w > h ? w : h
- if maxdim > @@max_width
+ @pixbuf_full = load_into_pixbuf_at_size { |w, h|
+ if @angle == 0
+ if w > @@max_width
#- save memory and speedup (+35%) loading
- loader.set_size(w * (factor = @@max_width.to_f/maxdim), h * factor)
+ [ w * (factor = @@max_width.to_f/w), h * factor ]
+ else
+ [ w, h ]
end
- }
- loader.signal_connect('area-prepared') { @pixbuf_full = loader.pixbuf }
- file = File.new(image_path)
- while (chunk = file.read(65536)) != nil
- loader.write(chunk)
- end
- file.close
- loader.close
- if @pixbuf_full.nil?
- raise "Loaded pixbuf nil - #{path} #{image_path}"
- end
- rescue Gdk::PixbufError
- msg 0, "Cannot load #{image_path}: #{$!}"
- return
- ensure
- if @type == 'video'
- File.delete(image_path)
- Dir.rmdir(tmpdir)
- end
- end
- if @pixbuf_full
- if @angle.nil?
- if @type == 'image'
- @angle = guess_rotate(image_path)
+ else
+ if h > @@max_width
+ [ w * (factor = @@max_width.to_f/h), h * factor ]
else
- @angle = 0
+ [ w, h ]
end
end
- if @angle != 0
- msg 3, ">>> load_into_pixbuf_full #{image_path} => rotate #{@angle}"
- @pixbuf_full = rotate_pixbuf(@pixbuf_full, @angle)
- end
+ }
+ end
+ end
+
+ def load_into_pixbuf_at_size(&specify_size)
+ pixbuf = nil
+ if @type == 'video'
+ tmp = Tempfile.new("boohclassifiertemp")
+ tmp.close!
+ Dir.mkdir(dest_dir = tmp.path)
+ 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))
+ }
+ loader.signal_connect('area-prepared') { pixbuf = loader.pixbuf }
+ file = File.new(image_path)
+ while (chunk = file.read(4096)) != nil
+ loader.write(chunk)
+ Gtk.main_iteration while Gtk.events_pending?
+ end
+ file.close
+ loader.close
+ if pixbuf.nil?
+ raise "Loaded pixbuf nil - #{path} #{image_path}"
+ end
+ rescue Gdk::PixbufError
+ msg 0, "Cannot load #{image_path}: #{$!}"
+ return
+ ensure
if @type == 'video'
- cleanup_dir(dest_dir)
+ 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
$allentries = []
+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
+ mem = get_mem
+ if mem > $config['cache-memory-use-figure']
+ msg 3, "too much RSS, stopping preloading, triggering GC"
+ $preloader_running = false
+ GC.start
+ msg 3, "GC finished"
+ return
+ end
+ index = $allentries.index($mainview.get_shown_entry)
+ for j in 1 .. $config['preload-distance'].to_i
+ i = index + j
+ if i < $allentries.size
+ $allentries[i].pixbuf_main
+ end
+ i = index - j
+ if i >= 0
+ $allentries[i].pixbuf_main
+ end
+ end
+ check_memory_free_cache_if_needed
+ end
+ $preloader_running = false
+ msg 3, "*** << main preloading finished"
+ #- if we're already on a different image, rerun the preloader
+ if index != $allentries.index($mainview.get_shown_entry)
+ run_preloader_real
+ end
+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
def initialize
super()
signal_connect('expose-event') { draw }
- signal_connect('configure-event') { update_shown; GC.start }
- signal_connect('realize') {
- @preloader = Thread.new {
- #- background preloading
- while true
- msg 3, "*** >> background main preloading triggered..."
- if ! @entry.nil?
- index = $allentries.index(@entry)
- w, h = window.size
- w = MainView.get_usable_width(w)
- h = MainView.get_usable_height(h)
- for j in 1 .. $config['preload-distance'].to_i
- i = index + j
- if i < $allentries.size
- $allentries[i].pixbuf_main(w, h)
- end
- i = index - j
- if i >= 0
- $allentries[i].pixbuf_main(w, h)
- end
- GC.start
- mem = get_mem
- if mem > $config['cache-memory-use-figure']
- msg 3, "too much RSS, stopping main preloading"
- break
- end
- end
- check_memory_free_cache_if_needed
- end
- msg 3, "*** << background main preloading stopping"
- Thread.stop
- end
- }
- @preloader.priority = -2
- }
+ signal_connect('configure-event') { update_shown }
+ @preloader_running = false
end
def set_shown_entry(entry)
+ t1 = Time.now
if ! entry.nil? && entry == @entry
return
end
end
@entry = entry
redraw
- @preloader.run
+ run_preloader
+ msg 3, "entry shown in: #{Time.now - t1} s"
end
def get_shown_entry
def update_shown
if @entry
+ @pixbuf = @entry.pixbuf_main
width, height = window.size
- usable_width = MainView.get_usable_width(width)
- usable_height = MainView.get_usable_height(height)
- @pixbuf = @entry.pixbuf_main(usable_width, usable_height)
@xpos = (width - @pixbuf.width)/2
@ypos = (height - @pixbuf.height)/2
- @preloader.run
else
@pixbuf = nil
end
end
def check_memory_free_cache_if_needed
- GC.start
- mem = get_mem
i = $allentries.index($mainview.get_shown_entry)
- msg 3, "mem: #{mem} index: #{i}"
return if i.nil?
+ if get_mem < $config['cache-memory-use-figure'] * 2 / 3
+ return
+ end
+ msg 3, "too much RSS, triggering GC"
+ GC.start
+ msg 3, "GC finished"
($allentries.size - 1).downto(1) { |j|
- if mem < $config['cache-memory-use-figure'] * 2 / 3
+ if get_mem < $config['cache-memory-use-figure'] / 2
break
end
index = i + j
msg 3, "too much RSS, freeing full size of #{i+j} and #{i-j}..."
- freedsomething = false
if i + j < $allentries.size
- freedsomething |= $allentries[i+j].free_pixbuf_full
+ $allentries[i+j].free_pixbuf_full
end
if i - j > 0
- freedsomething |= $allentries[i-j].free_pixbuf_full
- end
- if freedsomething
- GC.start
- mem = get_mem
- msg 3, "\tmem now: #{mem}"
+ $allentries[i-j].free_pixbuf_full
end
}
end
end
end
-def real_show_entry(entry, tooltips, grab_focus)
- #- for scoping the actual reference to 'entry'
- gtk_thread_protect(proc {
- msg 3, "using 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.set_image(entry.image)
+def show_entry(entry, i)
+ #- 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.set_image(entry.image)
+ end
+ Gtk::Tooltips.new.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') {
+ if (last_shown = $mainview.get_shown_entry) != entry
+ $mainview.set_shown_entry(entry)
+ entry.alignment.set(0.5, 0, 0, 0)
+ sb_msg(_("Selected %s") % entry.get_beautified_name)
+ last_shown and last_shown.alignment.set(0.5, 1, 0, 0)
end
- tooltips.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') {
- if (last_shown = $mainview.get_shown_entry) != entry
- $mainview.set_shown_entry(entry)
- entry.alignment.set(0.5, 0, 0, 0)
- sb_msg(_("Selected %s") % entry.get_beautified_name)
- last_shown.nil? or last_shown.alignment.set(0.5, 1, 0, 0)
- 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; autoscroll_if_needed(entry.button) }
- entry.button.signal_connect('key-press-event') { |w, e| thumbnail_keypressed(entry, e) }
- if grab_focus
- entry.button.grab_focus
+ }
+ entry.button.signal_connect('button-press-event') { |w, event|
+ if entry.type == 'video' && event.event_type == Gdk::Event::BUTTON2_PRESS
+ video_view(entry)
end
- update_visibility(entry)
- })
+ }
+ entry.button.signal_connect('focus-in-event') { entry.button.clicked; autoscroll_if_needed(entry.button) }
+ 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
- $images_loader = Thread.new {
- t1 = Time.now
- sb_msg(_("Loading images..."))
- tooltips = Gtk::Tooltips.new
- counter = 0
- toremove = []
- grab_focus = true
- i = 0
- total_size = 0
- while i < $allentries.size
- entry = $allentries[i]
- if entry.pixbuf_full
- total_size += file_size(entry.path)
- entry.pixbuf_thumbnail
- real_show_entry(entry, tooltips, grab_focus)
- grab_focus = false
- if i % 4 == 0
- check_memory_free_cache_if_needed
- end
- counter += 1
- i = i + 1
+ sb_msg(_("Loading images..."))
+ t1 = Time.now
+ total_loaded_files = 0
+ total_loaded_size = 0
+ i = 0
+ while i < $allentries.size
+# printf "%d %s\n", i, __LINE__
+ entry = $allentries[i]
+ if i == 0
+ loaded_pixbuf = entry.pixbuf_main
+ else
+ loaded_pixbuf = entry.pixbuf_thumbnail
+ end
- else
- $allentries.delete_at(i)
+ if loaded_pixbuf
+ show_entry(entry, i)
+
+ total_loaded_size += file_size(entry.path)
+ if i % 4 == 0
+ check_memory_free_cache_if_needed
end
+ 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
+ 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
- check_memory_free_cache_if_needed
- sb_msg(_("%d images of total %s kB loaded in %3.2f seconds.") % [ counter, commify(total_size / 1024), Time.now - t1 ])
- }
- $images_loader.priority = -1
+ 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 ])
end
def reset_all
fc.destroy
end
-def gtk_thread_protect(proc, *params)
- if Thread.current == Thread.main
- proc.call(*params)
- else
- $protect_gtk_pending_calls.synchronize {
- $gtk_pending_calls << [ proc, params ]
- }
- end
-end
-
-def gtk_thread_flush
- $protect_gtk_pending_calls.synchronize {
- if $gtk_pending_calls.size > 0
- elem = $gtk_pending_calls.shift
- elem[0].call(*elem[1])
- end
- }
-end
-
def try_quit(*options)
Gtk.main_quit
end
false
}
- $protect_gtk_pending_calls = Mutex.new
- $gtk_pending_calls = []
- Gtk.timeout_add(50) {
- gtk_thread_flush
- true
- }
-
$main_window.show_all
end
handle_options
-Thread.abort_on_exception = true
read_config
Gtk.init