From 16c2d73037a4ccd88920d50c603bd52bc0f41089 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 18 Sep 2022 14:08:58 +0100 Subject: [PATCH 1/2] Enhancement #1989813: Option to quickly select filtered tag browser categories Done using a new Virtual Field, in_tag_browser that is searchable using 'in_tag_browser:true' (or false or ...). The feature is discoverable through a new menu item in Configure tag browser: Filter book lists. This menu item can be 'clicked' using a shortcut key. --- manual/gui.rst | 4 ++++ src/calibre/db/backend.py | 2 ++ src/calibre/db/view.py | 31 ++++++++++++++++++++++++--- src/calibre/gui2/tag_browser/model.py | 25 +++++++++++++++++++++ src/calibre/gui2/tag_browser/ui.py | 18 ++++++++++++++++ src/calibre/library/field_metadata.py | 10 +++++++++ 6 files changed, 87 insertions(+), 3 deletions(-) diff --git a/manual/gui.rst b/manual/gui.rst index 8077fbbcf8..c4360d2d8d 100644 --- a/manual/gui.rst +++ b/manual/gui.rst @@ -526,6 +526,10 @@ Identifiers (e.g., ISBN, DOI, LCCN, etc.) use an extended syntax. An identifier * ``identifiers:=isbn:=123456789`` will find books with a type equal to ISBN having a value equal to `123456789`. * ``identifiers:i:1`` will find books with a type containing an `i` having a value containing a `1`. +*Categories in the Tag browser* + +The search ``in_tag_browser:true`` finds all books with items (values in categories) currently shown in the :guilabel:`Tag browser`. This is useful if you set the preferences :guilabel:`Preferences . Look & feel . Tag browser . Hide empty categories` and :guilabel:`. Find shows all items that match`. With those two preferences set, doing a ``find`` in the :guilabel:`Tag browser` shows only categories containing items matched by the ``find``. The search ``in_tag_browser:true`` finds books with these categories / items. + *Search using templates* You can search using a template in :ref:`templatelangcalibre` instead of a metadata field. To do so you enter a template, a search type, and the value to search for. The syntax is:: diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index 5e490b55bf..12d82963b7 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -924,6 +924,8 @@ class DB: self.field_metadata.set_field_record_index('marked', base, prefer_custom=False) self.FIELD_MAP['series_sort'] = base = base+1 self.field_metadata.set_field_record_index('series_sort', base, prefer_custom=False) + self.FIELD_MAP['in_tag_browser'] = base = base+1 + self.field_metadata.set_field_record_index('in_tag_browser', base, prefer_custom=False) # }}} diff --git a/src/calibre/db/view.py b/src/calibre/db/view.py index b9c3268df9..1bbb7b8069 100644 --- a/src/calibre/db/view.py +++ b/src/calibre/db/view.py @@ -35,6 +35,21 @@ class MarkedVirtualField: return lambda book_id:g(book_id, '') +class InTagBrowserVirtualField: + + def __init__(self, _ids): + self._ids = _ids + + def iter_searchable_values(self, get_metadata, candidates, default_value=None): + for book_id in candidates: + yield str(book_id) if self._ids is None or book_id in self._ids else default_value, {book_id} + + def sort_keys_for_books(self, get_metadata, lang_map): + def key(_id): + return _id if self._ids is not None and _id in self._ids else '' + return key + + class TableRow: def __init__(self, book_id, view): @@ -80,6 +95,7 @@ class View: def __init__(self, cache): self.cache = cache self.marked_ids = {} + self.tag_browser_ids = None; self.marked_listeners = {} self.search_restriction_book_count = 0 self.search_restriction = self.base_restriction = '' @@ -235,7 +251,8 @@ class View: def get_virtual_libraries_for_books(self, ids): return self.cache.virtual_libraries_for_books( - ids, virtual_fields={'marked':MarkedVirtualField(self.marked_ids)}) + ids, virtual_fields={'marked':MarkedVirtualField(self.marked_ids), + 'in_tag_browser': InTagBrowserVirtualField(self.tag_browser_ids)}) def _do_sort(self, ids_to_sort, fields=(), subsort=False): fields = [(sanitize_sort_field_name(self.field_metadata, x), bool(y)) for x, y in fields] @@ -248,7 +265,8 @@ class View: return self.cache.multisort( fields, ids_to_sort=ids_to_sort, - virtual_fields={'marked':MarkedVirtualField(self.marked_ids)}) + virtual_fields={'marked':MarkedVirtualField(self.marked_ids), + 'in_tag_browser': InTagBrowserVirtualField(self.tag_browser_ids)}) def multisort(self, fields=[], subsort=False, only_ids=None): sorted_book_ids = self._do_sort(self._map if only_ids is None else only_ids, fields=fields, subsort=subsort) @@ -309,7 +327,8 @@ class View: self.full_map_is_sorted = True return rv matches = self.cache.search( - query, search_restriction, virtual_fields={'marked':MarkedVirtualField(self.marked_ids)}) + query, search_restriction, virtual_fields={'marked':MarkedVirtualField(self.marked_ids), + 'in_tag_browser': InTagBrowserVirtualField(self.tag_browser_ids)}) if len(matches) == len(self._map): rv = list(self._map) else: @@ -361,6 +380,12 @@ class View: def change_search_locations(self, newlocs): self.cache.change_search_locations(newlocs) + def set_in_tag_browser(self, id_set): + self.tag_browser_ids = id_set + + def get_in_tag_browser(self): + return self.tag_browser_ids + def set_marked_ids(self, id_dict): ''' ids in id_dict are "marked". They can be searched for by diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index b008934ec2..ea868281ca 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -323,6 +323,7 @@ class TagsModel(QAbstractItemModel): # {{{ search_item_renamed = pyqtSignal() tag_item_renamed = pyqtSignal() refresh_required = pyqtSignal() + research_required = pyqtSignal() restriction_error = pyqtSignal(object) drag_drop_finished = pyqtSignal(object) user_categories_edited = pyqtSignal(object, object) @@ -806,6 +807,24 @@ class TagsModel(QAbstractItemModel): # {{{ new_children.append(node) self.root_item.children = new_children self.root_item.children.sort(key=lambda x: self.row_map.index(x.category_key)) + if self.set_in_tag_browser(): + self.research_required.emit() + + def set_in_tag_browser(self): + # rebuild the list even if there is no filter. This lets us support + # in_tag_browser:true or :false based on the displayed categories. It is + # a form of shorthand for searching for all the visible categories with + # category1:true OR category2:true etc. The cost: walking the tree and + # building the set for a case that will certainly rarely be different + # from all books because all books have authors. + id_set = set() + for x in [a for a in self.root_item.children if a.category_key != 'search' and not a.is_gst]: + for t in x.child_tags(): + id_set |= t.tag.id_set + changed = self.db.data.get_in_tag_browser() != id_set + self.db.data.set_in_tag_browser(id_set) + return changed + def get_category_editor_data(self, category): for cat in self.root_item.children: @@ -1151,9 +1170,15 @@ class TagsModel(QAbstractItemModel): # {{{ # Get the categories try: + # We must disable the in_tag_browser ids because we want all the + # categories that will be filtered later. They might be restricted + # by a VL or extra restriction. + old_in_tb = self.db.data.get_in_tag_browser() + self.db.data.set_in_tag_browser(None) data = self.db.new_api.get_categories(sort=sort, book_ids=self.get_book_ids_to_use(), first_letter_sort=self.collapse_model == 'first letter') + self.db.data.set_in_tag_browser(old_in_tb) except Exception as e: traceback.print_exc() data = self.db.new_api.get_categories(sort=sort, diff --git a/src/calibre/gui2/tag_browser/ui.py b/src/calibre/gui2/tag_browser/ui.py index 6e54306f1f..bc612be407 100644 --- a/src/calibre/gui2/tag_browser/ui.py +++ b/src/calibre/gui2/tag_browser/ui.py @@ -97,6 +97,12 @@ class TagBrowserMixin: # {{{ self.tags_view.model().user_category_added.connect(self.user_categories_edited, type=Qt.ConnectionType.QueuedConnection) self.tags_view.edit_enum_values.connect(self.edit_enum_values) + self.tags_view.model().research_required.connect(self.do_gui_research, type=Qt.ConnectionType.QueuedConnection) + + def do_gui_research(self): + self.library_view.model().research() + # The count can change if the current search uses in_tag_browser, perhaps in a VL + self.library_view.model().count_changed() def user_categories_edited(self): self.library_view.model().refresh() @@ -728,6 +734,14 @@ class TagBrowserWidget(QFrame): # {{{ mt.m = l.manage_menu = QMenu(l.m) mt.setMenu(mt.m) + l.m.filter_action = ac = l.m.addAction(QIcon.ic('filter.png'), _('Filter book list (search %s)') % 'in_tag_browser:true') + # Give it a (complicated) shortcut so people can discover a shortcut + # is possible, I hope without creating collisions. + parent.keyboard.register_shortcut('tag browser filter booklist', + _('Filter book list'), default_keys=('Ctrl+Alt+Shift+F',), + action=ac, group=_('Tag browser')) + ac.triggered.connect(self.filter_book_list) + ac = QAction(parent) parent.addAction(ac) parent.keyboard.register_shortcut('tag browser toggle item', @@ -754,6 +768,10 @@ class TagBrowserWidget(QFrame): # {{{ ac.setText(_('Hide average rating') if config['show_avg_rating'] else _('Show average rating')) ac.setIcon(QIcon.ic('minus.png' if config['show_avg_rating'] else 'plus.png')) + def filter_book_list(self): + self.tags_view.model().set_in_tag_browser() + self._parent.search.set_search_string('in_tag_browser:true') + def toggle_counts(self): gprefs['tag_browser_show_counts'] ^= True diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 09bcfb7139..30e8c97d93 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -250,6 +250,16 @@ def _builtin_field_metadata(): 'is_custom':False, 'is_category':False, 'is_csp': False}), + ('in_tag_browser', {'table':None, + 'column':None, + 'datatype':'text', + 'is_multiple':{}, + 'kind':'field', + 'name': None, + 'search_terms':['in_tag_browser'], + 'is_custom':False, + 'is_category':False, + 'is_csp': False}), ('series_index',{'table':None, 'column':None, 'datatype':'float', From ec578eb4ae2e95bb78e8c936ed456c2e45212ead Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sun, 18 Sep 2022 15:14:30 +0100 Subject: [PATCH 2/2] Fix failing tests --- src/calibre/db/search.py | 2 ++ src/calibre/library/caches.py | 10 +++++++++- src/calibre/library/database2.py | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/calibre/db/search.py b/src/calibre/db/search.py index 03c86b36c9..afbbe7de1e 100644 --- a/src/calibre/db/search.py +++ b/src/calibre/db/search.py @@ -472,6 +472,8 @@ class Parser(SearchQueryParser): # {{{ self.virtual_fields = virtual_fields or {} if 'marked' not in self.virtual_fields: self.virtual_fields['marked'] = self + if 'in_tag_browser' not in self.virtual_fields: + self.virtual_fields['in_tag_browser'] = self SearchQueryParser.__init__(self, locations, optimize=True, lookup_saved_search=lookup_saved_search, parse_cache=parse_cache) @property diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index be4b24cf89..2bfcb2c564 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -938,6 +938,10 @@ class ResultCache(SearchQueryParser): # {{{ except: pass + in_tag_browser_col = self.FIELD_MAP['in_tag_browser'] + for r in self.iterall(): + r[in_tag_browser_col] = None + def get_marked(self, idx, index_is_id=True, default_value=None): id_ = idx if index_is_id else self[idx][0] return self.marked_ids_dict.get(id_, default_value) @@ -1056,7 +1060,7 @@ class ResultCache(SearchQueryParser): # {{{ if item is not None: item.append(db.book_on_device_string(item[0])) # Temp mark and series_sort columns - item.extend((None, None)) + item.extend((None, None, None)) marked_col = self.FIELD_MAP['marked'] for id_,val in iteritems(self.marked_ids_dict): @@ -1065,6 +1069,10 @@ class ResultCache(SearchQueryParser): # {{{ except: pass + in_tag_browser_col = self.FIELD_MAP['in_tag_browser'] + for r in self.iterall(): + r[in_tag_browser_col] = None + self._map = [i[0] for i in self._data if i is not None] if field is not None: self.sort(field, ascending) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 233a3b2ab7..61a714cb4a 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -462,6 +462,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.field_metadata.set_field_record_index('marked', base, prefer_custom=False) self.FIELD_MAP['series_sort'] = base = base+1 self.field_metadata.set_field_record_index('series_sort', base, prefer_custom=False) + self.FIELD_MAP['in_tag_browser'] = base = base+1 + self.field_metadata.set_field_record_index('in_tag_browser', base, prefer_custom=False) script = ''' DROP VIEW IF EXISTS meta2;