Commit a81ea64b authored by Jim Nelson's avatar Jim Nelson

More work to be done for aesthetics, but this fixes #93 in a big way.

parent 4d87f53d
......@@ -263,9 +263,11 @@ public class AppWindow : Gtk.Window {
Gtk.drag_finish(context, true, false, time);
// import
collectionPage.begin_adding();
foreach (string uri in uris) {
import(File.new_for_uri(uri));
}
collectionPage.end_adding();
}
}
public class CollectionLayout : Gtk.Layout {
private Gee.ArrayList<Thumbnail> thumbnails = new Gee.ArrayList<Thumbnail>();
private int currentX = 0;
private int currentY = 0;
private int rowTallest = 0;
public CollectionLayout() {
modify_bg(Gtk.StateType.NORMAL, parse_color(CollectionPage.BG_COLOR));
expose_event += on_expose;
size_allocate += on_resize;
}
public void append(Thumbnail thumbnail) {
thumbnails.add(thumbnail);
// need to do this to have its size requisitioned
thumbnail.show_all();
Gtk.Requisition req;
thumbnail.size_request(out req);
if (rowTallest < req.height)
rowTallest = req.height;
// carriage return
if (currentX + req.width > allocation.width) {
currentX = 0;
currentY += rowTallest;
rowTallest = 0;
}
put(thumbnail, currentX, currentY);
currentX += req.width;
}
public void remove_thumbnail(Thumbnail thumbnail) {
thumbnails.remove(thumbnail);
remove(thumbnail);
}
public void refresh() {
currentX = 0;
currentY = 0;
rowTallest = 0;
foreach (Thumbnail thumbnail in thumbnails) {
Gtk.Requisition req;
thumbnail.size_request(out req);
if (req.height > rowTallest)
rowTallest = req.height;
// carriage return
if (currentX + req.width > allocation.width) {
currentX = 0;
currentY += rowTallest;
rowTallest = 0;
}
move(thumbnail, currentX, currentY);
currentX += req.width;
}
set_size(allocation.width, currentY + rowTallest);
}
private int lastWidth = 0;
private void on_resize() {
// only refresh if the viewport width has changed
if (allocation.width != lastWidth) {
lastWidth = allocation.width;
refresh();
}
}
private bool on_expose(CollectionLayout cl, Gdk.EventExpose event) {
Gdk.Rectangle visibleRect = Gdk.Rectangle();
visibleRect.x = (int) get_hadjustment().get_value();
visibleRect.y = (int) get_vadjustment().get_value();
visibleRect.width = allocation.width;
visibleRect.height = allocation.height;
/*
debug("on_client_exposed x:%d y:%d w:%d h:%d", visibleRect.x, visibleRect.y,
visibleRect.width, visibleRect.height);
*/
Gdk.Rectangle bitbucket = Gdk.Rectangle();
int exposedCount = 0;
int unexposedCount = 0;
foreach (Thumbnail thumbnail in thumbnails) {
if (visibleRect.intersect((Gdk.Rectangle) thumbnail.allocation, bitbucket)) {
thumbnail.exposed();
exposedCount++;
} else {
thumbnail.unexposed();
unexposedCount++;
}
}
debug("exposed:%d unexposed:%d", exposedCount, unexposedCount);
return false;
}
}
......@@ -12,17 +12,12 @@ public class CollectionPage : Gtk.ScrolledWindow {
private static const int IMPROVAL_DELAY_MS = 500;
private PhotoTable photoTable = new PhotoTable();
private Gtk.Viewport viewport = new Gtk.Viewport(null, null);
private Gtk.Table layoutTable = new Gtk.Table(0, 0, false);
private CollectionLayout layout = new CollectionLayout();
private Gtk.MenuBar menubar = null;
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;
......@@ -79,39 +74,30 @@ public class CollectionPage : Gtk.ScrolledWindow {
// scrollbar policy
set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
// set table column and row padding ... this is done globally rather than per-thumbnail
layoutTable.set_col_spacings(THUMB_X_PADDING);
layoutTable.set_row_spacings(THUMB_Y_PADDING);
// this schedules thumbnail improvement whenever the window size changes (and new thumbnails
// may be exposed)
size_allocate += schedule_thumbnail_improval;
// need to manually build viewport to set its background color
viewport.add(layoutTable);
viewport.modify_bg(Gtk.StateType.NORMAL, parse_color(BG_COLOR));
// notice that this is capturing the viewport's resize, not the scrolled window's,
// as that's what interesting when laying out the photos
viewport.size_allocate += on_viewport_resize;
// This signal handler is to load the collection page with photos when its viewport is
// realized ... this is because if the collection page is loaded during construction, the
// viewport does not respond properly to the layout table's resizing and it winds up tagging
// extra space to the tail of the view. This allows us to wait until the viewport is realized
// and responds properly to resizing
viewport.realize += on_viewport_realized;
// when the viewport is exposed, the thumbnails are informed when they are exposed (and
// should be showing their image) and when they're unexposed (so they can destroy the image,
// 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
// this schedules thumbnail improvement whenever the window is scrolled (and new
// thumbnails may be exposed)
get_hadjustment().value_changed += schedule_thumbnail_improval;
get_vadjustment().value_changed += schedule_thumbnail_improval;
add(viewport);
add(layout);
button_press_event += on_click;
File[] photoFiles = photoTable.get_photo_files();
foreach (File file in photoFiles) {
PhotoID photoID = photoTable.get_id(file);
add_photo(photoID, file);
}
layout.refresh();
schedule_thumbnail_improval();
show_all();
}
public Gtk.Toolbar get_toolbar() {
......@@ -122,92 +108,31 @@ public class CollectionPage : Gtk.ScrolledWindow {
return menubar;
}
public void begin_adding() {
}
public void add_photo(PhotoID photoID, File file) {
Thumbnail thumbnail = new Thumbnail(photoID, file, scale);
thumbnailList.add(thumbnail);
thumbCount++;
attach_thumbnail(thumbnail);
thumbnail.show();
layout.append(thumbnail);
}
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);
thumbCount--;
public void end_adding() {
layout.refresh();
}
private Timer repackTimer = new Timer();
public void repack() {
int rows = (thumbCount / cols) + 1;
debug("repack() scale=%d thumbCount=%d rows=%d cols=%d", scale, thumbCount, rows, cols);
repackTimer.start();
viewport.size_allocate -= on_viewport_resize;
viewport.realize -= on_viewport_realized;
viewport.expose_event -= on_viewport_exposed;
layoutTable.resize(rows, cols);
currentX = 0;
currentY = 0;
foreach (Thumbnail thumbnail in thumbnailList) {
layoutTable.remove(thumbnail);
attach_thumbnail(thumbnail);
}
viewport.size_allocate += on_viewport_resize;
viewport.realize += on_viewport_realized;
viewport.expose_event += on_viewport_exposed;
debug("repack: %lfms", repackTimer.elapsed());
show_all();
schedule_thumbnail_improval();
public int get_count() {
return thumbnailList.size;
}
private void attach_thumbnail(Thumbnail thumbnail) {
layoutTable.attach(thumbnail, currentX, currentX + 1, currentY, currentY + 1,
Gtk.AttachOptions.SHRINK | Gtk.AttachOptions.EXPAND,
Gtk.AttachOptions.SHRINK | Gtk.AttachOptions.FILL,
0, 0);
if(++currentX >= cols) {
currentX = 0;
currentY++;
}
}
private void on_viewport_resize(Gtk.Viewport v, Gdk.Rectangle allocation) {
int newCols = allocation.width / (Thumbnail.get_max_width(scale) + (THUMB_X_PADDING * 2));
if (newCols < 1)
newCols = 1;
if (cols != newCols) {
cols = newCols;
repack();
}
}
public Thumbnail? get_thumbnail_at(double xd, double yd) {
int x = (int) xd;
int y = (int) yd;
int xadj = (int) viewport.get_hadjustment().get_value();
int yadj = (int) viewport.get_vadjustment().get_value();
int xadj = (int) layout.get_hadjustment().get_value();
int yadj = (int) layout.get_vadjustment().get_value();
x += xadj;
y += yadj;
......@@ -223,10 +148,6 @@ public class CollectionPage : Gtk.ScrolledWindow {
return null;
}
public int get_count() {
return thumbCount;
}
public void select_all() {
foreach (Thumbnail thumbnail in thumbnailList) {
selectedList.add(thumbnail);
......@@ -288,13 +209,7 @@ public class CollectionPage : Gtk.ScrolledWindow {
scale = Thumbnail.MAX_SCALE;
}
foreach (Thumbnail thumbnail in thumbnailList) {
thumbnail.resize(scale);
}
layoutTable.resize_children();
schedule_thumbnail_improval();
set_thumb_size(scale);
return scale;
}
......@@ -308,14 +223,8 @@ public class CollectionPage : Gtk.ScrolledWindow {
scale = Thumbnail.MIN_SCALE;
}
foreach (Thumbnail thumbnail in thumbnailList) {
thumbnail.resize(scale);
}
layoutTable.resize_children();
schedule_thumbnail_improval();
set_thumb_size(scale);
return scale;
}
......@@ -329,19 +238,30 @@ public class CollectionPage : Gtk.ScrolledWindow {
thumbnail.resize(scale);
}
layoutTable.resize_children();
layout.refresh();
schedule_thumbnail_improval();
}
private bool reschedule_improval = false;
private void schedule_thumbnail_improval() {
if (improval_scheduled == false) {
improval_scheduled = true;
Timeout.add_full(IMPROVAL_PRIORITY, IMPROVAL_DELAY_MS, improve_thumbnail_quality);
} else {
reschedule_improval = true;
}
}
private bool improve_thumbnail_quality() {
if (reschedule_improval) {
debug("rescheduled improval");
reschedule_improval = false;
return true;
}
foreach (Thumbnail thumbnail in thumbnailList) {
if (thumbnail.is_exposed()) {
thumbnail.paint_high_quality();
......@@ -355,26 +275,6 @@ public class CollectionPage : Gtk.ScrolledWindow {
return false;
}
private void on_viewport_realized() {
File[] photoFiles = photoTable.get_photo_files();
foreach (File file in photoFiles) {
PhotoID photoID = photoTable.get_id(file);
add_photo(photoID, file);
}
show_all();
schedule_thumbnail_improval();
}
private bool on_viewport_exposed(Gtk.Viewport v, Gdk.EventExpose event) {
// since expose events can stack up, wait until the last one to do the full
// search
if (event.count == 0)
check_exposure();
return false;
}
private void on_about() {
AppWindow.get_main_window().about_box();
}
......@@ -486,37 +386,18 @@ public class CollectionPage : Gtk.ScrolledWindow {
// from a list you're iterating over
Thumbnail[] thumbnails = get_selected();
foreach (Thumbnail thumbnail in thumbnails) {
remove_photo(thumbnail);
}
repack();
}
private void check_exposure() {
Gdk.Rectangle viewrect = Gdk.Rectangle();
viewrect.x = (int) viewport.get_hadjustment().get_value();
viewrect.y = (int) viewport.get_vadjustment().get_value();
viewrect.width = viewport.allocation.width;
viewrect.height = viewport.allocation.height;
Gdk.Rectangle thumbrect = Gdk.Rectangle();
Gdk.Rectangle bitbucket = Gdk.Rectangle();
thumbnailList.remove(thumbnail);
selectedList.remove(thumbnail);
foreach (Thumbnail thumbnail in thumbnailList) {
Gtk.Allocation alloc = thumbnail.get_exposure();
thumbrect.x = alloc.x;
thumbrect.y = alloc.y;
thumbrect.width = alloc.width;
thumbrect.height = alloc.height;
ThumbnailCache.remove(thumbnail.get_photo_id());
photoTable.remove(thumbnail.get_photo_id());
if (viewrect.intersect(thumbrect, bitbucket)) {
thumbnail.exposed();
} else {
thumbnail.unexposed();
}
layout.remove_thumbnail(thumbnail);
}
layout.refresh();
}
private double scaleToSlider(int value) {
assert(value >= Thumbnail.MIN_SCALE);
assert(value <= Thumbnail.MAX_SCALE);
......
......@@ -11,7 +11,8 @@ SRC_FILES = \
Thumbnail.vala \
DatabaseTables.vala \
ThumbnailCache.vala \
image_util.vala
image_util.vala \
CollectionLayout.vala
PKGS = \
gtk+-2.0 \
......
......@@ -10,7 +10,7 @@ public class Thumbnail : Gtk.Alignment {
public static const int MAX_SCALE = 360;
public static const int DEFAULT_SCALE = 128;
public static const Gdk.InterpType LOW_QUALITY_INTERP = Gdk.InterpType.NEAREST;
public static const Gdk.InterpType HIGH_QUALITY_INTERP = Gdk.InterpType.HYPER;
public static const Gdk.InterpType HIGH_QUALITY_INTERP = Gdk.InterpType.BILINEAR;
// Due to the potential for thousands or tens of thousands of thumbnails being present in a
// particular view, all widgets used here should be NOWINDOW widgets.
......@@ -41,11 +41,12 @@ public class Thumbnail : Gtk.Alignment {
// not present, the widget will collapse, and so the layout manager won't account for it
// properly when it's off the viewport. The solution is to manually set the widget's
// requisition size, even when it contains no pixbuf
image.requisition.width = scaledDim.width;
image.requisition.height = scaledDim.height;
image.set_size_request(scaledDim.width, scaledDim.height);
title = new Gtk.Label(file.get_basename());
title.set_use_underline(false);
title.set_justify(Gtk.Justification.LEFT);
title.set_alignment(0, 0);
title.modify_fg(Gtk.StateType.NORMAL, parse_color(TEXT_COLOR));
Gtk.VBox vbox = new Gtk.VBox(false, 0);
......@@ -61,12 +62,6 @@ public class Thumbnail : Gtk.Alignment {
add(frame);
}
public static int get_max_width(int scale) {
// TODO: Be more precise about this ... the magic 32 at the end is merely a dart on the board
// for accounting for extra pixels used by the frame
return scale + (FRAME_PADDING * 2) + 32;
}
public File get_file() {
return file;
}
......@@ -75,10 +70,6 @@ public class Thumbnail : Gtk.Alignment {
return photoID;
}
public Gtk.Allocation get_exposure() {
return image.allocation;
}
public void select() {
selected = true;
......@@ -108,7 +99,8 @@ public class Thumbnail : Gtk.Alignment {
}
public void resize(int newScale) {
assert((newScale >= MIN_SCALE) && (newScale <= MAX_SCALE));
assert(newScale >= MIN_SCALE);
assert(newScale <= MAX_SCALE);
if (scale == newScale)
return;
......@@ -118,6 +110,8 @@ public class Thumbnail : Gtk.Alignment {
scaledDim = get_scaled_dimensions(originalDim, scale);
if (isExposed) {
assert(cached != null);
if (ThumbnailCache.refresh_pixbuf(oldScale, newScale)) {
cached = ThumbnailCache.fetch(photoID, newScale);
}
......@@ -125,20 +119,18 @@ public class Thumbnail : Gtk.Alignment {
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;
image.requisition.height = scaledDim.height;
}
// set the image widget's size regardless of the presence of an image
image.set_size_request(scaledDim.width, scaledDim.height);
}
public void paint_high_quality() {
if (cached == null) {
if (cached == null)
return;
}
if (scaledInterp == HIGH_QUALITY_INTERP) {
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
......@@ -160,6 +152,8 @@ public class Thumbnail : Gtk.Alignment {
Gdk.Pixbuf scaled = cached.scale_simple(scaledDim.width, scaledDim.height, LOW_QUALITY_INTERP);
scaledInterp = LOW_QUALITY_INTERP;
image.set_from_pixbuf(scaled);
image.set_size_request(scaledDim.width, scaledDim.height);
isExposed = true;
}
......@@ -169,8 +163,8 @@ public class Thumbnail : Gtk.Alignment {
cached = null;
image.clear();
image.requisition.width = scaledDim.width;
image.requisition.height = scaledDim.height;
image.set_size_request(scaledDim.width, scaledDim.height);
isExposed = false;
}
......
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