Commit 6c956029 authored by Jim Nelson's avatar Jim Nelson

Speed improvements for large photo collections. More work to be done.

#95: Three-tier thumbnail system in memory (JPEG -> unscaled pixbuf -> 
scaled pixbuf).  #94: Fixed.
parent 14d2eb57
......@@ -7,6 +7,9 @@ public class CollectionPage : Gtk.ScrolledWindow {
// steppings should divide evenly into (Thumbnail.MAX_SCALE - Thumbnail.MIN_SCALE)
public static const int MANUAL_STEPPING = 16;
public static const int SLIDER_STEPPING = 1;
private static const int IMPROVAL_PRIORITY = Priority.LOW;
private static const int IMPROVAL_DELAY_MS = 500;
private PhotoTable photoTable = new PhotoTable();
private Gtk.Viewport viewport = new Gtk.Viewport(null, null);
......@@ -15,12 +18,14 @@ public class CollectionPage : Gtk.ScrolledWindow {
private Gtk.Toolbar toolbar = new Gtk.Toolbar();
private Gtk.HScale slider = null;
private Gee.ArrayList<Thumbnail> thumbnailList = new Gee.ArrayList<Thumbnail>();
private Gee.HashSet<Thumbnail> selectedList = new Gee.HashSet<Thumbnail>();
private int currentX = 0;
private int currentY = 0;
private int cols = 0;
private int thumbCount = 0;
private int scale = Thumbnail.DEFAULT_SCALE;
private bool improval_scheduled = false;
// TODO: Mark fields for translation
private const Gtk.ActionEntry[] ACTIONS = {
{ "File", null, "_File", null, null, null },
......@@ -98,6 +103,12 @@ public class CollectionPage : Gtk.ScrolledWindow {
// freeing up memory)
viewport.expose_event += on_viewport_exposed;
// don't want to schedule thumbnail improvement in on_viewport_exposed because the redraws
// will signal another expose event ... this schedules thumbnail improvement whenever the
// window is scrolled
get_hadjustment().value_changed += schedule_thumbnail_improval;
get_vadjustment().value_changed += schedule_thumbnail_improval;
add(viewport);
button_press_event += on_click;
......@@ -118,12 +129,17 @@ public class CollectionPage : Gtk.ScrolledWindow {
thumbCount++;
attach_thumbnail(thumbnail);
layoutTable.show_all();
thumbnail.show();
}
public void remove_photo(Thumbnail thumbnail) {
thumbnailList.remove(thumbnail);
selectedList.remove(thumbnail);
ThumbnailCache.remove(thumbnail.get_photo_id());
photoTable.remove(thumbnail.get_photo_id());
layoutTable.remove(thumbnail);
assert(thumbCount > 0);
......@@ -133,7 +149,11 @@ public class CollectionPage : Gtk.ScrolledWindow {
public void repack() {
int rows = (thumbCount / cols) + 1;
//debug("repack() scale=%d thumbCount=%d rows=%d cols=%d", scale, thumbCount, rows, cols);
debug("repack() scale=%d thumbCount=%d rows=%d cols=%d", scale, thumbCount, rows, cols);
viewport.size_allocate -= on_viewport_resize;
viewport.realize -= on_viewport_realized;
viewport.expose_event -= on_viewport_exposed;
layoutTable.resize(rows, cols);
......@@ -144,6 +164,12 @@ public class CollectionPage : Gtk.ScrolledWindow {
layoutTable.remove(thumbnail);
attach_thumbnail(thumbnail);
}
viewport.size_allocate += on_viewport_resize;
viewport.realize += on_viewport_realized;
viewport.expose_event += on_viewport_exposed;
show_all();
}
private void attach_thumbnail(Thumbnail thumbnail) {
......@@ -196,34 +222,54 @@ public class CollectionPage : Gtk.ScrolledWindow {
public void select_all() {
foreach (Thumbnail thumbnail in thumbnailList) {
selectedList.add(thumbnail);
thumbnail.select();
}
}
public void unselect_all() {
foreach (Thumbnail thumbnail in thumbnailList) {
foreach (Thumbnail thumbnail in selectedList) {
assert(thumbnail.is_selected());
thumbnail.unselect();
}
selectedList = new Gee.HashSet<Thumbnail>();
}
public Thumbnail[] get_selected() {
Thumbnail[] thumbnails = new Thumbnail[0];
foreach (Thumbnail thumbnail in thumbnailList) {
if (thumbnail.is_selected())
thumbnails += thumbnail;
Thumbnail[] thumbnails = new Thumbnail[selectedList.size];
int ctr = 0;
foreach (Thumbnail thumbnail in selectedList) {
assert(thumbnail.is_selected());
thumbnails[ctr++] = thumbnail;
}
return thumbnails;
}
public int get_selected_count() {
int count = 0;
foreach (Thumbnail thumbnail in thumbnailList) {
if (thumbnail.is_selected())
count++;
public void select(Thumbnail thumbnail) {
thumbnail.select();
selectedList.add(thumbnail);
}
public void unselect(Thumbnail thumbnail) {
thumbnail.unselect();
selectedList.remove(thumbnail);
}
public void toggle_select(Thumbnail thumbnail) {
if (thumbnail.toggle_select()) {
// now selected
selectedList.add(thumbnail);
} else {
// now unselected
selectedList.remove(thumbnail);
}
return count;
}
public int get_selected_count() {
return selectedList.size;
}
public int increase_thumb_size() {
......@@ -281,12 +327,10 @@ public class CollectionPage : Gtk.ScrolledWindow {
schedule_thumbnail_improval();
}
private bool improval_scheduled = false;
private void schedule_thumbnail_improval() {
if (improval_scheduled == false) {
improval_scheduled = true;
Timeout.add_full(Priority.LOW, 1000, improve_thumbnail_quality);
Timeout.add_full(IMPROVAL_PRIORITY, IMPROVAL_DELAY_MS, improve_thumbnail_quality);
}
}
......@@ -311,6 +355,7 @@ public class CollectionPage : Gtk.ScrolledWindow {
add_photo(photoID, file);
}
show_all();
schedule_thumbnail_improval();
}
......@@ -381,7 +426,7 @@ public class CollectionPage : Gtk.ScrolledWindow {
case Gdk.ModifierType.CONTROL_MASK: {
// with only Ctrl pressed, multiple selections are possible ... chosen item
// is toggled
thumbnail.toggle_select();
toggle_select(thumbnail);
} break;
case Gdk.ModifierType.SHIFT_MASK: {
......@@ -395,7 +440,7 @@ public class CollectionPage : Gtk.ScrolledWindow {
default: {
// a "raw" click deselects all thumbnails and selects the single chosen
unselect_all();
thumbnail.select();
select(thumbnail);
} break;
}
} else {
......@@ -416,7 +461,7 @@ public class CollectionPage : Gtk.ScrolledWindow {
if (thumbnail != null) {
// this counts as a select
unselect_all();
thumbnail.select();
select(thumbnail);
Gtk.Menu contextMenu = (Gtk.Menu) AppWindow.get_ui_manager().get_widget("/CollectionContextMenu");
contextMenu.popup(null, null, null, event.button, event.time);
......@@ -430,11 +475,11 @@ public class CollectionPage : Gtk.ScrolledWindow {
}
private void on_remove() {
// get a full list of the selected thumbnails, then iterate over that, as you can't remove
// from a list you're iterating over
Thumbnail[] thumbnails = get_selected();
foreach (Thumbnail thumbnail in thumbnails) {
remove_photo(thumbnail);
ThumbnailCache.big.remove(photoTable.get_id(thumbnail.get_file()));
photoTable.remove(thumbnail.get_file());
}
repack();
......@@ -450,8 +495,6 @@ public class CollectionPage : Gtk.ScrolledWindow {
Gdk.Rectangle thumbrect = Gdk.Rectangle();
Gdk.Rectangle bitbucket = Gdk.Rectangle();
int exposedCount = 0;
int unexposedCount = 0;
foreach (Thumbnail thumbnail in thumbnailList) {
Gtk.Allocation alloc = thumbnail.get_exposure();
thumbrect.x = alloc.x;
......@@ -461,14 +504,10 @@ public class CollectionPage : Gtk.ScrolledWindow {
if (viewrect.intersect(thumbrect, bitbucket)) {
thumbnail.exposed();
exposedCount++;
} else {
thumbnail.unexposed();
unexposedCount++;
}
}
//message("%d exposed, %d unexposed", exposedCount, unexposedCount);
}
private double scaleToSlider(int value) {
......
......@@ -78,7 +78,7 @@ public class PhotoTable : DatabaseTable {
return true;
}
public bool remove(File file) {
public bool remove_by_file(File file) {
Sqlite.Statement stmt;
int res = db.prepare_v2("DELETE FROM PhotoTable WHERE filename=?", -1, out stmt);
assert(res == Sqlite.OK);
......@@ -88,7 +88,25 @@ public class PhotoTable : DatabaseTable {
res = stmt.step();
if (res != Sqlite.DONE) {
warning("remove_photo", res);
warning("remove", res);
return false;
}
return true;
}
public bool remove(PhotoID photoID) {
Sqlite.Statement stmt;
int res = db.prepare_v2("DELETE FROM PhotoTable WHERE id=?", -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int(1, photoID.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.DONE) {
warning("remove", res);
return false;
}
......
......@@ -25,6 +25,7 @@ public class Thumbnail : Gtk.Alignment {
private Dimensions originalDim;
private Dimensions scaledDim;
private Gdk.Pixbuf cached = null;
private Gdk.InterpType scaledInterp = LOW_QUALITY_INTERP;
public Thumbnail(PhotoID photoID, File file, int scale = DEFAULT_SCALE) {
this.photoID = photoID;
......@@ -70,6 +71,10 @@ public class Thumbnail : Gtk.Alignment {
return file;
}
public PhotoID get_photo_id() {
return photoID;
}
public Gtk.Allocation get_exposure() {
return image.allocation;
}
......@@ -116,8 +121,9 @@ public class Thumbnail : Gtk.Alignment {
if (ThumbnailCache.refresh_pixbuf(oldScale, newScale)) {
cached = ThumbnailCache.fetch(photoID, newScale);
}
Gdk.Pixbuf scaled = cached.scale_simple(scaledDim.width, scaledDim.height, LOW_QUALITY_INTERP);
scaledInterp = LOW_QUALITY_INTERP;
image.set_from_pixbuf(scaled);
} else {
image.requisition.width = scaledDim.width;
......@@ -130,8 +136,20 @@ public class Thumbnail : Gtk.Alignment {
return;
}
Gdk.Pixbuf scaled = cached.scale_simple(scaledDim.width, scaledDim.height, HIGH_QUALITY_INTERP);
image.set_from_pixbuf(scaled);
if (scaledInterp == HIGH_QUALITY_INTERP) {
return;
}
// only go through the scaling if indeed the image is going to be scaled ... although
// scale_simple() will probably just return the pixbuf if it sees the stupid case, Gtk.Image
// does not, and will fire off resized events when the new image (which is not really new)
// is added
if ((cached.get_width() != scaledDim.width) || (cached.get_height() != scaledDim.height)) {
Gdk.Pixbuf scaled = cached.scale_simple(scaledDim.width, scaledDim.height, HIGH_QUALITY_INTERP);
image.set_from_pixbuf(scaled);
}
scaledInterp = HIGH_QUALITY_INTERP;
}
public void exposed() {
......@@ -140,6 +158,7 @@ public class Thumbnail : Gtk.Alignment {
cached = ThumbnailCache.fetch(photoID, scale);
Gdk.Pixbuf scaled = cached.scale_simple(scaledDim.width, scaledDim.height, LOW_QUALITY_INTERP);
scaledInterp = LOW_QUALITY_INTERP;
image.set_from_pixbuf(scaled);
isExposed = true;
}
......
......@@ -23,9 +23,9 @@ public class ThumbnailCache : Object {
public static const int MEDIUM_SCALE = 128;
public static const int SMALL_SCALE = 64;
public static ThumbnailCache big = null;
public static ThumbnailCache medium = null;
public static ThumbnailCache small = null;
private static ThumbnailCache big = null;
private static ThumbnailCache medium = null;
private static ThumbnailCache small = null;
// Doing this because static construct {} not working nor new'ing in the above statement
public static void init() {
......
......@@ -19,15 +19,8 @@ public struct Dimensions {
}
}
static int lastScale = 0;
static Dimensions lastDimensions;
Dimensions get_scaled_dimensions(Dimensions original, int scale) {
assert(scale > 0);
if (scale == lastScale) {
return lastDimensions;
}
int diffWidth = original.width - scale;
int diffHeight = original.height - scale;
......@@ -61,9 +54,6 @@ Dimensions get_scaled_dimensions(Dimensions original, int scale) {
scaled.height = scale;
}
lastScale = scale;
lastDimensions = scaled;
return scaled;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment