Commit 539682c7 authored by Jim Nelson's avatar Jim Nelson

#3259: Search bar now dynamically indicates the availability of content on...

#3259: Search bar now dynamically indicates the availability of content on each page and makes those filters insensitive when it's not.
parent 7c23ea35
......@@ -98,7 +98,8 @@ UNUNITIZED_SRC_FILES = \
MediaMonitor.vala \
PhotoMonitor.vala \
VideoMonitor.vala \
SearchFilter.vala
SearchFilter.vala \
MediaViewTracker.vala
VAPI_FILES = \
libexif.vapi \
......
......@@ -794,7 +794,8 @@ public abstract class AppWindow : PageWindow {
new_page.get_view().selection_group_altered.connect(on_update_actions);
new_page.get_view().items_state_changed.connect(on_update_actions);
update_actions(new_page.get_view().get_selected_count(), new_page.get_view().get_count());
update_actions(new_page, new_page.get_view().get_selected_count(),
new_page.get_view().get_count());
}
base.switched_pages(old_page, new_page);
......@@ -803,7 +804,7 @@ public abstract class AppWindow : PageWindow {
// This is a counterpart to Page.update_actions(), but for common Gtk.Actions
// NOTE: Although CommonFullscreen is declared here, it's implementation is up to the subclasses,
// therefore they need to update its action.
protected virtual void update_actions(int selected_count, int count) {
protected virtual void update_actions(Page page, int selected_count, int count) {
set_common_action_sensitive("CommonSelectAll", count > 0);
set_common_action_sensitive("CommonJumpToFile", selected_count == 1);
......@@ -814,7 +815,7 @@ public abstract class AppWindow : PageWindow {
private void on_update_actions() {
Page? page = get_current_page();
if (page != null)
update_actions(page.get_view().get_selected_count(), page.get_view().get_count());
update_actions(page, page.get_view().get_selected_count(), page.get_view().get_count());
}
public static CommandManager get_command_manager() {
......
......@@ -324,6 +324,76 @@ class ImportPreview : MediaSourceItem {
}
}
public class CameraViewTracker : Core.ViewTracker {
public CameraAccumulator all = new CameraAccumulator();
public CameraAccumulator visible = new CameraAccumulator();
public CameraAccumulator selected = new CameraAccumulator();
public CameraViewTracker(ViewCollection collection) {
base (collection);
start(all, visible, selected);
}
}
public class CameraAccumulator : Object, Core.TrackerAccumulator {
public int total { get; private set; default = 0; }
public int photos { get; private set; default = 0; }
public int videos { get; private set; default = 0; }
public int raw { get; private set; default = 0; }
public bool include(DataObject object) {
ImportSource source = (ImportSource) ((DataView) object).get_source();
total++;
PhotoImportSource? photo = source as PhotoImportSource;
if (photo != null) {
photos++;
if (photo.get_file_format() == PhotoFileFormat.RAW)
raw++;
} else if (source is VideoImportSource) {
videos++;
}
// because of total, always fire "updated"
return true;
}
public bool uninclude(DataObject object) {
ImportSource source = (ImportSource) ((DataView) object).get_source();
total++;
PhotoImportSource? photo = source as PhotoImportSource;
if (photo != null) {
assert(photos > 0);
photos--;
if (photo.get_file_format() == PhotoFileFormat.RAW) {
assert(raw > 0);
raw--;
}
} else if (source is VideoImportSource) {
assert(videos > 0);
videos--;
}
// because of total, always fire "updated"
return true;
}
public bool altered(DataObject object, Alteration alteration) {
// no alteration affects accumulated data
return false;
}
public string to_string() {
return "%d total/%d photos/%d videos/%d raw".printf(total, photos, videos, raw);
}
}
public class ImportPage : CheckerboardPage {
private const string UNMOUNT_FAILED_MSG = _("Unable to unmount camera. Try unmounting the camera from the file manager.");
......@@ -442,7 +512,7 @@ public class ImportPage : CheckerboardPage {
return false;
ImportSource source = ((ImportPreview) view).get_import_source();
// Media type.
if ((bool) (SearchFilterCriteria.MEDIA & get_criteria()) && filter_by_media_type()) {
if (source is VideoImportSource) {
......@@ -458,9 +528,11 @@ public class ImportPage : CheckerboardPage {
}
// Text filter.
if ((bool) (SearchFilterCriteria.TEXT & get_criteria())) {
if (!source.get_filename().down().contains(get_search_filter()))
return false;
if (!is_string_empty(get_search_filter())) {
if ((bool) (SearchFilterCriteria.TEXT & get_criteria())) {
if (!source.get_filename().down().contains(get_search_filter()))
return false;
}
}
return true;
......@@ -485,6 +557,7 @@ public class ImportPage : CheckerboardPage {
private ImportPage? local_ref = null;
private GLib.Icon? icon;
private ImportPageSearchViewFilter search_filter = new ImportPageSearchViewFilter();
private CameraViewTracker tracker;
public enum RefreshResult {
OK,
......@@ -500,6 +573,8 @@ public class ImportPage : CheckerboardPage {
this.import_sources = new ImportSourceCollection("ImportSources for %s".printf(uri));
this.icon = icon;
tracker = new CameraViewTracker(get_view());
// Get camera name.
if (null != display_name) {
camera_name = display_name;
......@@ -606,6 +681,10 @@ public class ImportPage : CheckerboardPage {
Video.global.contents_altered.disconnect(on_media_added_removed);
}
public override Core.ViewTracker? get_view_tracker() {
return tracker;
}
public override GLib.Icon? get_icon() {
return icon != null ? icon : new GLib.ThemedIcon(Resources.ICON_CAMERAS);
}
......
......@@ -157,6 +157,7 @@ public class LibraryWindow : AppWindow {
// Want to instantiate this in the constructor rather than here because the search bar has its
// own UIManager which will suck up the accelerators, and we want them to be associated with
// AppWindows instead.
private SearchFilterActions search_actions = new SearchFilterActions();
private SearchFilterToolbar search_toolbar;
private Gtk.VBox top_section = new Gtk.VBox(false, 0);
......@@ -222,7 +223,7 @@ public class LibraryWindow : AppWindow {
sidebar.cursor_changed.connect(on_sidebar_cursor_changed);
// set search bar's visibility to default state and add its accelerators to the window
search_toolbar = new SearchFilterToolbar();
search_toolbar = new SearchFilterToolbar(search_actions);
search_toolbar.visible = is_search_toolbar_visible;
create_layout(library_page);
......@@ -449,7 +450,7 @@ public class LibraryWindow : AppWindow {
}
groups += common_action_group;
groups += SearchFilterActions.get_instance().get_action_group();
groups += search_actions.get_action_group();
return groups;
}
......@@ -472,6 +473,8 @@ public class LibraryWindow : AppWindow {
if (new_page != null)
new_page.get_view().view_filter_changed.connect(on_view_filter_changed);
search_actions.monitor_page_contents(old_page, new_page);
}
private void on_view_filter_changed(ViewCollection view, ViewFilter? old_filter, ViewFilter? new_filter) {
......@@ -662,27 +665,24 @@ public class LibraryWindow : AppWindow {
import_dialog.destroy();
}
protected override void update_actions(int selected_count, int count) {
protected override void update_actions(Page page, int selected_count, int count) {
// see on_fullscreen for the logic here ... both CollectionPage and EventsDirectoryPage
// are CheckerboardPages (but in on_fullscreen have to be handled differently to locate
// the view controller)
bool can_fullscreen = false;
Page? page = get_current_page();
if (page != null) {
if (page is CheckerboardPage) {
CheckerboardItem? item = ((CheckerboardPage) page).get_fullscreen_photo();
if (item != null)
can_fullscreen = item.get_source() is Photo;
} else if (page is LibraryPhotoPage) {
can_fullscreen = true;
}
if (page is CheckerboardPage) {
CheckerboardItem? item = ((CheckerboardPage) page).get_fullscreen_photo();
if (item != null)
can_fullscreen = item.get_source() is Photo;
} else if (page is LibraryPhotoPage) {
can_fullscreen = true;
}
set_common_action_sensitive("CommonEmptyTrash", can_empty_trash());
set_common_action_sensitive("CommonJumpToEvent", can_jump_to_event());
set_common_action_sensitive("CommonFullscreen", can_fullscreen);
base.update_actions(selected_count, count);
base.update_actions(page, selected_count, count);
}
private void on_trashcan_contents_altered() {
......@@ -803,9 +803,9 @@ public class LibraryWindow : AppWindow {
// if dismissing the toolbar, reset the filter
if (!display)
search_toolbar.reset();
search_actions.reset();
// Ticket #3222 - remember search bar status between sessions.
// Ticket #3222 - remember search bar status between sessions.
Config.get_instance().set_search_bar_hidden(is_search_toolbar_visible);
}
......
......@@ -218,10 +218,13 @@ public abstract class MediaPage : CheckerboardPage {
private ZoomSliderAssembly? connected_slider = null;
private DragAndDropHandler dnd_handler = null;
private MediaViewTracker tracker;
public MediaPage(string page_name) {
base (page_name);
tracker = new MediaViewTracker(get_view());
get_view().set_comparator(get_sort_comparator(), get_sort_comparator_predicate());
get_view().items_altered.connect(on_media_altered);
......@@ -575,6 +578,10 @@ public abstract class MediaPage : CheckerboardPage {
}
}
public override Core.ViewTracker? get_view_tracker() {
return tracker;
}
public void set_display_ratings(bool display) {
get_view().freeze_notifications();
get_view().set_property(Thumbnail.PROP_SHOW_RATINGS, display);
......
/* Copyright 2011 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 MediaViewTracker : Core.ViewTracker {
public MediaAccumulator all = new MediaAccumulator();
public MediaAccumulator visible = new MediaAccumulator();
public MediaAccumulator selected = new MediaAccumulator();
public MediaViewTracker(ViewCollection collection) {
base (collection);
start(all, visible, selected);
}
}
public class MediaAccumulator : Object, Core.TrackerAccumulator {
public int total { get; private set; default = 0; }
public int photos { get; private set; default = 0; }
public int videos { get; private set; default = 0; }
public int raw { get; private set; default = 0; }
public int flagged { get; private set; default = 0; }
public bool include(DataObject object) {
DataSource source = ((DataView) object).get_source();
total++;
Photo? photo = source as Photo;
if (photo != null) {
photos++;
if (photo.get_master_file_format() == PhotoFileFormat.RAW)
raw++;
} else if (source is VideoSource) {
videos++;
}
Flaggable? flaggable = source as Flaggable;
if (flaggable != null && flaggable.is_flagged())
flagged++;
// because of total, always fire "updated"
return true;
}
public bool uninclude(DataObject object) {
DataSource source = ((DataView) object).get_source();
assert(total > 0);
total--;
Photo? photo = source as Photo;
if (photo != null) {
assert(photos > 0);
photos--;
if (photo.get_master_file_format() == PhotoFileFormat.RAW) {
assert(raw > 0);
raw--;
}
} else if (source is Video) {
assert(videos > 0);
videos--;
}
Flaggable? flaggable = source as Flaggable;
if (flaggable != null && flaggable.is_flagged()) {
assert(flagged > 0);
flagged--;
}
// because of total, always fire "updated"
return true;
}
public bool altered(DataObject object, Alteration alteration) {
// the only alteration that can happen to MediaSources this accumulator is concerned with is
// flagging; typeness and raw-ness don't change at runtime
if (!alteration.has_detail("metadata", "flagged"))
return false;
Flaggable? flaggable = ((DataView) object).get_source() as Flaggable;
if (flaggable == null)
return false;
if (flaggable.is_flagged()) {
flagged++;
} else {
assert(flagged > 0);
flagged--;
}
return true;
}
public string to_string() {
return "%d photos/%d videos/%d raw/%d flagged".printf(photos, videos, raw, flagged);
}
}
......@@ -38,6 +38,7 @@ public class OfflinePage : CheckerboardPage {
}
private OfflineSearchViewFilter search_filter = new OfflineSearchViewFilter();
private MediaViewTracker tracker;
private OfflinePage(string name) {
base (name);
......@@ -45,6 +46,8 @@ public class OfflinePage : CheckerboardPage {
init_item_context_menu("/OfflineContextMenu");
init_toolbar("/OfflineToolbar");
tracker = new MediaViewTracker(get_view());
// monitor offline and initialize view with all items in it
LibraryPhoto.global.offline_contents_altered.connect(on_offline_contents_altered);
Video.global.offline_contents_altered.connect(on_offline_contents_altered);
......@@ -96,6 +99,10 @@ public class OfflinePage : CheckerboardPage {
return new Stub();
}
public override Core.ViewTracker? get_view_tracker() {
return tracker;
}
protected override void update_actions(int selected_count, int count) {
set_action_sensitive("RemoveFromLibrary", selected_count > 0);
set_action_important("RemoveFromLibrary", true);
......
......@@ -1348,6 +1348,10 @@ public abstract class CheckerboardPage : Page {
// Gets the search view filter for this page.
public abstract SearchViewFilter get_search_view_filter();
public virtual Core.ViewTracker? get_view_tracker() {
return null;
}
public override void switching_from() {
layout.set_in_view(false);
get_search_view_filter().refresh.disconnect(on_view_filter_refresh);
......
This diff is collapsed.
......@@ -44,6 +44,7 @@ public class TrashPage : CheckerboardPage {
}
private TrashSearchViewFilter search_filter = new TrashSearchViewFilter();
private MediaViewTracker tracker;
private TrashPage(string name) {
base (name);
......@@ -52,6 +53,8 @@ public class TrashPage : CheckerboardPage {
init_page_context_menu("/TrashPageMenu");
init_toolbar("/TrashToolbar");
tracker = new MediaViewTracker(get_view());
// monitor trashcans and initialize view with all items in them
LibraryPhoto.global.trashcan_contents_altered.connect(on_trashcan_contents_altered);
Video.global.trashcan_contents_altered.connect(on_trashcan_contents_altered);
......@@ -103,6 +106,10 @@ public class TrashPage : CheckerboardPage {
return new Stub();
}
public override Core.ViewTracker? get_view_tracker() {
return tracker;
}
protected override void update_actions(int selected_count, int count) {
bool has_selected = selected_count > 0;
......
/* Copyright 2011 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.connect(on_items_added);
collection.items_removed.connect(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);
}
}
}
......@@ -24,7 +24,8 @@ UNIT_FILES := \
DataSource.vala \
DataSourceTypes.vala \
DataView.vala \
DataViewTypes.vala
DataViewTypes.vala \
Tracker.vala
# Any unit this unit relies upon (and should be initialized before it's initialized) should
# be listed here using its Vala namespace.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment