diff options
Diffstat (limited to 'src/core/ViewCollection.vala')
-rw-r--r-- | src/core/ViewCollection.vala | 1287 |
1 files changed, 1287 insertions, 0 deletions
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); +} + |