diff options
Diffstat (limited to 'src/core')
-rw-r--r-- | src/core/Alteration.vala | 316 | ||||
-rw-r--r-- | src/core/ContainerSourceCollection.vala | 237 | ||||
-rw-r--r-- | src/core/Core.vala | 29 | ||||
-rw-r--r-- | src/core/DataCollection.vala | 623 | ||||
-rw-r--r-- | src/core/DataObject.vala | 137 | ||||
-rw-r--r-- | src/core/DataSet.vala | 183 | ||||
-rw-r--r-- | src/core/DataSource.vala | 679 | ||||
-rw-r--r-- | src/core/DataSourceTypes.vala | 108 | ||||
-rw-r--r-- | src/core/DataView.vala | 132 | ||||
-rw-r--r-- | src/core/DataViewTypes.vala | 50 | ||||
-rw-r--r-- | src/core/DatabaseSourceCollection.vala | 86 | ||||
-rw-r--r-- | src/core/SourceCollection.vala | 221 | ||||
-rw-r--r-- | src/core/SourceHoldingTank.vala | 209 | ||||
-rw-r--r-- | src/core/SourceInterfaces.vala | 44 | ||||
-rw-r--r-- | src/core/Tracker.vala | 216 | ||||
-rw-r--r-- | src/core/ViewCollection.vala | 1287 | ||||
-rw-r--r-- | src/core/mk/core.mk | 43 | ||||
-rw-r--r-- | src/core/util.vala | 196 |
18 files changed, 4796 insertions, 0 deletions
diff --git a/src/core/Alteration.vala b/src/core/Alteration.vala new file mode 100644 index 0000000..865be84 --- /dev/null +++ b/src/core/Alteration.vala @@ -0,0 +1,316 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// +// Alteration represents a description of what has changed in the DataObject (reported via the +// "altered" signal). Since the descriptions can vary wildly depending on the semantics of each +// DataObject, no assumptions or requirements are placed on Alteration other than it must have +// one or more "subjects", each with a "detail". Subscribers to the "altered" signal can query +// the Alteration object to determine if the change is important to them. +// +// Alteration is an immutable type. This means it's possible to store const Alterations of oft-used +// values for reuse. +// +// Alterations may be compressed, merging their subjects and details into a new aggregated +// Alteration. Generally this is handled automatically by DataObject and DataCollection, when +// necessary. +// +// NOTE: subjects and details should be ASCII labels (as in, plain-old ASCII, no code pages). +// They are treated as case-sensitive strings. +// +// Recommended subjects include: image, thumbnail, metadata. +// + +public class Alteration { + private string subject = null; + private string detail = null; + private Gee.MultiMap<string, string> map = null; + + public Alteration(string subject, string detail) { + add_detail(subject, detail); + } + + // Create an Alteration that has more than one subject/detail. list is a comma-delimited + // string of colon-separated subject:detail pairs. + public Alteration.from_list(string list) requires (list.length > 0) { + string[] pairs = list.split(","); + assert(pairs.length >= 1); + + foreach (string pair in pairs) { + string[] subject_detail = pair.split(":", 2); + assert(subject_detail.length == 2); + + add_detail(subject_detail[0], subject_detail[1]); + } + } + + // Create an Alteration that has more than one subject/detail from an array of comma-delimited + // strings of colon-separate subject:detail pairs + public Alteration.from_array(string[] array) requires (array.length > 0) { + foreach (string pair in array) { + string[] subject_detail = pair.split(":", 2); + assert(subject_detail.length == 2); + + add_detail(subject_detail[0], subject_detail[1]); + } + } + + // Used for compression. + private Alteration.from_map(Gee.MultiMap<string, string> map) { + this.map = map; + } + + private void add_detail(string sub, string det) { + // strip leading and trailing whitespace + string subject = sub.strip(); + assert(subject.length > 0); + + string detail = det.strip(); + assert(detail.length > 0); + + // if a simple Alteration, store in singleton refs + if (this.subject == null && map == null) { + assert(this.detail == null); + + this.subject = subject; + this.detail = detail; + + return; + } + + // Now a complex Alteration, requiring a Map. + if (map == null) + map = create_map(); + + // Move singletons into Map + if (this.subject != null) { + assert(this.detail != null); + + map.set(this.subject, this.detail); + this.subject = null; + this.detail = null; + } + + // Store new subject:detail in Map as well + map.set(subject, detail); + } + + private Gee.MultiMap<string, string> create_map() { + return new Gee.HashMultiMap<string, string>(case_hash, case_equal, case_hash, case_equal); + } + + private static bool case_equal(string? a, string? b) { + return equal_values(a, b); + } + + private static uint case_hash(string? a) { + return hash_value(a); + } + + private static inline bool equal_values(string str1, string str2) { + return str1.ascii_casecmp(str2) == 0; + } + + private static inline uint hash_value(string str) { + return str_hash(str); + } + + public bool has_subject(string subject) { + if (this.subject != null) + return equal_values(this.subject, subject); + + assert(map != null); + Gee.Set<string>? keys = map.get_keys(); + if (keys != null) { + foreach (string key in keys) { + if (equal_values(key, subject)) + return true; + } + } + + return false; + } + + public bool has_detail(string subject, string detail) { + if (this.subject != null && this.detail != null) + return equal_values(this.subject, subject) && equal_values(this.detail, detail); + + assert(map != null); + Gee.Collection<string>? values = map.get(subject); + if (values != null) { + foreach (string value in values) { + if (equal_values(value, detail)) + return true; + } + } + + return false; + } + + public Gee.Collection<string>? get_details(string subject) { + if (this.subject != null && detail != null && equal_values(this.subject, subject)) { + Gee.ArrayList<string> details = new Gee.ArrayList<string>(); + details.add(detail); + + return details; + } + + return (map != null) ? map.get(subject) : null; + } + + public string to_string() { + if (subject != null) { + assert(detail != null); + + return "%s:%s".printf(subject, detail); + } + + assert(map != null); + + string str = ""; + foreach (string key in map.get_keys()) { + foreach (string value in map.get(key)) { + if (str.length != 0) + str += ", "; + + str += "%s:%s".printf(key, value); + } + } + + return str; + } + + // Returns true if this object has any subject:detail matches with the supplied Alteration. + public bool contains_any(Alteration other) { + // identity + if (this == other) + return true; + + // if both singletons, check for singleton match + if (subject != null && other.subject != null && detail != null && other.detail != null) + return equal_values(subject, other.subject) && equal_values(detail, other.detail); + + // if one is singleton and the other a multiple, search for singleton in multiple + if ((map != null && other.map == null) || (map == null && other.map != null)) { + string single_subject = subject != null ? subject : other.subject; + string single_detail = detail != null ? detail : other.detail; + Gee.MultiMap<string, string> multimap = map != null ? map : other.map; + + return multimap.contains(single_subject) && map.get(single_subject).contains(single_detail); + } + + // if both multiples, check for any match at all + if (map != null && other.map != null) { + Gee.Set<string>? keys = map.get_keys(); + assert(keys != null); + Gee.Set<string>? other_keys = other.map.get_keys(); + assert(other_keys != null); + + foreach (string subject in other_keys) { + if (!keys.contains(subject)) + continue; + + Gee.Collection<string>? details = map.get(subject); + Gee.Collection<string>? other_details = other.map.get(subject); + + if (details != null && other_details != null) { + foreach (string detail in other_details) { + if (details.contains(detail)) + return true; + } + } + } + } + + return false; + } + + public bool equals(Alteration other) { + // identity + if (this == other) + return true; + + // if both singletons, check for singleton match + if (subject != null && other.subject != null && detail != null && other.detail != null) + return equal_values(subject, other.subject) && equal_values(detail, other.detail); + + // if both multiples, check for across-the-board matches + if (map != null && other.map != null) { + // see if both maps contain the same set of keys + Gee.Set<string>? keys = map.get_keys(); + assert(keys != null); + Gee.Set<string>? other_keys = other.map.get_keys(); + assert(other_keys != null); + + if (keys.size != other_keys.size) + return false; + + if (!keys.contains_all(other_keys)) + return false; + + if (!other_keys.contains_all(keys)) + return false; + + foreach (string key in keys) { + Gee.Collection<string> values = map.get(key); + Gee.Collection<string> other_values = other.map.get(key); + + if (values.size != other_values.size) + return false; + + if (!values.contains_all(other_values)) + return false; + + if (!other_values.contains_all(values)) + return false; + } + + // maps are identical + return true; + } + + // one singleton and one multiple, not equal + return false; + } + + private static void multimap_add_all(Gee.MultiMap<string, string> dest, + Gee.MultiMap<string, string> src) { + Gee.Set<string> keys = src.get_keys(); + foreach (string key in keys) { + Gee.Collection<string> values = src.get(key); + foreach (string value in values) + dest.set(key, value); + } + } + + // This merges the Alterations, returning a new Alteration with both represented. If both + // Alterations are equal, this will return this object rather than create a new one. + public Alteration compress(Alteration other) { + if (equals(other)) + return this; + + // Build a new Alteration with both represented ... if they're unequal, then the new one + // is guaranteed not to be a singleton + Gee.MultiMap<string, string> compressed = create_map(); + + if (subject != null && detail != null) { + compressed.set(subject, detail); + } else { + assert(map != null); + multimap_add_all(compressed, map); + } + + if (other.subject != null && other.detail != null) { + compressed.set(other.subject, other.detail); + } else { + assert(other.map != null); + multimap_add_all(compressed, other.map); + } + + return new Alteration.from_map(compressed); + } +} + diff --git a/src/core/ContainerSourceCollection.vala b/src/core/ContainerSourceCollection.vala new file mode 100644 index 0000000..655cfa0 --- /dev/null +++ b/src/core/ContainerSourceCollection.vala @@ -0,0 +1,237 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// A ContainerSourceCollection is for DataSources which maintain links to one or more other +// DataSources, assumed to be of a different type. ContainerSourceCollection automates the task +// of handling unlinking and relinking and maintaining backlinks. Unlinked DataSources are +// held in a holding tank, until they are either relinked or destroyed. +// +// If the ContainerSourceCollection's DataSources are types that "evaporate" (i.e. they disappear +// when they hold no items), they should use the evaporate() method, which will either destroy +// the DataSource or hold it in the tank (if backlinks are outstanding). +public abstract class ContainerSourceCollection : DatabaseSourceCollection { + private Gee.HashSet<SourceCollection> attached_collections = new Gee.HashSet<SourceCollection>(); + private string backlink_name; + private Gee.HashSet<ContainerSource> holding_tank = new Gee.HashSet<ContainerSource>(); + + public virtual signal void container_contents_added(ContainerSource container, + Gee.Collection<DataSource> added, bool relinked) { + } + + public virtual signal void container_contents_removed(ContainerSource container, + Gee.Collection<DataSource> removed, bool unlinked) { + } + + public virtual signal void container_contents_altered(ContainerSource container, + Gee.Collection<DataSource>? added, bool relinked, Gee.Collection<DataSource>? removed, + bool unlinked) { + } + + public virtual signal void backlink_to_container_removed(ContainerSource container, + Gee.Collection<DataSource> sources) { + } + + public ContainerSourceCollection(string backlink_name, string name, + GetSourceDatabaseKey source_key_func) { + base (name, source_key_func); + + this.backlink_name = backlink_name; + } + + ~ContainerSourceCollection() { + detach_all_collections(); + } + + protected override void notify_backlink_removed(SourceBacklink backlink, + Gee.Collection<DataSource> sources) { + base.notify_backlink_removed(backlink, sources); + + ContainerSource? container = convert_backlink_to_container(backlink); + if (container != null) + notify_backlink_to_container_removed(container, sources); + } + + public virtual void notify_container_contents_added(ContainerSource container, + Gee.Collection<DataSource> added, bool relinked) { + // if container is in holding tank, remove it now and relink to collection + if (holding_tank.contains(container)) { + bool removed = holding_tank.remove(container); + assert(removed); + + relink(container); + } + + container_contents_added(container, added, relinked); + } + + public virtual void notify_container_contents_removed(ContainerSource container, + Gee.Collection<DataSource> removed, bool unlinked) { + container_contents_removed(container, removed, unlinked); + } + + public virtual void notify_container_contents_altered(ContainerSource container, + Gee.Collection<DataSource>? added, bool relinked, Gee.Collection<DataSource>? removed, + bool unlinked) { + container_contents_altered(container, added, relinked, removed, unlinked); + } + + public virtual void notify_backlink_to_container_removed(ContainerSource container, + Gee.Collection<DataSource> sources) { + backlink_to_container_removed(container, sources); + } + + protected abstract Gee.Collection<ContainerSource>? get_containers_holding_source(DataSource source); + + // Looks in holding_tank as well. + protected abstract ContainerSource? convert_backlink_to_container(SourceBacklink backlink); + + protected void freeze_attached_notifications() { + foreach(SourceCollection collection in attached_collections) + collection.freeze_notifications(); + } + + protected void thaw_attached_notifications() { + foreach(SourceCollection collection in attached_collections) + collection.thaw_notifications(); + } + + public Gee.Collection<ContainerSource> get_holding_tank() { + return holding_tank.read_only_view; + } + + public void init_add_unlinked(ContainerSource unlinked) { + holding_tank.add(unlinked); + } + + public void init_add_many_unlinked(Gee.Collection<ContainerSource> unlinked) { + holding_tank.add_all(unlinked); + } + + public bool relink_from_holding_tank(ContainerSource source) { + if (!holding_tank.remove(source)) + return false; + + relink(source); + + return true; + } + + private void on_contained_sources_unlinking(Gee.Collection<DataSource> unlinking) { + freeze_attached_notifications(); + + Gee.HashMultiMap<ContainerSource, DataSource> map = + new Gee.HashMultiMap<ContainerSource, DataSource>(); + + foreach (DataSource source in unlinking) { + Gee.Collection<ContainerSource>? containers = get_containers_holding_source(source); + if (containers == null || containers.size == 0) + continue; + + foreach (ContainerSource container in containers) { + map.set(container, source); + source.set_backlink(container.get_backlink()); + } + } + + foreach (ContainerSource container in map.get_keys()) + container.break_link_many(map.get(container)); + + thaw_attached_notifications(); + } + + private void on_contained_sources_relinked(Gee.Collection<DataSource> relinked) { + freeze_attached_notifications(); + + Gee.HashMultiMap<ContainerSource, DataSource> map = + new Gee.HashMultiMap<ContainerSource, DataSource>(); + + foreach (DataSource source in relinked) { + Gee.List<SourceBacklink>? backlinks = source.get_backlinks(backlink_name); + if (backlinks == null || backlinks.size == 0) + continue; + + foreach (SourceBacklink backlink in backlinks) { + ContainerSource? container = convert_backlink_to_container(backlink); + if (container != null) { + map.set(container, source); + } else { + warning("Unable to relink %s to container backlink %s", source.to_string(), + backlink.to_string()); + } + } + } + + foreach (ContainerSource container in map.get_keys()) + container.establish_link_many(map.get(container)); + + thaw_attached_notifications(); + } + + private void on_contained_source_destroyed(DataSource source) { + Gee.Iterator<ContainerSource> iter = holding_tank.iterator(); + while (iter.next()) { + ContainerSource container = iter.get(); + + // By design, we no longer discard 'orphan' tags, that is, tags with zero media sources + // remaining, since empty tags are explicitly allowed to persist as of the 0.12 dev cycle. + if ((!container.has_links()) && !(container is Tag)) { + iter.remove(); + container.destroy_orphan(true); + } + } + } + + protected override void notify_item_destroyed(DataSource source) { + foreach (SourceCollection collection in attached_collections) { + collection.remove_backlink(((ContainerSource) source).get_backlink()); + } + + base.notify_item_destroyed(source); + } + + // This method should be called by a ContainerSource when it needs to "evaporate" -- it no + // longer holds any source objects and should not be available to the user any longer. If link + // state persists for this ContainerSource, it will be held in the holding tank. Otherwise, it's + // destroyed. + public void evaporate(ContainerSource container) { + foreach (SourceCollection collection in attached_collections) { + if (collection.has_backlink(container.get_backlink())) { + unlink_marked(mark(container)); + bool added = holding_tank.add(container); + assert(added); + return; + } + } + + destroy_marked(mark(container), true); + } + + public void attach_collection(SourceCollection collection) { + if (attached_collections.contains(collection)) { + warning("attempted to multiple-attach '%s' to '%s'", collection.to_string(), to_string()); + return; + } + + attached_collections.add(collection); + + collection.items_unlinking.connect(on_contained_sources_unlinking); + collection.items_relinked.connect(on_contained_sources_relinked); + collection.item_destroyed.connect(on_contained_source_destroyed); + collection.unlinked_destroyed.connect(on_contained_source_destroyed); + } + + public void detach_all_collections() { + foreach (SourceCollection collection in attached_collections) { + collection.items_unlinking.disconnect(on_contained_sources_unlinking); + collection.items_relinked.disconnect(on_contained_sources_relinked); + collection.item_destroyed.disconnect(on_contained_source_destroyed); + collection.unlinked_destroyed.disconnect(on_contained_source_destroyed); + } + + attached_collections.clear(); + } +} + diff --git a/src/core/Core.vala b/src/core/Core.vala new file mode 100644 index 0000000..1b9958e --- /dev/null +++ b/src/core/Core.vala @@ -0,0 +1,29 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/* This file is the master unit file for the Core unit. It should be edited to include + * whatever code is deemed necessary. + * + * The init() and terminate() methods are mandatory. + * + * If the unit needs to be configured prior to initialization, add the proper parameters to + * the preconfigure() method, implement it, and ensure in init() that it's been called. + */ + +namespace Core { + +// preconfigure may be deleted if not used. +public void preconfigure() { +} + +public void init() throws Error { +} + +public void terminate() { +} + +} + 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(); + } +} + diff --git a/src/core/DataObject.vala b/src/core/DataObject.vala new file mode 100644 index 0000000..1fe133d --- /dev/null +++ b/src/core/DataObject.vala @@ -0,0 +1,137 @@ +/* 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. + */ + +// +// 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). +// + +// Have to inherit from Object due to ContainerSource and this bug: +// https://bugzilla.gnome.org/show_bug.cgi?id=615904 +public abstract class DataObject : Object { + public const int64 INVALID_OBJECT_ID = -1; + + private static int64 object_id_generator = 0; + +#if TRACE_DTORS + // because calling to_string() in a destructor is dangerous, stash to_string()'s result in + // this variable for reporting + protected string dbg_to_string = null; +#endif + + private int64 object_id = INVALID_OBJECT_ID; + private DataCollection member_of = null; + private int64 ordinal = DataCollection.INVALID_OBJECT_ORDINAL; + + // 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(Alteration alteration) { + if (member_of != null) + member_of.internal_notify_altered(this, alteration); + } + + // There is no membership_changed signal as it's expensive (esp. at startup) and not needed + // at this time. The notify_membership_changed mechanism is still in place for subclasses. + // + // This is called after the change has occurred (i.e., after the DataObject has been added + // to the DataCollection, or after it has been remove from the same). It is also called after + // the DataCollection has reported the change on its own signals, so it and its children can + // properly integrate the DataObject into its pools. + // + // This is only called by DataCollection. + public virtual void notify_membership_changed(DataCollection? collection) { + } + + // Generally, this is only called by DataCollection. No signal is bound to this because + // it's not needed currently and affects performance. + public virtual void notify_collection_property_set(string name, Value? old, Value val) { + } + + // Generally, this is only called by DataCollection. No signal is bound to this because + // it's not needed currently and affects performance. + public virtual void notify_collection_property_cleared(string name) { + } + + public abstract string get_name(); + + public abstract string to_string(); + + public DataCollection? get_membership() { + return member_of; + } + + public bool has_membership() { + return member_of != null; + } + + // This method is only called by DataCollection. It's called after the DataObject has been + // assigned to a DataCollection. + public void internal_set_membership(DataCollection collection, int64 ordinal) { + assert(member_of == null); + + member_of = collection; + this.ordinal = ordinal; + +#if TRACE_DTORS + dbg_to_string = to_string(); +#endif + } + + // This method is only called by SourceHoldingTank (to give ordinality to its unassociated + // members). DataCollections should call internal_set_membership. + public void internal_set_ordinal(int64 ordinal) { + assert(member_of == null); + + this.ordinal = ordinal; + } + + // This method is only called by DataCollection. It's called after the DataObject has been + // assigned to a DataCollection. + public void internal_clear_membership() { + member_of = null; + ordinal = DataCollection.INVALID_OBJECT_ORDINAL; + } + + // This method is only called by DataCollection, DataSet, and SourceHoldingTank. + public inline int64 internal_get_ordinal() { + return ordinal; + } + + public inline int64 get_object_id() { + return object_id; + } + + public Value? get_collection_property(string name, Value? def = null) { + if (member_of == null) + return def; + + Value? result = member_of.get_property(name); + + return (result != null) ? result : def; + } + + public void set_collection_property(string name, Value val, ValueEqualFunc? value_equals = null) { + if (member_of != null) + member_of.set_property(name, val, value_equals); + } + + public void clear_collection_property(string name) { + if (member_of != null) + member_of.clear_property(name); + } +} + diff --git a/src/core/DataSet.vala b/src/core/DataSet.vala new file mode 100644 index 0000000..ebb5500 --- /dev/null +++ b/src/core/DataSet.vala @@ -0,0 +1,183 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// +// DataSet +// +// A DataSet is a collection class used for internal implementations of DataCollection +// and its children. It may be of use to other classes, however. +// +// The general purpose of DataSet is to provide low-cost implementations of various collection +// operations at a cost of internally maintaining its objects in more than one simple collection. +// contains(), for example, can return a result with hash-table performance while notions of +// ordering are maintained by a SortedList. The cost is in adding and removing objects (in general, +// there are others). +// +// Because this class has no signalling mechanisms and does not manipulate DataObjects in ways +// they expect to be manipulated (these features are performed by DataCollection), it's probably +// best not to use this class. Even in cases of building a list of DataObjects for some quick +// operation is probably best done by a Gee.ArrayList. +// + +// ComparatorPredicate is used to determine if a re-sort operation is necessary; it has no +// effect on adding a DataObject to a DataSet in sorted order. +public delegate bool ComparatorPredicate(DataObject object, Alteration alteration); + +public class DataSet { + private SortedList<DataObject> list = new SortedList<DataObject>(); + private Gee.HashSet<DataObject> hash_set = new Gee.HashSet<DataObject>(); + private unowned Comparator user_comparator = null; + private unowned ComparatorPredicate? comparator_predicate = null; + + public DataSet() { + reset_comparator(); + } + + private int64 order_added_comparator(void *a, void *b) { + return ((DataObject *) a)->internal_get_ordinal() - ((DataObject *) b)->internal_get_ordinal(); + } + + private bool order_added_predicate(DataObject object, Alteration alteration) { + // ordinals don't change (shouldn't change!) while a part of the DataSet + return false; + } + + private int64 comparator_wrapper(void *a, void *b) { + if (a == b) + return 0; + + // use the order-added comparator if the user's compare returns equal, to stabilize the + // sort + int64 result = 0; + + if (user_comparator != null) + result = user_comparator(a, b); + + if (result == 0) + result = order_added_comparator(a, b); + + assert(result != 0); + + return result; + } + + public bool contains(DataObject object) { + return hash_set.contains(object); + } + + public inline int get_count() { + return list.get_count(); + } + + public void reset_comparator() { + user_comparator = null; + comparator_predicate = order_added_predicate; + list.resort(order_added_comparator); + } + + public unowned Comparator get_comparator() { + return user_comparator; + } + + public unowned ComparatorPredicate get_comparator_predicate() { + return comparator_predicate; + } + + public void set_comparator(Comparator user_comparator, ComparatorPredicate? comparator_predicate) { + this.user_comparator = user_comparator; + this.comparator_predicate = comparator_predicate; + list.resort(comparator_wrapper); + } + + public Gee.List<DataObject> get_all() { + return list.read_only_view_as_list; + } + + public DataSet copy() { + DataSet clone = new DataSet(); + clone.list = list.copy(); + clone.hash_set.add_all(hash_set); + + return clone; + } + + public DataObject? get_at(int index) { + return list.get_at(index); + } + + public int index_of(DataObject object) { + return list.locate(object, false); + } + + // DataObject's ordinal should be set before adding. + public bool add(DataObject object) { + if (!list.add(object)) + return false; + + if (!hash_set.add(object)) { + // attempt to back out of previous operation + list.remove(object); + + return false; + } + + return true; + } + + // DataObjects' ordinals should be set before adding. + public bool add_many(Gee.Collection<DataObject> objects) { + int count = objects.size; + if (count == 0) + return true; + + if (!list.add_all(objects)) + return false; + + if (!hash_set.add_all(objects)) { + // back out previous operation + list.remove_all(objects); + + return false; + } + + return true; + } + + public bool remove(DataObject object) { + bool success = true; + + if (!list.remove(object)) + success = false; + + if (!hash_set.remove(object)) + success = false; + + return success; + } + + public bool remove_many(Gee.Collection<DataObject> objects) { + bool success = true; + + if (!list.remove_all(objects)) + success = false; + + if (!hash_set.remove_all(objects)) + success = false; + + return success; + } + + // Returns true if the item has moved. + public bool resort_object(DataObject object, Alteration? alteration) { + if (comparator_predicate != null && alteration != null + && !comparator_predicate(object, alteration)) { + return false; + } + + return list.resort_item(object); + } +} + diff --git a/src/core/DataSource.vala b/src/core/DataSource.vala new file mode 100644 index 0000000..e4d2d34 --- /dev/null +++ b/src/core/DataSource.vala @@ -0,0 +1,679 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// +// DataSource +// +// A DataSource is an object that is unique throughout the system. DataSources +// commonly have external and/or persistent representations, hence they have a notion of being +// destroyed (versus removed or freed). Several DataViews may exist that reference a single +// DataSource. Note that DataSources MUST be destroyed (rather than simply removed) from their +// SourceCollection, and that they MUST be destroyed via their SourceCollection (rather than +// calling DataSource.destroy() directly.) +// +// Destroying a DataSource indicates it should remove all secondary and tertiary structures (such +// as thumbnails) and any records pointing to its backing store. SourceCollection.destroy_marked() +// has a parameter indicating if the backing should be destroyed as well; that is when +// internal_delete_backing() is called. +// +// There are no provisions (currently) for a DataSource to be removed from its SourceCollection +// without destroying its backing and/or secondary and tertiary structures. DataSources are intended +// to go to the grave with their SourceCollection otherwise. If a need arises for a DataSource to +// be peaceably removed from its SourceCollection, code will need to be written. SourceSnapshots +// may be one solution to this problem. +// +// 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 DataSource : DataObject { + protected delegate void ContactSubscriber(DataView view); + protected delegate void ContactSubscriberAlteration(DataView view, Alteration alteration); + + private DataView[] subscribers = new DataView[4]; + private SourceHoldingTank holding_tank = null; + private weak SourceCollection unlinked_from_collection = null; + private Gee.HashMap<string, Gee.List<string>> backlinks = null; + private bool in_contact = false; + private bool marked_for_destroy = false; + private bool is_destroyed = false; + + // This signal is fired after the DataSource has been unlinked from its SourceCollection. + public virtual signal void unlinked(SourceCollection sources) { + } + + // This signal is fired after the DataSource has been relinked to a SourceCollection. + public virtual signal void relinked(SourceCollection sources) { + } + + // 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); + } + + ~DataSource() { +#if TRACE_DTORS + debug("DTOR: DataSource %s", dbg_to_string); +#endif + } + + public override void notify_membership_changed(DataCollection? collection) { + // DataSources can only be removed once they've been destroyed or unlinked. + if (collection == null) { + assert(is_destroyed || backlinks != null); + } else { + assert(!is_destroyed); + } + + // If removed from a collection but have backlinks, then that's an unlink. + if (collection == null && backlinks != null) + notify_unlinked(); + + base.notify_membership_changed(collection); + } + + public virtual void notify_held_in_tank(SourceHoldingTank? holding_tank) { + // this should never be called if part of a collection + assert(get_membership() == null); + + // DataSources can only be held in a tank if not already in one, and must be removed from + // one before being put in another + if (holding_tank != null) { + assert(this.holding_tank == null); + } else { + assert(this.holding_tank != null); + } + + this.holding_tank = holding_tank; + } + + public override void notify_altered(Alteration alteration) { + // re-route this to the SourceHoldingTank if held in one + if (holding_tank != null) { + holding_tank.internal_notify_altered(this, alteration); + } else { + contact_subscribers_alteration(alteration); + + base.notify_altered(alteration); + } + } + + // This method is called by SourceCollection. It should not be called otherwise. + public virtual void notify_unlinking(SourceCollection collection) { + assert(backlinks == null && unlinked_from_collection == null); + + unlinked_from_collection = collection; + backlinks = new Gee.HashMap<string, Gee.List<string>>(); + } + + // This method is called by DataSource. It should not be called otherwise. + protected virtual void notify_unlinked() { + assert(unlinked_from_collection != null && backlinks != null); + + unlinked(unlinked_from_collection); + + // give the DataSource a chance to persist the link state, if any + if (backlinks.size > 0) + commit_backlinks(unlinked_from_collection, dehydrate_backlinks()); + } + + // This method is called by SourceCollection. It should not be called otherwise. + public virtual void notify_relinking(SourceCollection collection) { + assert((backlinks != null) && (unlinked_from_collection == collection)); + } + + // This method is called by SourceCollection. It should not be called otherwise. + public virtual void notify_relinked() { + assert(backlinks != null && unlinked_from_collection != null); + + SourceCollection relinked_to = unlinked_from_collection; + backlinks = null; + unlinked_from_collection = null; + relinked(relinked_to); + + // have the DataSource delete any persisted link state + commit_backlinks(null, null); + } + + // Each DataSource has a unique typename. All DataSources of the same type should have the + // same typename. This method should be thread-safe. + // + // NOTE: Because this value may be persisted in various ways, it should not be changed once + // defined. + public abstract string get_typename(); + + // Each DataSource of a particular typename has an instance ID. Many DataSources can have a + // typename of "tag" and many DataSources can have an ID of 42, but only one DataSource may + // have a typename of "tag" AND an ID of 42. If the DataSource is persisted, this number should + // be persisted as well. This method should be thread-safe. + public abstract int64 get_instance_id(); + + // This returns a string that can be used to uniquely identify the DataSource throughout the + // system. This method should be thread-safe. + public virtual string get_source_id() { + return ("%s-%016" + int64.FORMAT_MODIFIER + "x").printf(get_typename(), get_instance_id()); + } + + public bool has_backlink(SourceBacklink backlink) { + if (backlinks == null) + return false; + + Gee.List<string>? values = backlinks.get(backlink.name); + + return values != null ? values.contains(backlink.value) : false; + } + + public Gee.List<SourceBacklink>? get_backlinks(string name) { + if (backlinks == null) + return null; + + Gee.List<string>? values = backlinks.get(name); + if (values == null || values.size == 0) + return null; + + Gee.List<SourceBacklink> backlinks = new Gee.ArrayList<SourceBacklink>(); + foreach (string value in values) + backlinks.add(new SourceBacklink(name, value)); + + return backlinks; + } + + public void set_backlink(SourceBacklink backlink) { + // can only be called during an unlink operation + assert(backlinks != null); + + Gee.List<string> values = backlinks.get(backlink.name); + if (values == null) { + values = new Gee.ArrayList<string>(); + backlinks.set(backlink.name, values); + } + + values.add(backlink.value); + + SourceCollection? sources = (SourceCollection?) get_membership(); + if (sources != null) + sources.internal_backlink_set(this, backlink); + } + + public bool remove_backlink(SourceBacklink backlink) { + if (backlinks == null) + return false; + + Gee.List<string> values = backlinks.get(backlink.name); + if (values == null) + return false; + + int original_size = values.size; + assert(original_size > 0); + + Gee.Iterator<string> iter = values.iterator(); + while (iter.next()) { + if (iter.get() == backlink.value) + iter.remove(); + } + + if (values.size == 0) + backlinks.unset(backlink.name); + + // Commit here because this can come at any time; setting the backlinks should only + // happen during an unlink, which commits at the end of the cycle. + commit_backlinks(unlinked_from_collection, dehydrate_backlinks()); + + SourceCollection? sources = (SourceCollection?) get_membership(); + if (sources != null) + sources.internal_backlink_removed(this, backlink); + + return values.size != original_size; + } + + // Base implementation is to do nothing; if DataSource wishes to persist link state across + // application sessions, it should do so when this is called. Do not call this base method + // when overriding; it will only issue a warning. + // + // If dehydrated is null, the persisted link state should be deleted. sources will be null + // as well. + protected virtual void commit_backlinks(SourceCollection? sources, string? dehydrated) { + if (sources != null || dehydrated != null) + warning("No implementation to commit link state for %s", to_string()); + } + + private string? dehydrate_backlinks() { + if (backlinks == null || backlinks.size == 0) + return null; + + StringBuilder builder = new StringBuilder(); + foreach (string name in backlinks.keys) { + Gee.List<string> values = backlinks.get(name); + if (values == null || values.size == 0) + continue; + + string value_field = ""; + foreach (string value in values) { + if (!is_string_empty(value)) + value_field += value + "|"; + } + + if (value_field.length > 0) + builder.append("%s=%s\n".printf(name, value_field)); + } + + return builder.str.length > 0 ? builder.str : null; + } + + // If dehydrated is null, this method will still put the DataSource into an unlinked state, + // simply without any backlinks to reestablish. + public void rehydrate_backlinks(SourceCollection unlinked_from, string? dehydrated) { + unlinked_from_collection = unlinked_from; + backlinks = new Gee.HashMap<string, Gee.List<string>>(); + + if (dehydrated == null) + return; + + string[] lines = dehydrated.split("\n"); + foreach (string line in lines) { + if (line.length == 0) + continue; + + string[] tokens = line.split("=", 2); + if (tokens.length < 2) { + warning("Unable to rehydrate \"%s\" for %s: name and value not present", line, + to_string()); + + continue; + } + + string[] decoded_values = tokens[1].split("|"); + Gee.List<string> values = new Gee.ArrayList<string>(); + foreach (string value in decoded_values) { + if (value != null && value.length > 0) + values.add(value); + } + + if (values.size > 0) + backlinks.set(tokens[0], values); + } + } + + // 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. + public void internal_mark_for_destroy() { + marked_for_destroy = true; + } + + // This method is called by SourceCollection. It should not be called otherwise. + // + // This method deletes whatever backing this DataSource represents. It should either return + // false or throw an error if the delete fails. + public virtual bool internal_delete_backing() throws Error { + return true; + } + + // Because of the rules of DataSources, a DataSource is only equal to itself; subclasses + // may override this to perform validations and/or assertions + public virtual bool equals(DataSource? source) { + return (this == source); + } + + // This method is called by SourceCollection. It should not be called otherwise. To destroy + // a DataSource, destroy it from its SourceCollection. + // + // Child classes should call this base class to ensure that the collection this object is + // a member of is notified and the signal is properly called. The collection will remove this + // object automatically. + public virtual void destroy() { + assert(marked_for_destroy); + + // mark as destroyed + is_destroyed = true; + + // unsubscribe all subscribers + for (int ctr = 0; ctr < subscribers.length; ctr++) { + if (subscribers[ctr] != null) { + DataView view = subscribers[ctr]; + subscribers[ctr] = null; + + view.notify_unsubscribed(this); + } + } + + // propagate the signal + destroyed(); + } + + // This method can be used to destroy a DataSource before it's added to a SourceCollection + // or has been unlinked from one. It should not be used otherwise. (In particular, don't + // automate destroys by removing and then calling this method -- that will happen automatically.) + // To destroy a DataSource already integrated into a SourceCollection, call + // SourceCollection.destroy_marked(). Returns true if the operation completed successfully, + // otherwise it will return false. + public bool destroy_orphan(bool delete_backing) { + bool ret = true; + if (delete_backing) { + try { + ret = internal_delete_backing(); + if (!ret) + warning("Unable to delete backing for %s", to_string()); + + } catch (Error err) { + warning("Unable to delete backing for %s: %s", to_string(), err.message); + ret = false; + } + } + + internal_mark_for_destroy(); + destroy(); + + if (unlinked_from_collection != null) + unlinked_from_collection.notify_unlinked_destroyed(this); + + return ret; + } + + // DataViews subscribe to the DataSource to inform it of their existence. Not only does this + // allow for signal reflection (i.e. DataSource.altered -> DataView.altered) it also makes + // them first-in-line for notification of destruction, so they can remove themselves from + // their ViewCollections automatically. + // + // This method is only called by DataView. + public void internal_subscribe(DataView view) { + assert(!in_contact); + + for (int ctr = 0; ctr < subscribers.length; ctr++) { + if (subscribers[ctr] == null) { + subscribers[ctr] = view; + + return; + } + } + + subscribers += view; + } + + // This method is only called by DataView. NOTE: This method does NOT call + // DataView.notify_unsubscribed(), as it's assumed the DataView itself will do so if appropriate. + public void internal_unsubscribe(DataView view) { + assert(!in_contact); + + for (int ctr = 0; ctr < subscribers.length; ctr++) { + if (subscribers[ctr] == view) { + subscribers[ctr] = null; + + return; + } + } + } + + protected void contact_subscribers(ContactSubscriber contact_subscriber) { + assert(!in_contact); + + in_contact = true; + for (int ctr = 0; ctr < subscribers.length; ctr++) { + if (subscribers[ctr] != null) + contact_subscriber(subscribers[ctr]); + } + in_contact = false; + } + + protected void contact_subscribers_alteration(Alteration alteration) { + assert(!in_contact); + + in_contact = true; + for (int ctr = 0; ctr < subscribers.length; ctr++) { + if (subscribers[ctr] != null) + subscribers[ctr].notify_altered(alteration); + } + in_contact = false; + } +} + +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; + } +} + +// Link state name may not contain the equal sign ("="). Link names and values may not contain the +// pipe-character ("|"). Both will be stripped of leading and trailing whitespace. This may +// affect retrieval. +public class SourceBacklink { + private string _name; + private string _value; + + public string name { + get { + return _name; + } + } + + public string value { + get { + return _value; + } + } + + // This only applies if the SourceBacklink comes from a DataSource. + public string typename { + get { + return _name; + } + } + + // This only applies if the SourceBacklink comes from a DataSource. + public int64 instance_id { + get { + return int64.parse(_value); + } + } + + public SourceBacklink(string name, string value) { + assert(validate_name_value(name, value)); + + _name = name.strip(); + _value = value.strip(); + } + + public SourceBacklink.from_source(DataSource source) { + _name = source.get_typename().strip(); + _value = source.get_instance_id().to_string().strip(); + + assert(validate_name_value(_name, _value)); + } + + private static bool validate_name_value(string name, string value) { + return !name.contains("=") && !name.contains("|") && !value.contains("|"); + } + + public string to_string() { + return "Backlink %s=%s".printf(name, value); + } + + public static uint hash_func(SourceBacklink? backlink) { + return str_hash(backlink._name) ^ str_hash(backlink._value); + } + + public static bool equal_func(SourceBacklink? alink, SourceBacklink? blink) { + return str_equal(alink._name, blink._name) && str_equal(alink._value, blink._value); + } +} + +// +// 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.connect(on_snapshot_broken); + + set_source(source); + + membership = (SourceCollection) source.get_membership(); + assert(membership != null); + membership.items_added.connect(on_source_added); + } + + ~SourceProxy() { + drop_source(); + membership.items_added.disconnect(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.connect(on_destroyed); + } + + private void drop_source() { + if (source == null) + return; + + source.destroyed.disconnect(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); + if (source != new_source) + source = new_source; + if (object_id != source.get_object_id()) + object_id = new_source.get_object_id(); + 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 : Object { + public abstract SourceProxy get_proxy(); +} + diff --git a/src/core/DataSourceTypes.vala b/src/core/DataSourceTypes.vala new file mode 100644 index 0000000..9f23ac6 --- /dev/null +++ b/src/core/DataSourceTypes.vala @@ -0,0 +1,108 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// +// Media sources +// + +public abstract class ThumbnailSource : DataSource { + public virtual signal void thumbnail_altered() { + } + + public ThumbnailSource(int64 object_id = INVALID_OBJECT_ID) { + base (object_id); + } + + public virtual void notify_thumbnail_altered() { + // fire signal on self + thumbnail_altered(); + + // signal reflection to DataViews + contact_subscribers(subscriber_thumbnail_altered); + } + + private void subscriber_thumbnail_altered(DataView view) { + ((ThumbnailView) view).notify_thumbnail_altered(); + } + + public abstract Gdk.Pixbuf? get_thumbnail(int scale) throws Error; + + // get_thumbnail( ) may return a cached pixbuf; create_thumbnail( ) is guaranteed to create + // a new pixbuf (e.g., by the source loading, decoding, and scaling image data) + public abstract Gdk.Pixbuf? create_thumbnail(int scale) throws Error; + + // A ThumbnailSource may use another ThumbnailSource as its representative. It's up to the + // subclass to forward on the appropriate methods to this ThumbnailSource. But, since multiple + // ThumbnailSources may be referring to a single ThumbnailSource, this allows for that to be + // detected and optimized (in caching). + // + // Note that it's the responsibility of this ThumbnailSource to fire "thumbnail-altered" if its + // representative does the same. + // + // Default behavior is to return the ID of this. + public virtual string get_representative_id() { + return get_source_id(); + } + + public abstract PhotoFileFormat get_preferred_thumbnail_format(); +} + +public abstract class PhotoSource : MediaSource { + public PhotoSource(int64 object_id = INVALID_OBJECT_ID) { + base (object_id); + } + + public abstract PhotoMetadata? get_metadata(); + + public abstract Gdk.Pixbuf get_pixbuf(Scaling scaling) throws Error; +} + +public abstract class VideoSource : MediaSource { +} + +// +// EventSource +// + +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(); + + public abstract uint64 get_total_filesize(); + + public abstract int get_media_count(); + + public abstract Gee.Collection<MediaSource> get_media(); + + public abstract string? get_comment(); + + public abstract bool set_comment(string? comment); +} + +// +// ContainerSource +// + +public interface ContainerSource : DataSource { + public abstract bool has_links(); + + public abstract SourceBacklink get_backlink(); + + public abstract void break_link(DataSource source); + + public abstract void break_link_many(Gee.Collection<DataSource> sources); + + public abstract void establish_link(DataSource source); + + public abstract void establish_link_many(Gee.Collection<DataSource> sources); +} + + diff --git a/src/core/DataView.vala b/src/core/DataView.vala new file mode 100644 index 0000000..07cd4fc --- /dev/null +++ b/src/core/DataView.vala @@ -0,0 +1,132 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class DataView : DataObject { + private DataSource source; + private bool selected = false; + private bool visible = true; + + // Indicates that the selection state has changed. + public virtual signal void state_changed(bool selected) { + } + + // Indicates the visible state has changed. + public virtual signal void visibility_changed(bool visible) { + } + + // Indicates that the display (what is seen by the user) of the DataView has changed. + public virtual signal void view_altered() { + } + + // Indicates that the geometry of the DataView has changed (which implies the view has altered, + // but only in that the same elements have changed size). + public virtual signal void geometry_altered() { + } + + public virtual signal void unsubscribed(DataSource source) { + } + + public DataView(DataSource source) { + this.source = source; + + // subscribe to the DataSource, which sets up signal reflection and gives the DataView + // first notification of destruction. + source.internal_subscribe(this); + } + + ~DataView() { +#if TRACE_DTORS + debug("DTOR: DataView %s", dbg_to_string); +#endif + source.internal_unsubscribe(this); + } + + public override string get_name() { + return "View of %s".printf(source.get_name()); + } + + public override string to_string() { + return "DataView %s [DataSource %s]".printf(get_name(), source.to_string()); + } + + public DataSource get_source() { + return source; + } + + public bool is_selected() { + return selected; + } + + // This method is only called by ViewCollection. + public void internal_set_selected(bool selected) { + if (this.selected == selected) + return; + + this.selected = selected; + state_changed(selected); + } + + // This method is only called by ViewCollection. Returns the toggled state. + public bool internal_toggle() { + selected = !selected; + state_changed(selected); + + return selected; + } + + public bool is_visible() { + return visible; + } + + // This method is only called by ViewCollection. + public void internal_set_visible(bool visible) { + if (this.visible == visible) + return; + + this.visible = visible; + visibility_changed(visible); + } + + protected virtual void notify_view_altered() { + // impossible when not visible + if (!visible) + return; + + ViewCollection vc = get_membership() as ViewCollection; + if (vc != null) { + if (!vc.are_notifications_frozen()) + view_altered(); + + // notify ViewCollection in any event + vc.internal_notify_view_altered(this); + } else { + view_altered(); + } + } + + protected virtual void notify_geometry_altered() { + // impossible when not visible + if (!visible) + return; + + ViewCollection vc = get_membership() as ViewCollection; + if (vc != null) { + if (!vc.are_notifications_frozen()) + geometry_altered(); + + // notify ViewCollection in any event + vc.internal_notify_geometry_altered(this); + } else { + geometry_altered(); + } + } + + // This is only called by DataSource + public virtual void notify_unsubscribed(DataSource source) { + unsubscribed(source); + } +} + diff --git a/src/core/DataViewTypes.vala b/src/core/DataViewTypes.vala new file mode 100644 index 0000000..fac7602 --- /dev/null +++ b/src/core/DataViewTypes.vala @@ -0,0 +1,50 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class ThumbnailView : DataView { + public virtual signal void thumbnail_altered() { + } + + public ThumbnailView(ThumbnailSource source) { + base(source); + } + + public virtual void notify_thumbnail_altered() { + // fire signal on self + thumbnail_altered(); + } +} + +public class PhotoView : ThumbnailView { + public PhotoView(PhotoSource source) { + base(source); + } + + public PhotoSource get_photo_source() { + return (PhotoSource) get_source(); + } +} + +public class VideoView : ThumbnailView { + public VideoView(VideoSource source) { + base(source); + } + + public VideoSource get_video_source() { + return (VideoSource) get_source(); + } +} + +public class EventView : ThumbnailView { + public EventView(EventSource source) { + base(source); + } + + public EventSource get_event_source() { + return (EventSource) get_source(); + } +} + diff --git a/src/core/DatabaseSourceCollection.vala b/src/core/DatabaseSourceCollection.vala new file mode 100644 index 0000000..0c704bb --- /dev/null +++ b/src/core/DatabaseSourceCollection.vala @@ -0,0 +1,86 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public delegate int64 GetSourceDatabaseKey(DataSource source); + +// A DatabaseSourceCollection is a SourceCollection that understands database keys (IDs) and the +// nature that a row in a database can only be instantiated once in the system, and so it tracks +// their existence in a map so they can be fetched by their key. +// +// TODO: This would be better implemented as an observer class, possibly with an interface to +// force subclasses to provide a fetch_by_key() method. +public abstract class DatabaseSourceCollection : SourceCollection { + private unowned GetSourceDatabaseKey source_key_func; + private Gee.HashMap<int64?, DataSource> map = new Gee.HashMap<int64?, DataSource>(int64_hash, + int64_equal); + + public DatabaseSourceCollection(string name, GetSourceDatabaseKey source_key_func) { + base (name); + + this.source_key_func = source_key_func; + } + + public override void notify_items_added(Gee.Iterable<DataObject> added) { + foreach (DataObject object in added) { + DataSource source = (DataSource) object; + int64 key = source_key_func(source); + + assert(!map.has_key(key)); + + map.set(key, source); + } + + base.notify_items_added(added); + } + + public override void notify_items_removed(Gee.Iterable<DataObject> removed) { + foreach (DataObject object in removed) { + int64 key = source_key_func((DataSource) object); + + bool is_removed = map.unset(key); + assert(is_removed); + } + + base.notify_items_removed(removed); + } + + protected DataSource fetch_by_key(int64 key) { + return map.get(key); + } +} + +public class DatabaseSourceHoldingTank : SourceHoldingTank { + private unowned GetSourceDatabaseKey get_key; + private Gee.HashMap<int64?, DataSource> map = new Gee.HashMap<int64?, DataSource>(int64_hash, + int64_equal); + + public DatabaseSourceHoldingTank(SourceCollection sources, + SourceHoldingTank.CheckToKeep check_to_keep, GetSourceDatabaseKey get_key) { + base (sources, check_to_keep); + + this.get_key = get_key; + } + + public DataSource? get_by_id(int64 id) { + return map.get(id); + } + + protected override void notify_contents_altered(Gee.Collection<DataSource>? added, + Gee.Collection<DataSource>? removed) { + if (added != null) { + foreach (DataSource source in added) + map.set(get_key(source), source); + } + + if (removed != null) { + foreach (DataSource source in removed) + map.unset(get_key(source)); + } + + base.notify_contents_altered(added, removed); + } +} + diff --git a/src/core/SourceCollection.vala b/src/core/SourceCollection.vala new file mode 100644 index 0000000..020df0e --- /dev/null +++ b/src/core/SourceCollection.vala @@ -0,0 +1,221 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public abstract class SourceCollection : DataCollection { + private class DestroyCounter : Object { + public Marker remove_marker; + public Gee.ArrayList<DataSource> notify_list = new Gee.ArrayList<DataSource>(); + public Gee.ArrayList<MediaSource> not_removed = new Gee.ArrayList<MediaSource>(); + + public DestroyCounter(Marker remove_marker) { + this.remove_marker = remove_marker; + } + } + + // When this signal is fired, the items are about to be unlinked from the collection. The + // appropriate remove signals will follow. + public virtual signal void items_unlinking(Gee.Collection<DataSource> unlinking) { + } + + // When this signal is fired, the items are being relinked to the collection. The appropriate + // add signals have already been fired. + public virtual signal void items_relinked(Gee.Collection<DataSource> relinked) { + } + + // 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) { + } + + // 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 items_destroyed(Gee.Collection<DataSource> destroyed) { + } + + // When this signal is fired, the unlinked item has been unlinked from the collection previously + // and its destroy() has been called. + public virtual signal void unlinked_destroyed(DataSource source) { + } + + // When this signal is fired, the backlink to the ContainerSource has already been removed. + public virtual signal void backlink_removed(SourceBacklink backlink, + Gee.Collection<DataSource> sources) { + } + + private Gee.MultiMap<SourceBacklink, DataSource>? backlinks = null; + + public SourceCollection(string name) { + base (name); + } + + public abstract bool holds_type_of_source(DataSource source); + + protected virtual void notify_items_unlinking(Gee.Collection<DataSource> unlinking) { + items_unlinking(unlinking); + } + + protected virtual void notify_items_relinked(Gee.Collection<DataSource> relinked) { + items_relinked(relinked); + } + + protected virtual void notify_item_destroyed(DataSource source) { + item_destroyed(source); + } + + protected virtual void notify_items_destroyed(Gee.Collection<DataSource> destroyed) { + items_destroyed(destroyed); + } + + // This is only called by DataSource. + public virtual void notify_unlinked_destroyed(DataSource unlinked) { + unlinked_destroyed(unlinked); + } + + protected virtual void notify_backlink_removed(SourceBacklink backlink, + Gee.Collection<DataSource> sources) { + backlink_removed(backlink, sources); + } + + protected override bool valid_type(DataObject object) { + return object is DataSource; + } + + // Destroy all marked items and optionally have them delete their backing. Returns the + // number of items which failed to delete their backing (if delete_backing is true) or zero. + public int destroy_marked(Marker marker, bool delete_backing, ProgressMonitor? monitor = null, + Gee.List<MediaSource>? not_removed = null) { + DestroyCounter counter = new DestroyCounter(start_marking()); + + if (delete_backing) + act_on_marked(marker, destroy_and_delete_source, monitor, counter); + else + act_on_marked(marker, destroy_source, monitor, counter); + + // notify of destruction + foreach (DataSource source in counter.notify_list) + notify_item_destroyed(source); + notify_items_destroyed(counter.notify_list); + + // remove once all destroyed + remove_marked(counter.remove_marker); + + if (null != not_removed) { + not_removed.add_all(counter.not_removed); + } + + return counter.not_removed.size; + } + + private bool destroy_and_delete_source(DataObject object, Object? user) { + bool success = false; + try { + success = ((DataSource) object).internal_delete_backing(); + } catch (Error err) { + success = false; + } + + if (!success && object is MediaSource) { + ((DestroyCounter) user).not_removed.add((MediaSource) object); + } + + return destroy_source(object, user) && success; + } + + private bool destroy_source(DataObject object, Object? user) { + DataSource source = (DataSource) object; + + source.internal_mark_for_destroy(); + source.destroy(); + + ((DestroyCounter) user).remove_marker.mark(source); + ((DestroyCounter) user).notify_list.add(source); + + return true; + } + + // This is only called by DataSource. + public void internal_backlink_set(DataSource source, SourceBacklink backlink) { + if (backlinks == null) { + backlinks = new Gee.HashMultiMap<SourceBacklink, DataSource>(SourceBacklink.hash_func, + SourceBacklink.equal_func); + } + + backlinks.set(backlink, source); + } + + // This is only called by DataSource. + public void internal_backlink_removed(DataSource source, SourceBacklink backlink) { + assert(backlinks != null); + + bool removed = backlinks.remove(backlink, source); + assert(removed); + } + + public virtual bool has_backlink(SourceBacklink backlink) { + return backlinks != null ? backlinks.contains(backlink) : false; + } + + public Gee.Collection<DataSource>? unlink_marked(Marker marker, ProgressMonitor? monitor = null) { + Gee.ArrayList<DataSource> list = new Gee.ArrayList<DataSource>(); + act_on_marked(marker, prepare_for_unlink, monitor, list); + + if (list.size == 0) + return null; + + notify_items_unlinking(list); + + remove_marked(mark_many(list)); + + return list; + } + + private bool prepare_for_unlink(DataObject object, Object? user) { + DataSource source = (DataSource) object; + + source.notify_unlinking(this); + ((Gee.List<DataSource>) user).add(source); + + return true; + } + + public void relink(DataSource source) { + source.notify_relinking(this); + + add(source); + notify_items_relinked((Gee.Collection<DataSource>) get_singleton(source)); + + source.notify_relinked(); + } + + public void relink_many(Gee.Collection<DataSource> relink) { + if (relink.size == 0) + return; + + foreach (DataSource source in relink) + source.notify_relinking(this); + + add_many(relink); + notify_items_relinked(relink); + + foreach (DataSource source in relink) + source.notify_relinked(); + } + + public virtual void remove_backlink(SourceBacklink backlink) { + if (backlinks == null) + return; + + // create copy because the DataSources will be removing the backlinks + Gee.ArrayList<DataSource> sources = new Gee.ArrayList<DataSource>(); + sources.add_all(backlinks.get(backlink)); + + foreach (DataSource source in sources) + source.remove_backlink(backlink); + + notify_backlink_removed(backlink, sources); + } +} + diff --git a/src/core/SourceHoldingTank.vala b/src/core/SourceHoldingTank.vala new file mode 100644 index 0000000..adfec8b --- /dev/null +++ b/src/core/SourceHoldingTank.vala @@ -0,0 +1,209 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// A SourceHoldingTank is similar to the holding tank used by ContainerSourceCollection, but for +// non-ContainerSources to be held offline from their natural SourceCollection (i.e. PhotoSources +// being held in a trashcan, for example). It is *not* a DataCollection (important!), but rather +// a signalled collection that moves DataSources to and from their SourceCollection. +// +// DataSources can be shuttled from their SourceCollection to the SourceHoldingTank manually +// (via unlink_and_hold) or can be automatically moved by installing a HoldingPredicate. +// Only one HoldingConditional may be installed. Because of assertions in the methods, it's unwise +// to use more than one method. add() and add_many() should ONLY be used for DataSources not +// first installed in their SourceCollection (i.e. they're born in the SourceHoldingTank). +// +// NOTE: DataSources should never be in more than one SourceHoldingTank. No tests are performed +// here to verify this. This is why a filter/predicate method (which could automatically move +// them in as they're altered) is not offered; there's no easy way to keep DataSources from being +// moved into more than one holding tank, or which should have preference. The CheckToRemove +// predicate is offered only to know when to release them. + +public class SourceHoldingTank { + // Return true if the DataSource should remain in the SourceHoldingTank, false otherwise. + public delegate bool CheckToKeep(DataSource source, Alteration alteration); + + private SourceCollection sources; + private unowned CheckToKeep check_to_keep; + private DataSet tank = new DataSet(); + private Gee.HashSet<DataSource> relinks = new Gee.HashSet<DataSource>(); + private Gee.HashSet<DataSource> unlinking = new Gee.HashSet<DataSource>(); + private int64 ordinal = 0; + + public virtual signal void contents_altered(Gee.Collection<DataSource>? added, + Gee.Collection<DataSource>? removed) { + } + + public SourceHoldingTank(SourceCollection sources, CheckToKeep check_to_keep) { + this.sources = sources; + this.check_to_keep = check_to_keep; + + this.sources.item_destroyed.connect(on_source_destroyed); + this.sources.thawed.connect(on_source_collection_thawed); + } + + ~SourceHoldingTank() { + sources.item_destroyed.disconnect(on_source_destroyed); + sources.thawed.disconnect(on_source_collection_thawed); + } + + protected virtual void notify_contents_altered(Gee.Collection<DataSource>? added, + Gee.Collection<DataSource>? removed) { + if (added != null) { + foreach (DataSource source in added) + source.notify_held_in_tank(this); + } + + if (removed != null) { + foreach (DataSource source in removed) + source.notify_held_in_tank(null); + } + + contents_altered(added, removed); + } + + public int get_count() { + return tank.get_count(); + } + + public Gee.Collection<DataSource> get_all() { + return (Gee.Collection<DataSource>) tank.get_all(); + } + + public bool contains(DataSource source) { + return tank.contains(source) || unlinking.contains(source); + } + + // Only use for DataSources that have not been installed in their SourceCollection. + public void add_many(Gee.Collection<DataSource> many) { + if (many.size == 0) + return; + + foreach (DataSource source in many) + source.internal_set_ordinal(ordinal++); + + bool added = tank.add_many(many); + assert(added); + + notify_contents_altered(many, null); + } + + // Do not pass in DataSources which have already been unlinked, including into this holding + // tank. + public void unlink_and_hold(Gee.Collection<DataSource> unlink) { + if (unlink.size == 0) + return; + + // store in the unlinking collection to guard against reentrancy + unlinking.add_all(unlink); + + sources.unlink_marked(sources.mark_many(unlink)); + + foreach (DataSource source in unlink) + source.internal_set_ordinal(ordinal++); + + bool added = tank.add_many(unlink); + assert(added); + + // remove from the unlinking pool, as they're now unlinked + unlinking.remove_all(unlink); + + notify_contents_altered(unlink, null); + } + + public bool has_backlink(SourceBacklink backlink) { + int count = tank.get_count(); + for (int ctr = 0; ctr < count; ctr++) { + if (((DataSource) tank.get_at(ctr)).has_backlink(backlink)) + return true; + } + + return false; + } + + public void remove_backlink(SourceBacklink backlink) { + int count = tank.get_count(); + for (int ctr = 0; ctr < count; ctr++) + ((DataSource) tank.get_at(ctr)).remove_backlink(backlink); + } + + public void destroy_orphans(Gee.List<DataSource> destroy, bool delete_backing, + ProgressMonitor? monitor = null, Gee.List<DataSource>? not_removed = null) { + if (destroy.size == 0) + return; + + bool removed = tank.remove_many(destroy); + assert(removed); + + notify_contents_altered(null, destroy); + + int count = destroy.size; + for (int ctr = 0; ctr < count; ctr++) { + DataSource source = destroy.get(ctr); + if (!source.destroy_orphan(delete_backing)) { + if (null != not_removed) { + not_removed.add(source); + } + } + if (monitor != null) + monitor(ctr + 1, count); + } + } + + private void on_source_destroyed(DataSource source) { + if (!tank.contains(source)) + return; + + bool removed = tank.remove(source); + assert(removed); + + notify_contents_altered(null, new SingletonCollection<DataSource>(source)); + } + + // This is only called by DataSource + public void internal_notify_altered(DataSource source, Alteration alteration) { + if (!tank.contains(source)) { + debug("SourceHoldingTank.internal_notify_altered called for %s not stored in %s", + source.to_string(), to_string()); + + return; + } + + // see if it should stay put + if (check_to_keep(source, alteration)) + return; + + bool removed = tank.remove(source); + assert(removed); + + if (sources.are_notifications_frozen()) { + relinks.add(source); + + return; + } + + notify_contents_altered(null, new SingletonCollection<DataSource>(source)); + + sources.relink(source); + } + + private void on_source_collection_thawed() { + if (relinks.size == 0) + return; + + // swap out to protect against reentrancy + Gee.HashSet<DataSource> copy = relinks; + relinks = new Gee.HashSet<DataSource>(); + + notify_contents_altered(null, copy); + + sources.relink_many(copy); + } + + public string to_string() { + return "SourceHoldingTank @ 0x%p".printf(this); + } +} + diff --git a/src/core/SourceInterfaces.vala b/src/core/SourceInterfaces.vala new file mode 100644 index 0000000..59956d3 --- /dev/null +++ b/src/core/SourceInterfaces.vala @@ -0,0 +1,44 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// See the note in MediaInterfaces.vala for some thoughts on the theory of expanding Shotwell's +// features via interfaces rather than class heirarchies. + +// Indexable DataSources provide raw strings that may be searched against (and, in the future, +// indexed) for free-text search queries. DataSources implementing Indexable must prepare and +// store (i.e. cache) these strings using prepare_indexable_string(s), as preparing the strings +// for each call is expensive. +// +// When the indexable string has changed, the object should fire an alteration of +// "indexable:keywords". The prepare methods will not do this. + +public interface Indexable : DataSource { + public abstract unowned string? get_indexable_keywords(); + + public static string? prepare_indexable_string(string? str) { + if(is_string_empty(str)) + return null; + return String.remove_diacritics(str.down()); + } + + public static string? prepare_indexable_strings(string[]? strs) { + if (strs == null || strs.length == 0) + return null; + + StringBuilder builder = new StringBuilder(); + int ctr = 0; + do { + if (!is_string_empty(strs[ctr])) { + builder.append(strs[ctr].down()); + if (ctr < strs.length - 1) + builder.append_c(' '); + } + } while (++ctr < strs.length); + + return !is_string_empty(builder.str) ? builder.str : null; + } +} + diff --git a/src/core/Tracker.vala b/src/core/Tracker.vala new file mode 100644 index 0000000..e72992b --- /dev/null +++ b/src/core/Tracker.vala @@ -0,0 +1,216 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace Core { + +// A TrackerAccumulator is called by Tracker indicating when a DataObject should be included or +// unincluded in its accumulated data. All methods return true if their data has changed, +// indicating that the Tracker's "updated" signal should be fired. +public interface TrackerAccumulator : Object { + public abstract bool include(DataObject object); + + public abstract bool uninclude(DataObject object); + + public abstract bool altered(DataObject object, Alteration alteration); +} + +// A Tracker monitors a DataCollection and reports to an installed TrackerAccumulator when objects +// are available and unavailable. This simplifies connecting to the DataCollection manually to +// monitoring availability (or subclassing for similar reasons, which may not always be available). +public class Tracker { + protected delegate bool IncludeUnincludeObject(DataObject object); + + private DataCollection collection; + private Gee.Collection<DataObject>? initial; + private TrackerAccumulator? acc = null; + + public virtual signal void updated() { + } + + public Tracker(DataCollection collection, Gee.Collection<DataObject>? initial = null) { + this.collection = collection; + this.initial = initial; + } + + ~Tracker() { + if (acc != null) { + collection.items_added.disconnect(on_items_added); + collection.items_removed.disconnect(on_items_removed); + collection.items_altered.disconnect(on_items_altered); + } + } + + public void start(TrackerAccumulator acc) { + // can only be started once + assert(this.acc == null); + + this.acc = acc; + + collection.items_added.connect(on_items_added); + collection.items_removed.connect(on_items_removed); + collection.items_altered.connect(on_items_altered); + + if (initial != null && initial.size > 0) + on_items_added(initial); + else if (initial == null) + on_items_added(collection.get_all()); + + initial = null; + } + + public DataCollection get_collection() { + return collection; + } + + private void on_items_added(Gee.Iterable<DataObject> added) { + include_uninclude(added, acc.include); + } + + private void on_items_removed(Gee.Iterable<DataObject> removed) { + include_uninclude(removed, acc.uninclude); + } + + // Subclasses can use this as a utility method. + protected void include_uninclude(Gee.Iterable<DataObject> objects, IncludeUnincludeObject cb) { + bool fire_updated = false; + foreach (DataObject object in objects) + fire_updated = cb(object) || fire_updated; + + if (fire_updated) + updated(); + } + + private void on_items_altered(Gee.Map<DataObject, Alteration> map) { + bool fire_updated = false; + foreach (DataObject object in map.keys) + fire_updated = acc.altered(object, map.get(object)) || fire_updated; + + if (fire_updated) + updated(); + } +} + +// A ViewTracker is Tracker designed for ViewCollections. It uses an internal mux to route +// Tracker's calls to three TrackerAccumulators: all (all objects in the ViewCollection), selected +// (only for selected objects) and visible (only for items not hidden or filtered out). +public class ViewTracker : Tracker { + private class Mux : Object, TrackerAccumulator { + public TrackerAccumulator? all; + public TrackerAccumulator? visible; + public TrackerAccumulator? selected; + + public Mux(TrackerAccumulator? all, TrackerAccumulator? visible, TrackerAccumulator? selected) { + this.all = all; + this.visible = visible; + this.selected = selected; + } + + public bool include(DataObject object) { + DataView view = (DataView) object; + + bool fire_updated = false; + + if (all != null) + fire_updated = all.include(view) || fire_updated; + + if (visible != null && view.is_visible()) + fire_updated = visible.include(view) || fire_updated; + + if (selected != null && view.is_selected()) + fire_updated = selected.include(view) || fire_updated; + + return fire_updated; + } + + public bool uninclude(DataObject object) { + DataView view = (DataView) object; + + bool fire_updated = false; + + if (all != null) + fire_updated = all.uninclude(view) || fire_updated; + + if (visible != null && view.is_visible()) + fire_updated = visible.uninclude(view) || fire_updated; + + if (selected != null && view.is_selected()) + fire_updated = selected.uninclude(view) || fire_updated; + + return fire_updated; + } + + public bool altered(DataObject object, Alteration alteration) { + DataView view = (DataView) object; + + bool fire_updated = false; + + if (all != null) + fire_updated = all.altered(view, alteration) || fire_updated; + + if (visible != null && view.is_visible()) + fire_updated = visible.altered(view, alteration) || fire_updated; + + if (selected != null && view.is_selected()) + fire_updated = selected.altered(view, alteration) || fire_updated; + + return fire_updated; + } + } + + private Mux? mux = null; + + public ViewTracker(ViewCollection collection) { + base (collection, collection.get_all_unfiltered()); + } + + ~ViewTracker() { + if (mux != null) { + ViewCollection? collection = get_collection() as ViewCollection; + assert(collection != null); + collection.items_shown.disconnect(on_items_shown); + collection.items_hidden.disconnect(on_items_hidden); + collection.items_selected.disconnect(on_items_selected); + collection.items_unselected.disconnect(on_items_unselected); + } + } + + public new void start(TrackerAccumulator? all, TrackerAccumulator? visible, TrackerAccumulator? selected) { + assert(mux == null); + + mux = new Mux(all, visible, selected); + + ViewCollection? collection = get_collection() as ViewCollection; + assert(collection != null); + collection.items_shown.connect(on_items_shown); + collection.items_hidden.connect(on_items_hidden); + collection.items_selected.connect(on_items_selected); + collection.items_unselected.connect(on_items_unselected); + + base.start(mux); + } + + private void on_items_shown(Gee.Collection<DataView> shown) { + if (mux.visible != null) + include_uninclude(shown, mux.visible.include); + } + + private void on_items_hidden(Gee.Collection<DataView> hidden) { + if (mux.visible != null) + include_uninclude(hidden, mux.visible.uninclude); + } + + private void on_items_selected(Gee.Iterable<DataView> selected) { + if (mux.selected != null) + include_uninclude(selected, mux.selected.include); + } + + private void on_items_unselected(Gee.Iterable<DataView> unselected) { + if (mux.selected != null) + include_uninclude(unselected, mux.selected.uninclude); + } +} + +} diff --git a/src/core/ViewCollection.vala b/src/core/ViewCollection.vala new file mode 100644 index 0000000..a34e23e --- /dev/null +++ b/src/core/ViewCollection.vala @@ -0,0 +1,1287 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// A ViewCollection holds DataView objects, which are view instances wrapping DataSource objects. +// Thus, multiple views can exist of a single SourceCollection, each view displaying all or some +// of that SourceCollection. A view collection also has a notion of order +// (first/last/next/previous) that can be overridden by child classes. It also understands hidden +// objects, which are withheld entirely from the collection until they're made visible. Currently +// the only way to hide objects is with a ViewFilter. +// +// A ViewCollection may also be locked. When locked, it will not (a) remove hidden items from the +// collection and (b) remove DataViews representing unlinked DataSources. This allows for the +// ViewCollection to be "frozen" while manipulating items within it. When the collection is +// unlocked, all changes are applied at once. +// +// The default implementation provides a browser which orders the view in the order they're +// stored in DataCollection, which is not specified. +public class ViewCollection : DataCollection { + public class Monitor { + } + + private class MonitorImpl : Monitor { + public ViewCollection owner; + public SourceCollection sources; + public ViewManager manager; + public Alteration? prereq; + + public MonitorImpl(ViewCollection owner, SourceCollection sources, ViewManager manager, + Alteration? prereq) { + this.owner = owner; + this.sources = sources; + this.manager = manager; + this.prereq = prereq; + + sources.items_added.connect(owner.on_sources_added); + sources.items_removed.connect(owner.on_sources_removed); + sources.items_altered.connect(owner.on_sources_altered); + } + + ~MonitorImpl() { + sources.items_added.disconnect(owner.on_sources_added); + sources.items_removed.disconnect(owner.on_sources_removed); + sources.items_altered.disconnect(owner.on_sources_altered); + } + } + + private class ToggleLists : Object { + public Gee.ArrayList<DataView> selected = new Gee.ArrayList<DataView>(); + public Gee.ArrayList<DataView> unselected = new Gee.ArrayList<DataView>(); + } + +#if MEASURE_VIEW_FILTERING + private static OpTimer filter_timer = new OpTimer("ViewCollection filter timer"); +#endif + + private Gee.HashMultiMap<SourceCollection, MonitorImpl> monitors = new Gee.HashMultiMap< + SourceCollection, MonitorImpl>(); + private ViewCollection mirroring = null; + private unowned CreateView mirroring_ctor = null; + private unowned CreateViewPredicate should_mirror = null; + private Gee.Set<ViewFilter> filters = new Gee.HashSet<ViewFilter>(); + private DataSet selected = new DataSet(); + private DataSet visible = null; + private Gee.HashSet<DataView> frozen_views_altered = null; + private Gee.HashSet<DataView> frozen_geometries_altered = null; + + // TODO: source-to-view mapping ... for now, only one view is allowed for each source. + // This may need to change in the future. + private Gee.HashMap<DataSource, DataView> source_map = new Gee.HashMap<DataSource, DataView>(); + + // Signal aggregator. + public virtual signal void items_selected(Gee.Iterable<DataView> selected) { + } + + // Signal aggregator. + public virtual signal void items_unselected(Gee.Iterable<DataView> unselected) { + } + + // Signal aggregator. + public virtual signal void items_state_changed(Gee.Iterable<DataView> changed) { + } + + // This signal is fired when the selection in the view has changed in any capacity. Items + // are not reported individually because they may have been removed (and are not reported as + // unselected). In other words, although individual DataViews' selection status may not have + // changed, what characterizes the total selection of the ViewCollection has changed. + public virtual signal void selection_group_altered() { + } + + // Signal aggregator. + public virtual signal void items_shown(Gee.Collection<DataView> visible) { + } + + // Signal aggregator. + public virtual signal void items_hidden(Gee.Collection<DataView> hidden) { + } + + // Signal aggregator. + public virtual signal void items_visibility_changed(Gee.Collection<DataView> changed) { + } + + // Signal aggregator. + public virtual signal void item_view_altered(DataView view) { + } + + // Signal aggregator. + public virtual signal void item_geometry_altered(DataView view) { + } + + public virtual signal void views_altered(Gee.Collection<DataView> views) { + } + + public virtual signal void geometries_altered(Gee.Collection<DataView> views) { + } + + public virtual signal void view_filter_installed(ViewFilter filer) { + } + + public virtual signal void view_filter_removed(ViewFilter filer) { + } + + public ViewCollection(string name) { + base (name); + } + + protected virtual void notify_items_selected_unselected(Gee.Collection<DataView>? selected, + Gee.Collection<DataView>? unselected) { + bool has_selected = (selected != null) && (selected.size > 0); + bool has_unselected = (unselected != null) && (unselected.size > 0); + + if (has_selected) + items_selected(selected); + + if (has_unselected) + items_unselected(unselected); + + Gee.Collection<DataView>? sum; + if (has_selected && !has_unselected) { + sum = selected; + } else if (!has_selected && has_unselected) { + sum = unselected; + } else if (!has_selected && !has_unselected) { + sum = null; + } else { + sum = new Gee.HashSet<DataView>(); + sum.add_all(selected); + sum.add_all(unselected); + } + + if (sum != null) { + items_state_changed(sum); + notify_selection_group_altered(); + } + } + + protected virtual void notify_selection_group_altered() { + selection_group_altered(); + } + + protected virtual void notify_item_view_altered(DataView view) { + item_view_altered(view); + } + + protected virtual void notify_views_altered(Gee.Collection<DataView> views) { + views_altered(views); + } + + protected virtual void notify_item_geometry_altered(DataView view) { + item_geometry_altered(view); + } + + protected virtual void notify_geometries_altered(Gee.Collection<DataView> views) { + geometries_altered(views); + } + + protected virtual void notify_items_shown(Gee.Collection<DataView> shown) { + items_shown(shown); + } + + protected virtual void notify_items_hidden(Gee.Collection<DataView> hidden) { + items_hidden(hidden); + } + + protected virtual void notify_items_visibility_changed(Gee.Collection<DataView> changed) { + items_visibility_changed(changed); + } + + protected virtual void notify_view_filter_installed(ViewFilter filter) { + view_filter_installed(filter); + } + + protected virtual void notify_view_filter_removed(ViewFilter filter) { + view_filter_removed(filter); + } + + public override void clear() { + // cannot clear a ViewCollection if it is monitoring a SourceCollection or mirroring a + // ViewCollection + if (monitors.size > 0 || mirroring != null) { + warning("Cannot clear %s: monitoring or mirroring in effect", to_string()); + + return; + } + + base.clear(); + } + + public override void close() { + halt_all_monitoring(); + halt_mirroring(); + foreach (ViewFilter f in filters) + f.refresh.disconnect(on_view_filter_refresh); + filters.clear(); + + base.close(); + } + + public Monitor monitor_source_collection(SourceCollection sources, ViewManager manager, + Alteration? prereq, Gee.Collection<DataSource>? initial = null, + ProgressMonitor? progress_monitor = null) { + // cannot use source monitoring and mirroring at the same time + halt_mirroring(); + + freeze_notifications(); + + // create a monitor, which will hook up all the signals and filter from there + MonitorImpl monitor = new MonitorImpl(this, sources, manager, prereq); + monitors.set(sources, monitor); + + if (initial != null && initial.size > 0) { + // add from the initial list handed to us, using the ViewManager to add/remove later + Gee.ArrayList<DataView> created_views = new Gee.ArrayList<DataView>(); + foreach (DataSource source in initial) + created_views.add(manager.create_view(source)); + + add_many(created_views, progress_monitor); + } else { + // load in all items from the SourceCollection, filtering with the manager + add_sources(sources, (Gee.Iterable<DataSource>) sources.get_all(), progress_monitor); + } + + thaw_notifications(); + + return monitor; + } + + public void halt_monitoring(Monitor m) { + MonitorImpl monitor = (MonitorImpl) m; + + bool removed = monitors.remove(monitor.sources, monitor); + assert(removed); + } + + public void halt_all_monitoring() { + monitors.clear(); + } + + public void mirror(ViewCollection to_mirror, CreateView mirroring_ctor, + CreateViewPredicate? should_mirror) { + halt_mirroring(); + halt_all_monitoring(); + clear(); + + mirroring = to_mirror; + this.mirroring_ctor = mirroring_ctor; + this.should_mirror = should_mirror; + set_comparator(to_mirror.get_comparator(), to_mirror.get_comparator_predicate()); + + // load up with current items + on_mirror_contents_added(mirroring.get_all()); + + mirroring.items_added.connect(on_mirror_contents_added); + mirroring.items_removed.connect(on_mirror_contents_removed); + } + + public void halt_mirroring() { + if (mirroring != null) { + mirroring.items_added.disconnect(on_mirror_contents_added); + mirroring.items_removed.disconnect(on_mirror_contents_removed); + } + + mirroring = null; + } + + public void copy_into(ViewCollection to_copy, CreateView copying_ctor, + CreateViewPredicate should_copy) { + // Copy into self. + Gee.ArrayList<DataObject> copy_view = new Gee.ArrayList<DataObject>(); + foreach (DataObject object in to_copy.get_all()) { + DataView view = (DataView) object; + if (should_copy(view.get_source())) { + copy_view.add(copying_ctor(view.get_source())); + } + } + add_many(copy_view); + } + + public bool is_view_filter_installed(ViewFilter f) { + return filters.contains(f); + } + + public void install_view_filter(ViewFilter f) { + if (is_view_filter_installed(f)) + return; + + filters.add(f); + f.refresh.connect(on_view_filter_refresh); + + // filter existing items + on_view_filter_refresh(); + + // notify of change after activating filter + notify_view_filter_installed(f); + } + + public void remove_view_filter(ViewFilter f) { + if (!is_view_filter_installed(f)) + return; + + filters.remove(f); + f.refresh.disconnect(on_view_filter_refresh); + + // filter existing items + on_view_filter_refresh(); + + // notify of change after activating filter + notify_view_filter_removed(f); + } + + private void on_view_filter_refresh() { + filter_altered_items((Gee.Collection<DataView>) base.get_all()); + } + + // Runs predicate on all filters, returns ANDed result. + private bool is_in_filter(DataView view) { + foreach (ViewFilter f in filters) { + if (!f.predicate(view)) + return false; + } + return true; + } + + public override bool valid_type(DataObject object) { + return object is DataView; + } + + private void on_sources_added(DataCollection sources, Gee.Iterable<DataSource> added) { + add_sources((SourceCollection) sources, added); + } + + private void add_sources(SourceCollection sources, Gee.Iterable<DataSource> added, + ProgressMonitor? progress_monitor = null) { + // add only source items which are to be included by the manager ... do this in batches + // to take advantage of add_many() + DataView created_view = null; + Gee.ArrayList<DataView> created_views = null; + foreach (DataSource source in added) { + CreateView factory = null; + foreach (MonitorImpl monitor in monitors.get(sources)) { + if (monitor.manager.include_in_view(source)) { + factory = monitor.manager.create_view; + + break; + } + } + + if (factory != null) { + DataView new_view = factory(source); + + // this bit of code is designed to avoid creating the ArrayList if only one item + // is being added to the ViewCollection + if (created_views != null) { + created_views.add(new_view); + } else if (created_view == null) { + created_view = new_view; + } else { + created_views = new Gee.ArrayList<DataView>(); + created_views.add(created_view); + created_view = null; + created_views.add(new_view); + } + } + } + + if (created_view != null) + add(created_view); + else if (created_views != null && created_views.size > 0) + add_many(created_views, progress_monitor); + } + + public override bool add(DataObject object) { + ((DataView) object).internal_set_visible(true); + + if (!base.add(object)) + return false; + + filter_altered_items((Gee.Collection<DataView>) get_singleton(object)); + + return true; + } + + public override Gee.Collection<DataObject> add_many(Gee.Collection<DataObject> objects, + ProgressMonitor? monitor = null) { + foreach (DataObject object in objects) + ((DataView) object).internal_set_visible(true); + + Gee.Collection<DataObject> return_list = base.add_many(objects, monitor); + + filter_altered_items((Gee.Collection<DataView>) return_list); + + return return_list; + } + + private void on_sources_removed(Gee.Iterable<DataSource> removed) { + // mark all view items associated with the source to be removed + Marker marker = null; + foreach (DataSource source in removed) { + DataView view = source_map.get(source); + + // ignore if not represented in this view + if (view != null) { + if (marker == null) + marker = start_marking(); + + marker.mark(view); + } + } + + if (marker != null && marker.get_count() != 0) + remove_marked(marker); + } + + private void on_sources_altered(DataCollection collection, Gee.Map<DataObject, Alteration> items) { + // let ViewManager decide whether or not to keep, but only add if not already present + // and only remove if already present + Gee.ArrayList<DataView> to_add = null; + Gee.ArrayList<DataView> to_remove = null; + bool ordering_changed = false; + foreach (DataObject object in items.keys) { + Alteration alteration = items.get(object); + DataSource source = (DataSource) object; + + MonitorImpl? monitor = null; + bool ignored = true; + foreach (MonitorImpl monitor_impl in monitors.get((SourceCollection) collection)) { + if (monitor_impl.prereq != null && !alteration.contains_any(monitor_impl.prereq)) + continue; + + ignored = false; + + if (monitor_impl.manager.include_in_view(source)) { + monitor = monitor_impl; + + break; + } + } + + if (ignored) { + assert(monitor == null); + + continue; + } + + if (monitor != null && !has_view_for_source(source)) { + if (to_add == null) + to_add = new Gee.ArrayList<DataView>(); + + to_add.add(monitor.manager.create_view(source)); + } else if (monitor == null && has_view_for_source(source)) { + if (to_remove == null) + to_remove = new Gee.ArrayList<DataView>(); + + to_remove.add(get_view_for_source(source)); + } else if (monitor != null && has_view_for_source(source)) { + DataView view = get_view_for_source(source); + + if (selected.contains(view)) + selected.resort_object(view, alteration); + + if (visible != null && is_visible(view)) { + if (visible.resort_object(view, alteration)) + ordering_changed = true; + } + } + } + + if (to_add != null) + add_many(to_add); + + if (to_remove != null) + remove_marked(mark_many(to_remove)); + + if (ordering_changed) + notify_ordering_changed(); + } + + private void on_mirror_contents_added(Gee.Iterable<DataObject> added) { + Gee.ArrayList<DataView> to_add = new Gee.ArrayList<DataView>(); + foreach (DataObject object in added) { + DataSource source = ((DataView) object).get_source(); + + if (should_mirror == null || should_mirror(source)) + to_add.add(mirroring_ctor(source)); + } + + if (to_add.size > 0) + add_many(to_add); + } + + private void on_mirror_contents_removed(Gee.Iterable<DataObject> removed) { + Marker marker = start_marking(); + foreach (DataObject object in removed) { + DataView view = (DataView) object; + + DataView? our_view = get_view_for_source(view.get_source()); + assert(our_view != null); + + marker.mark(our_view); + } + + remove_marked(marker); + } + + // Keep the source map and state tables synchronized + protected override void notify_items_added(Gee.Iterable<DataObject> added) { + Gee.ArrayList<DataView> added_visible = null; + Gee.ArrayList<DataView> added_selected = null; + + foreach (DataObject object in added) { + DataView view = (DataView) object; + source_map.set(view.get_source(), view); + + if (view.is_selected() && view.is_visible()) { + if (added_selected == null) + added_selected = new Gee.ArrayList<DataView>(); + + added_selected.add(view); + } + + // add to visible list only if there is one + if (view.is_visible() && visible != null) { + if (added_visible == null) + added_visible = new Gee.ArrayList<DataView>(); + + added_visible.add(view); + } + } + + if (added_visible != null) { + bool is_added = add_many_visible(added_visible); + assert(is_added); + } + + if (added_selected != null) { + add_many_selected(added_selected); + notify_items_selected_unselected(added_selected, null); + } + + base.notify_items_added(added); + } + + // Keep the source map and state tables synchronized + protected override void notify_items_removed(Gee.Iterable<DataObject> removed) { + Gee.ArrayList<DataView>? selected_removed = null; + foreach (DataObject object in removed) { + DataView view = (DataView) object; + + // It's possible for execution to get here in direct mode with the source + // in question already having been removed from the source map, but the + // double removal is unimportant to direct mode, so if this happens, the + // remove is skipped the second time (to prevent crashing). + if (source_map.has_key(view.get_source())) { + bool is_removed = source_map.unset(view.get_source()); + assert(is_removed); + + if (view.is_selected()) { + // hidden items may be selected, but they won't be in the selected pool + assert(selected.contains(view) == view.is_visible()); + + if (view.is_visible()) { + if (selected_removed == null) + selected_removed = new Gee.ArrayList<DataView>(); + + selected_removed.add(view); + } + } + + if (view.is_visible() && visible != null) { + is_removed = visible.remove(view); + assert(is_removed); + } + } + } + + if (selected_removed != null) { + remove_many_selected(selected_removed); + + // If a selected item was removed, only fire the selected_removed signal, as the total + // selection character of the ViewCollection has changed, but not the individual items' + // state. + notify_selection_group_altered(); + } + + base.notify_items_removed(removed); + } + + private void filter_altered_items(Gee.Collection<DataView> views) { + // Can't use the marker system because ViewCollection completely overrides DataCollection + // and hidden items cannot be marked. + Gee.ArrayList<DataView> to_show = null; + Gee.ArrayList<DataView> to_hide = null; + +#if MEASURE_VIEW_FILTERING + filter_timer.start(); +#endif + foreach (DataView view in views) { + if (is_in_filter(view)) { + if (!view.is_visible()) { + if (to_show == null) + to_show = new Gee.ArrayList<DataView>(); + + to_show.add(view); + } + } else { + if (view.is_visible()) { + if (to_hide == null) + to_hide = new Gee.ArrayList<DataView>(); + + to_hide.add(view); + } + } + } +#if MEASURE_VIEW_FILTERING + filter_timer.stop(); + debug("Filtering for %s: %s", to_string(), filter_timer.to_string()); +#endif + + if (to_show != null) + show_items(to_show); + + if (to_hide != null) + hide_items(to_hide); + } + + public override void items_altered(Gee.Map<DataObject, Alteration> map) { + filter_altered_items(map.keys); + + base.items_altered(map); + } + + public override void set_comparator(Comparator comparator, ComparatorPredicate? predicate) { + selected.set_comparator(comparator, predicate); + if (visible != null) + visible.set_comparator(comparator, predicate); + + base.set_comparator(comparator, predicate); + } + + public override void reset_comparator() { + selected.reset_comparator(); + if (visible != null) + visible.reset_comparator(); + + base.reset_comparator(); + } + + public override Gee.Collection<DataObject> get_all() { + return (visible != null) ? visible.get_all() : base.get_all(); + } + + public Gee.Collection<DataObject> get_all_unfiltered() { + return base.get_all(); + } + + public override int get_count() { + return (visible != null) ? visible.get_count() : base.get_count(); + } + + public int get_unfiltered_count() { + return base.get_count(); + } + + public override DataObject? get_at(int index) { + return (visible != null) ? visible.get_at(index) : base.get_at(index); + } + + public override int index_of(DataObject object) { + return (visible != null) ? visible.index_of(object) : base.index_of(object); + } + + public override bool contains(DataObject object) { + // use base method first, which can quickly ascertain if the object is *not* a member of + // this collection + if (!base.contains(object)) + return false; + + // even if a member, must be visible to be "contained" + return is_visible((DataView) object); + } + + public virtual DataView? get_first() { + return (get_count() > 0) ? (DataView?) get_at(0) : null; + } + + /** + * @brief A helper method for places in the app that need a + * non-rejected media source (namely Events, when looking to + * automatically choose a thumbnail). + * + * @note If every view in this collection is rejected, we + * return the first view; this is intentional. This prevents + * pathological events that have nothing but rejected images + * in them from breaking. + */ + public virtual DataView? get_first_unrejected() { + // We have no media, unrejected or otherwise... + if (get_count() < 1) + return null; + + // Loop through media we do have... + DataView dv = get_first(); + int num_views = get_count(); + + while ((dv != null) && (index_of(dv) < (num_views - 1))) { + MediaSource tmp = dv.get_source() as MediaSource; + + if ((tmp != null) && (tmp.get_rating() != Rating.REJECTED)) { + // ...found a good one; return it. + return dv; + } else { + dv = get_next(dv); + } + } + + // Got to the end of the collection, none found, need to return + // _something_... + return get_first(); + } + + public virtual DataView? get_last() { + return (get_count() > 0) ? (DataView?) get_at(get_count() - 1) : null; + } + + public virtual DataView? get_next(DataView view) { + if (get_count() == 0) + return null; + + int index = index_of(view); + if (index < 0) + return null; + + index++; + if (index >= get_count()) + index = 0; + + return (DataView?) get_at(index); + } + + public virtual DataView? get_previous(DataView view) { + if (get_count() == 0) + return null; + + int index = index_of(view); + if (index < 0) + return null; + + index--; + if (index < 0) + index = get_count() - 1; + + return (DataView?) get_at(index); + } + + public bool get_immediate_neighbors(DataSource home, out DataSource? next, + out DataSource? prev, string? type_selector = null) { + next = null; + prev = null; + + DataView home_view = get_view_for_source(home); + if (home_view == null) + return false; + + DataView? next_view = get_next(home_view); + while (next_view != home_view) { + if ((type_selector == null) || (next_view.get_source().get_typename() == type_selector)) { + next = next_view.get_source(); + break; + } + next_view = get_next(next_view); + } + + DataView? prev_view = get_previous(home_view); + while (prev_view != home_view) { + if ((type_selector == null) || (prev_view.get_source().get_typename() == type_selector)) { + prev = prev_view.get_source(); + break; + } + prev_view = get_previous(prev_view); + } + + return true; + } + + // "Extended" as in immediate neighbors and their neighbors + public Gee.Set<DataSource> get_extended_neighbors(DataSource home, string? typename = null) { + // build set of neighbors + Gee.Set<DataSource> neighbors = new Gee.HashSet<DataSource>(); + + // immediate neighbors + DataSource next, prev; + if (!get_immediate_neighbors(home, out next, out prev, typename)) + return neighbors; + + // add next and its distant neighbor + if (next != null) { + neighbors.add(next); + + DataSource next_next, next_prev; + get_immediate_neighbors(next, out next_next, out next_prev, typename); + + // only add next-next because next-prev is home + if (next_next != null) + neighbors.add(next_next); + } + + // add previous and its distant neighbor + if (prev != null) { + neighbors.add(prev); + + DataSource next_prev, prev_prev; + get_immediate_neighbors(prev, out next_prev, out prev_prev, typename); + + // only add prev-prev because next-prev is home + if (prev_prev != null) + neighbors.add(prev_prev); + } + + // finally, in a small collection a neighbor could be home itself, so exclude it + neighbors.remove(home); + + return neighbors; + } + + // Do NOT add hidden items to the selection collection, mark them as selected and they will be + // added when/if they are made visible. + private void add_many_selected(Gee.Collection<DataView> views) { + if (views.size == 0) + return; + + foreach (DataView view in views) + assert(view.is_visible()); + + bool added = selected.add_many(views); + assert(added); + } + + private void remove_many_selected(Gee.Collection<DataView> views) { + if (views.size == 0) + return; + + bool removed = selected.remove_many(views); + assert(removed); + } + + // Selects all the marked items. The marker will be invalid after this call. + public void select_marked(Marker marker) { + Gee.ArrayList<DataView> selected = new Gee.ArrayList<DataView>(); + act_on_marked(marker, select_item, null, selected); + + if (selected.size > 0) { + add_many_selected(selected); + notify_items_selected_unselected(selected, null); + } + } + + // Selects all items. + public void select_all() { + Marker marker = start_marking(); + marker.mark_all(); + select_marked(marker); + } + + private bool select_item(DataObject object, Object? user) { + DataView view = (DataView) object; + if (view.is_selected()) { + if (view.is_visible()) + assert(selected.contains(view)); + + return true; + } + + view.internal_set_selected(true); + + // Do NOT add hidden items to the selection collection, merely mark them as selected + // and they will be re-added when/if they are made visible + if (view.is_visible()) + ((Gee.ArrayList<DataView>) user).add(view); + + return true; + } + + // Unselects all the marked items. The marker will be invalid after this call. + public void unselect_marked(Marker marker) { + Gee.ArrayList<DataView> unselected = new Gee.ArrayList<DataView>(); + act_on_marked(marker, unselect_item, null, unselected); + + if (unselected.size > 0) { + remove_many_selected(unselected); + notify_items_selected_unselected(null, unselected); + } + } + + // Unselects all items. + public void unselect_all() { + if (selected.get_count() == 0) + return; + + Marker marker = start_marking(); + marker.mark_many(get_selected()); + + unselect_marked(marker); + } + + // Unselects all items but the one specified. + public void unselect_all_but(DataView exception) { + Marker marker = start_marking(); + foreach (DataObject object in get_all()) { + DataView view = (DataView) object; + if (view != exception) + marker.mark(view); + } + + unselect_marked(marker); + } + + private bool unselect_item(DataObject object, Object? user) { + DataView view = (DataView) object; + if (!view.is_selected()) { + assert(!selected.contains(view)); + + return true; + } + + view.internal_set_selected(false); + ((Gee.ArrayList<DataView>) user).add(view); + + return true; + } + + // Performs the operations in that order: unselects the marked then selects the marked + public void unselect_and_select_marked(Marker unselect, Marker select) { + Gee.ArrayList<DataView> unselected = new Gee.ArrayList<DataView>(); + act_on_marked(unselect, unselect_item, null, unselected); + + remove_many_selected(unselected); + + Gee.ArrayList<DataView> selected = new Gee.ArrayList<DataView>(); + act_on_marked(select, select_item, null, selected); + + add_many_selected(selected); + + notify_items_selected_unselected(selected, unselected); + } + + // Toggle the selection state of all marked items. The marker will be invalid after this + // call. + public void toggle_marked(Marker marker) { + ToggleLists lists = new ToggleLists(); + act_on_marked(marker, toggle_item, null, lists); + + // add and remove selected before firing the signals + add_many_selected(lists.selected); + remove_many_selected(lists.unselected); + + notify_items_selected_unselected(lists.selected, lists.unselected); + } + + private bool toggle_item(DataObject object, Object? user) { + DataView view = (DataView) object; + ToggleLists lists = (ToggleLists) user; + + // toggle the selection state of the view, adding or removing it from the selected list + // to maintain state and adding it to the ToggleLists for the caller to signal with + // + // See add_many_selected for rules on not adding hidden items to the selection pool + if (view.internal_toggle()) { + if (view.is_visible()) + lists.selected.add(view); + } else { + lists.unselected.add(view); + } + + return true; + } + + public int get_selected_count() { + return selected.get_count(); + } + + public Gee.List<DataView> get_selected() { + return (Gee.List<DataView>) selected.get_all(); + } + + public DataView? get_selected_at(int index) { + return (DataView?) selected.get_at(index); + } + + private bool is_visible(DataView view) { + return (visible != null) ? visible.contains(view) : true; + } + + private bool add_many_visible(Gee.Collection<DataView> many) { + if (visible == null) + return true; + + if (!visible.add_many(many)) + return false; + + // if all are visible, then revert to using base class's set + if (visible.get_count() == base.get_count()) + visible = null; + + return true; + } + + // This method requires that all items in to_hide are not hidden already. + private void hide_items(Gee.List<DataView> to_hide) { + Gee.ArrayList<DataView> unselected = new Gee.ArrayList<DataView>(); + + int count = to_hide.size; + for (int ctr = 0; ctr < count; ctr++) { + DataView view = to_hide.get(ctr); + assert(view.is_visible()); + + if (view.is_selected()) { + view.internal_set_selected(false); + unselected.add(view); + } else { + assert(!selected.contains(view)); + } + + view.internal_set_visible(false); + } + + if (visible == null) { + // make a copy of the full set before removing items + visible = get_dataset_copy(); + } + + bool removed = visible.remove_many(to_hide); + assert(removed); + + remove_many_selected(unselected); + + if (unselected.size > 0) + notify_items_selected_unselected(null, unselected); + + if (to_hide.size > 0) { + notify_items_hidden(to_hide); + notify_items_visibility_changed(to_hide); + } + } + + // This method requires that all items in to_show are hidden already. + private void show_items(Gee.List<DataView> to_show) { + Gee.ArrayList<DataView> added_selected = new Gee.ArrayList<DataView>(); + + int count = to_show.size; + for (int ctr = 0; ctr < count; ctr++) { + DataView view = to_show.get(ctr); + assert(!view.is_visible()); + + view.internal_set_visible(true); + + // See note in add_selected for selection handling with hidden/visible items + if (view.is_selected()) { + assert(!selected.contains(view)); + added_selected.add(view); + } + } + + bool added = add_many_visible(to_show); + assert(added); + + add_many_selected(added_selected); + + if (to_show.size > 0) { + notify_items_shown(to_show); + notify_items_visibility_changed(to_show); + } + } + + // This currently does not respect filtering. + public bool has_view_for_source(DataSource source) { + return get_view_for_source(source) != null; + } + + // This currently does not respect filtering. + public DataView? get_view_for_source(DataSource source) { + return source_map.get(source); + } + + // Respects filtering. + public bool has_view_for_source_with_filtered(DataSource source) { + return get_view_for_source_filtered(source) != null; + } + + // Respects filtering. + public DataView? get_view_for_source_filtered(DataSource source) { + DataView? view = source_map.get(source); + // Consult with filter to make sure DataView is visible. + if (view != null && !is_in_filter(view)) + return null; + return view; + } + + // TODO: This currently does not respect filtering. + public Gee.Collection<DataSource> get_sources() { + return source_map.keys.read_only_view; + } + + // TODO: This currently does not respect filtering. + public bool has_source_of_type(Type t) { + assert(t.is_a(typeof(DataSource))); + + foreach (DataSource source in source_map.keys) { + if (source.get_type().is_a(t)) + return true; + } + + return false; + } + + public int get_sources_of_type_count(Type t) { + assert(t.is_a(typeof(DataSource))); + + int count = 0; + foreach (DataObject object in get_all()) { + if (((DataView) object).get_source().get_type().is_a(t)) + count++; + } + + return count; + } + + public Gee.List<DataSource>? get_sources_of_type(Type t) { + assert(t.is_a(typeof(DataSource))); + + Gee.List<DataSource>? sources = null; + foreach (DataObject object in get_all()) { + DataSource source = ((DataView) object).get_source(); + if (source.get_type().is_a(t)) { + if (sources == null) + sources = new Gee.ArrayList<DataSource>(); + + sources.add(source); + } + } + + return sources; + } + + public Gee.List<DataSource> get_selected_sources() { + Gee.List<DataSource> sources = new Gee.ArrayList<DataSource>(); + + int count = selected.get_count(); + for (int ctr = 0; ctr < count; ctr++) + sources.add(((DataView) selected.get_at(ctr)).get_source()); + + return sources; + } + + public DataSource? get_selected_source_at(int index) { + DataObject? object = selected.get_at(index); + + return (object != null) ? ((DataView) object).get_source() : null; + } + + public Gee.List<DataSource>? get_selected_sources_of_type(Type t) { + Gee.List<DataSource>? sources = null; + foreach (DataView view in get_selected()) { + DataSource source = view.get_source(); + if (source.get_type().is_a(t)) { + if (sources == null) + sources = new Gee.ArrayList<DataSource>(); + + sources.add(source); + } + } + + return sources; + } + + // Returns -1 if source is not in the ViewCollection. + public int index_of_source(DataSource source) { + DataView? view = get_view_for_source(source); + + return (view != null) ? index_of(view) : -1; + } + + // This is only used by DataView. + public void internal_notify_view_altered(DataView view) { + if (!are_notifications_frozen()) { + notify_item_view_altered(view); + notify_views_altered((Gee.Collection<DataView>) get_singleton(view)); + } else { + if (frozen_views_altered == null) + frozen_views_altered = new Gee.HashSet<DataView>(); + frozen_views_altered.add(view); + } + } + + // This is only used by DataView. + public void internal_notify_geometry_altered(DataView view) { + if (!are_notifications_frozen()) { + notify_item_geometry_altered(view); + notify_geometries_altered((Gee.Collection<DataView>) get_singleton(view)); + } else { + if (frozen_geometries_altered == null) + frozen_geometries_altered = new Gee.HashSet<DataView>(); + frozen_geometries_altered.add(view); + } + } + + protected override void notify_thawed() { + if (frozen_views_altered != null) { + foreach (DataView view in frozen_views_altered) + notify_item_view_altered(view); + notify_views_altered(frozen_views_altered); + frozen_views_altered = null; + } + + if (frozen_geometries_altered != null) { + foreach (DataView view in frozen_geometries_altered) + notify_item_geometry_altered(view); + notify_geometries_altered(frozen_geometries_altered); + frozen_geometries_altered = null; + } + + base.notify_thawed(); + } + + public bool are_items_filtered_out() { + return base.get_count() != get_count(); + } +} + +// A ViewManager allows an interface for ViewCollection to monitor a SourceCollection and +// (selectively) add DataViews automatically. +public abstract class ViewManager { + // This predicate function can be used to filter which DataView objects should be included + // in the collection as new source objects appear in the SourceCollection. May be called more + // than once for any DataSource object. + public virtual bool include_in_view(DataSource source) { + return true; + } + + // If include_in_view returns true, this method will be called to instantiate a DataView object + // for the ViewCollection. + public abstract DataView create_view(DataSource source); +} + +// CreateView is a construction delegate used when mirroring or copying a ViewCollection +// in another ViewCollection. +public delegate DataView CreateView(DataSource source); + +// CreateViewPredicate is a filter delegate used when copy a ViewCollection in another +// ViewCollection. +public delegate bool CreateViewPredicate(DataSource source); + +// A ViewFilter allows for items in a ViewCollection to be shown or hidden depending on the +// supplied predicate method. For now, only one ViewFilter may be installed, although this may +// change in the future. The ViewFilter is used whenever an object is added to the collection +// and when its altered/metadata_altered signals fire. +public abstract class ViewFilter { + // Fire this signal whenever a refresh is needed. The ViewCollection listens + // to this signal to know when to reapply the filter. + public virtual signal void refresh() { + } + + // Return true if view should be visible, false if it should be hidden. + public abstract bool predicate(DataView view); +} + diff --git a/src/core/mk/core.mk b/src/core/mk/core.mk new file mode 100644 index 0000000..c35c93a --- /dev/null +++ b/src/core/mk/core.mk @@ -0,0 +1,43 @@ + +# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with +# a init() and terminate() function declared in the namespace. +UNIT_NAME := Core + +# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all +# lowercase. The name of this file should be UNIT_DIR.mk. +UNIT_DIR := core + +# All Vala files in the unit should be listed here with no subdirectory prefix. +# +# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala. +UNIT_FILES := \ + DataCollection.vala \ + DataSet.vala \ + util.vala \ + SourceCollection.vala \ + SourceHoldingTank.vala \ + DatabaseSourceCollection.vala \ + ContainerSourceCollection.vala \ + ViewCollection.vala \ + DataObject.vala \ + Alteration.vala \ + DataSource.vala \ + DataSourceTypes.vala \ + DataView.vala \ + DataViewTypes.vala \ + Tracker.vala \ + SourceInterfaces.vala + +# Any unit this unit relies upon (and should be initialized before it's initialized) should +# be listed here using its Vala namespace. +# +# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here. +UNIT_USES := + +# List any additional files that are used in the build process as a part of this unit that should +# be packaged in the tarball. File names should be relative to the unit's home directory. +UNIT_RC := + +# unitize.mk must be called at the end of each UNIT_DIR.mk file. +include unitize.mk + diff --git a/src/core/util.vala b/src/core/util.vala new file mode 100644 index 0000000..1846380 --- /dev/null +++ b/src/core/util.vala @@ -0,0 +1,196 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// SingletonCollection is a read-only collection designed to hold exactly one item in it. This +// is far more efficient than creating a dummy collection (such as ArrayList) merely to pass around +// a single item, particularly for signals which require Iterables and Collections. +// +// This collection cannot be used to store null. + +public class SingletonCollection<G> : Gee.AbstractCollection<G> { + private class SingletonIterator<G> : Object, Gee.Traversable<G>, Gee.Iterator<G> { + private SingletonCollection<G> c; + private bool done = false; + private G? current = null; + + public SingletonIterator(SingletonCollection<G> c) { + this.c = c; + } + + public bool read_only { + get { return done; } + } + + public bool valid { + get { return done; } + } + + public bool foreach(Gee.ForallFunc<G> f) { + return f(c.object); + } + + public new G? get() { + return current; + } + + public bool has_next() { + return false; + } + + public bool next() { + if (done) + return false; + + done = true; + current = c.object; + + return true; + } + + public void remove() { + if (!done) { + c.object = null; + current = null; + } + + done = true; + } + } + + private G? object; + + public SingletonCollection(G object) { + this.object = object; + } + + public override bool read_only { + get { return false; } + } + + public override bool add(G object) { + warning("Cannot add to SingletonCollection"); + + return false; + } + + public override void clear() { + object = null; + } + + public override bool contains(G object) { + return this.object == object; + } + + public override Gee.Iterator<G> iterator() { + return new SingletonIterator<G>(this); + } + + public override bool remove(G item) { + if (item == object) { + object = null; + + return true; + } + + return false; + } + + public override int size { + get { + return (object != null) ? 1 : 0; + } + } +} + +// A Marker is an object for marking (selecting) DataObjects in a DataCollection to then perform +// an action on all of them. This mechanism allows for performing mass operations in a generic +// way, as well as dealing with the (perpetual) issue of removing items from a Collection within +// an iterator. +public interface Marker : Object { + public abstract void mark(DataObject object); + + public abstract void unmark(DataObject object); + + public abstract bool toggle(DataObject object); + + public abstract void mark_many(Gee.Collection<DataObject> list); + + public abstract void unmark_many(Gee.Collection<DataObject> list); + + public abstract void mark_all(); + + // Returns the number of marked items, or the number of items when the marker was frozen + // and used. + public abstract int get_count(); + + // Returns a copy of the collection of marked items. + public abstract Gee.Collection<DataObject> get_all(); +} + +// MarkedAction is a callback to perform an action on the marked DataObject. Return false to +// end iterating. +public delegate bool MarkedAction(DataObject object, Object? user); + +// A ProgressMonitor allows for notifications of progress on operations on multiple items (via +// the marked interfaces). Return false if the operation is cancelled and should end immediately. +public delegate bool ProgressMonitor(uint64 current, uint64 total, bool do_event_loop = true); + +// UnknownTotalMonitor is useful when an interface cannot report the total count to a ProgressMonitor, +// only a count, but the total is known by the caller. +public class UnknownTotalMonitor { + private uint64 total; + private unowned ProgressMonitor wrapped_monitor; + + public UnknownTotalMonitor(uint64 total, ProgressMonitor wrapped_monitor) { + this.total = total; + this.wrapped_monitor = wrapped_monitor; + } + + public bool monitor(uint64 count, uint64 total) { + return wrapped_monitor(count, this.total); + } +} + +// AggregateProgressMonitor is useful when several discrete operations are being performed against +// a single ProgressMonitor. +public class AggregateProgressMonitor { + private uint64 grand_total; + private unowned ProgressMonitor wrapped_monitor; + private uint64 aggregate_count = 0; + private uint64 last_count = uint64.MAX; + + public AggregateProgressMonitor(uint64 grand_total, ProgressMonitor wrapped_monitor) { + this.grand_total = grand_total; + this.wrapped_monitor = wrapped_monitor; + } + + public void next_step(string name) { + debug("next step: %s (%s/%s)", name, aggregate_count.to_string(), grand_total.to_string()); + last_count = uint64.MAX; + } + + public bool monitor(uint64 count, uint64 total) { + // add the difference from the last, unless a new step has started + aggregate_count += (last_count != uint64.MAX) ? (count - last_count) : count; + if (aggregate_count > grand_total) + aggregate_count = grand_total; + + // save for next time + last_count = count; + + return wrapped_monitor(aggregate_count, grand_total); + } +} + +// Useful when debugging. +public bool null_progress_monitor(uint64 count, uint64 total) { + return true; +} + + +double degrees_to_radians(double theta) { + return (theta * (GLib.Math.PI / 180.0)); +} |