Commit 7a9e8528 authored by Jim Nelson's avatar Jim Nelson

#67: Events. Lots of refactoring to accomdate database and UI changes. ...

#67: Events.  Lots of refactoring to accomdate database and UI changes.  Another valac bug found in 
CollectionLayout.vala, probably related to generics.  Will fix soon.
parent e3afaf17
This diff is collapsed.
......@@ -43,7 +43,7 @@ public abstract class LayoutItem : Gtk.Alignment {
add(frame);
}
public Gtk.Widget? get_control_panel() {
public virtual Gtk.Widget? get_control_panel() {
return null;
}
......@@ -51,11 +51,7 @@ public abstract class LayoutItem : Gtk.Alignment {
return title.get_text();
}
public abstract Gdk.Pixbuf? get_full_pixbuf();
public abstract Exif.Orientation get_orientation();
public abstract void set_orientation(Exif.Orientation orientation);
public abstract void on_backing_changed();
public virtual void exposed() {
}
......@@ -122,23 +118,31 @@ public class CollectionLayout : Gtk.Layout {
public static const int RIGHT_PADDING = 16;
public static const int COLUMN_GUTTER_PADDING = 24;
private SortedList<LayoutItem> items = new SortedList<LayoutItem>(new Gee.ArrayList<LayoutItem>());
private Gtk.Label message = null;
private Gtk.Label message = new Gtk.Label("");
public SortedList<LayoutItem> items = new SortedList<LayoutItem>(new Gee.ArrayList<LayoutItem>());
public CollectionLayout() {
modify_bg(Gtk.StateType.NORMAL, AppWindow.BG_COLOR);
// This is commented out because Vala is generating bogus ccode from it (it's trying to
// unref the Gdk.Color as though it was a Gee.Collection) ... I suspect it has to do with
// the SortedList class. Will probably need to rip out SortedList and sort all lists by
// hand ...
/*
Gdk.Color color = parse_color(LayoutItem.UNSELECTED_COLOR);
message.modify_fg(Gtk.StateType.NORMAL, color);
*/
message.set_single_line_mode(false);
message.set_use_underline(false);
expose_event += on_expose;
size_allocate += on_resize;
}
public void set_message(string message) {
public void set_message(string text) {
clear();
this.message = new Gtk.Label(message);
this.message.set_single_line_mode(false);
this.message.set_use_underline(false);
this.message.modify_fg(Gtk.StateType.NORMAL, parse_color(LayoutItem.UNSELECTED_COLOR));
message.set_text(text);
display_message();
}
......@@ -160,9 +164,9 @@ public class CollectionLayout : Gtk.Layout {
items.add(item);
// this demolishes any message that's been set
if (message != null) {
if (message.get_text().length > 0) {
remove(message);
message = null;
message.set_text("");
}
// need to do this to have its size requisitioned in refresh()
......@@ -191,9 +195,9 @@ public class CollectionLayout : Gtk.Layout {
public void clear() {
// remove page message
if (message != null) {
if (message.get_text().length > 0) {
remove(message);
message = null;
message.set_text("");
}
// remove all items from Gtk.Layout
......@@ -206,7 +210,7 @@ public class CollectionLayout : Gtk.Layout {
}
public void refresh() {
if (message != null) {
if (message.get_text().length > 0) {
display_message();
return;
......@@ -234,7 +238,11 @@ public class CollectionLayout : Gtk.Layout {
// perform size requests first time through, but not thereafter
Gtk.Requisition req;
item.size_request(out req);
// the items must be requisitioned for this code to work
assert(req.height > 0);
assert(req.width > 0);
// carriage return (i.e. this item will overflow the view)
if ((x + req.width + RIGHT_PADDING) > allocation.width) {
if (rowWidth > widestRow) {
......@@ -368,7 +376,7 @@ public class CollectionLayout : Gtk.Layout {
}
private void display_message() {
assert(message != null);
assert(message.get_text().length > 0);
Gtk.Requisition req;
message.size_request(out req);
......@@ -413,23 +421,15 @@ public class CollectionLayout : Gtk.Layout {
*/
Gdk.Rectangle bitbucket = Gdk.Rectangle();
int exposedCount = 0;
int unexposedCount = 0;
foreach (LayoutItem item in items) {
if (visibleRect.intersect((Gdk.Rectangle) item.allocation, bitbucket)) {
item.exposed();
exposedCount++;
} else {
item.unexposed();
unexposedCount++;
}
}
/*
debug("exposed:%d unexposed:%d", exposedCount, unexposedCount);
*/
return false;
}
}
public class CollectionPage : CheckerboardPage {
public static const int THUMB_X_PADDING = 20;
public static const int THUMB_Y_PADDING = 20;
public static const int SORT_BY_MIN = 0;
public static const int SORT_BY_NAME = 0;
public static const int SORT_BY_EXPOSURE_DATE = 1;
......@@ -20,7 +17,7 @@ public class CollectionPage : CheckerboardPage {
private static const int IMPROVAL_PRIORITY = Priority.LOW;
private static const int IMPROVAL_DELAY_MS = 250;
private PhotoTable photoTable = new PhotoTable();
private PhotoTable photo_table = new PhotoTable();
private Gtk.Toolbar toolbar = new Gtk.Toolbar();
private Gtk.HScale slider = null;
private Gtk.ToolButton rotateButton = null;
......@@ -62,7 +59,7 @@ public class CollectionPage : CheckerboardPage {
{ "SortDescending", null, "D_escending", null, "Sort photos in a descending order", SORT_ORDER_DESCENDING }
};
construct {
public CollectionPage(PhotoID[] photos) {
init_ui_start("collection.ui", "CollectionActionGroup", ACTIONS, TOGGLE_ACTIONS);
actionGroup.add_radio_actions(SORT_CRIT_ACTIONS, SORT_BY_NAME, on_sort_changed);
actionGroup.add_radio_actions(SORT_ORDER_ACTIONS, SORT_ORDER_ASCENDING, on_sort_changed);
......@@ -109,18 +106,15 @@ public class CollectionPage : CheckerboardPage {
get_hadjustment().value_changed += schedule_thumbnail_improval;
get_vadjustment().value_changed += schedule_thumbnail_improval;
File[] photoFiles = photoTable.get_photo_files();
foreach (File file in photoFiles) {
PhotoID photoID = photoTable.get_id(file);
debug("Loading [%lld] %s", photoID.id, file.get_path());
add_photo(photoID, file);
// load photos into view
foreach (PhotoID photo_id in photos) {
add_photo(photo_id);
}
show_all();
refresh();
show_all();
schedule_thumbnail_improval();
}
......@@ -167,13 +161,41 @@ public class CollectionPage : CheckerboardPage {
return false;
}
public void add_photo(PhotoID photoID, File file) {
Thumbnail thumbnail = Thumbnail.create(photoID, file, scale);
public void add_photo(PhotoID photoID) {
Thumbnail thumbnail = new Thumbnail(photoID, scale);
thumbnail.display_title(display_titles());
add_item(thumbnail);
}
public bool remove_photo(PhotoID photo_id) {
Thumbnail found = null;
foreach (LayoutItem item in get_items()) {
Thumbnail thumbnail = (Thumbnail) item;
if (thumbnail.get_photo_id().id == photo_id.id) {
found = thumbnail;
break;
}
}
if (found != null)
remove_item(found);
return (found != null);
}
public void report_backing_changed(PhotoID photo_id) {
foreach (LayoutItem item in get_items()) {
Thumbnail thumbnail = (Thumbnail) item;
if (thumbnail.get_photo_id().id == photo_id.id) {
thumbnail.on_backing_changed();
break;
}
}
}
public int increase_thumb_size() {
if (scale == Thumbnail.MAX_SCALE)
return scale;
......@@ -280,13 +302,10 @@ public class CollectionPage : CheckerboardPage {
}
private void on_remove() {
// iterate over selected and remove them from cache and database
// iterate over selected and remove them from cache and databases
foreach (LayoutItem item in get_selected()) {
Thumbnail thumbnail = (Thumbnail) item;
Thumbnail.remove_instance(thumbnail);
ThumbnailCache.remove(thumbnail.get_photo_id());
photoTable.remove(thumbnail.get_photo_id());
AppWindow.get_instance().remove_photo(thumbnail.get_photo_id(), this);
}
remove_selected();
......@@ -296,15 +315,36 @@ public class CollectionPage : CheckerboardPage {
private delegate Exif.Orientation RotationFunc(Exif.Orientation orientation);
private void do_rotations(string desc, Gee.Iterable<LayoutItem> c, RotationFunc func) {
private void do_rotations(string desc, Gee.Iterable<LayoutItem> c, RotationFunc rotate) {
bool rotationPerformed = false;
foreach (LayoutItem item in c) {
Thumbnail thumbnail = (Thumbnail) item;
Exif.Orientation orientation = thumbnail.get_orientation();
Exif.Orientation rotated = func(orientation);
File file = thumbnail.get_file();
PhotoExif exif = PhotoExif.create(file);
Exif.Orientation orientation = exif.get_orientation();
Exif.Orientation rotated = rotate(orientation);
debug("Rotating %s %s from %s to %s", desc, thumbnail.get_file().get_path(),
orientation.get_description(), rotated.get_description());
thumbnail.set_orientation(rotated);
// update file itself
exif.set_orientation(rotated);
// TODO: Write this in background
try {
exif.commit();
} catch (Error err) {
error("%s", err.message);
}
// update database
photo_table.set_orientation(thumbnail.get_photo_id(), rotated);
// update everyone who cares (including this thumbnail)
AppWindow.get_instance().report_backing_changed(thumbnail.get_photo_id());
rotationPerformed = true;
}
......@@ -424,13 +464,13 @@ public class CollectionPage : CheckerboardPage {
private class CompareDate : Comparator<LayoutItem> {
public override int64 compare(LayoutItem a, LayoutItem b) {
return (int64) (((Thumbnail) a).get_exposure_time() - ((Thumbnail) b).get_exposure_time());
return ((Thumbnail) a).get_exposure_time() - ((Thumbnail) b).get_exposure_time();
}
}
private class ReverseCompareDate : Comparator<LayoutItem> {
public override int64 compare(LayoutItem a, LayoutItem b) {
return (int) (((Thumbnail) b).get_exposure_time() - ((Thumbnail) a).get_exposure_time());
return ((Thumbnail) b).get_exposure_time() - ((Thumbnail) a).get_exposure_time();
}
}
......
......@@ -59,6 +59,24 @@ public struct PhotoID {
}
}
public struct ImportID {
public static const int64 INVALID = -1;
public int64 id;
public ImportID(int64 id = INVALID) {
this.id = id;
}
public bool is_invalid() {
return (id == INVALID);
}
public bool is_valid() {
return (id != INVALID);
}
}
public class PhotoTable : DatabaseTable {
public PhotoTable() {
Sqlite.Statement stmt;
......@@ -82,7 +100,17 @@ public class PhotoTable : DatabaseTable {
}
}
public PhotoID add(File file, Dimensions dim, int64 filesize, long timestamp, long exposure_time,
public ImportID generate_import_id() {
// TODO: Use a guid here? Key here is that last imported photos can be easily identified
// by finding the largest value in the PhotoTable
TimeVal timestamp = TimeVal();
timestamp.get_current_time();
int64 id = timestamp.tv_sec;
return ImportID(id);
}
public PhotoID add(File file, Dimensions dim, int64 filesize, long timestamp, time_t exposure_time,
Exif.Orientation orientation, ImportID importID) {
Sqlite.Statement stmt;
int res = db.prepare_v2(
......@@ -123,38 +151,31 @@ public class PhotoTable : DatabaseTable {
return PhotoID(db.last_insert_rowid());
}
public bool update(PhotoID photoID, File file, Dimensions dim, int64 filesize, long timestamp, long exposure_time,
Exif.Orientation orientation) {
TimeVal time_imported = TimeVal();
time_imported.get_current_time();
public bool update(PhotoID photoID, Dimensions dim, int64 filesize, long timestamp,
time_t exposure_time, Exif.Orientation orientation) {
Sqlite.Statement stmt;
int res = db.prepare_v2(
"UPDATE PhotoTable SET filename = ?, width = ?, height = ?, filesize = ?, timestamp = ?, "
+ "exposure_time = ?, orientation = ?, time_imported = ? WHERE id = ?",
-1, out stmt);
"UPDATE PhotoTable SET width = ?, height = ?, filesize = ?, timestamp = ?, "
+ "exposure_time = ?, orientation = ? WHERE id = ?", -1, out stmt);
assert(res == Sqlite.OK);
debug("Update [%lld] %s %dx%d size=%lld mod=%ld exp=%ld or=%d", photoID.id, file.get_path(), dim.width,
debug("Update [%lld] %dx%d size=%lld mod=%ld exp=%ld or=%d", photoID.id, dim.width,
dim.height, filesize, timestamp, exposure_time, (int) orientation);
res = stmt.bind_text(1, file.get_path());
res = stmt.bind_int(1, dim.width);
assert(res == Sqlite.OK);
res = stmt.bind_int(2, dim.width);
res = stmt.bind_int(2, dim.height);
assert(res == Sqlite.OK);
res = stmt.bind_int(3, dim.height);
res = stmt.bind_int64(3, filesize);
assert(res == Sqlite.OK);
res = stmt.bind_int64(4, filesize);
res = stmt.bind_int64(4, timestamp);
assert(res == Sqlite.OK);
res = stmt.bind_int64(5, timestamp);
res = stmt.bind_int64(5, exposure_time);
assert(res == Sqlite.OK);
res = stmt.bind_int64(6, exposure_time);
res = stmt.bind_int64(6, orientation);
assert(res == Sqlite.OK);
res = stmt.bind_int64(7, orientation);
assert(res == Sqlite.OK);
res = stmt.bind_int64(8, time_imported.tv_sec);
assert(res == Sqlite.OK);
res = stmt.bind_int64(9, photoID.id);
res = stmt.bind_int64(7, photoID.id);
assert(res == Sqlite.OK);
res = stmt.step();
......@@ -168,6 +189,19 @@ public class PhotoTable : DatabaseTable {
return true;
}
public bool exists(PhotoID photo_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT id FROM PhotoTable WHERE id=?", -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
return (res == Sqlite.ROW);
}
public bool get_photo(PhotoID photoID, out PhotoRow row) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT filename, width, height, filesize, timestamp, exposure_time, orientation, import_id, event_id FROM PhotoTable WHERE id=?", -1, out stmt);
......@@ -208,7 +242,7 @@ public class PhotoTable : DatabaseTable {
return null;
}
public long get_exposure_time(PhotoID photoID) {
public time_t get_exposure_time(PhotoID photoID) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT exposure_time FROM PhotoTable WHERE id=?", -1, out stmt);
assert(res == Sqlite.OK);
......@@ -220,7 +254,7 @@ public class PhotoTable : DatabaseTable {
if (res != Sqlite.ROW)
return 0;
return (long) stmt.column_int64(0);
return (time_t) stmt.column_int64(0);
}
public bool remove_by_file(File file) {
......@@ -281,29 +315,7 @@ public class PhotoTable : DatabaseTable {
return PhotoID(stmt.column_int64(0));
}
public File[] get_photo_files() {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT filename FROM PhotoTable", -1, out stmt);
assert(res == Sqlite.OK);
File[] photoFiles = new File[0];
for (;;) {
res = stmt.step();
if (res == Sqlite.DONE) {
break;
} else if (res != Sqlite.ROW) {
fatal("get_photo_files", res);
break;
}
photoFiles += File.new_for_path(stmt.column_text(0));
}
return photoFiles;
}
public PhotoID[] get_photo_ids() {
public PhotoID[] get_photos() {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT id FROM PhotoTable", -1, out stmt);
assert(res == Sqlite.OK);
......@@ -335,12 +347,101 @@ public class PhotoTable : DatabaseTable {
res = stmt.step();
if (res == Sqlite.ROW) {
if (res != Sqlite.DONE) {
fatal("get_dimensions", res);
}
return Dimensions(stmt.column_int(0), stmt.column_int(1));
}
return null;
}
public Exif.Orientation get_orientation(PhotoID photo_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT orientation FROM PhotoTable WHERE id=?", -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.ROW) {
if (res != Sqlite.DONE) {
fatal("get_orientation", res);
}
return Exif.Orientation.TOP_LEFT;
}
return (Exif.Orientation) stmt.column_int(0);
}
public bool set_orientation(PhotoID photo_id, Exif.Orientation orientation) {
Sqlite.Statement stmt;
int res = db.prepare_v2("UPDATE PhotoTable SET orientation = ? WHERE id = ?", -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int(1, (int) orientation);
assert(res == Sqlite.OK);
res = stmt.bind_int64(2, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.DONE) {
fatal("photo set_orientation", res);
return false;
}
return true;
}
public EventID get_event(PhotoID photo_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT event_id FROM PhotoTable WHERE id=?", -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.ROW) {
if (res != Sqlite.DONE) {
fatal("get_event", res);
}
return EventID();
}
return EventID(stmt.column_int(0));
}
public PhotoID[] get_event_photos(EventID event_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("SELECT id FROM PhotoTable WHERE event_id = ?", -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, event_id.id);
assert(res == Sqlite.OK);
PhotoID[] photos = new PhotoID[0];
for(;;) {
res = stmt.step();
if (res == Sqlite.DONE) {
break;
} else if (res != Sqlite.ROW) {
fatal("get_event_photos", res);
break;
}
photos += PhotoID(stmt.column_int64(0));
}
return photos;
}
public bool set_event(PhotoID photo_id, EventID event_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("UPDATE PhotoTable SET event_id = ? WHERE id = ?", -1, out stmt);
......@@ -491,12 +592,12 @@ public class ThumbnailCacheTable : DatabaseTable {
}
}
public struct ImportID {
public struct EventID {
public static const int64 INVALID = -1;
public int64 id;
public ImportID(int64 id = INVALID) {
public EventID(int64 id = INVALID) {
this.id = id;
}
......@@ -509,12 +610,16 @@ public struct ImportID {
}
}
public class ImportTable : DatabaseTable {
public ImportTable() {
public class EventTable : DatabaseTable {
public EventTable() {
Sqlite.Statement stmt;
int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS ImportTable ("
int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS EventTable ("
+ "id INTEGER PRIMARY KEY, "
+ "time_imported INTEGER"
+ "name TEXT, "
+ "primary_photo_id INTEGER, "
+ "start_time INTEGER, "
+ "end_time INTEGER, "
+ "time_created INTEGER"
+ ")", -1, out stmt);
assert(res == Sqlite.OK);
......@@ -523,93 +628,73 @@ public class ImportTable : DatabaseTable {
fatal("create photo table", res);
}
}
public ImportID generate() {
TimeVal time_imported = TimeVal();
time_imported.get_current_time();
public EventID create(PhotoID primary_photo_id, time_t start_time) {
assert(primary_photo_id.is_valid());
assert(start_time != 0);
TimeVal time_created = TimeVal();
time_created.get_current_time();
Sqlite.Statement stmt;
int res = db.prepare_v2(
"INSERT INTO ImportTable (time_imported) VALUES (?)",
"INSERT INTO EventTable (primary_photo_id, time_created, start_time) VALUES (?, ?, ?)",
-1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, time_imported.tv_sec);
res = stmt.bind_int64(1, primary_photo_id.id);
assert(res == Sqlite.OK);
res = stmt.bind_int64(2, time_created.tv_sec);
assert(res == Sqlite.OK);
res = stmt.bind_int64(3, start_time);
assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.DONE) {
fatal("import generate", res);
fatal("create_event", res);
return ImportID();
return EventID();
}
return ImportID(db.last_insert_rowid());
}
}
public struct EventID {
public static const int64 INVALID = -1;
public int64 id;
public EventID(int64 id = INVALID) {
this.id = id;
}
public bool is_invalid() {
return (id == INVALID);
return EventID(db.last_insert_rowid());;
}
public bool is_valid() {
return (id != INVALID);
}
}
public class EventTable : DatabaseTable {
public EventTable() {
public bool remove(EventID event_id) {
Sqlite.Statement stmt;
int res = db.prepare_v2("CREATE TABLE IF NOT EXISTS EventTable ("
+ "id INTEGER PRIMARY KEY, "
+ "name TEXT, "
+ "primary_photo_id INTEGER, "
+ "time_created INTEGER"
+ ")", -1, out stmt);
int res = db.prepare_v2("DELETE FROM EventTable WHERE id=?", -1, out stmt);
assert(res == Sqlite.OK);
res = stmt.bind_int64(1, event_id.id);
assert(res == Sqlite.OK);