This commit is contained in:
Kovid Goyal 2022-09-20 07:07:37 +05:30
commit 0d3e4a232a
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
9 changed files with 100 additions and 4 deletions

View File

@ -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::

View File

@ -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)
# }}}

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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;

View File

@ -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',