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)); +} | 
