1/* Copyright 2016 Software Freedom Conservancy Inc. 2 * 3 * This software is licensed under the GNU LGPL (version 2.1 or later). 4 * See the COPYING file in this distribution. 5 */ 6 7// For specifying whether a search should be ORed (any) or ANDed (all). 8public enum SearchOperator { 9 ANY = 0, 10 ALL, 11 NONE; 12 13 public string to_string() { 14 switch (this) { 15 case SearchOperator.ANY: 16 return "ANY"; 17 18 case SearchOperator.ALL: 19 return "ALL"; 20 21 case SearchOperator.NONE: 22 return "NONE"; 23 24 default: 25 error("unrecognized search operator enumeration value"); 26 } 27 } 28 29 public static SearchOperator from_string(string str) { 30 if (str == "ANY") 31 return SearchOperator.ANY; 32 33 else if (str == "ALL") 34 return SearchOperator.ALL; 35 36 else if (str == "NONE") 37 return SearchOperator.NONE; 38 39 else 40 error("unrecognized search operator name: %s", str); 41 } 42} 43 44// Important note: if you are adding, removing, or otherwise changing 45// this table, you're going to have to modify SavedSearchDBTable.vala 46// as well. 47public abstract class SearchCondition { 48 // Type of search condition. 49 public enum SearchType { 50 ANY_TEXT = 0, 51 TITLE, 52 TAG, 53 EVENT_NAME, 54 FILE_NAME, 55#if ENABLE_FACES 56 FACE, 57#endif 58 MEDIA_TYPE, 59 FLAG_STATE, 60 MODIFIED_STATE, 61 RATING, 62 COMMENT, 63 DATE; 64 // Note: when adding new types, be sure to update all functions below. 65 66 public static SearchType[] as_array() { 67 return { ANY_TEXT, TITLE, TAG, COMMENT, EVENT_NAME, FILE_NAME, 68#if ENABLE_FACES 69 FACE, 70#endif 71 MEDIA_TYPE, FLAG_STATE, MODIFIED_STATE, RATING, DATE }; 72 } 73 74 // Sorts an array alphabetically by display name. 75 public static void sort_array(ref SearchType[] array) { 76 Posix.qsort(array, array.length, sizeof(SearchType), (a, b) => { 77 return utf8_cs_compare(((*(SearchType*) a)).display_text(), 78 ((*(SearchType*) b)).display_text()); 79 }); 80 } 81 82 public string to_string() { 83 switch (this) { 84 case SearchType.ANY_TEXT: 85 return "ANY_TEXT"; 86 87 case SearchType.TITLE: 88 return "TITLE"; 89 90 case SearchType.TAG: 91 return "TAG"; 92 93 case SearchType.COMMENT: 94 return "COMMENT"; 95 96 case SearchType.EVENT_NAME: 97 return "EVENT_NAME"; 98 99 case SearchType.FILE_NAME: 100 return "FILE_NAME"; 101#if ENABLE_FACES 102 case SearchType.FACE: 103 return "FACE"; 104#endif 105 case SearchType.MEDIA_TYPE: 106 return "MEDIA_TYPE"; 107 108 case SearchType.FLAG_STATE: 109 return "FLAG_STATE"; 110 111 case SearchType.MODIFIED_STATE: 112 return "MODIFIED_STATE"; 113 114 case SearchType.RATING: 115 return "RATING"; 116 117 case SearchType.DATE: 118 return "DATE"; 119 120 default: 121 error("unrecognized search type enumeration value"); 122 } 123 } 124 125 public static SearchType from_string(string str) { 126 if (str == "ANY_TEXT") 127 return SearchType.ANY_TEXT; 128 129 else if (str == "TITLE") 130 return SearchType.TITLE; 131 132 else if (str == "TAG") 133 return SearchType.TAG; 134 135 else if (str == "COMMENT") 136 return SearchType.COMMENT; 137 138 else if (str == "EVENT_NAME") 139 return SearchType.EVENT_NAME; 140 141 else if (str == "FILE_NAME") 142 return SearchType.FILE_NAME; 143#if ENABLE_FACES 144 else if (str == "FACE") 145 return SearchType.FACE; 146#endif 147 else if (str == "MEDIA_TYPE") 148 return SearchType.MEDIA_TYPE; 149 150 else if (str == "FLAG_STATE") 151 return SearchType.FLAG_STATE; 152 153 else if (str == "MODIFIED_STATE") 154 return SearchType.MODIFIED_STATE; 155 156 else if (str == "RATING") 157 return SearchType.RATING; 158 159 else if (str == "DATE") 160 return SearchType.DATE; 161 162 else 163 error("unrecognized search type name: %s", str); 164 } 165 166 public string display_text() { 167 switch (this) { 168 case SearchType.ANY_TEXT: 169 return _("Any text"); 170 171 case SearchType.TITLE: 172 return _("Title"); 173 174 case SearchType.TAG: 175 return _("Tag"); 176 177 case SearchType.COMMENT: 178 return _("Comment"); 179 180 case SearchType.EVENT_NAME: 181 return _("Event name"); 182 183 case SearchType.FILE_NAME: 184 return _("File name"); 185#if ENABLE_FACES 186 case SearchType.FACE: 187 return _("Face"); 188#endif 189 case SearchType.MEDIA_TYPE: 190 return _("Media type"); 191 192 case SearchType.FLAG_STATE: 193 return _("Flag state"); 194 195 case SearchType.MODIFIED_STATE: 196 return _("Photo state"); 197 198 case SearchType.RATING: 199 return _("Rating"); 200 201 case SearchType.DATE: 202 return _("Date"); 203 204 default: 205 error("unrecognized search type enumeration value"); 206 } 207 } 208 } 209 210 public SearchType search_type { get; protected set; } 211 212 // Determines whether the source is included. 213 public abstract bool predicate(MediaSource source); 214} 215 216// Condition for text matching. 217public class SearchConditionText : SearchCondition { 218 public enum Context { 219 CONTAINS = 0, 220 IS_EXACTLY, 221 STARTS_WITH, 222 ENDS_WITH, 223 DOES_NOT_CONTAIN, 224 IS_NOT_SET, 225 IS_SET; 226 227 public string to_string() { 228 switch (this) { 229 case Context.CONTAINS: 230 return "CONTAINS"; 231 232 case Context.IS_EXACTLY: 233 return "IS_EXACTLY"; 234 235 case Context.STARTS_WITH: 236 return "STARTS_WITH"; 237 238 case Context.ENDS_WITH: 239 return "ENDS_WITH"; 240 241 case Context.DOES_NOT_CONTAIN: 242 return "DOES_NOT_CONTAIN"; 243 244 case Context.IS_NOT_SET: 245 return "IS_NOT_SET"; 246 247 case Context.IS_SET: 248 return "IS_SET"; 249 250 default: 251 error("unrecognized text search context enumeration value"); 252 } 253 } 254 255 public static Context from_string(string str) { 256 if (str == "CONTAINS") 257 return Context.CONTAINS; 258 259 else if (str == "IS_EXACTLY") 260 return Context.IS_EXACTLY; 261 262 else if (str == "STARTS_WITH") 263 return Context.STARTS_WITH; 264 265 else if (str == "ENDS_WITH") 266 return Context.ENDS_WITH; 267 268 else if (str == "DOES_NOT_CONTAIN") 269 return Context.DOES_NOT_CONTAIN; 270 271 else if (str == "IS_NOT_SET") 272 return Context.IS_NOT_SET; 273 274 else if (str == "IS_SET") 275 return Context.IS_SET; 276 277 else 278 error("unrecognized text search context name: %s", str); 279 } 280 } 281 282 // What to search for. 283 public string text { get; private set; } 284 285 // How to match. 286 public Context context { get; private set; } 287 288 public SearchConditionText(SearchCondition.SearchType search_type, string? text, Context context) { 289 this.search_type = search_type; 290 this.text = (text != null) ? String.remove_diacritics(text.down()) : ""; 291 this.context = context; 292 } 293 294 // Match string by context. 295 private bool string_match(string needle, string? haystack) { 296 switch (context) { 297 case Context.CONTAINS: 298 case Context.DOES_NOT_CONTAIN: 299 return !is_string_empty(haystack) && haystack.contains(needle); 300 301 case Context.IS_EXACTLY: 302 return !is_string_empty(haystack) && haystack == needle; 303 304 case Context.STARTS_WITH: 305 return !is_string_empty(haystack) && haystack.has_prefix(needle); 306 307 case Context.ENDS_WITH: 308 return !is_string_empty(haystack) && haystack.has_suffix(needle); 309 310 case Context.IS_NOT_SET: 311 return (is_string_empty(haystack)); 312 313 case Context.IS_SET: 314 return (!is_string_empty(haystack)); 315 } 316 317 return false; 318 } 319 320 // Determines whether the source is included. 321 public override bool predicate(MediaSource source) { 322 bool ret = false; 323 324 // title 325 if (SearchType.ANY_TEXT == search_type || SearchType.TITLE == search_type) { 326 string? title = (null != source.get_title()) ? 327 String.remove_diacritics(source.get_title().down()) : null; 328 ret |= string_match(text, title); 329 } 330 331 // tags 332 if (SearchType.ANY_TEXT == search_type || SearchType.TAG == search_type) { 333 Gee.List<Tag>? tag_list = Tag.global.fetch_for_source(source); 334 if (null != tag_list) { 335 string itag; 336 foreach (Tag tag in tag_list) { 337 itag = tag.get_searchable_name().down(); // get_searchable already remove diacritics 338 ret |= string_match(text, itag); 339 } 340 } else { 341 ret |= string_match(text, null); // for IS_NOT_SET 342 } 343 } 344 345 // event name 346 if (SearchType.ANY_TEXT == search_type || SearchType.EVENT_NAME == search_type) { 347 string? event_name = (null != source.get_event()) ? 348 String.remove_diacritics(source.get_event().get_name().down()) : null; 349 ret |= string_match(text, event_name); 350 } 351 352 // comment 353 if (SearchType.ANY_TEXT == search_type || SearchType.COMMENT == search_type) { 354 string? comment = source.get_comment(); 355 if(null != comment) 356 ret |= string_match(text, String.remove_diacritics(comment.down())); 357 } 358 359 // file name 360 if (SearchType.ANY_TEXT == search_type || SearchType.FILE_NAME == search_type) { 361 ret |= string_match(text, String.remove_diacritics(source.get_basename().down())); 362 } 363 364#if ENABLE_FACES 365 if (SearchType.ANY_TEXT == search_type || SearchType.FACE == search_type) { 366 Gee.List<Face>? face_list = Face.global.fetch_for_source(source); 367 if (null != face_list) { 368 foreach (Face face in face_list) { 369 ret |= string_match(text, face.get_name().down()); 370 } 371 } else { 372 ret |= string_match(text, null); // for IS_NOT_SET 373 } 374 } 375#endif 376 377 return (context == Context.DOES_NOT_CONTAIN) ? !ret : ret; 378 } 379} 380 381// Condition for media type matching. 382public class SearchConditionMediaType : SearchCondition { 383 public enum Context { 384 IS = 0, 385 IS_NOT; 386 387 public string to_string() { 388 switch (this) { 389 case Context.IS: 390 return "IS"; 391 392 case Context.IS_NOT: 393 return "IS_NOT"; 394 395 default: 396 error("unrecognized media search context enumeration value"); 397 } 398 } 399 400 public static Context from_string(string str) { 401 if (str == "IS") 402 return Context.IS; 403 404 else if (str == "IS_NOT") 405 return Context.IS_NOT; 406 407 else 408 error("unrecognized media search context name: %s", str); 409 } 410 } 411 412 public enum MediaType { 413 PHOTO_ALL = 0, 414 PHOTO_RAW, 415 VIDEO; 416 417 public string to_string() { 418 switch (this) { 419 case MediaType.PHOTO_ALL: 420 return "PHOTO_ALL"; 421 422 case MediaType.PHOTO_RAW: 423 return "PHOTO_RAW"; 424 425 case MediaType.VIDEO: 426 return "VIDEO"; 427 428 default: 429 error("unrecognized media search type enumeration value"); 430 } 431 } 432 433 public static MediaType from_string(string str) { 434 if (str == "PHOTO_ALL") 435 return MediaType.PHOTO_ALL; 436 437 else if (str == "PHOTO_RAW") 438 return MediaType.PHOTO_RAW; 439 440 else if (str == "VIDEO") 441 return MediaType.VIDEO; 442 443 else 444 error("unrecognized media search type name: %s", str); 445 } 446 } 447 448 // What to search for. 449 public MediaType media_type { get; private set; } 450 451 // How to match. 452 public Context context { get; private set; } 453 454 public SearchConditionMediaType(SearchCondition.SearchType search_type, Context context, MediaType media_type) { 455 this.search_type = search_type; 456 this.context = context; 457 this.media_type = media_type; 458 } 459 460 // Determines whether the source is included. 461 public override bool predicate(MediaSource source) { 462 // For the given type, check it against the MediaSource type 463 // and the given search context. 464 switch (media_type) { 465 case MediaType.PHOTO_ALL: 466 if (source is Photo) 467 return context == Context.IS; 468 else 469 return context == Context.IS_NOT; 470 471 case MediaType.PHOTO_RAW: 472 if (source is Photo && ((Photo) source).get_master_file_format() == PhotoFileFormat.RAW) 473 return context == Context.IS; 474 else 475 return context == Context.IS_NOT; 476 477 case MediaType.VIDEO: 478 if (source is VideoSource) 479 return context == Context.IS; 480 else 481 return context == Context.IS_NOT; 482 483 default: 484 error("unrecognized media search type enumeration value"); 485 } 486 } 487} 488 489// Condition for flag state matching. 490public class SearchConditionFlagged : SearchCondition { 491 public enum State { 492 FLAGGED = 0, 493 UNFLAGGED; 494 495 public string to_string() { 496 switch (this) { 497 case State.FLAGGED: 498 return "FLAGGED"; 499 500 case State.UNFLAGGED: 501 return "UNFLAGGED"; 502 503 default: 504 error("unrecognized flagged search state enumeration value"); 505 } 506 } 507 508 public static State from_string(string str) { 509 if (str == "FLAGGED") 510 return State.FLAGGED; 511 512 else if (str == "UNFLAGGED") 513 return State.UNFLAGGED; 514 515 else 516 error("unrecognized flagged search state name: %s", str); 517 } 518 } 519 520 // What to match. 521 public State state { get; private set; } 522 523 public SearchConditionFlagged(SearchCondition.SearchType search_type, State state) { 524 this.search_type = search_type; 525 this.state = state; 526 } 527 528 // Determines whether the source is included. 529 public override bool predicate(MediaSource source) { 530 if (state == State.FLAGGED) { 531 return ((Flaggable) source).is_flagged(); 532 } else if (state == State.UNFLAGGED) { 533 return !((Flaggable) source).is_flagged(); 534 } else { 535 error("unrecognized flagged search state"); 536 } 537 } 538} 539 540// Condition for modified state matching. 541public class SearchConditionModified : SearchCondition { 542 543 public enum Context { 544 HAS = 0, 545 HAS_NO; 546 547 public string to_string() { 548 switch (this) { 549 case Context.HAS: 550 return "HAS"; 551 552 case Context.HAS_NO: 553 return "HAS_NO"; 554 555 default: 556 error("unrecognized modified search context enumeration value"); 557 } 558 } 559 560 public static Context from_string(string str) { 561 if (str == "HAS") 562 return Context.HAS; 563 564 else if (str == "HAS_NO") 565 return Context.HAS_NO; 566 567 else 568 error("unrecognized modified search context name: %s", str); 569 } 570 } 571 572 public enum State { 573 MODIFIED = 0, 574 INTERNAL_CHANGES, 575 EXTERNAL_CHANGES; 576 577 public string to_string() { 578 switch (this) { 579 case State.MODIFIED: 580 return "MODIFIED"; 581 582 case State.INTERNAL_CHANGES: 583 return "INTERNAL_CHANGES"; 584 585 case State.EXTERNAL_CHANGES: 586 return "EXTERNAL_CHANGES"; 587 588 default: 589 error("unrecognized modified search state enumeration value"); 590 } 591 } 592 593 public static State from_string(string str) { 594 if (str == "MODIFIED") 595 return State.MODIFIED; 596 597 else if (str == "INTERNAL_CHANGES") 598 return State.INTERNAL_CHANGES; 599 600 else if (str == "EXTERNAL_CHANGES") 601 return State.EXTERNAL_CHANGES; 602 603 else 604 error("unrecognized modified search state name: %s", str); 605 } 606 } 607 608 // What to match. 609 public State state { get; private set; } 610 611 // How to match. 612 public Context context { get; private set; } 613 614 public SearchConditionModified(SearchCondition.SearchType search_type, Context context, State state) { 615 this.search_type = search_type; 616 this.context = context; 617 this.state = state; 618 } 619 620 // Determines whether the source is included. 621 public override bool predicate(MediaSource source) { 622 // check against state and the given search context. 623 Photo? photo = source as Photo; 624 if (photo == null) 625 return false; 626 627 bool match; 628 if (state == State.MODIFIED) 629 match = photo.has_transformations() || photo.has_editable(); 630 else if (state == State.INTERNAL_CHANGES) 631 match = photo.has_transformations(); 632 else if (state == State.EXTERNAL_CHANGES) 633 match = photo.has_editable(); 634 else 635 error("unrecognized modified search state"); 636 637 if (match) 638 return context == Context.HAS; 639 else 640 return context == Context.HAS_NO; 641 } 642} 643 644 645// Condition for rating matching. 646public class SearchConditionRating : SearchCondition { 647 public enum Context { 648 AND_HIGHER = 0, 649 ONLY, 650 AND_LOWER; 651 652 public string to_string() { 653 switch (this) { 654 case Context.AND_HIGHER: 655 return "AND_HIGHER"; 656 657 case Context.ONLY: 658 return "ONLY"; 659 660 case Context.AND_LOWER: 661 return "AND_LOWER"; 662 663 default: 664 error("unrecognized rating search context enumeration value"); 665 } 666 } 667 668 public static Context from_string(string str) { 669 if (str == "AND_HIGHER") 670 return Context.AND_HIGHER; 671 672 else if (str == "ONLY") 673 return Context.ONLY; 674 675 else if (str == "AND_LOWER") 676 return Context.AND_LOWER; 677 678 else 679 error("unrecognized rating search context name: %s", str); 680 } 681 } 682 683 // Rating to check against. 684 public Rating rating { get; private set; } 685 686 // How to match. 687 public Context context { get; private set; } 688 689 public SearchConditionRating(SearchCondition.SearchType search_type, Rating rating, Context context) { 690 this.search_type = search_type; 691 this.rating = rating; 692 this.context = context; 693 } 694 695 // Determines whether the source is included. 696 public override bool predicate(MediaSource source) { 697 Rating source_rating = source.get_rating(); 698 if (context == Context.AND_HIGHER) 699 return source_rating >= rating; 700 else if (context == Context.ONLY) 701 return source_rating == rating; 702 else if (context == Context.AND_LOWER) 703 return source_rating <= rating; 704 else 705 error("unknown rating search context"); 706 } 707} 708 709 710// Condition for date range. 711public class SearchConditionDate : SearchCondition { 712 public enum Context { 713 EXACT = 0, 714 AFTER, 715 BEFORE, 716 BETWEEN, 717 IS_NOT_SET; 718 719 public string to_string() { 720 switch (this) { 721 case Context.EXACT: 722 return "EXACT"; 723 724 case Context.AFTER: 725 return "AFTER"; 726 727 case Context.BEFORE: 728 return "BEFORE"; 729 730 case Context.BETWEEN: 731 return "BETWEEN"; 732 733 case Context.IS_NOT_SET: 734 return "IS_NOT_SET"; 735 736 default: 737 error("unrecognized date search context enumeration value"); 738 } 739 } 740 741 public static Context from_string(string str) { 742 if (str == "EXACT") 743 return Context.EXACT; 744 745 if (str == "AFTER") 746 return Context.AFTER; 747 748 else if (str == "BEFORE") 749 return Context.BEFORE; 750 751 else if (str == "BETWEEN") 752 return Context.BETWEEN; 753 754 else if (str == "IS_NOT_SET") 755 return Context.IS_NOT_SET; 756 757 else 758 error("unrecognized date search context name: %s", str); 759 } 760 } 761 762 // Date to check against. Second date only used for between searches. 763 public DateTime date_one { get; private set; } 764 public DateTime date_two { get; private set; } 765 766 // How to match. 767 public Context context { get; private set; } 768 769 public SearchConditionDate(SearchCondition.SearchType search_type, Context context, 770 DateTime date_one, DateTime date_two) { 771 this.search_type = search_type; 772 this.context = context; 773 if (context != Context.BETWEEN || date_two.compare(date_one) >= 1) { 774 this.date_one = date_one; 775 this.date_two = date_two; 776 } else { 777 this.date_one = date_two; 778 this.date_two = date_one; 779 } 780 781 } 782 783 // Determines whether the source is included. 784 public override bool predicate(MediaSource source) { 785 time_t exposure_time = source.get_exposure_time(); 786 if (exposure_time == 0) 787 return context == Context.IS_NOT_SET; 788 789 DateTime dt = new DateTime.from_unix_local(exposure_time); 790 switch (context) { 791 case Context.EXACT: 792 DateTime second = date_one.add_days(1); 793 return (dt.compare(date_one) >= 0 && dt.compare(second) < 0); 794 795 case Context.AFTER: 796 return (dt.compare(date_one) >= 0); 797 798 case Context.BEFORE: 799 return (dt.compare(date_one) <= 0); 800 801 case Context.BETWEEN: 802 DateTime second = date_two.add_days(1); 803 return (dt.compare(date_one) >= 0 && dt.compare(second) < 0); 804 805 case Context.IS_NOT_SET: 806 return false; // Already checked above. 807 808 default: 809 error("unrecognized date search context enumeration value"); 810 } 811 } 812} 813 814// Contains the logic of a search. 815// A saved search requires a name, an AND/OR (all/any) operator, as well as a list of one or more conditions. 816public class SavedSearch : DataSource { 817 public const string TYPENAME = "saved_search"; 818 819 // Row from the database. 820 private SavedSearchRow row; 821 822 public SavedSearch(SavedSearchRow row, int64 object_id = INVALID_OBJECT_ID) { 823 base (object_id); 824 825 this.row = row; 826 } 827 828 public override string get_name() { 829 return row.name; 830 } 831 832 public override string to_string() { 833 return "SavedSearch " + get_name(); 834 } 835 836 public override string get_typename() { 837 return TYPENAME; 838 } 839 840 public SavedSearchID get_saved_search_id() { 841 return row.search_id; 842 } 843 844 public override int64 get_instance_id() { 845 return get_saved_search_id().id; 846 } 847 848 public static int compare_names(void *a, void *b) { 849 SavedSearch *asearch = (SavedSearch *) a; 850 SavedSearch *bsearch = (SavedSearch *) b; 851 852 return String.collated_compare(asearch->get_name(), bsearch->get_name()); 853 } 854 855 public bool predicate(MediaSource source) { 856 bool ret; 857 if (SearchOperator.ALL == row.operator || SearchOperator.NONE == row.operator) 858 ret = true; 859 else 860 ret = false; // assumes conditions.size() > 0 861 862 foreach (SearchCondition c in row.conditions) { 863 if (SearchOperator.ALL == row.operator) 864 ret &= c.predicate(source); 865 else if (SearchOperator.ANY == row.operator) 866 ret |= c.predicate(source); 867 else if (SearchOperator.NONE == row.operator) 868 ret &= !c.predicate(source); 869 } 870 return ret; 871 } 872 873 public void reconstitute() { 874 try { 875 row.search_id = SavedSearchDBTable.get_instance().create_from_row(row); 876 } catch (DatabaseError err) { 877 AppWindow.database_error(err); 878 } 879 880 SavedSearchTable.get_instance().add_to_map(this); 881 debug("Reconstituted %s", to_string()); 882 } 883 884 // Returns false if the name already exists or a bad name. 885 public bool rename(string new_name) { 886 if (is_string_empty(new_name)) 887 return false; 888 889 if (SavedSearchTable.get_instance().exists(new_name)) 890 return false; 891 892 try { 893 SavedSearchDBTable.get_instance().rename(row.search_id, new_name); 894 } catch (DatabaseError err) { 895 AppWindow.database_error(err); 896 return false; 897 } 898 899 SavedSearchTable.get_instance().remove_from_map(this); 900 row.name = new_name; 901 SavedSearchTable.get_instance().add_to_map(this); 902 903 LibraryWindow.get_app().switch_to_saved_search(this); 904 return true; 905 } 906 907 public Gee.List<SearchCondition> get_conditions() { 908 return row.conditions.read_only_view; 909 } 910 911 public SearchOperator get_operator() { 912 return row.operator; 913 } 914} 915 916// This table contains every saved search. It's the preferred way to add and destroy a saved 917// search as well, since this table's create/destroy methods are tied to the database. 918public class SavedSearchTable { 919 private static SavedSearchTable? instance = null; 920 private Gee.HashMap<string, SavedSearch> search_map = new Gee.HashMap<string, SavedSearch>(); 921 922 public signal void search_added(SavedSearch search); 923 public signal void search_removed(SavedSearch search); 924 925 private SavedSearchTable() { 926 // Load existing searches from DB. 927 try { 928 foreach(SavedSearchRow row in SavedSearchDBTable.get_instance().get_all_rows()) 929 add_to_map(new SavedSearch(row)); 930 } catch (DatabaseError err) { 931 AppWindow.database_error(err); 932 } 933 934 } 935 936 public static SavedSearchTable get_instance() { 937 if (instance == null) 938 instance = new SavedSearchTable(); 939 940 return instance; 941 } 942 943 public Gee.Collection<SavedSearch> get_all() { 944 return search_map.values; 945 } 946 947 // Creates a saved search with the given name, operator, and conditions. The saved search is 948 // added to the database and to this table. 949 public SavedSearch create(string name, SearchOperator operator, 950 Gee.ArrayList<SearchCondition> conditions) { 951 SavedSearch? search = null; 952 // Create a new SavedSearch in the database. 953 try { 954 search = new SavedSearch(SavedSearchDBTable.get_instance().add(name, operator, conditions)); 955 } catch (DatabaseError err) { 956 AppWindow.database_error(err); 957 } 958 959 // Add search to table. 960 add_to_map(search); 961 LibraryWindow.get_app().switch_to_saved_search(search); 962 return search; 963 } 964 965 // Removes a saved search, both from here and from the table. 966 public void remove(SavedSearch search) { 967 try { 968 SavedSearchDBTable.get_instance().remove(search.get_saved_search_id()); 969 } catch (DatabaseError err) { 970 AppWindow.database_error(err); 971 } 972 973 remove_from_map(search); 974 } 975 976 public void add_to_map(SavedSearch search) { 977 search_map.set(search.get_name(), search); 978 search_added(search); 979 } 980 981 public void remove_from_map(SavedSearch search) { 982 search_map.unset(search.get_name()); 983 search_removed(search); 984 } 985 986 public Gee.Iterable<SavedSearch> get_saved_searches() { 987 return search_map.values; 988 } 989 990 public int get_count() { 991 return search_map.size; 992 } 993 994 public bool exists(string search_name) { 995 return search_map.has_key(search_name); 996 } 997 998 // Generate a unique search name (not thread safe) 999 public string generate_unique_name() { 1000 for (int ctr = 1; ctr < int.MAX; ctr++) { 1001 string name = "%s %d".printf(Resources.DEFAULT_SAVED_SEARCH_NAME, ctr); 1002 1003 if (!exists(name)) 1004 return name; 1005 } 1006 return ""; // If all names are used (unlikely!) 1007 } 1008} 1009