summaryrefslogtreecommitdiff
path: root/src/searches
diff options
context:
space:
mode:
Diffstat (limited to 'src/searches')
-rw-r--r--src/searches/Branch.vala150
-rw-r--r--src/searches/SavedSearchDialog.vala829
-rw-r--r--src/searches/SavedSearchPage.vala92
-rw-r--r--src/searches/SearchBoolean.vala971
-rw-r--r--src/searches/Searches.vala31
-rw-r--r--src/searches/mk/searches.mk31
6 files changed, 2104 insertions, 0 deletions
diff --git a/src/searches/Branch.vala b/src/searches/Branch.vala
new file mode 100644
index 0000000..229c710
--- /dev/null
+++ b/src/searches/Branch.vala
@@ -0,0 +1,150 @@
+/* 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.
+ */
+
+public class Searches.Branch : Sidebar.Branch {
+ private Gee.HashMap<SavedSearch, Searches.SidebarEntry> entry_map =
+ new Gee.HashMap<SavedSearch, Searches.SidebarEntry>();
+
+ public Branch() {
+ base (new Searches.Grouping(),
+ Sidebar.Branch.Options.HIDE_IF_EMPTY
+ | Sidebar.Branch.Options.AUTO_OPEN_ON_NEW_CHILD
+ | Sidebar.Branch.Options.STARTUP_EXPAND_TO_FIRST_CHILD,
+ comparator);
+
+ // seed the branch with existing searches
+ foreach (SavedSearch search in SavedSearchTable.get_instance().get_all())
+ on_saved_search_added(search);
+
+ // monitor collection for future events
+ SavedSearchTable.get_instance().search_added.connect(on_saved_search_added);
+ SavedSearchTable.get_instance().search_removed.connect(on_saved_search_removed);
+ }
+
+ ~Branch() {
+ SavedSearchTable.get_instance().search_added.disconnect(on_saved_search_added);
+ SavedSearchTable.get_instance().search_removed.disconnect(on_saved_search_removed);
+ }
+
+ public Searches.SidebarEntry? get_entry_for_saved_search(SavedSearch search) {
+ return entry_map.get(search);
+ }
+
+ private static int comparator(Sidebar.Entry a, Sidebar.Entry b) {
+ if (a == b)
+ return 0;
+
+ return SavedSearch.compare_names(((Searches.SidebarEntry) a).for_saved_search(),
+ ((Searches.SidebarEntry) b).for_saved_search());
+ }
+
+ private void on_saved_search_added(SavedSearch search) {
+ debug("search added");
+ Searches.SidebarEntry entry = new Searches.SidebarEntry(search);
+ entry_map.set(search, entry);
+ graft(get_root(), entry);
+ }
+
+ private void on_saved_search_removed(SavedSearch search) {
+ debug("search removed");
+ Searches.SidebarEntry? entry = entry_map.get(search);
+ assert(entry != null);
+
+ bool is_removed = entry_map.unset(search);
+ assert(is_removed);
+
+ prune(entry);
+ }
+}
+
+public class Searches.Grouping : Sidebar.Grouping, Sidebar.Contextable {
+ private Gtk.UIManager ui = new Gtk.UIManager();
+ private Gtk.Menu? context_menu = null;
+
+ public Grouping() {
+ base (_("Saved Searches"), new ThemedIcon(Gtk.Stock.FIND));
+ setup_context_menu();
+ }
+
+ private void setup_context_menu() {
+ Gtk.ActionGroup group = new Gtk.ActionGroup("SidebarDefault");
+ Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0];
+
+ Gtk.ActionEntry new_search = { "CommonNewSearch", null, TRANSLATABLE, null, null, on_new_search };
+ new_search.label = _("Ne_w Saved Search...");
+ actions += new_search;
+
+ group.add_actions(actions, this);
+ ui.insert_action_group(group, 0);
+
+ File ui_file = Resources.get_ui("search_sidebar_context.ui");
+ try {
+ ui.add_ui_from_file(ui_file.get_path());
+ } catch (Error err) {
+ AppWindow.error_message("Error loading UI file %s: %s".printf(
+ ui_file.get_path(), err.message));
+ Application.get_instance().panic();
+ }
+ context_menu = (Gtk.Menu) ui.get_widget("/SidebarSearchContextMenu");
+
+ ui.ensure_update();
+ }
+
+ public Gtk.Menu? get_sidebar_context_menu(Gdk.EventButton? event) {
+ return context_menu;
+ }
+
+ private void on_new_search() {
+ (new SavedSearchDialog()).show();
+ }
+}
+
+public class Searches.SidebarEntry : Sidebar.SimplePageEntry, Sidebar.RenameableEntry,
+ Sidebar.DestroyableEntry {
+ private static Icon single_search_icon;
+
+ private SavedSearch search;
+
+ public SidebarEntry(SavedSearch search) {
+ this.search = search;
+ }
+
+ internal static void init() {
+ single_search_icon = new ThemedIcon(Gtk.Stock.FIND);
+ }
+
+ internal static void terminate() {
+ single_search_icon = null;
+ }
+
+ public SavedSearch for_saved_search() {
+ return search;
+ }
+
+ public override string get_sidebar_name() {
+ return search.get_name();
+ }
+
+ public override Icon? get_sidebar_icon() {
+ return single_search_icon;
+ }
+
+ protected override Page create_page() {
+ return new SavedSearchPage(search);
+ }
+
+ public void rename(string new_name) {
+ if (!SavedSearchTable.get_instance().exists(new_name))
+ AppWindow.get_command_manager().execute(new RenameSavedSearchCommand(search, new_name));
+ else if (new_name != search.get_name())
+ AppWindow.error_message(Resources.rename_search_exists_message(new_name));
+ }
+
+ public void destroy_source() {
+ if (Dialogs.confirm_delete_saved_search(search))
+ AppWindow.get_command_manager().execute(new DeleteSavedSearchCommand(search));
+ }
+}
diff --git a/src/searches/SavedSearchDialog.vala b/src/searches/SavedSearchDialog.vala
new file mode 100644
index 0000000..b425cfb
--- /dev/null
+++ b/src/searches/SavedSearchDialog.vala
@@ -0,0 +1,829 @@
+/* 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.
+ */
+
+// This dialog displays a boolean search configuration.
+public class SavedSearchDialog {
+
+ // Conatins a search row, with a type selector and remove button.
+ private class SearchRowContainer {
+ public signal void remove(SearchRowContainer this_row);
+ public signal void changed(SearchRowContainer this_row);
+
+ private Gtk.ComboBoxText type_combo;
+ private Gtk.Box box;
+ private Gtk.Alignment align;
+ private Gtk.Button remove_button;
+ private SearchCondition.SearchType[] search_types;
+ private Gee.HashMap<SearchCondition.SearchType, int> search_types_index;
+
+ private SearchRow? my_row = null;
+
+ public SearchRowContainer() {
+ setup_gui();
+ set_type(SearchCondition.SearchType.ANY_TEXT);
+ }
+
+ public SearchRowContainer.edit_existing(SearchCondition sc) {
+ setup_gui();
+ set_type(sc.search_type);
+ set_type_combo_box(sc.search_type);
+ my_row.populate(sc);
+ }
+
+ // Creates the GUI for this row.
+ private void setup_gui() {
+ search_types = SearchCondition.SearchType.as_array();
+ search_types_index = new Gee.HashMap<SearchCondition.SearchType, int>();
+ SearchCondition.SearchType.sort_array(ref search_types);
+
+ type_combo = new Gtk.ComboBoxText();
+ for (int i = 0; i < search_types.length; i++) {
+ SearchCondition.SearchType st = search_types[i];
+ search_types_index.set(st, i);
+ type_combo.append_text(st.display_text());
+ }
+ set_type_combo_box(SearchCondition.SearchType.ANY_TEXT); // Sets default.
+ type_combo.changed.connect(on_type_changed);
+
+ remove_button = new Gtk.Button();
+ remove_button.set_label(" – ");
+ remove_button.button_press_event.connect(on_removed);
+
+ align = new Gtk.Alignment(0,0,0,0);
+
+ box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
+ box.pack_start(type_combo, false, false, 0);
+ box.pack_start(align, false, false, 0);
+ box.pack_start(new Gtk.Alignment(0,0,0,0), true, true, 0); // Fill space.
+ box.pack_start(remove_button, false, false, 0);
+ box.show_all();
+ }
+
+ private void on_type_changed() {
+ set_type(get_search_type());
+ changed(this);
+ }
+
+ private void set_type_combo_box(SearchCondition.SearchType st) {
+ type_combo.set_active(search_types_index.get(st));
+ }
+
+ private void set_type(SearchCondition.SearchType type) {
+ if (my_row != null)
+ align.remove(my_row.get_widget());
+
+ switch (type) {
+ case SearchCondition.SearchType.ANY_TEXT:
+ case SearchCondition.SearchType.EVENT_NAME:
+ case SearchCondition.SearchType.FILE_NAME:
+ case SearchCondition.SearchType.TAG:
+ case SearchCondition.SearchType.COMMENT:
+ case SearchCondition.SearchType.TITLE:
+ my_row = new SearchRowText(this);
+ break;
+
+ case SearchCondition.SearchType.MEDIA_TYPE:
+ my_row = new SearchRowMediaType(this);
+ break;
+
+ case SearchCondition.SearchType.FLAG_STATE:
+ my_row = new SearchRowFlagged(this);
+ break;
+
+ case SearchCondition.SearchType.MODIFIED_STATE:
+ my_row = new SearchRowModified(this);
+ break;
+
+ case SearchCondition.SearchType.RATING:
+ my_row = new SearchRowRating(this);
+ break;
+
+ case SearchCondition.SearchType.DATE:
+ my_row = new SearchRowDate(this);
+ break;
+
+ default:
+ assert(false);
+ break;
+ }
+
+ align.add(my_row.get_widget());
+ }
+
+ public SearchCondition.SearchType get_search_type() {
+ return search_types[type_combo.get_active()];
+ }
+
+ private bool on_removed(Gdk.EventButton event) {
+ remove(this);
+ return false;
+ }
+
+ public void allow_removal(bool allow) {
+ remove_button.sensitive = allow;
+ }
+
+ public Gtk.Widget get_widget() {
+ return box;
+ }
+
+ public SearchCondition get_search_condition() {
+ return my_row.get_search_condition();
+ }
+
+ public bool is_complete() {
+ return my_row.is_complete();
+ }
+ }
+
+ // Represents a row-type.
+ private abstract class SearchRow {
+ // Returns the GUI widget for this row.
+ public abstract Gtk.Widget get_widget();
+
+ // Returns the search condition for this row.
+ public abstract SearchCondition get_search_condition();
+
+ // Fills out the fields in this row based on an existing search condition (for edit mode.)
+ public abstract void populate(SearchCondition sc);
+
+ // Returns true if the row is valid and complete.
+ public abstract bool is_complete();
+ }
+
+ private class SearchRowText : SearchRow {
+ private Gtk.Box box;
+ private Gtk.ComboBoxText text_context;
+ private Gtk.Entry entry;
+
+ private SearchRowContainer parent;
+
+ public SearchRowText(SearchRowContainer parent) {
+ this.parent = parent;
+
+ // Ordering must correspond with SearchConditionText.Context
+ text_context = new Gtk.ComboBoxText();
+ text_context.append_text(_("contains"));
+ text_context.append_text(_("is exactly"));
+ text_context.append_text(_("starts with"));
+ text_context.append_text(_("ends with"));
+ text_context.append_text(_("does not contain"));
+ text_context.append_text(_("is not set"));
+ text_context.set_active(0);
+ text_context.changed.connect(on_changed);
+
+ entry = new Gtk.Entry();
+ entry.set_width_chars(25);
+ entry.set_activates_default(true);
+ entry.changed.connect(on_changed);
+
+ box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
+ box.pack_start(text_context, false, false, 0);
+ box.pack_start(entry, false, false, 0);
+ box.show_all();
+ }
+
+ ~SearchRowText() {
+ text_context.changed.disconnect(on_changed);
+ entry.changed.disconnect(on_changed);
+ }
+
+ public override Gtk.Widget get_widget() {
+ return box;
+ }
+
+ public override SearchCondition get_search_condition() {
+ SearchCondition.SearchType type = parent.get_search_type();
+ string text = entry.get_text();
+ SearchConditionText.Context context = get_text_context();
+ SearchConditionText c = new SearchConditionText(type, text, context);
+ return c;
+ }
+
+ public override void populate(SearchCondition sc) {
+ SearchConditionText? text = sc as SearchConditionText;
+ assert(text != null);
+ text_context.set_active(text.context);
+ entry.set_text(text.text);
+ on_changed();
+ }
+
+ public override bool is_complete() {
+ return entry.text.chomp() != "" || get_text_context() == SearchConditionText.Context.IS_NOT_SET;
+ }
+
+ private SearchConditionText.Context get_text_context() {
+ return (SearchConditionText.Context) text_context.get_active();
+ }
+
+ private void on_changed() {
+ if (get_text_context() == SearchConditionText.Context.IS_NOT_SET) {
+ entry.hide();
+ } else {
+ entry.show();
+ }
+
+ parent.changed(parent);
+ }
+ }
+
+ private class SearchRowMediaType : SearchRow {
+ private Gtk.Box box;
+ private Gtk.ComboBoxText media_context;
+ private Gtk.ComboBoxText media_type;
+
+ private SearchRowContainer parent;
+
+ public SearchRowMediaType(SearchRowContainer parent) {
+ this.parent = parent;
+
+ // Ordering must correspond with SearchConditionMediaType.Context
+ media_context = new Gtk.ComboBoxText();
+ media_context.append_text(_("is"));
+ media_context.append_text(_("is not"));
+ media_context.set_active(0);
+ media_context.changed.connect(on_changed);
+
+ // Ordering must correspond with SearchConditionMediaType.MediaType
+ media_type = new Gtk.ComboBoxText();
+ media_type.append_text(_("any photo"));
+ media_type.append_text(_("a raw photo"));
+ media_type.append_text(_("a video"));
+ media_type.set_active(0);
+ media_type.changed.connect(on_changed);
+
+ box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
+ box.pack_start(media_context, false, false, 0);
+ box.pack_start(media_type, false, false, 0);
+ box.show_all();
+ }
+
+ ~SearchRowMediaType() {
+ media_context.changed.disconnect(on_changed);
+ media_type.changed.disconnect(on_changed);
+ }
+
+ public override Gtk.Widget get_widget() {
+ return box;
+ }
+
+ public override SearchCondition get_search_condition() {
+ SearchCondition.SearchType search_type = parent.get_search_type();
+ SearchConditionMediaType.Context context = (SearchConditionMediaType.Context) media_context.get_active();
+ SearchConditionMediaType.MediaType type = (SearchConditionMediaType.MediaType) media_type.get_active();
+ SearchConditionMediaType c = new SearchConditionMediaType(search_type, context, type);
+ return c;
+ }
+
+ public override void populate(SearchCondition sc) {
+ SearchConditionMediaType? media = sc as SearchConditionMediaType;
+ assert(media != null);
+ media_context.set_active(media.context);
+ media_type.set_active(media.media_type);
+ }
+
+ public override bool is_complete() {
+ return true;
+ }
+
+ private void on_changed() {
+ parent.changed(parent);
+ }
+ }
+
+ private class SearchRowModified : SearchRow {
+ private Gtk.Box box;
+ private Gtk.ComboBoxText modified_context;
+ private Gtk.ComboBoxText modified_state;
+
+ private SearchRowContainer parent;
+
+ public SearchRowModified(SearchRowContainer parent) {
+ this.parent = parent;
+
+ modified_context = new Gtk.ComboBoxText();
+ modified_context.append_text(_("has"));
+ modified_context.append_text(_("has no"));
+ modified_context.set_active(0);
+ modified_context.changed.connect(on_changed);
+
+ modified_state = new Gtk.ComboBoxText();
+ modified_state.append_text(_("modifications"));
+ modified_state.append_text(_("internal modifications"));
+ modified_state.append_text(_("external modifications"));
+ modified_state.set_active(0);
+ modified_state.changed.connect(on_changed);
+
+ box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
+ box.pack_start(modified_context, false, false, 0);
+ box.pack_start(modified_state, false, false, 0);
+ box.show_all();
+ }
+
+ ~SearchRowModified() {
+ modified_state.changed.disconnect(on_changed);
+ modified_context.changed.disconnect(on_changed);
+ }
+
+ public override Gtk.Widget get_widget() {
+ return box;
+ }
+
+ public override SearchCondition get_search_condition() {
+ SearchCondition.SearchType search_type = parent.get_search_type();
+ SearchConditionModified.Context context = (SearchConditionModified.Context) modified_context.get_active();
+ SearchConditionModified.State state = (SearchConditionModified.State) modified_state.get_active();
+ SearchConditionModified c = new SearchConditionModified(search_type, context, state);
+ return c;
+ }
+
+ public override void populate(SearchCondition sc) {
+ SearchConditionModified? scm = sc as SearchConditionModified;
+ assert(scm != null);
+ modified_state.set_active(scm.state);
+ modified_context.set_active(scm.context);
+ }
+
+ public override bool is_complete() {
+ return true;
+ }
+
+ private void on_changed() {
+ parent.changed(parent);
+ }
+ }
+
+ private class SearchRowFlagged : SearchRow {
+ private Gtk.Box box;
+ private Gtk.ComboBoxText flagged_state;
+
+ private SearchRowContainer parent;
+
+ public SearchRowFlagged(SearchRowContainer parent) {
+ this.parent = parent;
+
+ // Ordering must correspond with SearchConditionFlagged.State
+ flagged_state = new Gtk.ComboBoxText();
+ flagged_state.append_text(_("flagged"));
+ flagged_state.append_text(_("not flagged"));
+ flagged_state.set_active(0);
+ flagged_state.changed.connect(on_changed);
+
+ box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
+ box.pack_start(new Gtk.Label(_("is")), false, false, 0);
+ box.pack_start(flagged_state, false, false, 0);
+ box.show_all();
+ }
+
+ ~SearchRowFlagged() {
+ flagged_state.changed.disconnect(on_changed);
+ }
+
+ public override Gtk.Widget get_widget() {
+ return box;
+ }
+
+ public override SearchCondition get_search_condition() {
+ SearchCondition.SearchType search_type = parent.get_search_type();
+ SearchConditionFlagged.State state = (SearchConditionFlagged.State) flagged_state.get_active();
+ SearchConditionFlagged c = new SearchConditionFlagged(search_type, state);
+ return c;
+ }
+
+ public override void populate(SearchCondition sc) {
+ SearchConditionFlagged? f = sc as SearchConditionFlagged;
+ assert(f != null);
+ flagged_state.set_active(f.state);
+ }
+
+ public override bool is_complete() {
+ return true;
+ }
+
+ private void on_changed() {
+ parent.changed(parent);
+ }
+ }
+
+ private class SearchRowRating : SearchRow {
+ private Gtk.Box box;
+ private Gtk.ComboBoxText rating;
+ private Gtk.ComboBoxText context;
+
+ private SearchRowContainer parent;
+
+ public SearchRowRating(SearchRowContainer parent) {
+ this.parent = parent;
+
+ // Ordering must correspond with Rating
+ rating = new Gtk.ComboBoxText();
+ rating.append_text(Resources.rating_combo_box(Rating.REJECTED));
+ rating.append_text(Resources.rating_combo_box(Rating.UNRATED));
+ rating.append_text(Resources.rating_combo_box(Rating.ONE));
+ rating.append_text(Resources.rating_combo_box(Rating.TWO));
+ rating.append_text(Resources.rating_combo_box(Rating.THREE));
+ rating.append_text(Resources.rating_combo_box(Rating.FOUR));
+ rating.append_text(Resources.rating_combo_box(Rating.FIVE));
+ rating.set_active(0);
+ rating.changed.connect(on_changed);
+
+ context = new Gtk.ComboBoxText();
+ context.append_text(_("and higher"));
+ context.append_text(_("only"));
+ context.append_text(_("and lower"));
+ context.set_active(0);
+ context.changed.connect(on_changed);
+
+ box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
+ box.pack_start(new Gtk.Label(_("is")), false, false, 0);
+ box.pack_start(rating, false, false, 0);
+ box.pack_start(context, false, false, 0);
+ box.show_all();
+ }
+
+ ~SearchRowRating() {
+ rating.changed.disconnect(on_changed);
+ context.changed.disconnect(on_changed);
+ }
+
+ public override Gtk.Widget get_widget() {
+ return box;
+ }
+
+ public override SearchCondition get_search_condition() {
+ SearchCondition.SearchType search_type = parent.get_search_type();
+ Rating search_rating = (Rating) rating.get_active() + Rating.REJECTED;
+ SearchConditionRating.Context search_context = (SearchConditionRating.Context) context.get_active();
+ SearchConditionRating c = new SearchConditionRating(search_type, search_rating, search_context);
+ return c;
+ }
+
+ public override void populate(SearchCondition sc) {
+ SearchConditionRating? r = sc as SearchConditionRating;
+ assert(r != null);
+ context.set_active(r.context);
+ rating.set_active(r.rating - Rating.REJECTED);
+ }
+
+ public override bool is_complete() {
+ return true;
+ }
+
+ private void on_changed() {
+ parent.changed(parent);
+ }
+ }
+
+ private class SearchRowDate : SearchRow {
+ private const string DATE_FORMAT = "%x";
+ private Gtk.Box box;
+ private Gtk.ComboBoxText context;
+ private Gtk.Button label_one;
+ private Gtk.Button label_two;
+ private Gtk.Calendar cal_one;
+ private Gtk.Calendar cal_two;
+ private Gtk.Label and;
+
+ private SearchRowContainer parent;
+
+ public SearchRowDate(SearchRowContainer parent) {
+ this.parent = parent;
+
+ // Ordering must correspond with Context
+ context = new Gtk.ComboBoxText();
+ context.append_text(_("is exactly"));
+ context.append_text(_("is after"));
+ context.append_text(_("is before"));
+ context.append_text(_("is between"));
+ context.append_text(_("is not set"));
+ context.set_active(0);
+ context.changed.connect(on_changed);
+
+ cal_one = new Gtk.Calendar();
+ cal_two = new Gtk.Calendar();
+
+ label_one = new Gtk.Button();
+ label_one.clicked.connect(on_one_clicked);
+ label_two = new Gtk.Button();
+ label_two.clicked.connect(on_two_clicked);
+
+ and = new Gtk.Label(_("and"));
+
+ box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8);
+ box.pack_start(context, false, false, 0);
+ box.pack_start(label_one, false, false, 0);
+ box.pack_start(and, false, false, 0);
+ box.pack_start(label_two, false, false, 0);
+
+ box.show_all();
+ update_date_labels();
+ }
+
+ ~SearchRowRating() {
+ context.changed.disconnect(on_changed);
+ }
+
+ private void update_date_labels() {
+ SearchConditionDate.Context c = (SearchConditionDate.Context) context.get_active();
+
+ // Only show "and" and 2nd date label for between mode.
+ if (c == SearchConditionDate.Context.BETWEEN) {
+ label_one.show();
+ and.show();
+ label_two.show();
+ } else if (c == SearchConditionDate.Context.IS_NOT_SET) {
+ label_one.hide();
+ and.hide();
+ label_two.hide();
+ } else {
+ label_one.show();
+ and.hide();
+ label_two.hide();
+ }
+
+ // Set label text to date.
+ label_one.label = get_date_one().format(DATE_FORMAT);
+ label_two.label = get_date_two().format(DATE_FORMAT);;
+ }
+
+ public override Gtk.Widget get_widget() {
+ return box;
+ }
+
+ private DateTime get_date_one() {
+ return new DateTime.local(cal_one.year, cal_one.month + 1, cal_one.day, 0, 0, 0.0);
+ }
+
+ private DateTime get_date_two() {
+ return new DateTime.local(cal_two.year, cal_two.month + 1, cal_two.day, 0, 0, 0.0);
+ }
+
+ private void set_date_one(DateTime date) {
+ cal_one.day = date.get_day_of_month();
+ cal_one.month = date.get_month() - 1;
+ cal_one.year = date.get_year();
+ }
+
+ private void set_date_two(DateTime date) {
+ cal_two.day = date.get_day_of_month();
+ cal_two.month = date.get_month() - 1;
+ cal_two.year = date.get_year();
+ }
+
+ public override SearchCondition get_search_condition() {
+ SearchCondition.SearchType search_type = parent.get_search_type();
+ SearchConditionDate.Context search_context = (SearchConditionDate.Context) context.get_active();
+ SearchConditionDate c = new SearchConditionDate(search_type, search_context, get_date_one(),
+ get_date_two());
+ return c;
+ }
+
+ public override void populate(SearchCondition sc) {
+ SearchConditionDate? cond = sc as SearchConditionDate;
+ assert(cond != null);
+ context.set_active(cond.context);
+ set_date_one(cond.date_one);
+ set_date_two(cond.date_two);
+ update_date_labels();
+ }
+
+ public override bool is_complete() {
+ return true;
+ }
+
+ private void on_changed() {
+ parent.changed(parent);
+ update_date_labels();
+ }
+
+ private void popup_calendar(Gtk.Calendar cal) {
+ int orig_day = cal.day;
+ int orig_month = cal.month;
+ int orig_year = cal.year;
+ Gtk.Dialog d = new Gtk.Dialog.with_buttons(null, null,
+ Gtk.DialogFlags.MODAL, Gtk.Stock.CANCEL, Gtk.ResponseType.REJECT,
+ Gtk.Stock.OK, Gtk.ResponseType.ACCEPT);
+ d.set_modal(true);
+ d.set_resizable(false);
+ d.set_decorated(false);
+ ((Gtk.Box) d.get_content_area()).add(cal);
+ ulong id_1 = cal.day_selected.connect(()=>{update_date_labels();});
+ ulong id_2 = cal.day_selected_double_click.connect(()=>{d.close();});
+ d.show_all();
+ int res = d.run();
+ if (res != Gtk.ResponseType.ACCEPT) {
+ // User hit cancel, restore original date.
+ cal.day = orig_day;
+ cal.month = orig_month;
+ cal.year = orig_year;
+ }
+ cal.disconnect(id_1);
+ cal.disconnect(id_2);
+ d.destroy();
+ update_date_labels();
+ }
+
+ private void on_one_clicked() {
+ popup_calendar(cal_one);
+ }
+
+ private void on_two_clicked() {
+ popup_calendar(cal_two);
+ }
+ }
+
+ private Gtk.Builder builder;
+ private Gtk.Dialog dialog;
+ private Gtk.Button add_criteria;
+ private Gtk.ComboBoxText operator;
+ private Gtk.Box row_box;
+ private Gtk.Entry search_title;
+ private Gee.ArrayList<SearchRowContainer> row_list = new Gee.ArrayList<SearchRowContainer>();
+ private bool edit_mode = false;
+ private SavedSearch? previous_search = null;
+ private bool valid = false;
+
+ public SavedSearchDialog() {
+ setup_dialog();
+
+ // Default name.
+ search_title.set_text(SavedSearchTable.get_instance().generate_unique_name());
+ search_title.select_region(0, -1); // select all
+
+ // Default is text search.
+ add_text_search();
+ row_list.get(0).allow_removal(false);
+
+ // Add buttons for new search.
+ dialog.add_action_widget(new Gtk.Button.from_stock(Gtk.Stock.CANCEL), Gtk.ResponseType.CANCEL);
+ Gtk.Button ok_button = new Gtk.Button.from_stock(Gtk.Stock.OK);
+ ok_button.can_default = true;
+ dialog.add_action_widget(ok_button, Gtk.ResponseType.OK);
+ dialog.set_default_response(Gtk.ResponseType.OK);
+
+ dialog.show_all();
+ set_valid(false);
+ }
+
+ public SavedSearchDialog.edit_existing(SavedSearch saved_search) {
+ previous_search = saved_search;
+ edit_mode = true;
+ setup_dialog();
+
+ // Add close button.
+ Gtk.Button close_button = new Gtk.Button.from_stock(Gtk.Stock.CLOSE);
+ close_button.can_default = true;
+ dialog.add_action_widget(close_button, Gtk.ResponseType.OK);
+ dialog.set_default_response(Gtk.ResponseType.OK);
+
+ dialog.show_all();
+
+ // Load existing search into dialog.
+ operator.set_active((SearchOperator) saved_search.get_operator());
+ search_title.set_text(saved_search.get_name());
+ foreach (SearchCondition sc in saved_search.get_conditions()) {
+ add_row(new SearchRowContainer.edit_existing(sc));
+ }
+
+ if (row_list.size == 1)
+ row_list.get(0).allow_removal(false);
+
+ set_valid(true);
+ }
+
+ ~SavedSearchDialog() {
+ search_title.changed.disconnect(on_title_changed);
+ }
+
+ // Builds the dialog UI. Doesn't add buttons to the dialog or call dialog.show().
+ private void setup_dialog() {
+ builder = AppWindow.create_builder();
+
+ dialog = builder.get_object("Search criteria") as Gtk.Dialog;
+ dialog.set_parent_window(AppWindow.get_instance().get_parent_window());
+ dialog.set_transient_for(AppWindow.get_instance());
+ dialog.response.connect(on_response);
+
+ add_criteria = builder.get_object("Add search button") as Gtk.Button;
+ add_criteria.button_press_event.connect(on_add_criteria);
+
+ search_title = builder.get_object("Search title") as Gtk.Entry;
+ search_title.set_activates_default(true);
+ search_title.changed.connect(on_title_changed);
+
+ row_box = builder.get_object("row_box") as Gtk.Box;
+
+ operator = builder.get_object("Type of search criteria") as Gtk.ComboBoxText;
+ operator.append_text(_("any"));
+ operator.append_text(_("all"));
+ operator.append_text(_("none"));
+ operator.set_active(0);
+ }
+
+ // Displays the dialog.
+ public void show() {
+ dialog.run();
+ dialog.destroy();
+ }
+
+ // Adds a row of search criteria.
+ private bool on_add_criteria(Gdk.EventButton event) {
+ add_text_search();
+ return false;
+ }
+
+ private void add_text_search() {
+ SearchRowContainer text = new SearchRowContainer();
+ add_row(text);
+ }
+
+ // Appends a row of search criteria to the list and table.
+ private void add_row(SearchRowContainer row) {
+ if (row_list.size == 1)
+ row_list.get(0).allow_removal(true);
+ row_box.add(row.get_widget());
+ row_list.add(row);
+ row.remove.connect(on_remove_row);
+ row.changed.connect(on_row_changed);
+ set_valid(row.is_complete());
+ }
+
+ // Removes a row of search criteria.
+ private void on_remove_row(SearchRowContainer row) {
+ row.remove.disconnect(on_remove_row);
+ row.changed.disconnect(on_row_changed);
+ row_box.remove(row.get_widget());
+ row_list.remove(row);
+ if (row_list.size == 1)
+ row_list.get(0).allow_removal(false);
+ set_valid(true); // try setting to "true" since we removed a row
+ }
+
+ private void on_response(int response_id) {
+ if (response_id == Gtk.ResponseType.OK) {
+ if (SavedSearchTable.get_instance().exists(search_title.get_text()) &&
+ !(edit_mode && previous_search.get_name() == search_title.get_text())) {
+ AppWindow.error_message(Resources.rename_search_exists_message(search_title.get_text()));
+ return;
+ }
+
+ if (edit_mode) {
+ // Remove previous search.
+ SavedSearchTable.get_instance().remove(previous_search);
+ }
+
+ // Build the condition list from the search rows, and add our new saved search to the table.
+ Gee.ArrayList<SearchCondition> conditions = new Gee.ArrayList<SearchCondition>();
+ foreach (SearchRowContainer c in row_list) {
+ conditions.add(c.get_search_condition());
+ }
+
+ // Create the object. It will be added to the DB and SearchTable automatically.
+ SearchOperator search_operator = (SearchOperator)operator.get_active();
+ SavedSearchTable.get_instance().create(search_title.get_text(), search_operator, conditions);
+ }
+ }
+
+ private void on_row_changed(SearchRowContainer row) {
+ set_valid(row.is_complete());
+ }
+
+ private void on_title_changed() {
+ set_valid(is_title_valid());
+ }
+
+ private bool is_title_valid() {
+ if (edit_mode && previous_search != null &&
+ previous_search.get_name() == search_title.get_text())
+ return true; // Title hasn't changed.
+ if (search_title.get_text().chomp() == "")
+ return false;
+ if (SavedSearchTable.get_instance().exists(search_title.get_text()))
+ return false;
+ return true;
+ }
+
+ // Call this with your new value for validity whenever a row or the title changes.
+ private void set_valid(bool v) {
+ if (!v) {
+ valid = false;
+ } else if (v != valid) {
+ if (is_title_valid()) {
+ // Go through rows to check validity.
+ int valid_rows = 0;
+ foreach (SearchRowContainer c in row_list) {
+ if (c.is_complete())
+ valid_rows++;
+ }
+ valid = (valid_rows == row_list.size);
+ } else {
+ valid = false; // title was invalid
+ }
+ }
+
+ dialog.set_response_sensitive(Gtk.ResponseType.OK, valid);
+ }
+}
diff --git a/src/searches/SavedSearchPage.vala b/src/searches/SavedSearchPage.vala
new file mode 100644
index 0000000..8e6672e
--- /dev/null
+++ b/src/searches/SavedSearchPage.vala
@@ -0,0 +1,92 @@
+/* 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.
+ */
+
+// Source monitoring for saved searches.
+private class SavedSearchManager : CollectionViewManager {
+ SavedSearch search;
+ public SavedSearchManager(SavedSearchPage owner, SavedSearch search) {
+ base (owner);
+ this.search = search;
+ }
+
+ public override bool include_in_view(DataSource source) {
+ return search.predicate((MediaSource) source);
+ }
+}
+
+// Page for displaying saved searches.
+public class SavedSearchPage : CollectionPage {
+
+ // The search logic and parameters are contained in the SavedSearch.
+ private SavedSearch search;
+
+ public SavedSearchPage(SavedSearch search) {
+ base (search.get_name());
+ this.search = search;
+
+
+ foreach (MediaSourceCollection sources in MediaCollectionRegistry.get_instance().get_all())
+ get_view().monitor_source_collection(sources, new SavedSearchManager(this, search), null);
+
+ init_page_context_menu("/SearchContextMenu");
+ }
+
+ protected override void get_config_photos_sort(out bool sort_order, out int sort_by) {
+ Config.Facade.get_instance().get_library_photos_sort(out sort_order, out sort_by);
+ }
+
+ protected override void set_config_photos_sort(bool sort_order, int sort_by) {
+ Config.Facade.get_instance().set_library_photos_sort(sort_order, sort_by);
+ }
+
+ protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
+ base.init_collect_ui_filenames(ui_filenames);
+ ui_filenames.add("savedsearch.ui");
+ }
+
+ protected override Gtk.ActionEntry[] init_collect_action_entries() {
+ Gtk.ActionEntry[] actions = base.init_collect_action_entries();
+
+ Gtk.ActionEntry rename_search = { "RenameSearch", null, TRANSLATABLE, null, null, on_rename_search };
+ actions += rename_search;
+
+ Gtk.ActionEntry edit_search = { "EditSearch", null, TRANSLATABLE, null, null, on_edit_search };
+ actions += edit_search;
+
+ Gtk.ActionEntry delete_search = { "DeleteSearch", null, TRANSLATABLE, null, null, on_delete_search };
+ actions += delete_search;
+
+ return actions;
+ }
+
+ private void on_delete_search() {
+ if (Dialogs.confirm_delete_saved_search(search))
+ AppWindow.get_command_manager().execute(new DeleteSavedSearchCommand(search));
+ }
+
+ private void on_rename_search() {
+ LibraryWindow.get_app().rename_search_in_sidebar(search);
+ }
+
+ private void on_edit_search() {
+ SavedSearchDialog ssd = new SavedSearchDialog.edit_existing(search);
+ ssd.show();
+ }
+
+ protected override void update_actions(int selected_count, int count) {
+ set_action_details("RenameSearch",
+ Resources.RENAME_SEARCH_MENU,
+ null, true);
+ set_action_details("EditSearch",
+ Resources.EDIT_SEARCH_MENU,
+ null, true);
+ set_action_details("DeleteSearch",
+ Resources.DELETE_SEARCH_MENU,
+ null, true);
+ base.update_actions(selected_count, count);
+ }
+}
+
diff --git a/src/searches/SearchBoolean.vala b/src/searches/SearchBoolean.vala
new file mode 100644
index 0000000..431e398
--- /dev/null
+++ b/src/searches/SearchBoolean.vala
@@ -0,0 +1,971 @@
+/* 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.
+ */
+
+// For specifying whether a search should be ORed (any) or ANDed (all).
+public enum SearchOperator {
+ ANY = 0,
+ ALL,
+ NONE;
+
+ public string to_string() {
+ switch (this) {
+ case SearchOperator.ANY:
+ return "ANY";
+
+ case SearchOperator.ALL:
+ return "ALL";
+
+ case SearchOperator.NONE:
+ return "NONE";
+
+ default:
+ error("unrecognized search operator enumeration value");
+ }
+ }
+
+ public static SearchOperator from_string(string str) {
+ if (str == "ANY")
+ return SearchOperator.ANY;
+
+ else if (str == "ALL")
+ return SearchOperator.ALL;
+
+ else if (str == "NONE")
+ return SearchOperator.NONE;
+
+ else
+ error("unrecognized search operator name: %s", str);
+ }
+}
+
+// Important note: if you are adding, removing, or otherwise changing
+// this table, you're going to have to modify SavedSearchDBTable.vala
+// as well.
+public abstract class SearchCondition {
+ // Type of search condition.
+ public enum SearchType {
+ ANY_TEXT = 0,
+ TITLE,
+ TAG,
+ EVENT_NAME,
+ FILE_NAME,
+ MEDIA_TYPE,
+ FLAG_STATE,
+ MODIFIED_STATE,
+ RATING,
+ COMMENT,
+ DATE;
+ // Note: when adding new types, be sure to update all functions below.
+
+ public static SearchType[] as_array() {
+ return { ANY_TEXT, TITLE, TAG, COMMENT, EVENT_NAME, FILE_NAME,
+ MEDIA_TYPE, FLAG_STATE, MODIFIED_STATE, RATING, DATE };
+ }
+
+ // Sorts an array alphabetically by display name.
+ public static void sort_array(ref SearchType[] array) {
+ Posix.qsort(array, array.length, sizeof(SearchType), (a, b) => {
+ return utf8_cs_compare(((*(SearchType*) a)).display_text(),
+ ((*(SearchType*) b)).display_text());
+ });
+ }
+
+ public string to_string() {
+ switch (this) {
+ case SearchType.ANY_TEXT:
+ return "ANY_TEXT";
+
+ case SearchType.TITLE:
+ return "TITLE";
+
+ case SearchType.TAG:
+ return "TAG";
+
+ case SearchType.COMMENT:
+ return "COMMENT";
+
+ case SearchType.EVENT_NAME:
+ return "EVENT_NAME";
+
+ case SearchType.FILE_NAME:
+ return "FILE_NAME";
+
+ case SearchType.MEDIA_TYPE:
+ return "MEDIA_TYPE";
+
+ case SearchType.FLAG_STATE:
+ return "FLAG_STATE";
+
+ case SearchType.MODIFIED_STATE:
+ return "MODIFIED_STATE";
+
+ case SearchType.RATING:
+ return "RATING";
+
+ case SearchType.DATE:
+ return "DATE";
+
+ default:
+ error("unrecognized search type enumeration value");
+ }
+ }
+
+ public static SearchType from_string(string str) {
+ if (str == "ANY_TEXT")
+ return SearchType.ANY_TEXT;
+
+ else if (str == "TITLE")
+ return SearchType.TITLE;
+
+ else if (str == "TAG")
+ return SearchType.TAG;
+
+ else if (str == "COMMENT")
+ return SearchType.COMMENT;
+
+ else if (str == "EVENT_NAME")
+ return SearchType.EVENT_NAME;
+
+ else if (str == "FILE_NAME")
+ return SearchType.FILE_NAME;
+
+ else if (str == "MEDIA_TYPE")
+ return SearchType.MEDIA_TYPE;
+
+ else if (str == "FLAG_STATE")
+ return SearchType.FLAG_STATE;
+
+ else if (str == "MODIFIED_STATE")
+ return SearchType.MODIFIED_STATE;
+
+ else if (str == "RATING")
+ return SearchType.RATING;
+
+ else if (str == "DATE")
+ return SearchType.DATE;
+
+ else
+ error("unrecognized search type name: %s", str);
+ }
+
+ public string display_text() {
+ switch (this) {
+ case SearchType.ANY_TEXT:
+ return _("Any text");
+
+ case SearchType.TITLE:
+ return _("Title");
+
+ case SearchType.TAG:
+ return _("Tag");
+
+ case SearchType.COMMENT:
+ return _("Comment");
+
+ case SearchType.EVENT_NAME:
+ return _("Event name");
+
+ case SearchType.FILE_NAME:
+ return _("File name");
+
+ case SearchType.MEDIA_TYPE:
+ return _("Media type");
+
+ case SearchType.FLAG_STATE:
+ return _("Flag state");
+
+ case SearchType.MODIFIED_STATE:
+ return _("Photo state");
+
+ case SearchType.RATING:
+ return _("Rating");
+
+ case SearchType.DATE:
+ return _("Date");
+
+ default:
+ error("unrecognized search type enumeration value");
+ }
+ }
+ }
+
+ public SearchType search_type { get; protected set; }
+
+ // Determines whether the source is included.
+ public abstract bool predicate(MediaSource source);
+}
+
+// Condition for text matching.
+public class SearchConditionText : SearchCondition {
+ public enum Context {
+ CONTAINS = 0,
+ IS_EXACTLY,
+ STARTS_WITH,
+ ENDS_WITH,
+ DOES_NOT_CONTAIN,
+ IS_NOT_SET;
+
+ public string to_string() {
+ switch (this) {
+ case Context.CONTAINS:
+ return "CONTAINS";
+
+ case Context.IS_EXACTLY:
+ return "IS_EXACTLY";
+
+ case Context.STARTS_WITH:
+ return "STARTS_WITH";
+
+ case Context.ENDS_WITH:
+ return "ENDS_WITH";
+
+ case Context.DOES_NOT_CONTAIN:
+ return "DOES_NOT_CONTAIN";
+
+ case Context.IS_NOT_SET:
+ return "IS_NOT_SET";
+
+ default:
+ error("unrecognized text search context enumeration value");
+ }
+ }
+
+ public static Context from_string(string str) {
+ if (str == "CONTAINS")
+ return Context.CONTAINS;
+
+ else if (str == "IS_EXACTLY")
+ return Context.IS_EXACTLY;
+
+ else if (str == "STARTS_WITH")
+ return Context.STARTS_WITH;
+
+ else if (str == "ENDS_WITH")
+ return Context.ENDS_WITH;
+
+ else if (str == "DOES_NOT_CONTAIN")
+ return Context.DOES_NOT_CONTAIN;
+
+ else if (str == "IS_NOT_SET")
+ return Context.IS_NOT_SET;
+
+ else
+ error("unrecognized text search context name: %s", str);
+ }
+ }
+
+ // What to search for.
+ public string text { get; private set; }
+
+ // How to match.
+ public Context context { get; private set; }
+
+ public SearchConditionText(SearchCondition.SearchType search_type, string? text, Context context) {
+ this.search_type = search_type;
+ this.text = (text != null) ? String.remove_diacritics(text.down()) : "";
+ this.context = context;
+ }
+
+ // Match string by context.
+ private bool string_match(string needle, string? haystack) {
+ switch (context) {
+ case Context.CONTAINS:
+ case Context.DOES_NOT_CONTAIN:
+ return !is_string_empty(haystack) && haystack.contains(needle);
+
+ case Context.IS_EXACTLY:
+ return !is_string_empty(haystack) && haystack == needle;
+
+ case Context.STARTS_WITH:
+ return !is_string_empty(haystack) && haystack.has_prefix(needle);
+
+ case Context.ENDS_WITH:
+ return !is_string_empty(haystack) && haystack.has_suffix(needle);
+
+ case Context.IS_NOT_SET:
+ return (is_string_empty(haystack));
+ }
+
+ return false;
+ }
+
+ // Determines whether the source is included.
+ public override bool predicate(MediaSource source) {
+ bool ret = false;
+
+ // title
+ if (SearchType.ANY_TEXT == search_type || SearchType.TITLE == search_type) {
+ string title = source.get_title();
+ if(title != null){
+ ret |= string_match(text, String.remove_diacritics(title.down()));
+ }
+ }
+
+ // tags
+ if (SearchType.ANY_TEXT == search_type || SearchType.TAG == search_type) {
+ Gee.List<Tag>? tag_list = Tag.global.fetch_for_source(source);
+ if (null != tag_list) {
+ string itag;
+ foreach (Tag tag in tag_list) {
+ itag = tag.get_searchable_name().down(); // get_searchable already remove diacritics
+ ret |= string_match(text, itag);
+ }
+ } else {
+ ret |= string_match(text, null); // for IS_NOT_SET
+ }
+ }
+
+ // event name
+ if (SearchType.ANY_TEXT == search_type || SearchType.EVENT_NAME == search_type) {
+ string? event_name = (null != source.get_event()) ?
+ String.remove_diacritics(source.get_event().get_name().down()) : null;
+ ret |= string_match(text, event_name);
+ }
+
+ // comment
+ if (SearchType.ANY_TEXT == search_type || SearchType.COMMENT == search_type) {
+ string? comment = source.get_comment();
+ if(null != comment)
+ ret |= string_match(text, String.remove_diacritics(comment.down()));
+ }
+
+ // file name
+ if (SearchType.ANY_TEXT == search_type || SearchType.FILE_NAME == search_type) {
+ ret |= string_match(text, String.remove_diacritics(source.get_basename().down()));
+ }
+
+ return (context == Context.DOES_NOT_CONTAIN) ? !ret : ret;
+ }
+}
+
+// Condition for media type matching.
+public class SearchConditionMediaType : SearchCondition {
+ public enum Context {
+ IS = 0,
+ IS_NOT;
+
+ public string to_string() {
+ switch (this) {
+ case Context.IS:
+ return "IS";
+
+ case Context.IS_NOT:
+ return "IS_NOT";
+
+ default:
+ error("unrecognized media search context enumeration value");
+ }
+ }
+
+ public static Context from_string(string str) {
+ if (str == "IS")
+ return Context.IS;
+
+ else if (str == "IS_NOT")
+ return Context.IS_NOT;
+
+ else
+ error("unrecognized media search context name: %s", str);
+ }
+ }
+
+ public enum MediaType {
+ PHOTO_ALL = 0,
+ PHOTO_RAW,
+ VIDEO;
+
+ public string to_string() {
+ switch (this) {
+ case MediaType.PHOTO_ALL:
+ return "PHOTO_ALL";
+
+ case MediaType.PHOTO_RAW:
+ return "PHOTO_RAW";
+
+ case MediaType.VIDEO:
+ return "VIDEO";
+
+ default:
+ error("unrecognized media search type enumeration value");
+ }
+ }
+
+ public static MediaType from_string(string str) {
+ if (str == "PHOTO_ALL")
+ return MediaType.PHOTO_ALL;
+
+ else if (str == "PHOTO_RAW")
+ return MediaType.PHOTO_RAW;
+
+ else if (str == "VIDEO")
+ return MediaType.VIDEO;
+
+ else
+ error("unrecognized media search type name: %s", str);
+ }
+ }
+
+ // What to search for.
+ public MediaType media_type { get; private set; }
+
+ // How to match.
+ public Context context { get; private set; }
+
+ public SearchConditionMediaType(SearchCondition.SearchType search_type, Context context, MediaType media_type) {
+ this.search_type = search_type;
+ this.context = context;
+ this.media_type = media_type;
+ }
+
+ // Determines whether the source is included.
+ public override bool predicate(MediaSource source) {
+ // For the given type, check it against the MediaSource type
+ // and the given search context.
+ switch (media_type) {
+ case MediaType.PHOTO_ALL:
+ if (source is Photo)
+ return context == Context.IS;
+ else
+ return context == Context.IS_NOT;
+
+ case MediaType.PHOTO_RAW:
+ if (source is Photo && ((Photo) source).get_master_file_format() == PhotoFileFormat.RAW)
+ return context == Context.IS;
+ else
+ return context == Context.IS_NOT;
+
+ case MediaType.VIDEO:
+ if (source is VideoSource)
+ return context == Context.IS;
+ else
+ return context == Context.IS_NOT;
+
+ default:
+ error("unrecognized media search type enumeration value");
+ }
+ }
+}
+
+// Condition for flag state matching.
+public class SearchConditionFlagged : SearchCondition {
+ public enum State {
+ FLAGGED = 0,
+ UNFLAGGED;
+
+ public string to_string() {
+ switch (this) {
+ case State.FLAGGED:
+ return "FLAGGED";
+
+ case State.UNFLAGGED:
+ return "UNFLAGGED";
+
+ default:
+ error("unrecognized flagged search state enumeration value");
+ }
+ }
+
+ public static State from_string(string str) {
+ if (str == "FLAGGED")
+ return State.FLAGGED;
+
+ else if (str == "UNFLAGGED")
+ return State.UNFLAGGED;
+
+ else
+ error("unrecognized flagged search state name: %s", str);
+ }
+ }
+
+ // What to match.
+ public State state { get; private set; }
+
+ public SearchConditionFlagged(SearchCondition.SearchType search_type, State state) {
+ this.search_type = search_type;
+ this.state = state;
+ }
+
+ // Determines whether the source is included.
+ public override bool predicate(MediaSource source) {
+ if (state == State.FLAGGED) {
+ return ((Flaggable) source).is_flagged();
+ } else if (state == State.UNFLAGGED) {
+ return !((Flaggable) source).is_flagged();
+ } else {
+ error("unrecognized flagged search state");
+ }
+ }
+}
+
+// Condition for modified state matching.
+public class SearchConditionModified : SearchCondition {
+
+ public enum Context {
+ HAS = 0,
+ HAS_NO;
+
+ public string to_string() {
+ switch (this) {
+ case Context.HAS:
+ return "HAS";
+
+ case Context.HAS_NO:
+ return "HAS_NO";
+
+ default:
+ error("unrecognized modified search context enumeration value");
+ }
+ }
+
+ public static Context from_string(string str) {
+ if (str == "HAS")
+ return Context.HAS;
+
+ else if (str == "HAS_NO")
+ return Context.HAS_NO;
+
+ else
+ error("unrecognized modified search context name: %s", str);
+ }
+ }
+
+ public enum State {
+ MODIFIED = 0,
+ INTERNAL_CHANGES,
+ EXTERNAL_CHANGES;
+
+ public string to_string() {
+ switch (this) {
+ case State.MODIFIED:
+ return "MODIFIED";
+
+ case State.INTERNAL_CHANGES:
+ return "INTERNAL_CHANGES";
+
+ case State.EXTERNAL_CHANGES:
+ return "EXTERNAL_CHANGES";
+
+ default:
+ error("unrecognized modified search state enumeration value");
+ }
+ }
+
+ public static State from_string(string str) {
+ if (str == "MODIFIED")
+ return State.MODIFIED;
+
+ else if (str == "INTERNAL_CHANGES")
+ return State.INTERNAL_CHANGES;
+
+ else if (str == "EXTERNAL_CHANGES")
+ return State.EXTERNAL_CHANGES;
+
+ else
+ error("unrecognized modified search state name: %s", str);
+ }
+ }
+
+ // What to match.
+ public State state { get; private set; }
+
+ // How to match.
+ public Context context { get; private set; }
+
+ public SearchConditionModified(SearchCondition.SearchType search_type, Context context, State state) {
+ this.search_type = search_type;
+ this.context = context;
+ this.state = state;
+ }
+
+ // Determines whether the source is included.
+ public override bool predicate(MediaSource source) {
+ // check against state and the given search context.
+ Photo? photo = source as Photo;
+ if (photo == null)
+ return false;
+
+ bool match;
+ if (state == State.MODIFIED)
+ match = photo.has_transformations() || photo.has_editable();
+ else if (state == State.INTERNAL_CHANGES)
+ match = photo.has_transformations();
+ else if (state == State.EXTERNAL_CHANGES)
+ match = photo.has_editable();
+ else
+ error("unrecognized modified search state");
+
+ if (match)
+ return context == Context.HAS;
+ else
+ return context == Context.HAS_NO;
+ }
+}
+
+
+// Condition for rating matching.
+public class SearchConditionRating : SearchCondition {
+ public enum Context {
+ AND_HIGHER = 0,
+ ONLY,
+ AND_LOWER;
+
+ public string to_string() {
+ switch (this) {
+ case Context.AND_HIGHER:
+ return "AND_HIGHER";
+
+ case Context.ONLY:
+ return "ONLY";
+
+ case Context.AND_LOWER:
+ return "AND_LOWER";
+
+ default:
+ error("unrecognized rating search context enumeration value");
+ }
+ }
+
+ public static Context from_string(string str) {
+ if (str == "AND_HIGHER")
+ return Context.AND_HIGHER;
+
+ else if (str == "ONLY")
+ return Context.ONLY;
+
+ else if (str == "AND_LOWER")
+ return Context.AND_LOWER;
+
+ else
+ error("unrecognized rating search context name: %s", str);
+ }
+ }
+
+ // Rating to check against.
+ public Rating rating { get; private set; }
+
+ // How to match.
+ public Context context { get; private set; }
+
+ public SearchConditionRating(SearchCondition.SearchType search_type, Rating rating, Context context) {
+ this.search_type = search_type;
+ this.rating = rating;
+ this.context = context;
+ }
+
+ // Determines whether the source is included.
+ public override bool predicate(MediaSource source) {
+ Rating source_rating = source.get_rating();
+ if (context == Context.AND_HIGHER)
+ return source_rating >= rating;
+ else if (context == Context.ONLY)
+ return source_rating == rating;
+ else if (context == Context.AND_LOWER)
+ return source_rating <= rating;
+ else
+ error("unknown rating search context");
+ }
+}
+
+
+// Condition for date range.
+public class SearchConditionDate : SearchCondition {
+ public enum Context {
+ EXACT = 0,
+ AFTER,
+ BEFORE,
+ BETWEEN,
+ IS_NOT_SET;
+
+ public string to_string() {
+ switch (this) {
+ case Context.EXACT:
+ return "EXACT";
+
+ case Context.AFTER:
+ return "AFTER";
+
+ case Context.BEFORE:
+ return "BEFORE";
+
+ case Context.BETWEEN:
+ return "BETWEEN";
+
+ case Context.IS_NOT_SET:
+ return "IS_NOT_SET";
+
+ default:
+ error("unrecognized date search context enumeration value");
+ }
+ }
+
+ public static Context from_string(string str) {
+ if (str == "EXACT")
+ return Context.EXACT;
+
+ if (str == "AFTER")
+ return Context.AFTER;
+
+ else if (str == "BEFORE")
+ return Context.BEFORE;
+
+ else if (str == "BETWEEN")
+ return Context.BETWEEN;
+
+ else if (str == "IS_NOT_SET")
+ return Context.IS_NOT_SET;
+
+ else
+ error("unrecognized date search context name: %s", str);
+ }
+ }
+
+ // Date to check against. Second date only used for between searches.
+ public DateTime date_one { get; private set; }
+ public DateTime date_two { get; private set; }
+
+ // How to match.
+ public Context context { get; private set; }
+
+ public SearchConditionDate(SearchCondition.SearchType search_type, Context context,
+ DateTime date_one, DateTime date_two) {
+ this.search_type = search_type;
+ this.context = context;
+ if (context != Context.BETWEEN || date_two.compare(date_one) >= 1) {
+ this.date_one = date_one;
+ this.date_two = date_two;
+ } else {
+ this.date_one = date_two;
+ this.date_two = date_one;
+ }
+
+ }
+
+ // Determines whether the source is included.
+ public override bool predicate(MediaSource source) {
+ time_t exposure_time = source.get_exposure_time();
+ if (exposure_time == 0)
+ return context == Context.IS_NOT_SET;
+
+ DateTime dt = new DateTime.from_unix_local(exposure_time);
+ switch (context) {
+ case Context.EXACT:
+ DateTime second = date_one.add_days(1);
+ return (dt.compare(date_one) >= 0 && dt.compare(second) < 0);
+
+ case Context.AFTER:
+ return (dt.compare(date_one) >= 0);
+
+ case Context.BEFORE:
+ return (dt.compare(date_one) <= 0);
+
+ case Context.BETWEEN:
+ DateTime second = date_two.add_days(1);
+ return (dt.compare(date_one) >= 0 && dt.compare(second) < 0);
+
+ case Context.IS_NOT_SET:
+ return false; // Already checked above.
+
+ default:
+ error("unrecognized date search context enumeration value");
+ }
+ }
+}
+
+// Contains the logic of a search.
+// A saved search requires a name, an AND/OR (all/any) operator, as well as a list of one or more conditions.
+public class SavedSearch : DataSource {
+ public const string TYPENAME = "saved_search";
+
+ // Row from the database.
+ private SavedSearchRow row;
+
+ public SavedSearch(SavedSearchRow row, int64 object_id = INVALID_OBJECT_ID) {
+ base (object_id);
+
+ this.row = row;
+ }
+
+ public override string get_name() {
+ return row.name;
+ }
+
+ public override string to_string() {
+ return "SavedSearch " + get_name();
+ }
+
+ public override string get_typename() {
+ return TYPENAME;
+ }
+
+ public SavedSearchID get_saved_search_id() {
+ return row.search_id;
+ }
+
+ public override int64 get_instance_id() {
+ return get_saved_search_id().id;
+ }
+
+ public static int compare_names(void *a, void *b) {
+ SavedSearch *asearch = (SavedSearch *) a;
+ SavedSearch *bsearch = (SavedSearch *) b;
+
+ return String.collated_compare(asearch->get_name(), bsearch->get_name());
+ }
+
+ public bool predicate(MediaSource source) {
+ bool ret;
+ if (SearchOperator.ALL == row.operator || SearchOperator.NONE == row.operator)
+ ret = true;
+ else
+ ret = false; // assumes conditions.size() > 0
+
+ foreach (SearchCondition c in row.conditions) {
+ if (SearchOperator.ALL == row.operator)
+ ret &= c.predicate(source);
+ else if (SearchOperator.ANY == row.operator)
+ ret |= c.predicate(source);
+ else if (SearchOperator.NONE == row.operator)
+ ret &= !c.predicate(source);
+ }
+ return ret;
+ }
+
+ public void reconstitute() {
+ try {
+ row.search_id = SavedSearchDBTable.get_instance().create_from_row(row);
+ } catch (DatabaseError err) {
+ AppWindow.database_error(err);
+ }
+
+ SavedSearchTable.get_instance().add_to_map(this);
+ debug("Reconstituted %s", to_string());
+ }
+
+ // Returns false if the name already exists or a bad name.
+ public bool rename(string new_name) {
+ if (is_string_empty(new_name))
+ return false;
+
+ if (SavedSearchTable.get_instance().exists(new_name))
+ return false;
+
+ try {
+ SavedSearchDBTable.get_instance().rename(row.search_id, new_name);
+ } catch (DatabaseError err) {
+ AppWindow.database_error(err);
+ return false;
+ }
+
+ SavedSearchTable.get_instance().remove_from_map(this);
+ row.name = new_name;
+ SavedSearchTable.get_instance().add_to_map(this);
+
+ LibraryWindow.get_app().switch_to_saved_search(this);
+ return true;
+ }
+
+ public Gee.List<SearchCondition> get_conditions() {
+ return row.conditions.read_only_view;
+ }
+
+ public SearchOperator get_operator() {
+ return row.operator;
+ }
+}
+
+// This table contains every saved search. It's the preferred way to add and destroy a saved
+// search as well, since this table's create/destroy methods are tied to the database.
+public class SavedSearchTable {
+ private static SavedSearchTable? instance = null;
+ private Gee.HashMap<string, SavedSearch> search_map = new Gee.HashMap<string, SavedSearch>();
+
+ public signal void search_added(SavedSearch search);
+ public signal void search_removed(SavedSearch search);
+
+ private SavedSearchTable() {
+ // Load existing searches from DB.
+ try {
+ foreach(SavedSearchRow row in SavedSearchDBTable.get_instance().get_all_rows())
+ add_to_map(new SavedSearch(row));
+ } catch (DatabaseError err) {
+ AppWindow.database_error(err);
+ }
+
+ }
+
+ public static SavedSearchTable get_instance() {
+ if (instance == null)
+ instance = new SavedSearchTable();
+
+ return instance;
+ }
+
+ public Gee.Collection<SavedSearch> get_all() {
+ return search_map.values;
+ }
+
+ // Creates a saved search with the given name, operator, and conditions. The saved search is
+ // added to the database and to this table.
+ public SavedSearch create(string name, SearchOperator operator,
+ Gee.ArrayList<SearchCondition> conditions) {
+ SavedSearch? search = null;
+ // Create a new SavedSearch in the database.
+ try {
+ search = new SavedSearch(SavedSearchDBTable.get_instance().add(name, operator, conditions));
+ } catch (DatabaseError err) {
+ AppWindow.database_error(err);
+ }
+
+ // Add search to table.
+ add_to_map(search);
+ LibraryWindow.get_app().switch_to_saved_search(search);
+ return search;
+ }
+
+ // Removes a saved search, both from here and from the table.
+ public void remove(SavedSearch search) {
+ try {
+ SavedSearchDBTable.get_instance().remove(search.get_saved_search_id());
+ } catch (DatabaseError err) {
+ AppWindow.database_error(err);
+ }
+
+ remove_from_map(search);
+ }
+
+ public void add_to_map(SavedSearch search) {
+ search_map.set(search.get_name(), search);
+ search_added(search);
+ }
+
+ public void remove_from_map(SavedSearch search) {
+ search_map.unset(search.get_name());
+ search_removed(search);
+ }
+
+ public Gee.Iterable<SavedSearch> get_saved_searches() {
+ return search_map.values;
+ }
+
+ public int get_count() {
+ return search_map.size;
+ }
+
+ public bool exists(string search_name) {
+ return search_map.has_key(search_name);
+ }
+
+ // Generate a unique search name (not thread safe)
+ public string generate_unique_name() {
+ for (int ctr = 1; ctr < int.MAX; ctr++) {
+ string name = "%s %d".printf(Resources.DEFAULT_SAVED_SEARCH_NAME, ctr);
+
+ if (!exists(name))
+ return name;
+ }
+ return ""; // If all names are used (unlikely!)
+ }
+}
diff --git a/src/searches/Searches.vala b/src/searches/Searches.vala
new file mode 100644
index 0000000..478de86
--- /dev/null
+++ b/src/searches/Searches.vala
@@ -0,0 +1,31 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/* This file is the master unit file for the Searches unit. It should be edited to include
+ * whatever code is deemed necessary.
+ *
+ * The init() and terminate() methods are mandatory.
+ *
+ * If the unit needs to be configured prior to initialization, add the proper parameters to
+ * the preconfigure() method, implement it, and ensure in init() that it's been called.
+ */
+
+namespace Searches {
+
+// preconfigure may be deleted if not used.
+public void preconfigure() {
+}
+
+public void init() throws Error {
+ Searches.SidebarEntry.init();
+}
+
+public void terminate() {
+ Searches.SidebarEntry.terminate();
+}
+
+}
+
diff --git a/src/searches/mk/searches.mk b/src/searches/mk/searches.mk
new file mode 100644
index 0000000..6df4b5d
--- /dev/null
+++ b/src/searches/mk/searches.mk
@@ -0,0 +1,31 @@
+
+# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with
+# a init() and terminate() function declared in the namespace.
+UNIT_NAME := Searches
+
+# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all
+# lowercase. The name of this file should be UNIT_DIR.mk.
+UNIT_DIR := searches
+
+# All Vala files in the unit should be listed here with no subdirectory prefix.
+#
+# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala.
+UNIT_FILES := \
+ Branch.vala \
+ SearchBoolean.vala \
+ SavedSearchPage.vala \
+ SavedSearchDialog.vala
+
+# Any unit this unit relies upon (and should be initialized before it's initialized) should
+# be listed here using its Vala namespace.
+#
+# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here.
+UNIT_USES :=
+
+# List any additional files that are used in the build process as a part of this unit that should
+# be packaged in the tarball. File names should be relative to the unit's home directory.
+UNIT_RC :=
+
+# unitize.mk must be called at the end of each UNIT_DIR.mk file.
+include unitize.mk
+