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: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::
|
||||
|
@ -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)
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user