mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
0d3e4a232a
@ -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:=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`.
|
* ``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*
|
*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::
|
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::
|
||||||
|
@ -924,6 +924,8 @@ class DB:
|
|||||||
self.field_metadata.set_field_record_index('marked', base, prefer_custom=False)
|
self.field_metadata.set_field_record_index('marked', base, prefer_custom=False)
|
||||||
self.FIELD_MAP['series_sort'] = base = base+1
|
self.FIELD_MAP['series_sort'] = base = base+1
|
||||||
self.field_metadata.set_field_record_index('series_sort', base, prefer_custom=False)
|
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)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -472,6 +472,8 @@ class Parser(SearchQueryParser): # {{{
|
|||||||
self.virtual_fields = virtual_fields or {}
|
self.virtual_fields = virtual_fields or {}
|
||||||
if 'marked' not in self.virtual_fields:
|
if 'marked' not in self.virtual_fields:
|
||||||
self.virtual_fields['marked'] = self
|
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)
|
SearchQueryParser.__init__(self, locations, optimize=True, lookup_saved_search=lookup_saved_search, parse_cache=parse_cache)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -35,6 +35,21 @@ class MarkedVirtualField:
|
|||||||
return lambda book_id:g(book_id, '')
|
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:
|
class TableRow:
|
||||||
|
|
||||||
def __init__(self, book_id, view):
|
def __init__(self, book_id, view):
|
||||||
@ -80,6 +95,7 @@ class View:
|
|||||||
def __init__(self, cache):
|
def __init__(self, cache):
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.marked_ids = {}
|
self.marked_ids = {}
|
||||||
|
self.tag_browser_ids = None;
|
||||||
self.marked_listeners = {}
|
self.marked_listeners = {}
|
||||||
self.search_restriction_book_count = 0
|
self.search_restriction_book_count = 0
|
||||||
self.search_restriction = self.base_restriction = ''
|
self.search_restriction = self.base_restriction = ''
|
||||||
@ -235,7 +251,8 @@ class View:
|
|||||||
|
|
||||||
def get_virtual_libraries_for_books(self, ids):
|
def get_virtual_libraries_for_books(self, ids):
|
||||||
return self.cache.virtual_libraries_for_books(
|
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):
|
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]
|
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(
|
return self.cache.multisort(
|
||||||
fields, ids_to_sort=ids_to_sort,
|
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):
|
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)
|
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
|
self.full_map_is_sorted = True
|
||||||
return rv
|
return rv
|
||||||
matches = self.cache.search(
|
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):
|
if len(matches) == len(self._map):
|
||||||
rv = list(self._map)
|
rv = list(self._map)
|
||||||
else:
|
else:
|
||||||
@ -361,6 +380,12 @@ class View:
|
|||||||
def change_search_locations(self, newlocs):
|
def change_search_locations(self, newlocs):
|
||||||
self.cache.change_search_locations(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):
|
def set_marked_ids(self, id_dict):
|
||||||
'''
|
'''
|
||||||
ids in id_dict are "marked". They can be searched for by
|
ids in id_dict are "marked". They can be searched for by
|
||||||
|
@ -323,6 +323,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
search_item_renamed = pyqtSignal()
|
search_item_renamed = pyqtSignal()
|
||||||
tag_item_renamed = pyqtSignal()
|
tag_item_renamed = pyqtSignal()
|
||||||
refresh_required = pyqtSignal()
|
refresh_required = pyqtSignal()
|
||||||
|
research_required = pyqtSignal()
|
||||||
restriction_error = pyqtSignal(object)
|
restriction_error = pyqtSignal(object)
|
||||||
drag_drop_finished = pyqtSignal(object)
|
drag_drop_finished = pyqtSignal(object)
|
||||||
user_categories_edited = pyqtSignal(object, object)
|
user_categories_edited = pyqtSignal(object, object)
|
||||||
@ -806,6 +807,24 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
new_children.append(node)
|
new_children.append(node)
|
||||||
self.root_item.children = new_children
|
self.root_item.children = new_children
|
||||||
self.root_item.children.sort(key=lambda x: self.row_map.index(x.category_key))
|
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):
|
def get_category_editor_data(self, category):
|
||||||
for cat in self.root_item.children:
|
for cat in self.root_item.children:
|
||||||
@ -1151,9 +1170,15 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
|
|
||||||
# Get the categories
|
# Get the categories
|
||||||
try:
|
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,
|
data = self.db.new_api.get_categories(sort=sort,
|
||||||
book_ids=self.get_book_ids_to_use(),
|
book_ids=self.get_book_ids_to_use(),
|
||||||
first_letter_sort=self.collapse_model == 'first letter')
|
first_letter_sort=self.collapse_model == 'first letter')
|
||||||
|
self.db.data.set_in_tag_browser(old_in_tb)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
data = self.db.new_api.get_categories(sort=sort,
|
data = self.db.new_api.get_categories(sort=sort,
|
||||||
|
@ -97,6 +97,12 @@ class TagBrowserMixin: # {{{
|
|||||||
self.tags_view.model().user_category_added.connect(self.user_categories_edited,
|
self.tags_view.model().user_category_added.connect(self.user_categories_edited,
|
||||||
type=Qt.ConnectionType.QueuedConnection)
|
type=Qt.ConnectionType.QueuedConnection)
|
||||||
self.tags_view.edit_enum_values.connect(self.edit_enum_values)
|
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):
|
def user_categories_edited(self):
|
||||||
self.library_view.model().refresh()
|
self.library_view.model().refresh()
|
||||||
@ -728,6 +734,14 @@ class TagBrowserWidget(QFrame): # {{{
|
|||||||
mt.m = l.manage_menu = QMenu(l.m)
|
mt.m = l.manage_menu = QMenu(l.m)
|
||||||
mt.setMenu(mt.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)
|
ac = QAction(parent)
|
||||||
parent.addAction(ac)
|
parent.addAction(ac)
|
||||||
parent.keyboard.register_shortcut('tag browser toggle item',
|
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.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'))
|
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):
|
def toggle_counts(self):
|
||||||
gprefs['tag_browser_show_counts'] ^= True
|
gprefs['tag_browser_show_counts'] ^= True
|
||||||
|
|
||||||
|
@ -938,6 +938,10 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
except:
|
except:
|
||||||
pass
|
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):
|
def get_marked(self, idx, index_is_id=True, default_value=None):
|
||||||
id_ = idx if index_is_id else self[idx][0]
|
id_ = idx if index_is_id else self[idx][0]
|
||||||
return self.marked_ids_dict.get(id_, default_value)
|
return self.marked_ids_dict.get(id_, default_value)
|
||||||
@ -1056,7 +1060,7 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
if item is not None:
|
if item is not None:
|
||||||
item.append(db.book_on_device_string(item[0]))
|
item.append(db.book_on_device_string(item[0]))
|
||||||
# Temp mark and series_sort columns
|
# Temp mark and series_sort columns
|
||||||
item.extend((None, None))
|
item.extend((None, None, None))
|
||||||
|
|
||||||
marked_col = self.FIELD_MAP['marked']
|
marked_col = self.FIELD_MAP['marked']
|
||||||
for id_,val in iteritems(self.marked_ids_dict):
|
for id_,val in iteritems(self.marked_ids_dict):
|
||||||
@ -1065,6 +1069,10 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
except:
|
except:
|
||||||
pass
|
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]
|
self._map = [i[0] for i in self._data if i is not None]
|
||||||
if field is not None:
|
if field is not None:
|
||||||
self.sort(field, ascending)
|
self.sort(field, ascending)
|
||||||
|
@ -462,6 +462,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.field_metadata.set_field_record_index('marked', base, prefer_custom=False)
|
self.field_metadata.set_field_record_index('marked', base, prefer_custom=False)
|
||||||
self.FIELD_MAP['series_sort'] = base = base+1
|
self.FIELD_MAP['series_sort'] = base = base+1
|
||||||
self.field_metadata.set_field_record_index('series_sort', base, prefer_custom=False)
|
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 = '''
|
script = '''
|
||||||
DROP VIEW IF EXISTS meta2;
|
DROP VIEW IF EXISTS meta2;
|
||||||
|
@ -250,6 +250,16 @@ def _builtin_field_metadata():
|
|||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False,
|
'is_category':False,
|
||||||
'is_csp': 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,
|
('series_index',{'table':None,
|
||||||
'column':None,
|
'column':None,
|
||||||
'datatype':'float',
|
'datatype':'float',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user