diff options
Diffstat (limited to 'src/SearchFilter.vala')
-rw-r--r-- | src/SearchFilter.vala | 1252 |
1 files changed, 1252 insertions, 0 deletions
diff --git a/src/SearchFilter.vala b/src/SearchFilter.vala new file mode 100644 index 0000000..9769195 --- /dev/null +++ b/src/SearchFilter.vala @@ -0,0 +1,1252 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +// Bitfield values used to specify which search bar features we want. +[Flags] +public enum SearchFilterCriteria { + NONE = 0, + RECURSIVE, + TEXT, + FLAG, + MEDIA, + RATING, + ALL = 0xFFFFFFFF +} + +public enum RatingFilter { + NO_FILTER = 0, + REJECTED_OR_HIGHER = 1, + UNRATED_OR_HIGHER = 2, + ONE_OR_HIGHER = 3, + TWO_OR_HIGHER = 4, + THREE_OR_HIGHER = 5, + FOUR_OR_HIGHER = 6, + FIVE_OR_HIGHER = 7, + REJECTED_ONLY = 8, + UNRATED_ONLY = 9, + ONE_ONLY = 10, + TWO_ONLY = 11, + THREE_ONLY = 12, + FOUR_ONLY = 13, + FIVE_ONLY = 14 +} + + // Handles filtering via rating and text. +public abstract class SearchViewFilter : ViewFilter { + // If this is true, allow the current rating or higher. + private bool rating_allow_higher = true; + + // Rating to filter by. + private Rating rating = Rating.UNRATED; + private RatingFilter rating_filter = RatingFilter.UNRATED_OR_HIGHER; + + // Show flagged only if set to true. + public bool flagged { get; set; default = false; } + + // Media types. + public bool show_media_video { get; set; default = true; } + public bool show_media_photos { get; set; default = true; } + public bool show_media_raw { get; set; default = true; } + + // Search text filter. Should only be set to lower-case. + private string? search_filter = null; + private string[]? search_filter_words = null; + + // Returns a bitmask of SearchFilterCriteria. + // IMPORTANT: There is no signal on this, changing this value after the + // view filter is installed will NOT update the GUI. + public abstract uint get_criteria(); + + public void set_rating_filter(RatingFilter rf) { + rating_filter = rf; + switch (rating_filter) { + case RatingFilter.REJECTED_ONLY: + rating = Rating.REJECTED; + rating_allow_higher = false; + break; + + case RatingFilter.REJECTED_OR_HIGHER: + rating = Rating.REJECTED; + rating_allow_higher = true; + break; + + case RatingFilter.ONE_OR_HIGHER: + rating = Rating.ONE; + rating_allow_higher = true; + break; + + case RatingFilter.ONE_ONLY: + rating = Rating.ONE; + rating_allow_higher = false; + break; + + case RatingFilter.TWO_OR_HIGHER: + rating = Rating.TWO; + rating_allow_higher = true; + break; + + case RatingFilter.TWO_ONLY: + rating = Rating.TWO; + rating_allow_higher = false; + break; + + case RatingFilter.THREE_OR_HIGHER: + rating = Rating.THREE; + rating_allow_higher = true; + break; + + case RatingFilter.THREE_ONLY: + rating = Rating.THREE; + rating_allow_higher = false; + break; + + case RatingFilter.FOUR_OR_HIGHER: + rating = Rating.FOUR; + rating_allow_higher = true; + break; + + case RatingFilter.FOUR_ONLY: + rating = Rating.FOUR; + rating_allow_higher = false; + break; + + case RatingFilter.FIVE_OR_HIGHER: + rating = Rating.FIVE; + rating_allow_higher = true; + break; + + case RatingFilter.FIVE_ONLY: + rating = Rating.FIVE; + rating_allow_higher = false; + break; + + case RatingFilter.UNRATED_OR_HIGHER: + default: + rating = Rating.UNRATED; + rating_allow_higher = true; + break; + } + } + + public bool has_search_filter() { + return !is_string_empty(search_filter); + } + + public unowned string? get_search_filter() { + return search_filter; + } + + public unowned string[]? get_search_filter_words() { + return search_filter_words; + } + + public void set_search_filter(string? text) { + search_filter = !is_string_empty(text) ? text.down() : null; + search_filter_words = search_filter != null ? search_filter.split(" ") : null; + } + + public void clear_search_filter() { + search_filter = null; + search_filter_words = null; + } + + public bool get_rating_allow_higher() { + return rating_allow_higher; + } + + public Rating get_rating() { + return rating; + } + + public bool filter_by_media_type() { + return ((show_media_video || show_media_photos || show_media_raw) && + !(show_media_video && show_media_photos && show_media_raw)); + } +} + +// This class provides a default predicate implementation used for CollectionPage +// as well as Trash and Offline. +public abstract class DefaultSearchViewFilter : SearchViewFilter { + public override bool predicate(DataView view) { + MediaSource source = ((Thumbnail) view).get_media_source(); + uint criteria = get_criteria(); + + // Ratings filter + if ((SearchFilterCriteria.RATING & criteria) != 0) { + if (get_rating_allow_higher() && source.get_rating() < get_rating()) + return false; + else if (!get_rating_allow_higher() && source.get_rating() != get_rating()) + return false; + } + + // Flag state. + if ((SearchFilterCriteria.FLAG & criteria) != 0) { + if (flagged && source is Flaggable && !((Flaggable) source).is_flagged()) + return false; + } + + // Media type. + if (((SearchFilterCriteria.MEDIA & criteria) != 0) && filter_by_media_type()) { + if (source is VideoSource) { + if (!show_media_video) + return false; + } else if (source is Photo) { + Photo photo = source as Photo; + if (photo.get_master_file_format() == PhotoFileFormat.RAW) { + if (photo.is_raw_developer_available(RawDeveloper.CAMERA)) { + if (!show_media_photos && !show_media_raw) + return false; + } else if (!show_media_raw) { + return false; + } + } else if (!show_media_photos) + return false; + } + } + + if (((SearchFilterCriteria.TEXT & criteria) != 0) && has_search_filter()) { + unowned string? media_keywords = source.get_indexable_keywords(); + + unowned string? event_keywords = null; + Event? event = source.get_event(); + if (event != null) + event_keywords = event.get_indexable_keywords(); + + Gee.List<Tag>? tags = Tag.global.fetch_for_source(source); + int tags_size = (tags != null) ? tags.size : 0; + + foreach (unowned string word in get_search_filter_words()) { + if (media_keywords != null && media_keywords.contains(word)) + continue; + + if (event_keywords != null && event_keywords.contains(word)) + continue; + + if (tags_size > 0) { + bool found = false; + for (int ctr = 0; ctr < tags_size; ctr++) { + unowned string? tag_keywords = tags[ctr].get_indexable_keywords(); + if (tag_keywords != null && tag_keywords.contains(word)) { + found = true; + + break; + } + } + + if (found) + continue; + } + + // failed all tests (this even works if none of the Indexables have strings, + // as they fail the implicit AND test) + return false; + } + } + + return true; + } +} + +public class DisabledViewFilter : SearchViewFilter { + public override bool predicate(DataView view) { + return true; + } + + public override uint get_criteria() { + return SearchFilterCriteria.RATING; + } +} + +public class TextAction { + public string? value { + get { + return text; + } + } + + private string? text = null; + private bool sensitive = true; + private bool visible = true; + + public signal void text_changed(string? text); + + public signal void sensitivity_changed(bool sensitive); + + public signal void visibility_changed(bool visible); + + public TextAction(string? init = null) { + text = init; + } + + public void set_text(string? text) { + if (this.text != text) { + this.text = text; + text_changed(text); + } + } + + public void clear() { + set_text(null); + } + + public bool is_sensitive() { + return sensitive; + } + + public void set_sensitive(bool sensitive) { + if (this.sensitive != sensitive) { + this.sensitive = sensitive; + sensitivity_changed(sensitive); + } + } + + public bool is_visible() { + return visible; + } + + public void set_visible(bool visible) { + if (this.visible != visible) { + this.visible = visible; + visibility_changed(visible); + } + } +} + + +public class SearchFilterActions { + public unowned Gtk.ToggleAction? flagged { + get { + return get_action("CommonDisplayFlagged") as Gtk.ToggleAction; + } + } + + public unowned Gtk.ToggleAction? photos { + get { + return get_action("CommonDisplayPhotos") as Gtk.ToggleAction; + } + } + + public unowned Gtk.ToggleAction? videos { + get { + return get_action("CommonDisplayVideos") as Gtk.ToggleAction; + } + } + + public unowned Gtk.ToggleAction? raw { + get { + return get_action("CommonDisplayRaw") as Gtk.ToggleAction; + } + } + + public unowned Gtk.RadioAction? rating { + get { + return get_action("CommonDisplayUnratedOrHigher") as Gtk.RadioAction; + } + } + + public unowned TextAction text { + get { + assert(_text != null); + return _text; + } + } + + private Gtk.ActionGroup action_group = new Gtk.ActionGroup("SearchFilterActionGroup"); + private SearchFilterCriteria criteria = SearchFilterCriteria.ALL; + private TextAction? _text = null; + private bool has_flagged = true; + private bool has_photos = true; + private bool has_videos = true; + private bool has_raw = true; + private bool can_filter_by_stars = true; + + public signal void flagged_toggled(bool on); + + public signal void photos_toggled(bool on); + + public signal void videos_toggled(bool on); + + public signal void raw_toggled(bool on); + + public signal void rating_changed(RatingFilter filter); + + public signal void text_changed(string? text); + + /** + * fired when the kinds of media present in the current view change (e.g., a video becomes + * available in the view through a new import operation or no raw photos are available in + * the view anymore because the last one was moved to the trash) + */ + public signal void media_context_changed(bool has_photos, bool has_videos, bool has_raw, + bool has_flagged); + + // Ticket #3290 - Hide some search bar fields when they + // cannot be used. + // Part 1 - we use this to announce when the criteria have changed, + // and the toolbar can listen for it and hide or show widgets accordingly. + public signal void criteria_changed(); + + public SearchFilterActions() { + // the getters defined above should not be used until register() returns + register(); + + text.text_changed.connect(on_text_changed); + } + + public Gtk.ActionGroup get_action_group() { + return action_group; + } + + public SearchFilterCriteria get_criteria() { + return criteria; + } + + public unowned Gtk.Action? get_action(string name) { + return action_group.get_action(name); + } + + public void set_action_sensitive(string name, bool sensitive) { + Gtk.Action? action = get_action(name); + if (action != null) + action.sensitive = sensitive; + } + + public void reset() { + flagged.active = false; + photos.active = false; + raw.active = false; + videos.active = false; + rating.current_value = RatingFilter.UNRATED_OR_HIGHER; + text.set_text(null); + } + + public void set_sensitive_for_search_criteria(SearchFilterCriteria criteria) { + this.criteria = criteria; + update_sensitivities(); + + // Announce that we've gotten a new criteria... + criteria_changed(); + } + + public void monitor_page_contents(Page? old_page, Page? new_page) { + CheckerboardPage? old_tracked_page = old_page as CheckerboardPage; + if (old_tracked_page != null) { + Core.ViewTracker? tracker = old_tracked_page.get_view_tracker(); + if (tracker is MediaViewTracker) + tracker.updated.disconnect(on_media_tracker_updated); + else if (tracker is CameraViewTracker) + tracker.updated.disconnect(on_camera_tracker_updated); + } + + CheckerboardPage? new_tracked_page = new_page as CheckerboardPage; + if (new_tracked_page != null) { + can_filter_by_stars = true; + + Core.ViewTracker? tracker = new_tracked_page.get_view_tracker(); + if (tracker is MediaViewTracker) { + tracker.updated.connect(on_media_tracker_updated); + on_media_tracker_updated(tracker); + + return; + } else if (tracker is CameraViewTracker) { + tracker.updated.connect(on_camera_tracker_updated); + on_camera_tracker_updated(tracker); + + return; + } + } + + // go with default behavior of making none of the filters available. + has_flagged = false; + has_photos = false; + has_videos = false; + has_raw = false; + can_filter_by_stars = false; + + update_sensitivities(); + } + + private void on_media_tracker_updated(Core.Tracker t) { + MediaViewTracker tracker = (MediaViewTracker) t; + + has_flagged = tracker.all.flagged > 0; + has_photos = tracker.all.photos > 0; + has_videos = tracker.all.videos > 0; + has_raw = tracker.all.raw > 0; + + update_sensitivities(); + } + + private void on_camera_tracker_updated(Core.Tracker t) { + CameraViewTracker tracker = (CameraViewTracker) t; + + has_flagged = false; + has_photos = tracker.all.photos > 0; + has_videos = tracker.all.videos > 0; + has_raw = tracker.all.raw > 0; + + update_sensitivities(); + } + + private void update_sensitivities() { + flagged.set_stock_id(((SearchFilterCriteria.FLAG & criteria) != 0 && has_flagged) ? + Resources.ICON_FILTER_FLAGGED : Resources.ICON_FILTER_FLAGGED_DISABLED); + + bool allow_media = (SearchFilterCriteria.MEDIA & criteria) != 0; + videos.set_stock_id((allow_media && has_videos) ? + Resources.ICON_FILTER_VIDEOS : Resources.ICON_FILTER_VIDEOS_DISABLED); + photos.set_stock_id((allow_media && has_photos) ? + Resources.ICON_FILTER_PHOTOS : Resources.ICON_FILTER_PHOTOS_DISABLED); + raw.set_stock_id((allow_media && has_raw) ? + Resources.ICON_FILTER_RAW : Resources.ICON_FILTER_RAW_DISABLED); + + bool allow_ratings = (SearchFilterCriteria.RATING & criteria) != 0; + set_action_sensitive("CommonDisplayRejectedOnly", allow_ratings & can_filter_by_stars); + set_action_sensitive("CommonDisplayRejectedOrHigher", allow_ratings & can_filter_by_stars); + set_action_sensitive("CommonDisplayUnratedOrHigher", allow_ratings & can_filter_by_stars); + set_action_sensitive("CommonDisplayOneOrHigher", allow_ratings & can_filter_by_stars); + set_action_sensitive("CommonDisplayTwoOrHigher", allow_ratings & can_filter_by_stars); + set_action_sensitive("CommonDisplayThreeOrHigher", allow_ratings & can_filter_by_stars); + set_action_sensitive("CommonDisplayFourOrHigher", allow_ratings & can_filter_by_stars); + set_action_sensitive("CommonDisplayFiveOrHigher", allow_ratings & can_filter_by_stars); + + // Ticket #3343 - Don't disable the text field, even + // when no searchable items are available. + text.set_sensitive(true); + + media_context_changed(has_photos, has_videos, has_raw, has_flagged); + } + + private void on_text_changed(TextAction action, string? text) { + text_changed(text); + } + + private void register() { + _text = new TextAction(); + + Gtk.RadioActionEntry[] view_filter_actions = new Gtk.RadioActionEntry[0]; + + Gtk.RadioActionEntry rejected_only = { "CommonDisplayRejectedOnly", null, TRANSLATABLE, + "<Ctrl>8", TRANSLATABLE, RatingFilter.REJECTED_ONLY }; + rejected_only.label = Resources.DISPLAY_REJECTED_ONLY_MENU; + rejected_only.tooltip = Resources.DISPLAY_REJECTED_ONLY_TOOLTIP; + view_filter_actions += rejected_only; + + Gtk.RadioActionEntry rejected_or_higher = { "CommonDisplayRejectedOrHigher", null, TRANSLATABLE, + "<Ctrl>9", TRANSLATABLE, RatingFilter.REJECTED_OR_HIGHER }; + rejected_or_higher.label = Resources.DISPLAY_REJECTED_OR_HIGHER_MENU; + rejected_or_higher.tooltip = Resources.DISPLAY_REJECTED_OR_HIGHER_TOOLTIP; + view_filter_actions += rejected_or_higher; + + Gtk.RadioActionEntry unrated_or_higher = { "CommonDisplayUnratedOrHigher", null, TRANSLATABLE, + "<Ctrl>0", TRANSLATABLE, RatingFilter.UNRATED_OR_HIGHER }; + unrated_or_higher.label = Resources.DISPLAY_UNRATED_OR_HIGHER_MENU; + unrated_or_higher.tooltip = Resources.DISPLAY_UNRATED_OR_HIGHER_TOOLTIP; + view_filter_actions += unrated_or_higher; + + Gtk.RadioActionEntry one_or_higher = { "CommonDisplayOneOrHigher", null, TRANSLATABLE, + "<Ctrl>1", TRANSLATABLE, RatingFilter.ONE_OR_HIGHER }; + one_or_higher.label = Resources.DISPLAY_ONE_OR_HIGHER_MENU; + one_or_higher.tooltip = Resources.DISPLAY_ONE_OR_HIGHER_TOOLTIP; + view_filter_actions += one_or_higher; + + Gtk.RadioActionEntry two_or_higher = { "CommonDisplayTwoOrHigher", null, TRANSLATABLE, + "<Ctrl>2", TRANSLATABLE, RatingFilter.TWO_OR_HIGHER }; + two_or_higher.label = Resources.DISPLAY_TWO_OR_HIGHER_MENU; + two_or_higher.tooltip = Resources.DISPLAY_TWO_OR_HIGHER_TOOLTIP; + view_filter_actions += two_or_higher; + + Gtk.RadioActionEntry three_or_higher = { "CommonDisplayThreeOrHigher", null, TRANSLATABLE, + "<Ctrl>3", TRANSLATABLE, RatingFilter.THREE_OR_HIGHER }; + three_or_higher.label = Resources.DISPLAY_THREE_OR_HIGHER_MENU; + three_or_higher.tooltip = Resources.DISPLAY_THREE_OR_HIGHER_TOOLTIP; + view_filter_actions += three_or_higher; + + Gtk.RadioActionEntry four_or_higher = { "CommonDisplayFourOrHigher", null, TRANSLATABLE, + "<Ctrl>4", TRANSLATABLE, RatingFilter.FOUR_OR_HIGHER }; + four_or_higher.label = Resources.DISPLAY_FOUR_OR_HIGHER_MENU; + four_or_higher.tooltip = Resources.DISPLAY_FOUR_OR_HIGHER_TOOLTIP; + view_filter_actions += four_or_higher; + + Gtk.RadioActionEntry five_or_higher = { "CommonDisplayFiveOrHigher", null, TRANSLATABLE, + "<Ctrl>5", TRANSLATABLE, RatingFilter.FIVE_OR_HIGHER }; + five_or_higher.label = Resources.DISPLAY_FIVE_OR_HIGHER_MENU; + five_or_higher.tooltip = Resources.DISPLAY_FIVE_OR_HIGHER_TOOLTIP; + view_filter_actions += five_or_higher; + + action_group.add_radio_actions(view_filter_actions, RatingFilter.UNRATED_OR_HIGHER, + on_rating_changed); + + Gtk.ToggleActionEntry[] toggle_actions = new Gtk.ToggleActionEntry[0]; + + Gtk.ToggleActionEntry flagged_action = { "CommonDisplayFlagged", Resources.ICON_FILTER_FLAGGED, + TRANSLATABLE, null, TRANSLATABLE, on_flagged_toggled, false }; + flagged_action.label = _("Flagged"); + flagged_action.tooltip = _("Flagged"); + toggle_actions += flagged_action; + + Gtk.ToggleActionEntry photos_action = { "CommonDisplayPhotos", Resources.ICON_FILTER_PHOTOS, + TRANSLATABLE, null, TRANSLATABLE, on_photos_toggled, false }; + photos_action.label = _("Photos"); + photos_action.tooltip = _("Photos"); + toggle_actions += photos_action; + + Gtk.ToggleActionEntry videos_action = { "CommonDisplayVideos", Resources.ICON_FILTER_VIDEOS, + TRANSLATABLE, null, TRANSLATABLE, on_videos_toggled, false }; + videos_action.label = _("Videos"); + videos_action.tooltip = _("Videos"); + toggle_actions += videos_action; + + Gtk.ToggleActionEntry raw_action = { "CommonDisplayRaw", Resources.ICON_FILTER_RAW, TRANSLATABLE, + null, TRANSLATABLE, on_raw_toggled, false }; + raw_action.label = _("RAW Photos"); + raw_action.tooltip = _("RAW photos"); + toggle_actions += raw_action; + + action_group.add_toggle_actions(toggle_actions, this); + } + + private void on_rating_changed(Gtk.Action action, Gtk.Action current) { + rating_changed((RatingFilter) ((Gtk.RadioAction) current).get_current_value()); + } + + private void on_flagged_toggled(Gtk.Action action) { + flagged_toggled(((Gtk.ToggleAction) action).active); + } + + private void on_photos_toggled(Gtk.Action action) { + photos_toggled(((Gtk.ToggleAction) action).active); + } + + private void on_videos_toggled(Gtk.Action action) { + videos_toggled(((Gtk.ToggleAction) action).active); + } + + private void on_raw_toggled(Gtk.Action action) { + raw_toggled(((Gtk.ToggleAction) action).active); + } + + public bool get_has_photos() { + return has_photos; + } + + public bool get_has_videos() { + return has_videos; + } + + public bool get_has_raw() { + return has_raw; + } + + public bool get_has_flagged() { + return has_flagged; + } +} + +public class SearchFilterToolbar : Gtk.Toolbar { + private const int FILTER_BUTTON_MARGIN = 12; // the distance between icon and edge of button + private const float FILTER_ICON_STAR_SCALE = 0.65f; // changes the size of the filter icon + private const float FILTER_ICON_SCALE = 0.75f; // changes the size of the all photos icon + + // filter_icon_base_width is the width (in px) of a single filter icon such as one star or an "X" + private const int FILTER_ICON_BASE_WIDTH = 30; + // filter_icon_plus_width is the width (in px) of the plus icon + private const int FILTER_ICON_PLUS_WIDTH = 20; + + private class LabelToolItem : Gtk.ToolItem { + private Gtk.Label label; + + public LabelToolItem(string s, int left_padding = 0, int right_padding = 0) { + label = new Gtk.Label(s); + if (left_padding != 0 || right_padding != 0) { + Gtk.Alignment alignment = new Gtk.Alignment(0, 0.5f, 0, 0); + alignment.add(label); + alignment.left_padding = left_padding; + alignment.right_padding = right_padding; + add(alignment); + } else { + add(label); + } + } + + public void set_color(Gdk.RGBA color) { + label.override_color(Gtk.StateFlags.NORMAL, color); + } + } + + private class ToggleActionToolButton : Gtk.ToolItem { + private Gtk.ToggleButton button; + private Gtk.ToggleAction action; + + public ToggleActionToolButton(Gtk.ToggleAction action) { + this.action = action; + button = new Gtk.ToggleButton(); + button.set_can_focus(false); + button.set_active(action.active); + button.clicked.connect(on_button_activate); + button.set_has_tooltip(true); + + restyle(); + + this.add(button); + } + + ~ToggleActionButton() { + button.clicked.disconnect(on_button_activate); + } + + private void on_button_activate() { + action.activate(); + } + + public void set_icon_name(string icon_name) { + Gtk.Image? image = null; + if (icon_name.contains("disabled")) + image = new Gtk.Image.from_stock(icon_name, Gtk.IconSize.SMALL_TOOLBAR); + else + image = new Gtk.Image.from_icon_name(icon_name, Gtk.IconSize.SMALL_TOOLBAR); + + button.set_image(image); + } + + public void restyle() { + string bgcolorname = + Resources.to_css_color(Config.Facade.get_instance().get_bg_color()); + string stylesheet = Resources.SEARCH_BUTTON_STYLESHEET_TEMPLATE.printf(bgcolorname); + + Resources.style_widget(button, stylesheet); + } + } + + // Ticket #3260 - Add a 'close' context menu to + // the searchbar. + // The close menu. Populated below in the constructor. + private Gtk.Menu close_menu = new Gtk.Menu(); + private Gtk.ImageMenuItem close_item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.CLOSE, null); + + // Text search box. + protected class SearchBox : Gtk.ToolItem { + private Gtk.SearchEntry search_entry; + private TextAction action; + + public SearchBox(TextAction action) { + this.action = action; + search_entry = new Gtk.SearchEntry(); + + search_entry.width_chars = 23; + search_entry.key_press_event.connect(on_escape_key); + add(search_entry); + + set_nullable_text(action.value); + + action.text_changed.connect(on_action_text_changed); + action.sensitivity_changed.connect(on_sensitivity_changed); + action.visibility_changed.connect(on_visibility_changed); + + search_entry.buffer.deleted_text.connect(on_entry_changed); + search_entry.buffer.inserted_text.connect(on_entry_changed); + } + + ~SearchBox() { + action.text_changed.disconnect(on_action_text_changed); + action.sensitivity_changed.disconnect(on_sensitivity_changed); + action.visibility_changed.disconnect(on_visibility_changed); + + search_entry.buffer.deleted_text.disconnect(on_entry_changed); + search_entry.buffer.inserted_text.disconnect(on_entry_changed); + } + + public void get_focus() { + search_entry.has_focus = true; + } + + // Ticket #3124 - user should be able to clear + // the search textbox by typing 'Esc'. + private bool on_escape_key(Gdk.EventKey e) { + if(Gdk.keyval_name(e.keyval) == "Escape") + action.clear(); + + // Continue processing this event, since the + // text entry functionality needs to see it too. + return false; + } + + private void on_action_text_changed(string? text) { + search_entry.buffer.deleted_text.disconnect(on_entry_changed); + search_entry.buffer.inserted_text.disconnect(on_entry_changed); + set_nullable_text(text); + search_entry.buffer.deleted_text.connect(on_entry_changed); + search_entry.buffer.inserted_text.connect(on_entry_changed); + } + + private void on_entry_changed() { + action.text_changed.disconnect(on_action_text_changed); + action.set_text(search_entry.get_text()); + action.text_changed.connect(on_action_text_changed); + } + + private void on_sensitivity_changed(bool sensitive) { + this.sensitive = sensitive; + } + + private void on_visibility_changed(bool visible) { + ((Gtk.Widget) this).visible = visible; + } + + private void set_nullable_text(string? text) { + search_entry.set_text(text != null ? text : ""); + } + } + + // Handles ratings filters. + protected class RatingFilterButton : Gtk.ToolItem { + public Gtk.Menu filter_popup = null; + public Gtk.Button button; + + public signal void clicked(); + + public RatingFilterButton() { + button = new Gtk.Button(); + button.set_image(get_filter_icon(RatingFilter.UNRATED_OR_HIGHER)); + button.set_can_focus(false); + + button.clicked.connect(on_clicked); + + restyle(); + + set_homogeneous(false); + + this.add(button); + } + + ~RatingFilterButton() { + button.clicked.disconnect(on_clicked); + } + + private void on_clicked() { + clicked(); + } + + private Gtk.Widget get_filter_icon(RatingFilter filter) { + string filename = null; + + switch (filter) { + case RatingFilter.ONE_OR_HIGHER: + filename = Resources.ICON_FILTER_ONE_OR_BETTER; + break; + + case RatingFilter.TWO_OR_HIGHER: + filename = Resources.ICON_FILTER_TWO_OR_BETTER; + break; + + case RatingFilter.THREE_OR_HIGHER: + filename = Resources.ICON_FILTER_THREE_OR_BETTER; + break; + + case RatingFilter.FOUR_OR_HIGHER: + filename = Resources.ICON_FILTER_FOUR_OR_BETTER; + break; + + case RatingFilter.FIVE_OR_HIGHER: + filename = Resources.ICON_FILTER_FIVE; + break; + + case RatingFilter.REJECTED_OR_HIGHER: + filename = Resources.ICON_FILTER_REJECTED_OR_BETTER; + break; + + case RatingFilter.REJECTED_ONLY: + filename = Resources.ICON_RATING_REJECTED; + break; + + case RatingFilter.UNRATED_OR_HIGHER: + default: + filename = Resources.ICON_FILTER_UNRATED_OR_BETTER; + break; + } + + return new Gtk.Image.from_pixbuf(Resources.load_icon(filename, + get_filter_icon_size(filter))); + } + + private int get_filter_icon_size(RatingFilter filter) { + int icon_base = (int) (FILTER_ICON_BASE_WIDTH * FILTER_ICON_SCALE); + int icon_star_base = (int) (FILTER_ICON_BASE_WIDTH * FILTER_ICON_STAR_SCALE); + int icon_plus = (int) (FILTER_ICON_PLUS_WIDTH * FILTER_ICON_STAR_SCALE); + + switch (filter) { + case RatingFilter.ONE_OR_HIGHER: + return icon_star_base + icon_plus; + case RatingFilter.TWO_OR_HIGHER: + return icon_star_base * 2 + icon_plus; + case RatingFilter.THREE_OR_HIGHER: + return icon_star_base * 3 + icon_plus; + case RatingFilter.FOUR_OR_HIGHER: + return icon_star_base * 4 + icon_plus; + case RatingFilter.FIVE_OR_HIGHER: + case RatingFilter.FIVE_ONLY: + return icon_star_base * 5; + case RatingFilter.REJECTED_OR_HIGHER: + return Resources.ICON_FILTER_REJECTED_OR_BETTER_FIXED_SIZE; + case RatingFilter.UNRATED_OR_HIGHER: + return Resources.ICON_FILTER_UNRATED_OR_BETTER_FIXED_SIZE; + case RatingFilter.REJECTED_ONLY: + return icon_plus; + default: + return icon_base; + } + } + + public void set_filter_icon(RatingFilter filter) { + button.set_image(get_filter_icon(filter)); + set_size_request(get_filter_button_size(filter), -1); + set_tooltip_text(Resources.get_rating_filter_tooltip(filter)); + set_has_tooltip(true); + show_all(); + } + + private int get_filter_button_size(RatingFilter filter) { + return get_filter_icon_size(filter) + 2 * FILTER_BUTTON_MARGIN; + } + + public void restyle() { + string bgcolorname = + Resources.to_css_color(Config.Facade.get_instance().get_bg_color()); + string stylesheet = Resources.SEARCH_BUTTON_STYLESHEET_TEMPLATE.printf(bgcolorname); + + Resources.style_widget(button, stylesheet); + } + } + + public Gtk.UIManager ui = new Gtk.UIManager(); + + private SearchFilterActions actions; + private SearchBox search_box; + private RatingFilterButton rating_button = new RatingFilterButton(); + private SearchViewFilter? search_filter = null; + private LabelToolItem label_type; + private LabelToolItem label_flagged; + private LabelToolItem label_rating; + private ToggleActionToolButton toolbtn_photos; + private ToggleActionToolButton toolbtn_videos; + private ToggleActionToolButton toolbtn_raw; + private ToggleActionToolButton toolbtn_flag; + private Gtk.SeparatorToolItem sepr_mediatype_flagged; + private Gtk.SeparatorToolItem sepr_flagged_rating; + + public SearchFilterToolbar(SearchFilterActions actions) { + this.actions = actions; + actions.media_context_changed.connect(on_media_context_changed); + search_box = new SearchBox(actions.text); + + set_name("search-filter-toolbar"); + set_icon_size(Gtk.IconSize.SMALL_TOOLBAR); + + File ui_file = Resources.get_ui("search_bar.ui"); + try { + ui.add_ui_from_file(ui_file.get_path()); + } catch (Error err) { + AppWindow.panic(_("Error loading UI file %s: %s").printf( + ui_file.get_path(), err.message)); + } + + ui.insert_action_group(actions.get_action_group(), 0); + + // Ticket #3260 - Add a 'close' context menu to + // the searchbar. + // Prepare the close menu for use, but don't + // display it yet; we'll connect it to secondary + // click later on. + ((Gtk.MenuItem) close_item).show(); + close_item.always_show_image = true; + close_item.activate.connect(on_context_menu_close_chosen); + close_menu.append(close_item); + + // Type label and toggles + label_type = new LabelToolItem(_("Type"), 10, 5); + insert(label_type, -1); + + toolbtn_photos = new ToggleActionToolButton(actions.photos); + toolbtn_photos.set_tooltip_text(actions.get_action_group().get_action("CommonDisplayPhotos").tooltip); + + toolbtn_videos = new ToggleActionToolButton(actions.videos); + toolbtn_videos.set_tooltip_text(actions.get_action_group().get_action("CommonDisplayVideos").tooltip); + + toolbtn_raw = new ToggleActionToolButton(actions.raw); + toolbtn_raw.set_tooltip_text(actions.get_action_group().get_action("CommonDisplayRaw").tooltip); + + insert(toolbtn_photos, -1); + insert(toolbtn_videos, -1); + insert(toolbtn_raw, -1); + + // separator + sepr_mediatype_flagged = new Gtk.SeparatorToolItem(); + insert(sepr_mediatype_flagged, -1); + + // Flagged label and toggle + label_flagged = new LabelToolItem(_("Flagged")); + insert(label_flagged, -1); + + toolbtn_flag = new ToggleActionToolButton(actions.flagged); + toolbtn_flag.set_tooltip_text(actions.get_action_group().get_action("CommonDisplayFlagged").tooltip); + + insert(toolbtn_flag, -1); + + // separator + sepr_flagged_rating = new Gtk.SeparatorToolItem(); + insert(sepr_flagged_rating, -1); + + // Rating label and button + label_rating = new LabelToolItem(_("Rating")); + insert(label_rating, -1); + rating_button.filter_popup = (Gtk.Menu) ui.get_widget("/FilterPopupMenu"); + rating_button.set_expand(false); + rating_button.clicked.connect(on_filter_button_clicked); + insert(rating_button, -1); + + // Separator to right-align the text box + Gtk.SeparatorToolItem separator_align = new Gtk.SeparatorToolItem(); + separator_align.set_expand(true); + separator_align.set_draw(false); + insert(separator_align, -1); + + // Search box. + insert(search_box, -1); + + // Set background color of toolbar and update them when the configuration is updated + Config.Facade.get_instance().bg_color_name_changed.connect(on_bg_color_name_changed); + on_bg_color_name_changed(); + + // hook up signals to actions to be notified when they change + actions.flagged_toggled.connect(on_flagged_toggled); + actions.photos_toggled.connect(on_photos_toggled); + actions.videos_toggled.connect(on_videos_toggled); + actions.raw_toggled.connect(on_raw_toggled); + actions.rating_changed.connect(on_rating_changed); + actions.text_changed.connect(on_search_text_changed); + actions.criteria_changed.connect(on_criteria_changed); + + // #3260 part II Hook up close menu. + popup_context_menu.connect(on_context_menu_requested); + + on_media_context_changed(actions.get_has_photos(), actions.get_has_videos(), + actions.get_has_raw(), actions.get_has_flagged()); + } + + ~SearchFilterToolbar() { + Config.Facade.get_instance().bg_color_name_changed.disconnect(on_bg_color_name_changed); + + actions.media_context_changed.disconnect(on_media_context_changed); + + actions.flagged_toggled.disconnect(on_flagged_toggled); + actions.photos_toggled.disconnect(on_photos_toggled); + actions.videos_toggled.disconnect(on_videos_toggled); + actions.raw_toggled.disconnect(on_raw_toggled); + actions.rating_changed.disconnect(on_rating_changed); + actions.text_changed.disconnect(on_search_text_changed); + actions.criteria_changed.disconnect(on_criteria_changed); + + popup_context_menu.disconnect(on_context_menu_requested); + } + + private void on_media_context_changed(bool has_photos, bool has_videos, bool has_raw, + bool has_flagged) { + if (has_photos) + toolbtn_photos.set_icon_name(Resources.ICON_FILTER_PHOTOS); + else + toolbtn_photos.set_icon_name(Resources.ICON_FILTER_PHOTOS_DISABLED); + + if (has_videos) + toolbtn_videos.set_icon_name(Resources.ICON_FILTER_VIDEOS); + else + toolbtn_videos.set_icon_name(Resources.ICON_FILTER_VIDEOS_DISABLED); + + if (has_raw) + toolbtn_raw.set_icon_name(Resources.ICON_FILTER_RAW); + else + toolbtn_raw.set_icon_name(Resources.ICON_FILTER_RAW_DISABLED); + + if (has_flagged) + toolbtn_flag.set_icon_name(Resources.ICON_FILTER_FLAGGED); + else + toolbtn_flag.set_icon_name(Resources.ICON_FILTER_FLAGGED_DISABLED); + } + + private void on_bg_color_name_changed() { + string bgcolorname = + Resources.to_css_color(Config.Facade.get_instance().get_bg_color()); + string toolbar_stylesheet = Resources.TOOLBAR_STYLESHEET_TEMPLATE.printf(bgcolorname); + Resources.style_widget(this, toolbar_stylesheet); + + label_type.set_color(Config.Facade.get_instance().get_unselected_color()); + label_flagged.set_color(Config.Facade.get_instance().get_unselected_color()); + label_rating.set_color(Config.Facade.get_instance().get_unselected_color()); + + toolbtn_photos.restyle(); + toolbtn_videos.restyle(); + toolbtn_raw.restyle(); + toolbtn_flag.restyle(); + rating_button.restyle(); + + } + + // Ticket #3260 part IV - display the context menu on secondary click + private bool on_context_menu_requested(int x, int y, int button) { + close_menu.popup(null, null, null, button, Gtk.get_current_event_time()); + return false; + } + + // Ticket #3260 part III - this runs whenever 'close' + // is chosen in the context menu. + private void on_context_menu_close_chosen() { + AppWindow aw = LibraryWindow.get_app(); + + // Try to obtain the action for toggling the searchbar. If + // it's null, then we're probably in direct edit mode, and + // shouldn't do anything anyway. + Gtk.ToggleAction searchbar_toggle = aw.get_common_action("CommonDisplaySearchbar") as Gtk.ToggleAction; + + // Could we find the appropriate action? + if(searchbar_toggle != null) { + // Yes, hide the search bar. + searchbar_toggle.set_active(false); + } + } + + private void on_flagged_toggled() { + update(); + } + + private void on_videos_toggled() { + update(); + } + + private void on_photos_toggled() { + update(); + } + + private void on_raw_toggled() { + update(); + } + + private void on_search_text_changed() { + update(); + } + + private void on_rating_changed() { + AppWindow aw = LibraryWindow.get_app(); + + if (aw == null) + return; + + Gtk.ToggleAction searchbar_toggle = aw.get_common_action("CommonDisplaySearchbar") as Gtk.ToggleAction; + if(searchbar_toggle != null) + searchbar_toggle.set_active(true); + + update(); + } + + // Ticket #3290, part II - listen for criteria change signals, + // and show or hide widgets based on the criteria we just + // changed to. + private void on_criteria_changed() { + update(); + } + + public void set_view_filter(SearchViewFilter search_filter) { + if (search_filter == this.search_filter) + return; + + this.search_filter = search_filter; + + // Enable/disable toolbar features depending on what the filter offers + actions.set_sensitive_for_search_criteria((SearchFilterCriteria) search_filter.get_criteria()); + rating_button.sensitive = (SearchFilterCriteria.RATING & search_filter.get_criteria()) != 0; + + update(); + } + + public void unset_view_filter() { + set_view_filter(new DisabledViewFilter()); + } + + // Forces an update of the search filter. + public void update() { + if (null == search_filter) { + // Search bar isn't being shown, need to toggle it. + LibraryWindow.get_app().show_search_bar(true); + } + + assert(null != search_filter); + + search_filter.set_search_filter(actions.text.value); + search_filter.flagged = actions.flagged.active; + search_filter.show_media_video = actions.videos.active; + search_filter.show_media_photos = actions.photos.active; + search_filter.show_media_raw = actions.raw.active; + + RatingFilter filter = (RatingFilter) actions.rating.current_value; + search_filter.set_rating_filter(filter); + rating_button.set_filter_icon(filter); + + // Ticket #3290, part III - check the current criteria + // and show or hide widgets as needed. + SearchFilterCriteria criteria = actions.get_criteria(); + + search_box.visible = ((criteria & SearchFilterCriteria.TEXT) != 0); + + rating_button.visible = ((criteria & SearchFilterCriteria.RATING) != 0); + label_rating.visible = ((criteria & SearchFilterCriteria.RATING) != 0); + + label_flagged.visible = ((criteria & SearchFilterCriteria.FLAG) != 0); + toolbtn_flag.visible = ((criteria & SearchFilterCriteria.FLAG) != 0); + + label_type.visible = ((criteria & SearchFilterCriteria.MEDIA) != 0); + toolbtn_photos.visible = ((criteria & SearchFilterCriteria.MEDIA) != 0); + toolbtn_videos.visible = ((criteria & SearchFilterCriteria.MEDIA) != 0); + toolbtn_raw.visible = ((criteria & SearchFilterCriteria.MEDIA) != 0); + + // Ticket #3290, part IV - ensure that the separators + // are shown and/or hidden as needed. + sepr_mediatype_flagged.visible = (label_type.visible && label_flagged.visible); + + sepr_flagged_rating.visible = ((label_type.visible && label_rating.visible) || + (label_flagged.visible && label_rating.visible)); + + // Send update to view collection. + search_filter.refresh(); + } + + private void position_filter_popup(Gtk.Menu menu, out int x, out int y, out bool push_in) { + menu.realize(); + int rx, ry; + rating_button.get_window().get_root_origin(out rx, out ry); + + Gtk.Allocation rating_button_allocation; + rating_button.get_allocation(out rating_button_allocation); + + Gtk.Allocation menubar_allocation; + AppWindow.get_instance().get_current_page().get_menubar().get_allocation(out menubar_allocation); + + int sidebar_w = Config.Facade.get_instance().get_sidebar_position(); + + x = rx + rating_button_allocation.x + sidebar_w; + y = ry + rating_button_allocation.y + rating_button_allocation.height + + menubar_allocation.height; + + push_in = false; + } + + private void on_filter_button_clicked() { + rating_button.filter_popup.popup(null, null, position_filter_popup, 0, + Gtk.get_current_event_time()); + } + + public void take_focus() { + search_box.get_focus(); + } +} + |