mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Fix #9169: not possible to hide user category with hierarchy
Fix #9166: hierarchy indicator for no children Fix removes all hidden tag browser categories. They must be re-hidden. Fix adds clicking on category nodes to search, plus four-state searching.
This commit is contained in:
parent
6afe430f8c
commit
7c22f4ffa1
BIN
resources/images/minusminus.png
Normal file
BIN
resources/images/minusminus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
resources/images/plusplus.png
Normal file
BIN
resources/images/plusplus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
@ -21,6 +21,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
|
||||
from calibre.ebooks.metadata import title_sort
|
||||
from calibre.gui2 import config, NONE, gprefs
|
||||
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
||||
from calibre.library.database2 import Tag
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.utils.icu import sort_key, lower, strcmp
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
@ -69,7 +70,8 @@ class TagDelegate(QItemDelegate): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_minus': 2}
|
||||
TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_plusplus': 2,
|
||||
'mark_minus': 3, 'mark_minusminus': 4}
|
||||
|
||||
class TagsView(QTreeView): # {{{
|
||||
|
||||
@ -127,12 +129,16 @@ class TagsView(QTreeView): # {{{
|
||||
self.set_new_model(self._model.get_filter_categories_by())
|
||||
|
||||
def set_database(self, db, tag_match, sort_by):
|
||||
self.hidden_categories = db.prefs.get('tag_browser_hidden_categories', None)
|
||||
hidden_cats = db.prefs.get('tag_browser_hidden_categories', None)
|
||||
self.hidden_categories = []
|
||||
# migrate from config to db prefs
|
||||
if self.hidden_categories is None:
|
||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||
if hidden_cats is None:
|
||||
hidden_cats = config['tag_browser_hidden_categories']
|
||||
# strip out any non-existence field keys
|
||||
for cat in hidden_cats:
|
||||
if cat in db.field_metadata:
|
||||
self.hidden_categories.append(cat)
|
||||
db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
|
||||
else:
|
||||
self.hidden_categories = set(self.hidden_categories)
|
||||
|
||||
old = getattr(self, '_model', None)
|
||||
@ -370,14 +376,15 @@ class TagsView(QTreeView): # {{{
|
||||
action='delete_user_category', key=key))
|
||||
self.context_menu.addSeparator()
|
||||
# Hide/Show/Restore categories
|
||||
if not key.startswith('@') or key.find('.') < 0:
|
||||
# if not key.startswith('@') or key.find('.') < 0:
|
||||
self.context_menu.addAction(_('Hide category %s') % category,
|
||||
partial(self.context_menu_handler, action='hide',
|
||||
category=category))
|
||||
category=key))
|
||||
if self.hidden_categories:
|
||||
m = self.context_menu.addMenu(_('Show category'))
|
||||
for col in sorted(self.hidden_categories, key=sort_key):
|
||||
m.addAction(col,
|
||||
for col in sorted(self.hidden_categories,
|
||||
key=lambda x: sort_key(self.db.field_metadata[x]['name'])):
|
||||
m.addAction(self.db.field_metadata[col]['name'],
|
||||
partial(self.context_menu_handler, action='show', category=col))
|
||||
|
||||
# search by category
|
||||
@ -540,6 +547,7 @@ class TagTreeItem(object): # {{{
|
||||
self.id_set = set()
|
||||
self.is_gst = False
|
||||
self.boxed = False
|
||||
self.icon_state_map = list(map(QVariant, icon_map))
|
||||
if self.parent is not None:
|
||||
self.parent.append(self)
|
||||
if data is None:
|
||||
@ -554,9 +562,11 @@ class TagTreeItem(object): # {{{
|
||||
self.bold_font = QVariant(self.bold_font)
|
||||
self.category_key = category_key
|
||||
self.temporary = temporary
|
||||
self.tag = Tag(data)
|
||||
self.tag.is_hierarchical = category_key.startswith('@')
|
||||
elif self.type == self.TAG:
|
||||
icon_map[0] = data.icon
|
||||
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
|
||||
self.tag = data
|
||||
if tooltip:
|
||||
self.tooltip = tooltip + ' '
|
||||
else:
|
||||
@ -593,6 +603,8 @@ class TagTreeItem(object): # {{{
|
||||
if role == Qt.EditRole:
|
||||
return QVariant(self.py_name)
|
||||
if role == Qt.DecorationRole:
|
||||
if self.tag.state:
|
||||
return self.icon_state_map[self.tag.state]
|
||||
return self.icon
|
||||
if role == Qt.FontRole:
|
||||
return self.bold_font
|
||||
@ -642,9 +654,20 @@ class TagTreeItem(object): # {{{
|
||||
'''
|
||||
set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES
|
||||
'''
|
||||
if self.type == self.TAG:
|
||||
# if self.type == self.TAG:
|
||||
if set_to is None:
|
||||
self.tag.state = (self.tag.state + 1)%3
|
||||
while True:
|
||||
self.tag.state = (self.tag.state + 1)%5
|
||||
if self.tag.state == TAG_SEARCH_STATES['mark_plus'] or \
|
||||
self.tag.state == TAG_SEARCH_STATES['mark_minus']:
|
||||
if self.tag.is_editable:
|
||||
break
|
||||
elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\
|
||||
self.tag.state == TAG_SEARCH_STATES['mark_minusminus']:
|
||||
if self.tag.is_hierarchical and len(self.children):
|
||||
break
|
||||
else:
|
||||
break
|
||||
else:
|
||||
self.tag.state = set_to
|
||||
|
||||
@ -677,7 +700,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
|
||||
self.drag_drop_finished = drag_drop_finished
|
||||
|
||||
self.icon_state_map = [None, QIcon(I('plus.png')), QIcon(I('minus.png'))]
|
||||
self.icon_state_map = [None, QIcon(I('plus.png')), QIcon(I('plusplus.png')),
|
||||
QIcon(I('minus.png')), QIcon(I('minusminus.png'))]
|
||||
self.db = db
|
||||
self.tags_view = parent
|
||||
self.hidden_categories = hidden_categories
|
||||
@ -691,26 +715,33 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
|
||||
data = self.get_node_tree(config['sort_tags_by'])
|
||||
gst = db.prefs.get('grouped_search_terms', {})
|
||||
self.root_item = TagTreeItem()
|
||||
self.root_item = TagTreeItem(icon_map=self.icon_state_map)
|
||||
self.category_nodes = []
|
||||
|
||||
last_category_node = None
|
||||
category_node_map = {}
|
||||
self.category_node_tree = {}
|
||||
for i, r in enumerate(self.row_map):
|
||||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||
for i, key in enumerate(self.row_map):
|
||||
if self.hidden_categories:
|
||||
if key in self.hidden_categories:
|
||||
continue
|
||||
found = False
|
||||
for cat in self.hidden_categories:
|
||||
if cat.startswith('@') and key.startswith(cat + '.'):
|
||||
found = True
|
||||
if found:
|
||||
continue
|
||||
is_gst = False
|
||||
if r.startswith('@') and r[1:] in gst:
|
||||
tt = _(u'The grouped search term name is "{0}"').format(r[1:])
|
||||
if key.startswith('@') and key[1:] in gst:
|
||||
tt = _(u'The grouped search term name is "{0}"').format(key[1:])
|
||||
is_gst = True
|
||||
elif r == 'news':
|
||||
elif key == 'news':
|
||||
tt = ''
|
||||
else:
|
||||
tt = _(u'The lookup/search name is "{0}"').format(r)
|
||||
tt = _(u'The lookup/search name is "{0}"').format(key)
|
||||
|
||||
if r.startswith('@'):
|
||||
path_parts = [p for p in r.split('.')]
|
||||
if key.startswith('@'):
|
||||
path_parts = [p for p in key.split('.')]
|
||||
path = ''
|
||||
last_category_node = self.root_item
|
||||
tree_root = self.category_node_tree
|
||||
@ -719,9 +750,10 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
if path not in category_node_map:
|
||||
node = TagTreeItem(parent=last_category_node,
|
||||
data=p[1:] if i == 0 else p,
|
||||
category_icon=self.category_icon_map[r],
|
||||
tooltip=tt if path == r else path,
|
||||
category_key=path)
|
||||
category_icon=self.category_icon_map[key],
|
||||
tooltip=tt if path == key else path,
|
||||
category_key=path,
|
||||
icon_map=self.icon_state_map)
|
||||
last_category_node = node
|
||||
category_node_map[path] = node
|
||||
self.category_nodes.append(node)
|
||||
@ -736,11 +768,12 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
path += '.'
|
||||
else:
|
||||
node = TagTreeItem(parent=self.root_item,
|
||||
data=self.categories[i],
|
||||
category_icon=self.category_icon_map[r],
|
||||
tooltip=tt, category_key=r)
|
||||
data=self.categories[key],
|
||||
category_icon=self.category_icon_map[key],
|
||||
tooltip=tt, category_key=key,
|
||||
icon_map=self.icon_state_map)
|
||||
node.is_gst = False
|
||||
category_node_map[r] = node
|
||||
category_node_map[key] = node
|
||||
last_category_node = node
|
||||
self.category_nodes.append(node)
|
||||
self.refresh(data=data)
|
||||
@ -1015,7 +1048,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
def get_node_tree(self, sort):
|
||||
old_row_map = self.row_map[:]
|
||||
self.row_map = []
|
||||
self.categories = []
|
||||
self.categories = {}
|
||||
|
||||
# Get the categories
|
||||
if self.search_restriction:
|
||||
@ -1062,7 +1095,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
for category in tb_categories:
|
||||
if category in data: # The search category can come and go
|
||||
self.row_map.append(category)
|
||||
self.categories.append(tb_categories[category]['name'])
|
||||
self.categories[category] = tb_categories[category]['name']
|
||||
|
||||
if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map):
|
||||
# A category has been added or removed. We must force a rebuild of
|
||||
@ -1163,7 +1196,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
sub_cat = TagTreeItem(parent=category, data = name,
|
||||
tooltip = None, temporary=True,
|
||||
category_icon = category_node.icon,
|
||||
category_key=category_node.category_key)
|
||||
category_key=category_node.category_key,
|
||||
icon_map=self.icon_state_map)
|
||||
self.endInsertRows()
|
||||
else: # by 'first letter'
|
||||
cl = cl_list[idx]
|
||||
@ -1173,7 +1207,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
data = collapse_letter,
|
||||
category_icon = category_node.icon,
|
||||
tooltip = None, temporary=True,
|
||||
category_key=category_node.category_key)
|
||||
category_key=category_node.category_key,
|
||||
icon_map=self.icon_state_map)
|
||||
node_parent = sub_cat
|
||||
else:
|
||||
node_parent = category
|
||||
@ -1477,7 +1512,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
def reset_all_states(self, except_=None):
|
||||
update_list = []
|
||||
def process_tag(tag_item):
|
||||
if tag_item.type != TagTreeItem.CATEGORY:
|
||||
# if tag_item.type != TagTreeItem.CATEGORY:
|
||||
tag = tag_item.tag
|
||||
if tag is except_:
|
||||
tag_index = self.createIndex(tag_item.row(), 0, tag_item)
|
||||
@ -1503,13 +1538,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
'''
|
||||
if not index.isValid(): return False
|
||||
item = index.internalPointer()
|
||||
if item.type == TagTreeItem.TAG:
|
||||
item.toggle(set_to=set_to)
|
||||
if exclusive:
|
||||
self.reset_all_states(except_=item.tag)
|
||||
self.dataChanged.emit(index, index)
|
||||
return True
|
||||
return False
|
||||
|
||||
def tokens(self):
|
||||
ans = []
|
||||
@ -1523,19 +1556,31 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
# into the search string only once. The nodes_seen set helps us do that
|
||||
nodes_seen = set()
|
||||
|
||||
node_searches = {TAG_SEARCH_STATES['mark_plus'] : 'true',
|
||||
TAG_SEARCH_STATES['mark_plusplus'] : '.true',
|
||||
TAG_SEARCH_STATES['mark_minus'] : 'false',
|
||||
TAG_SEARCH_STATES['mark_minusminus'] : '.false'}
|
||||
|
||||
for node in self.category_nodes:
|
||||
if node.tag.state:
|
||||
ans.append('%s:%s'%(node.category_key, node_searches[node.tag.state]))
|
||||
|
||||
key = node.category_key
|
||||
for tag_item in node.child_tags():
|
||||
tag = tag_item.tag
|
||||
if tag.state != TAG_SEARCH_STATES['clear']:
|
||||
prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \
|
||||
else ''
|
||||
if tag.state == TAG_SEARCH_STATES['mark_minus'] or \
|
||||
tag.state == TAG_SEARCH_STATES['mark_minusminus']:
|
||||
prefix = ' not '
|
||||
else:
|
||||
prefix = ''
|
||||
category = tag.category if key != 'news' else 'tag'
|
||||
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
|
||||
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
|
||||
else:
|
||||
name = original_name(tag)
|
||||
use_prefix = tag.is_hierarchical
|
||||
use_prefix = tag.state in [TAG_SEARCH_STATES['mark_plusplus'],
|
||||
TAG_SEARCH_STATES['mark_minusminus']]
|
||||
if category == 'tags':
|
||||
if name in tags_seen:
|
||||
continue
|
||||
|
@ -419,28 +419,23 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
|
||||
def get_user_category_matches(self, location, query, candidates):
|
||||
res = set([])
|
||||
if self.db_prefs is None:
|
||||
if self.db_prefs is None or len(query) < 2:
|
||||
return res
|
||||
user_cats = self.db_prefs.get('user_categories', [])
|
||||
c = set(candidates)
|
||||
l = location.rfind('.')
|
||||
if l > 0:
|
||||
alt_loc = location[0:l]
|
||||
alt_item = location[l+1:]
|
||||
|
||||
if query.startswith('.'):
|
||||
check_subcats = True
|
||||
query = query[1:]
|
||||
else:
|
||||
alt_loc = None
|
||||
check_subcats = False
|
||||
|
||||
for key in user_cats:
|
||||
if key == location or key.startswith(location + '.'):
|
||||
if key == location or (check_subcats and key.startswith(location + '.')):
|
||||
for (item, category, ign) in user_cats[key]:
|
||||
s = self.get_matches(category, '=' + item, candidates=c)
|
||||
c -= s
|
||||
res |= s
|
||||
elif key == alt_loc:
|
||||
for (item, category, ign) in user_cats[key]:
|
||||
if item == alt_item:
|
||||
s = self.get_matches(category, '=' + item, candidates=c)
|
||||
c -= s
|
||||
res |= s
|
||||
if query == 'false':
|
||||
return candidates - res
|
||||
return res
|
||||
|
Loading…
x
Reference in New Issue
Block a user