diff options
Diffstat (limited to 'src/searches')
-rw-r--r-- | src/searches/Branch.vala | 150 | ||||
-rw-r--r-- | src/searches/SavedSearchDialog.vala | 829 | ||||
-rw-r--r-- | src/searches/SavedSearchPage.vala | 92 | ||||
-rw-r--r-- | src/searches/SearchBoolean.vala | 971 | ||||
-rw-r--r-- | src/searches/Searches.vala | 31 | ||||
-rw-r--r-- | src/searches/mk/searches.mk | 31 |
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 + |