Commit 362f0afa authored by Jim Nelson's avatar Jim Nelson

For #65/#1001: Create Event now undoable. Allison needs this to complete #964.

parent 3c9780dd
......@@ -132,8 +132,8 @@ public class CollectionPage : CheckerboardPage {
// create new event
new_event_button = new Gtk.ToolButton.from_stock(Gtk.STOCK_NEW);
new_event_button.set_label(_("New Event"));
new_event_button.set_tooltip_text(_("Create new event from the selected photos"));
new_event_button.set_label(Resources.NEW_EVENT_LABEL);
new_event_button.set_tooltip_text(Resources.NEW_EVENT_TOOLTIP);
new_event_button.sensitive = false;
new_event_button.is_important = true;
new_event_button.clicked += on_new_event;
......@@ -287,7 +287,8 @@ public class CollectionPage : CheckerboardPage {
Gtk.ActionEntry new_event = { "NewEvent", Gtk.STOCK_NEW, TRANSLATABLE, "<Ctrl>N",
TRANSLATABLE, on_new_event };
new_event.label = _("_New Event");
new_event.label = Resources.NEW_EVENT_MENU;
new_event.tooltip = Resources.NEW_EVENT_TOOLTIP;
actions += new_event;
Gtk.ActionEntry help = { "HelpMenu", null, TRANSLATABLE, null, null, null };
......@@ -899,16 +900,8 @@ public class CollectionPage : CheckerboardPage {
}
private void on_new_event() {
Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>();
foreach (DataView view in get_view().get_selected()) {
photos.add(((Thumbnail) view).get_photo());
}
Event new_event = Event.generate_event(photos);
// switch to new event page
if (new_event != null)
LibraryWindow.get_app().switch_to_event(new_event);
NewEventCommand command = new NewEventCommand(get_view().get_selected());
get_command_manager().execute(command);
}
}
......
......@@ -441,3 +441,76 @@ public class RedeyeCommand : GenericPhotoTransformationCommand {
}
}
public class NewEventCommand : MultipleDataSourceCommand {
private SourceProxy new_event_proxy = null;
private Gee.HashMap<LibraryPhoto, SourceProxy?> old_photo_events = new Gee.HashMap<
LibraryPhoto, SourceProxy?>();
public NewEventCommand(Gee.Iterable<DataView> iter) {
base(iter, _("Creating New Event..."), _("Removing Event..."), Resources.NEW_EVENT_LABEL,
Resources.NEW_EVENT_TOOLTIP);
// get proxies for the photos' events as well as the key photo for the new event (which is
// simply the first one)
LibraryPhoto key_photo = null;
foreach (DataSource source in source_list) {
LibraryPhoto photo = (LibraryPhoto) source;
Event? event = photo.get_event();
SourceProxy? event_proxy = (event != null) ? event.get_proxy() : null;
// if any of the proxies break, the show's off
if (event_proxy != null)
event_proxy.broken += on_proxy_broken;
old_photo_events.set(photo, event_proxy);
if (key_photo == null)
key_photo = photo;
}
// key photo is required for an event
assert(key_photo != null);
// create the new event but only stash the proxy
Event new_event = Event.create_empty_event(key_photo);
new_event_proxy = new_event.get_proxy();
new_event_proxy.broken += on_proxy_broken;
}
~NewEventCommand() {
new_event_proxy.broken -= on_proxy_broken;
foreach (SourceProxy? proxy in old_photo_events.values) {
if (proxy != null)
proxy.broken -= on_proxy_broken;
}
}
public override void execute() {
// create the new event
base.execute();
// switch to new event page
LibraryWindow.get_app().switch_to_event((Event) new_event_proxy.get_source());
}
public override void execute_on_source(DataSource source) {
LibraryPhoto photo = (LibraryPhoto) source;
photo.set_event((Event?) new_event_proxy.get_source());
}
public override void undo_on_source(DataSource source) {
LibraryPhoto photo = (LibraryPhoto) source;
SourceProxy old_event_proxy = old_photo_events.get(photo);
Event? old_event = (old_event_proxy != null) ? (Event) old_event_proxy.get_source() : null;
photo.set_event(old_event);
}
private void on_proxy_broken() {
AppWindow.get_command_manager().reset();
}
}
......@@ -31,6 +31,8 @@ public delegate bool MarkedAction(DataObject object, Object user);
public delegate bool ProgressMonitor(uint64 current, uint64 total);
public class DataCollection {
public const int64 INVALID_OBJECT_ORDINAL = -1;
private class MarkerImpl : Object, Marker {
public DataCollection owner;
public Gee.HashSet<DataObject> marked = new Gee.HashSet<DataObject>();
......@@ -119,9 +121,10 @@ public class DataCollection {
private static OrderAddedComparator order_added_comparator = null;
private string name;
private SortedList<DataObject> list = new SortedList<DataObject>();
private Gee.HashSet<DataObject> hash_set = new Gee.HashSet<DataObject>();
private int added_counter = 0;
private int64 object_ordinal_generator = 0;
// When this signal has been fired, the added items are part of the collection
public virtual signal void items_added(Gee.Iterable<DataObject> added) {
......@@ -152,10 +155,16 @@ public class DataCollection {
public virtual signal void ordering_changed() {
}
public DataCollection() {
public DataCollection(string name) {
this.name = name;
list.resort(get_order_added_comparator());
}
public virtual string to_string() {
return "%s (%d)".printf(name, get_count());
}
// use notifies to ensure proper chronology of signal handling
public virtual void notify_items_added(Gee.Iterable<DataObject> added) {
items_added(added);
......@@ -164,7 +173,24 @@ public class DataCollection {
public virtual void notify_items_removed(Gee.Iterable<DataObject> removed) {
items_removed(removed);
}
public virtual void notify_contents_altered(Gee.Iterable<DataObject>? added,
Gee.Iterable<DataObject>? removed) {
contents_altered(added, removed);
}
public virtual void notify_item_altered(DataObject item) {
item_altered(item);
}
public virtual void notify_item_metadata_altered(DataObject item) {
item_metadata_altered(item);
}
public virtual void notify_ordering_changed() {
ordering_changed();
}
// A singleton list is used when a single item has been added/remove/selected/unselected
// and needs to be reported via a signal, which uses a list as a parameter ... although this
// seems wasteful, can't reuse a single singleton list because it's possible for a method
......@@ -190,13 +216,13 @@ public class DataCollection {
public virtual void set_comparator(Comparator<DataObject> comparator) {
list.resort(new ComparatorWrapper(comparator));
ordering_changed();
notify_ordering_changed();
}
// Return to natural ordering of DataObjects, which is order-added
public virtual void reset_comparator() {
list.resort(get_order_added_comparator());
ordering_changed();
notify_ordering_changed();
}
public virtual Gee.Iterable<DataObject> get_all() {
......@@ -227,7 +253,7 @@ public class DataCollection {
private void internal_add(DataObject object) {
assert(valid_type(object));
object.internal_set_membership(this, added_counter++);
object.internal_set_membership(this, object_ordinal_generator++);
bool added = list.add(object);
assert(added);
......@@ -247,7 +273,7 @@ public class DataCollection {
// Returns false if item is already part of the collection.
public bool add(DataObject object) {
if (contains(object)) {
debug("Cannot add %s: already present", object.to_string());
debug("%s cannot add %s: already present", to_string(), object.to_string());
return false;
}
......@@ -257,7 +283,7 @@ public class DataCollection {
// fire signal after added using singleton list
Gee.List<DataObject> added = get_singleton(object);
notify_items_added(added);
contents_altered(added, null);
notify_contents_altered(added, null);
return true;
}
......@@ -267,7 +293,7 @@ public class DataCollection {
Gee.ArrayList<DataObject> added = new Gee.ArrayList<DataObject>();
foreach (DataObject object in objects) {
if (contains(object)) {
debug("Cannot add %s: already present", object.to_string());
debug("%s cannot add %s: already present", to_string(), object.to_string());
continue;
}
......@@ -279,7 +305,7 @@ public class DataCollection {
// signal once all have been added
if (added.size > 0) {
notify_items_added(added);
contents_altered(added, null);
notify_contents_altered(added, null);
}
return added.size;
......@@ -359,7 +385,7 @@ public class DataCollection {
// signal after removing
if (marker.marked.size > 0) {
notify_items_removed(marker.marked);
contents_altered(null, marker.marked);
notify_contents_altered(null, marker.marked);
}
// invalidate the marker
......@@ -384,7 +410,7 @@ public class DataCollection {
// report after removal
notify_items_removed(removed);
contents_altered(null, removed);
notify_contents_altered(null, removed);
// the hash set should be cleared as well when finished
assert(hash_set.size == 0);
......@@ -399,7 +425,7 @@ public class DataCollection {
list.remove(object);
list.add(object);
item_altered(object);
notify_item_altered(object);
}
// This method is only called by DataObject to report when its metadata has been altered, so
......@@ -407,7 +433,7 @@ public class DataCollection {
public void internal_notify_metadata_altered(DataObject object) {
assert(contains(object));
item_metadata_altered(object);
notify_item_metadata_altered(object);
}
}
......@@ -416,10 +442,19 @@ public class DataCollection {
//
public class SourceCollection : DataCollection {
// When this signal is fired, the item is still part of the collection
// When this signal is fired, the item is still part of the collection but its own destroy()
// has already been called.
public virtual signal void item_destroyed(DataSource source) {
}
public SourceCollection(string name) {
base (name);
}
public virtual void notify_item_destroyed(DataSource source) {
item_destroyed(source);
}
public override bool valid_type(DataObject object) {
return object is DataSource;
}
......@@ -438,7 +473,7 @@ public class SourceCollection : DataCollection {
source.internal_mark_for_destroy();
source.destroy();
item_destroyed(source);
notify_item_destroyed(source);
((Marker) user).mark(source);
......@@ -456,7 +491,9 @@ public class DatabaseSourceCollection : SourceCollection {
private Gee.HashMap<int64?, DataSource> map = new Gee.HashMap<int64?, DataSource>(int64_hash,
int64_equal, direct_equal);
public DatabaseSourceCollection(GetSourceDatabaseKey source_key_func) {
public DatabaseSourceCollection(string name, GetSourceDatabaseKey source_key_func) {
base (name);
this.source_key_func = source_key_func;
}
......@@ -580,21 +617,20 @@ public class ViewCollection : DataCollection {
public virtual signal void geometries_altered() {
}
public ViewCollection() {
public ViewCollection(string name) {
base (name);
selected.resort(get_order_added_comparator());
visible.resort(get_order_added_comparator());
}
~ViewCollection() {
if (sources != null) {
sources.items_added -= on_sources_added;
sources.items_removed -= on_sources_removed;
sources.item_altered -= on_source_altered;
sources.item_metadata_altered -= on_source_altered;
}
halt_monitoring();
}
public void monitor_source_collection(SourceCollection sources, ViewManager manager) {
assert(this.sources == null && this.manager == null);
this.sources = sources;
this.manager = manager;
......@@ -609,6 +645,18 @@ public class ViewCollection : DataCollection {
sources.item_metadata_altered += on_source_altered;
}
public void halt_monitoring() {
if (sources != null) {
sources.items_added -= on_sources_added;
sources.items_removed -= on_sources_removed;
sources.item_altered -= on_source_altered;
sources.item_metadata_altered -= on_source_altered;
}
sources = null;
manager = null;
}
public void install_view_filter(ViewFilter filter) {
// this currently replaces any existing ViewFilter
this.filter = filter;
......
......@@ -7,11 +7,24 @@
//
// DataObject
//
// Object IDs are incremented for each DataObject, and therefore may be used to compare
// creation order. This behavior may be relied upon elsewhere. Object IDs may be recycled when
// DataObjects are reconstituted by a proxy.
//
// Ordinal IDs are supplied by DataCollections to record the ordering of the object being added
// to the collection. This value is primarily only used by DataCollection, but may be used
// elsewhere to resolve ordering questions (including stabilizing a sort).
//
public abstract class DataObject {
public const int64 INVALID_OBJECT_ID = -1;
private static int64 object_id_generator = 0;
private int64 object_id = INVALID_OBJECT_ID;
private DataCollection member_of = null;
private int ordinal = 0;
private int64 ordinal = DataCollection.INVALID_OBJECT_ORDINAL;
// This signal is fired when the source of the data is altered in a way that's significant
// to how it's represented in the application. This base signal must be called by child
// classes if the collection it is a member of is to be notified.
......@@ -24,10 +37,17 @@ public abstract class DataObject {
public virtual signal void metadata_altered() {
}
// XXX: Because the "this" variable is not available in virtual signals, using this method
// to signal until bug is fixed.
//
// See: https://bugzilla.gnome.org/show_bug.cgi?id=593734
// This signal is fired when the membership of a DataObject changes. This may be called twice
// in succession: once when the DataObject leaves a collection and again when it joins another.
public virtual signal void membership_changed(DataCollection? collection) {
}
// NOTE: Supplying an object ID should *only* be used when reconstituting the object (generally
// only done by DataSources).
public DataObject(int64 object_id = INVALID_OBJECT_ID) {
this.object_id = (object_id == INVALID_OBJECT_ID) ? object_id_generator++ : object_id;
}
public virtual void notify_altered() {
// fire signal on self
altered();
......@@ -37,10 +57,6 @@ public abstract class DataObject {
member_of.internal_notify_altered(this);
}
// XXX: Because the "this" variable is not available in virtual signals, using this method
// to signal until bug is fixed.
//
// See: https://bugzilla.gnome.org/show_bug.cgi?id=593734
public virtual void notify_metadata_altered() {
// fire signal on self
metadata_altered();
......@@ -50,6 +66,10 @@ public abstract class DataObject {
member_of.internal_notify_metadata_altered(this);
}
public virtual void notify_membership_changed(DataCollection? collection) {
membership_changed(collection);
}
public abstract string get_name();
public abstract string to_string();
......@@ -59,23 +79,33 @@ public abstract class DataObject {
}
// This method is only called by DataCollection.
public void internal_set_membership(DataCollection collection, int ordinal) {
public void internal_set_membership(DataCollection collection, int64 ordinal) {
assert(member_of == null);
member_of = collection;
this.ordinal = ordinal;
notify_membership_changed(member_of);
}
// This method is only called by DataCollection
public void internal_clear_membership() {
member_of = null;
ordinal = DataCollection.INVALID_OBJECT_ORDINAL;
notify_membership_changed(null);
}
// This method is only called by DataCollection
public int internal_get_ordinal() {
public int64 internal_get_ordinal() {
assert(member_of != null);
return ordinal;
}
public int64 get_object_id() {
return object_id;
}
}
//
......@@ -86,6 +116,34 @@ public abstract class DataObject {
// destroyed (versus removed or freed). Several DataViews may exist that reference a single
// DataSource.
//
// Some DataSources cannot be reconstituted (for example, if its backing file is deleted). In
// that case, dehydrate() should return null. When reconstituted, it is the responsibility of the
// implementation to ensure an exact clone is produced, minus any details that are not relevant or
// exposed (such as a database ID).
//
// If other DataSources refer to this DataSource, their state will *not* be
// saved/restored. This must be achieved via other means. However, implementations *should*
// track when changes to external state would break the proxy and call notify_broken();
//
public abstract class SourceSnapshot {
private bool snapshot_broken = false;
// This is signalled when the DataSource, for whatever reason, can no longer be reconstituted
// from this Snapshot.
public virtual signal void broken() {
}
public virtual void notify_broken() {
snapshot_broken = true;
broken();
}
public bool is_broken() {
return snapshot_broken;
}
}
public abstract class DataSource : DataObject {
protected delegate void ContactSubscriber(DataView view);
......@@ -94,6 +152,15 @@ public abstract class DataSource : DataObject {
private bool in_contact = false;
private bool marked_for_destroy = false;
// This signal is fired at the end of the destroy() chain. The object's state is either fragile
// or unusable. It is up to all observers to drop their references to the DataObject.
public virtual signal void destroyed() {
}
public DataSource(int64 object_id = INVALID_OBJECT_ID) {
base (object_id);
}
public override void notify_altered() {
// signal reflection
contact_subscribers(subscriber_altered);
......@@ -116,9 +183,9 @@ public abstract class DataSource : DataObject {
view.notify_metadata_altered();
}
// This signal is fired prior to the object being destroyed. It is up to all observers to
// drop their references to the DataObject.
public virtual signal void destroyed() {
// If a DataSource cannot produce snapshots, return null.
public virtual SourceSnapshot? save_snapshot() {
return null;
}
// This method is called by SourceCollection. It should not be called otherwise.
......@@ -173,13 +240,13 @@ public abstract class DataSource : DataObject {
}
public abstract class ThumbnailSource : DataSource {
public ThumbnailSource(int64 object_id = INVALID_OBJECT_ID) {
base (object_id);
}
public virtual signal void thumbnail_altered() {
}
// XXX: Because the "this" variable is not available in virtual signals, using this method
// to signal until bug is fixed.
//
// See: https://bugzilla.gnome.org/show_bug.cgi?id=593734
public virtual void notify_thumbnail_altered() {
// fire signal on self
thumbnail_altered();
......@@ -196,6 +263,10 @@ public abstract class ThumbnailSource : DataSource {
}
public abstract class PhotoSource : ThumbnailSource {
public PhotoSource(int64 object_id = INVALID_OBJECT_ID) {
base (object_id);
}
public abstract time_t get_exposure_time();
public abstract Dimensions get_dimensions();
......@@ -208,6 +279,10 @@ public abstract class PhotoSource : ThumbnailSource {
}
public abstract class EventSource : ThumbnailSource {
public EventSource(int64 object_id = INVALID_OBJECT_ID) {
base (object_id);
}
public abstract time_t get_start_time();
public abstract time_t get_end_time();
......@@ -219,6 +294,154 @@ public abstract class EventSource : ThumbnailSource {
public abstract Gee.Iterable<PhotoSource> get_photos();
}
//
// SourceProxy
//
// A SourceProxy allows for a DataSource's state to be maintained in memory regardless of
// whether or not the DataSource has been destroyed. If a user of SourceProxy
// requests the represented object and it is still in memory, it will be returned. If not, it
// is reconstituted and the new DataSource is returned.
//
// Several SourceProxy can be wrapped around the same DataSource. If the DataSource is
// destroyed, all Proxys drop their reference. When a Proxy reconstitutes the DataSource, all
// will be aware of it and re-establish their reference.
//
// The snapshot that is maintained is the snapshot in regards to the time of the Proxy's creation.
// Proxys do not update their snapshot thereafter. If a snapshot reports it is broken, the
// Proxy will not reconstitute the DataSource and get_source() will return null thereafter.
//
// There is no preferential treatment in regards to snapshots of the DataSources. The first
// Proxy to reconstitute the DataSource wins.
//
public abstract class SourceProxy {
private int64 object_id;
private string source_string;
private DataSource source;
private SourceSnapshot snapshot;
private SourceCollection membership;
// This is only signalled by the SourceProxy that reconstituted the DataSource. All
// Proxys will signal when this occurs.
public virtual signal void reconstituted(DataSource source) {
}
// This is signalled when the SourceProxy has dropped a destroyed DataSource. Calling
// get_source() will force it to be reconstituted.
public virtual signal void dehydrated() {
}
// This is signalled when the held DataSourceSnapshot reports it is broken. The DataSource
// will not be reconstituted and get_source() will return null thereafter.
public virtual signal void broken() {
}
public SourceProxy(DataSource source) {
object_id = source.get_object_id();
source_string = source.to_string();
snapshot = source.save_snapshot();
assert(snapshot != null);
snapshot.broken += on_snapshot_broken;
set_source(source);
membership = (SourceCollection) source.get_membership();
assert(membership != null);
membership.items_added += on_source_added;
}
~SourceProxy() {
drop_source();
membership.items_added -= on_source_added;
}
public abstract DataSource reconstitute(int64 object_id, SourceSnapshot snapshot);
public virtual void notify_reconstituted(DataSource source) {
reconstituted(source);
}
public virtual void notify_dehydrated() {
dehydrated();
}
public virtual void notify_broken() {
broken();
}
private void on_snapshot_broken() {
drop_source();
notify_broken();
}
private void set_source(DataSource source) {
drop_source();
this.source = source;
source.destroyed += on_destroyed;
}
private void drop_source() {
if (source == null)
return;
source.destroyed -= on_destroyed;
source = null;
}
public DataSource? get_source() {
if (snapshot.is_broken())
return null;
if (source != null)
return source;
// without the source, need to reconstitute it and re-add to its original SourceCollection
// it should also automatically add itself to its original collection (which is trapped
// in on_source_added)
DataSource new_source = reconstitute(object_id, snapshot);
assert(source == new_source);
assert(source.get_object_id() == object_id);
assert(membership.contains(source));
return source;
}
private void on_destroyed() {
assert(source != null);
// drop the reference ... will need to reconstitute later if requested
drop_source();
notify_dehydrated();
}
private void on_source_added(Gee.Iterable<DataObject> added) {
// only interested in new objects when the proxied object has gone away
if (source != null)
return;
foreach (DataObject object in added) {
// looking for new objects with original source object's id
if (object.get_object_id() != object_id)
continue;
// this is it; stash for future use
set_source((DataSource) object);
notify_reconstituted((DataSource) object);
break;
}
}
}
public interface Proxyable {
public abstract SourceProxy get_proxy();
}
//
// DataView
//
......@@ -299,10 +522,6 @@ public class DataView : DataObject {
visibility_changed(visible);
}
// XXX: Because the "this" variable is not available in virtual signals, using this method
// to signal until bug is fixed.
//
// See: https://bugzilla.gnome.org/show_bug.cgi?id=593734
public virtual void notify_view_altered() {
ViewCollection vc = get_membership() as ViewCollection;
if (vc != null && vc.are_view_notifications_frozen())
......@@ -313,11 +532,7 @@ public class DataView : DataObject {
if (vc != null)
vc.internal_notify_view_altered(this);
}
// XXX: Because the "this" variable is not available in virtual signals, using this method
// to signal until bug is fixed.
//
// See: https://bugzilla.gnome.org/show_bug.cgi?id=593734
public virtual void notify_geometry_altered() {
ViewCollection vc = get_membership() as ViewCollection;
if (vc != null && vc.are_geometry_notifications_frozen())
......@@ -337,11 +552,7 @@ public class ThumbnailView : DataView {
public ThumbnailView(ThumbnailSource source) {
base(source);
}
// XXX: Because the "this" variable is not available in virtual signals, using this method
// to signal until bug is fixed.
//
// See: https://bugzilla.gnome.org/show_bug.cgi?id=593734
public virtual void notify_thumbnail_altered() {
// fire signal on self