/* Copyright 2016 Software Freedom Conservancy Inc. * * 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 selected = new Gee.ArrayList(); public Gee.ArrayList unselected = new Gee.ArrayList(); } #if MEASURE_VIEW_FILTERING private static OpTimer filter_timer = new OpTimer("ViewCollection filter timer"); #endif private Gee.HashMultiMap 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 filters = new Gee.HashSet(); private DataSet selected = new DataSet(); private DataSet visible = null; private Gee.HashSet frozen_views_altered = null; private Gee.HashSet 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 source_map = new Gee.HashMap(); // Signal aggregator. public virtual signal void items_selected(Gee.Iterable selected) { } // Signal aggregator. public virtual signal void items_unselected(Gee.Iterable unselected) { } // Signal aggregator. public virtual signal void items_state_changed(Gee.Iterable 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 visible) { } // Signal aggregator. public virtual signal void items_hidden(Gee.Collection hidden) { } // Signal aggregator. public virtual signal void items_visibility_changed(Gee.Collection 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 views) { } public virtual signal void geometries_altered(Gee.Collection 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? selected, Gee.Collection? 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? 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(); 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 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 views) { geometries_altered(views); } protected virtual void notify_items_shown(Gee.Collection shown) { items_shown(shown); } protected virtual void notify_items_hidden(Gee.Collection hidden) { items_hidden(hidden); } protected virtual void notify_items_visibility_changed(Gee.Collection 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? 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 created_views = new Gee.ArrayList(); 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) 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 copy_view = new Gee.ArrayList(); 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); } // for use in shuffled slideshow public ViewCollection shuffled_copy(Photo? start = null) { ViewCollection shuffled = new ViewCollection("shuffled copy of " + to_string()); Gee.Collection data = get_all(); Gee.ArrayList data_copy = new Gee.ArrayList(); foreach (DataObject object in data) { DataView view = (DataView) object; if (view.get_source() is PhotoSource && (start == null || view.get_source() != start)) { data_copy.add(new PhotoView((PhotoSource) ((DataView) object).get_source())); } } for (int ctr = data_copy.size - 1; ctr > 0; ctr--) { int rand = GLib.Random.int_range(0, ctr + 1); DataObject temp = data_copy.get(rand); data_copy.set(rand, data_copy.get(ctr)); data_copy.set(ctr, temp); } if (start != null) shuffled.add(new PhotoView(start)); shuffled.add_many(data_copy); return shuffled; } 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) 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 added) { add_sources((SourceCollection) sources, added); } private void add_sources(SourceCollection sources, Gee.Iterable 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 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(); 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) get_singleton(object)); return true; } public override Gee.Collection add_many(Gee.Collection objects, ProgressMonitor? monitor = null) { foreach (DataObject object in objects) ((DataView) object).internal_set_visible(true); Gee.Collection return_list = base.add_many(objects, monitor); filter_altered_items((Gee.Collection) return_list); return return_list; } private void on_sources_removed(Gee.Iterable 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 items) { // let ViewManager decide whether or not to keep, but only add if not already present // and only remove if already present Gee.ArrayList to_add = null; Gee.ArrayList 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(); 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(); 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 added) { Gee.ArrayList to_add = new Gee.ArrayList(); 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 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 added) { Gee.ArrayList added_visible = null; Gee.ArrayList 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(); 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(); 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 removed) { Gee.ArrayList? 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(); 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 views) { // Can't use the marker system because ViewCollection completely overrides DataCollection // and hidden items cannot be marked. Gee.ArrayList to_show = null; Gee.ArrayList 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(); to_show.add(view); } } else { if (view.is_visible()) { if (to_hide == null) to_hide = new Gee.ArrayList(); 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 map) { // Cast - our DataObjects are DataViews. filter_altered_items((Gee.Collection)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 get_all() { return (visible != null) ? visible.get_all() : base.get_all(); } public Gee.Collection 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_first_photo() { if (get_count() < 1) return null; DataView dv = get_first(); int num_views = get_count(); while ((dv != null) && (index_of(dv) < (num_views))) { if (dv.get_source() is PhotoSource) return dv; else dv = get_next(dv); } return null; } public virtual DataView? get_last() { return (get_count() > 0) ? (DataView?) get_at(get_count() - 1) : null; } public virtual DataView? get_next(DataView view, out bool? wrapped = null) { wrapped = false; if (get_count() == 0) return null; int index = index_of(view); if (index < 0) return null; index++; if (index >= get_count()) { index = 0; wrapped = true; } 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 get_extended_neighbors(DataSource home, string? typename = null) { // build set of neighbors Gee.Set neighbors = new Gee.HashSet(); // 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 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 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 selected = new Gee.ArrayList(); 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) 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 unselected = new Gee.ArrayList(); 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) 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 unselected = new Gee.ArrayList(); act_on_marked(unselect, unselect_item, null, unselected); remove_many_selected(unselected); Gee.ArrayList selected = new Gee.ArrayList(); 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 get_selected() { return (Gee.List) 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 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 to_hide) { Gee.ArrayList unselected = new Gee.ArrayList(); 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 to_show) { Gee.ArrayList added_selected = new Gee.ArrayList(); 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 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? get_sources_of_type(Type t) { assert(t.is_a(typeof(DataSource))); Gee.List? 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(); sources.add(source); } } return sources; } public Gee.List get_selected_sources() { Gee.List sources = new Gee.ArrayList(); 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? get_selected_sources_of_type(Type t) { Gee.List? 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(); 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) get_singleton(view)); } else { if (frozen_views_altered == null) frozen_views_altered = new Gee.HashSet(); 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) get_singleton(view)); } else { if (frozen_geometries_altered == null) frozen_geometries_altered = new Gee.HashSet(); 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); }