summaryrefslogtreecommitdiff
path: root/src/searches/SearchBoolean.vala
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff-webhosting.net>2014-07-23 09:06:59 +0200
committerJörg Frings-Fürst <debian@jff-webhosting.net>2014-07-23 09:06:59 +0200
commit4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d (patch)
treed2e54377d14d604356c86862a326f64ae64dadd6 /src/searches/SearchBoolean.vala
Imported Upstream version 0.18.1upstream/0.18.1
Diffstat (limited to 'src/searches/SearchBoolean.vala')
-rw-r--r--src/searches/SearchBoolean.vala971
1 files changed, 971 insertions, 0 deletions
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!)
+ }
+}