diff options
Diffstat (limited to 'src/core/DataCollection.vala')
-rw-r--r-- | src/core/DataCollection.vala | 623 |
1 files changed, 623 insertions, 0 deletions
diff --git a/src/core/DataCollection.vala b/src/core/DataCollection.vala new file mode 100644 index 0000000..615c6ac --- /dev/null +++ b/src/core/DataCollection.vala @@ -0,0 +1,623 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +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>(); + public int freeze_count = 0; + + public MarkerImpl(DataCollection owner) { + this.owner = owner; + + // if items are removed from main collection, they're removed from the marked list + // as well + owner.items_removed.connect(on_items_removed); + } + + ~MarkerImpl() { + owner.items_removed.disconnect(on_items_removed); + } + + public void mark(DataObject object) { + assert(owner.internal_contains(object)); + + marked.add(object); + } + + public void unmark(DataObject object) { + assert(owner.internal_contains(object)); + + marked.remove(object); + } + + public bool toggle(DataObject object) { + assert(owner.internal_contains(object)); + + if (marked.contains(object)) { + marked.remove(object); + } else { + marked.add(object); + } + + return marked.contains(object); + } + + public void mark_many(Gee.Collection<DataObject> list) { + foreach (DataObject object in list) { + assert(owner.internal_contains(object)); + + marked.add(object); + } + } + + public void unmark_many(Gee.Collection<DataObject> list) { + foreach (DataObject object in list) { + assert(owner.internal_contains(object)); + + marked.remove(object); + } + } + + public void mark_all() { + foreach (DataObject object in owner.get_all()) + marked.add(object); + } + + public int get_count() { + return (marked != null) ? marked.size : freeze_count; + } + + public Gee.Collection<DataObject> get_all() { + Gee.ArrayList<DataObject> copy = new Gee.ArrayList<DataObject>(); + copy.add_all(marked); + + return copy; + } + + private void on_items_removed(Gee.Iterable<DataObject> removed) { + foreach (DataObject object in removed) + marked.remove(object); + } + + // This method is called by DataCollection when it starts iterating over the marked list ... + // the marker at this point stops monitoring the collection, preventing a possible + // removal during an iteration, which is bad. + public void freeze() { + owner.items_removed.disconnect(on_items_removed); + } + + public void finished() { + if (marked != null) + freeze_count = marked.size; + + marked = null; + } + + public bool is_valid(DataCollection collection) { + return (collection == owner) && (marked != null); + } + } + + private string name; + private DataSet dataset = new DataSet(); + private Gee.HashMap<string, Value?> properties = new Gee.HashMap<string, Value?>(); + private int64 object_ordinal_generator = 0; + private int notifies_frozen = 0; + private Gee.HashMap<DataObject, Alteration> frozen_items_altered = null; + private bool fire_ordering_changed = false; + + // When this signal has been fired, the added items are part of the collection + public virtual signal void items_added(Gee.Iterable<DataObject> added) { + } + + // When this signal is fired, the removed items are no longer part of the collection + public virtual signal void items_removed(Gee.Iterable<DataObject> removed) { + } + + // When this signal is fired, the removed items are no longer part of the collection + public virtual signal void contents_altered(Gee.Iterable<DataObject>? added, + Gee.Iterable<DataObject>? removed) { + } + + // This signal fires whenever any (or multiple) items in the collection signal they've been + // altered. + public virtual signal void items_altered(Gee.Map<DataObject, Alteration> items) { + } + + // Fired when a new sort comparator is registered or an item has moved in the ordering due to + // an alteration. + public virtual signal void ordering_changed() { + } + + // Fired when a collection property is set. The old value is passed as well, null if not set + // previously. + public virtual signal void property_set(string name, Value? old, Value val) { + } + + // Fired when a collection property is cleared. + public virtual signal void property_cleared(string name) { + } + + // Fired when "altered" signal (and possibly other related signals, depending on the subclass) + // is frozen. + public virtual signal void frozen() { + } + + // Fired when "altered" signal (and other related signals, depending on the subclass) is + // restored (thawed). + public virtual signal void thawed() { + } + + public DataCollection(string name) { + this.name = name; + } + + ~DataCollection() { +#if TRACE_DTORS + debug("DTOR: DataCollection %s", name); +#endif + } + + public virtual string to_string() { + return "%s (%d)".printf(name, get_count()); + } + + // use notifies to ensure proper chronology of signal handling + protected virtual void notify_items_added(Gee.Iterable<DataObject> added) { + items_added(added); + } + + protected virtual void notify_items_removed(Gee.Iterable<DataObject> removed) { + items_removed(removed); + } + + protected virtual void notify_contents_altered(Gee.Iterable<DataObject>? added, + Gee.Iterable<DataObject>? removed) { + contents_altered(added, removed); + } + + protected virtual void notify_items_altered(Gee.Map<DataObject, Alteration> items) { + items_altered(items); + } + + protected virtual void notify_ordering_changed() { + ordering_changed(); + } + + protected virtual void notify_property_set(string name, Value? old, Value val) { + property_set(name, old, val); + } + + protected virtual void notify_property_cleared(string name) { + property_cleared(name); + } + + // 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 + // that needs it to be called from within a signal handler for another method, corrupting the + // shared list's contents mid-signal + protected static Gee.Collection<DataObject> get_singleton(DataObject object) { + return new SingletonCollection<DataObject>(object); + } + + protected static Gee.Map<DataObject, Alteration> get_alteration_singleton(DataObject object, + Alteration alteration) { + Gee.Map<DataObject, Alteration> map = new Gee.HashMap<DataObject, Alteration>(); + map.set(object, alteration); + + return map; + } + + public virtual bool valid_type(DataObject object) { + return true; + } + + public unowned Comparator get_comparator() { + return dataset.get_comparator(); + } + + public unowned ComparatorPredicate get_comparator_predicate() { + return dataset.get_comparator_predicate(); + } + + public virtual void set_comparator(Comparator comparator, ComparatorPredicate? predicate) { + dataset.set_comparator(comparator, predicate); + notify_ordering_changed(); + } + + // Return to natural ordering of DataObjects, which is order-added + public virtual void reset_comparator() { + dataset.reset_comparator(); + notify_ordering_changed(); + } + + public virtual Gee.Collection<DataObject> get_all() { + return dataset.get_all(); + } + + protected DataSet get_dataset_copy() { + return dataset.copy(); + } + + public virtual int get_count() { + return dataset.get_count(); + } + + public virtual DataObject? get_at(int index) { + return dataset.get_at(index); + } + + public virtual int index_of(DataObject object) { + return dataset.index_of(object); + } + + public virtual bool contains(DataObject object) { + return internal_contains(object); + } + + // Because subclasses may filter out objects (by overriding key methods here), need an + // internal_contains for consistency checking. + private bool internal_contains(DataObject object) { + if (!dataset.contains(object)) + return false; + + assert(object.get_membership() == this); + + return true; + } + + private void internal_add(DataObject object) { + assert(valid_type(object)); + + object.internal_set_membership(this, object_ordinal_generator++); + + bool added = dataset.add(object); + assert(added); + } + + private void internal_add_many(Gee.List<DataObject> objects, ProgressMonitor? monitor) { + int count = objects.size; + for (int ctr = 0; ctr < count; ctr++) { + DataObject object = objects.get(ctr); + assert(valid_type(object)); + + object.internal_set_membership(this, object_ordinal_generator++); + + if (monitor != null) + monitor(ctr, count); + } + + bool added = dataset.add_many(objects); + assert(added); + } + + private void internal_remove(DataObject object) { + bool removed = dataset.remove(object); + assert(removed); + + object.internal_clear_membership(); + } + + // Returns false if item is already part of the collection. + public virtual bool add(DataObject object) { + if (internal_contains(object)) { + debug("%s cannot add %s: already present", to_string(), object.to_string()); + + return false; + } + + internal_add(object); + + // fire signal after added using singleton list + Gee.Collection<DataObject> added = get_singleton(object); + notify_items_added(added); + notify_contents_altered(added, null); + + // This must be called *after* the DataCollection has signalled. + object.notify_membership_changed(this); + + return true; + } + + // Returns the items added to the collection. + public virtual Gee.Collection<DataObject> add_many(Gee.Collection<DataObject> objects, + ProgressMonitor? monitor = null) { + Gee.ArrayList<DataObject> added = new Gee.ArrayList<DataObject>(); + foreach (DataObject object in objects) { + if (internal_contains(object)) { + debug("%s cannot add %s: already present", to_string(), object.to_string()); + + continue; + } + + added.add(object); + } + + int count = added.size; + if (count == 0) + return added; + + internal_add_many(added, monitor); + + // signal once all have been added + notify_items_added(added); + notify_contents_altered(added, null); + + // This must be called *after* the DataCollection signals have fired. + for (int ctr = 0; ctr < count; ctr++) + added.get(ctr).notify_membership_changed(this); + + return added; + } + + // Obtain a marker to build a list of objects to perform an action upon. + public Marker start_marking() { + return new MarkerImpl(this); + } + + // Obtain a marker with a single item marked. More can be added. + public Marker mark(DataObject object) { + Marker marker = new MarkerImpl(this); + marker.mark(object); + + return marker; + } + + // Obtain a marker for all items in a collection. More can be added. + public Marker mark_many(Gee.Collection<DataObject> objects) { + Marker marker = new MarkerImpl(this); + marker.mark_many(objects); + + return marker; + } + + // Iterate over all the marked objects performing a user-supplied action on each one. The + // marker is invalid after calling this method. + public void act_on_marked(Marker m, MarkedAction action, ProgressMonitor? monitor = null, + Object? user = null) { + MarkerImpl marker = (MarkerImpl) m; + + assert(marker.is_valid(this)); + + // freeze the marker to prepare it for iteration + marker.freeze(); + + uint64 count = 0; + uint64 total = marker.marked.size; + + // iterate, breaking if the callback asks to stop + foreach (DataObject object in marker.marked) { + // although marker tracks when items are removed, catch it here as well + if (!internal_contains(object)) { + warning("act_on_marked: marker holding ref to unknown %s", object.to_string()); + + continue; + } + + if (!action(object, user)) + break; + + if (monitor != null) { + if (!monitor(++count, total)) + break; + } + } + + // invalidate the marker + marker.finished(); + } + + // Remove marked items from collection. This two-step process allows for iterating in a foreach + // loop and removing without creating a separate list. The marker is invalid after this call. + public virtual void remove_marked(Marker m) { + MarkerImpl marker = (MarkerImpl) m; + + assert(marker.is_valid(this)); + + // freeze the marker before signalling, so it doesn't remove all its items + marker.freeze(); + + // remove everything in the marked list + Gee.ArrayList<DataObject> skipped = null; + foreach (DataObject object in marker.marked) { + // although marker should track items already removed, catch it here as well + if (!internal_contains(object)) { + warning("remove_marked: marker holding ref to unknown %s", object.to_string()); + + if (skipped == null) + skipped = new Gee.ArrayList<DataObject>(); + + skipped.add(object); + + continue; + } + + internal_remove(object); + } + + if (skipped != null) + marker.marked.remove_all(skipped); + + // signal after removing + if (marker.marked.size > 0) { + notify_items_removed(marker.marked); + notify_contents_altered(null, marker.marked); + + // this must be called after the DataCollection has signalled. + foreach (DataObject object in marker.marked) + object.notify_membership_changed(null); + } + + // invalidate the marker + marker.finished(); + } + + public virtual void clear() { + if (dataset.get_count() == 0) + return; + + // remove everything in the list, but have to maintain a new list for reporting the signal. + // Don't use an iterator, as list is modified in internal_remove(). + Gee.ArrayList<DataObject> removed = new Gee.ArrayList<DataObject>(); + do { + DataObject? object = dataset.get_at(0); + assert(object != null); + + removed.add(object); + internal_remove(object); + } while (dataset.get_count() > 0); + + // report after removal + notify_items_removed(removed); + notify_contents_altered(null, removed); + + // This must be called after the DataCollection has signalled. + foreach (DataObject object in removed) + object.notify_membership_changed(null); + } + + // close() must be called before disposing of the DataCollection, so all signals may be + // disconnected and all internal references to the collection can be dropped. In the bare + // minimum, all items will be removed from the collection (and the appropriate signals and + // notify calls will be made). Subclasses may fire other signals while disposing of their + // references. However, if they are entirely synchronized on DataCollection's signals, that + // may be enough for them to clean up. + public virtual void close() { + clear(); + } + + // This method is only called by DataObject to report when it has been altered, so observers of + // this collection may be notified as well. + public void internal_notify_altered(DataObject object, Alteration alteration) { + assert(internal_contains(object)); + + bool resort_occurred = dataset.resort_object(object, alteration); + + if (are_notifications_frozen()) { + if (frozen_items_altered == null) + frozen_items_altered = new Gee.HashMap<DataObject, Alteration>(); + + // if an alteration for the object is already in place, compress the two and add the + // new one, otherwise set the supplied one + Alteration? current = frozen_items_altered.get(object); + if (current != null) + current = current.compress(alteration); + else + current = alteration; + + frozen_items_altered.set(object, current); + + fire_ordering_changed = fire_ordering_changed || resort_occurred; + + return; + } + + if (resort_occurred) + notify_ordering_changed(); + + notify_items_altered(get_alteration_singleton(object, alteration)); + } + + public Value? get_property(string name) { + return properties.get(name); + } + + public void set_property(string name, Value val, ValueEqualFunc? value_equals = null) { + if (value_equals == null) { + if (val.holds(typeof(bool))) + value_equals = bool_value_equals; + else if (val.holds(typeof(int))) + value_equals = int_value_equals; + else + error("value_equals must be specified for this type"); + } + + Value? old = properties.get(name); + if (old != null) { + if (value_equals(old, val)) + return; + } + + properties.set(name, val); + + notify_property_set(name, old, val); + + // notify all items in the collection of the change + int count = dataset.get_count(); + for (int ctr = 0; ctr < count; ctr++) + dataset.get_at(ctr).notify_collection_property_set(name, old, val); + } + + public void clear_property(string name) { + if (!properties.unset(name)) + return; + + // only notify if the propery was unset (that is, was set to begin with) + notify_property_cleared(name); + + // notify all items + int count = dataset.get_count(); + for (int ctr = 0; ctr < count; ctr++) + dataset.get_at(ctr).notify_collection_property_cleared(name); + } + + // This is only guaranteed to freeze notifications that come in from contained objects and + // need to be propagated with collection signals. Thus, the caller can freeze notifications, + // make modifications to many or all member objects, then unthaw and have the aggregated signals + // fired at once. + // + // DataObject/DataSource/DataView should also "eat" their signals as well, to prevent observers + // from being notified while their collection is frozen, and only fire them when + // internal_collection_thawed is called. + // + // For DataCollection, the signals affected are items_altered and ordering_changed. + public void freeze_notifications() { + if (notifies_frozen++ == 0) + notify_frozen(); + } + + public void thaw_notifications() { + if (notifies_frozen == 0) + return; + + if (--notifies_frozen == 0) + notify_thawed(); + } + + public bool are_notifications_frozen() { + return notifies_frozen > 0; + } + + // This is called when notifications have frozen. Child collections should halt notifications + // until thawed() is called. + protected virtual void notify_frozen() { + frozen(); + } + + // This is called when enough thaw_notifications() calls have been made. Child collections + // should issue caught notifications. + protected virtual void notify_thawed() { + if (frozen_items_altered != null) { + // refs are swapped around due to reentrancy + Gee.Map<DataObject, Alteration> copy = frozen_items_altered; + frozen_items_altered = null; + + notify_items_altered(copy); + } + + if (fire_ordering_changed) { + fire_ordering_changed = false; + notify_ordering_changed(); + } + + thawed(); + } +} + |