diff options
Diffstat (limited to 'src/searches/SearchBoolean.vala')
-rw-r--r-- | src/searches/SearchBoolean.vala | 971 |
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!) + } +} |