Tag browser: Clicking on a nested category now searches for the category alone. Clicking twice searches fo rthe category and all its descendants and so on. Fixes #9166 (hierarchy indicator for no children). Also fix 9169

This commit is contained in:
Kovid Goyal 2011-02-27 09:37:48 -07:00
commit bc27bf06ce
6 changed files with 127 additions and 80 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -178,8 +178,10 @@ class TagCategories(QDialog, Ui_TagCategories):
'multiple periods in a row or spaces before ' 'multiple periods in a row or spaces before '
'or after periods.')).exec_() 'or after periods.')).exec_()
return False return False
for c in self.categories: for c in sorted(self.categories.keys(), key=sort_key):
if strcmp(c, cat_name) == 0: if strcmp(c, cat_name) == 0 or \
(icu_lower(cat_name).startswith(icu_lower(c) + '.') and\
not cat_name.startswith(c + '.')):
error_dialog(self, _('Name already used'), error_dialog(self, _('Name already used'),
_('That name is already used, perhaps with different case.')).exec_() _('That name is already used, perhaps with different case.')).exec_()
return False return False

View File

@ -217,11 +217,15 @@ class SearchBox2(QComboBox): # {{{
self.clear() self.clear()
else: else:
self.normalize_state() self.normalize_state()
self.lineEdit().setCompleter(None)
self.setEditText(txt) self.setEditText(txt)
self.line_edit.end(False) self.line_edit.end(False)
if emit_changed: if emit_changed:
self.changed.emit() self.changed.emit()
self._do_search(store_in_history=store_in_history) self._do_search(store_in_history=store_in_history)
c = QCompleter()
self.lineEdit().setCompleter(c)
c.setCompletionMode(c.PopupCompletion)
self.focus_to_library.emit() self.focus_to_library.emit()
finally: finally:
if not store_in_history: if not store_in_history:

View File

@ -21,6 +21,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE, gprefs from calibre.gui2 import config, NONE, gprefs
from calibre.library.field_metadata import TagsIcons, category_icon_map 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.config import tweaks
from calibre.utils.icu import sort_key, lower, strcmp from calibre.utils.icu import sort_key, lower, strcmp
from calibre.utils.search_query_parser import saved_searches 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): # {{{ class TagsView(QTreeView): # {{{
@ -127,12 +129,16 @@ class TagsView(QTreeView): # {{{
self.set_new_model(self._model.get_filter_categories_by()) self.set_new_model(self._model.get_filter_categories_by())
def set_database(self, db, tag_match, sort_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 # migrate from config to db prefs
if self.hidden_categories is None: if hidden_cats is None:
self.hidden_categories = config['tag_browser_hidden_categories'] 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)) db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
else:
self.hidden_categories = set(self.hidden_categories) self.hidden_categories = set(self.hidden_categories)
old = getattr(self, '_model', None) old = getattr(self, '_model', None)
@ -370,14 +376,15 @@ class TagsView(QTreeView): # {{{
action='delete_user_category', key=key)) action='delete_user_category', key=key))
self.context_menu.addSeparator() self.context_menu.addSeparator()
# Hide/Show/Restore categories # 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, self.context_menu.addAction(_('Hide category %s') % category,
partial(self.context_menu_handler, action='hide', partial(self.context_menu_handler, action='hide',
category=category)) category=key))
if self.hidden_categories: if self.hidden_categories:
m = self.context_menu.addMenu(_('Show category')) m = self.context_menu.addMenu(_('Show category'))
for col in sorted(self.hidden_categories, key=sort_key): for col in sorted(self.hidden_categories,
m.addAction(col, 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)) partial(self.context_menu_handler, action='show', category=col))
# search by category # search by category
@ -540,6 +547,7 @@ class TagTreeItem(object): # {{{
self.id_set = set() self.id_set = set()
self.is_gst = False self.is_gst = False
self.boxed = False self.boxed = False
self.icon_state_map = list(map(QVariant, icon_map))
if self.parent is not None: if self.parent is not None:
self.parent.append(self) self.parent.append(self)
if data is None: if data is None:
@ -554,9 +562,11 @@ class TagTreeItem(object): # {{{
self.bold_font = QVariant(self.bold_font) self.bold_font = QVariant(self.bold_font)
self.category_key = category_key self.category_key = category_key
self.temporary = temporary self.temporary = temporary
self.tag = Tag(data)
self.tag.is_hierarchical = category_key.startswith('@')
elif self.type == self.TAG: elif self.type == self.TAG:
icon_map[0] = data.icon icon_map[0] = data.icon
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) self.tag = data
if tooltip: if tooltip:
self.tooltip = tooltip + ' ' self.tooltip = tooltip + ' '
else: else:
@ -593,6 +603,8 @@ class TagTreeItem(object): # {{{
if role == Qt.EditRole: if role == Qt.EditRole:
return QVariant(self.py_name) return QVariant(self.py_name)
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
if self.tag.state:
return self.icon_state_map[self.tag.state]
return self.icon return self.icon
if role == Qt.FontRole: if role == Qt.FontRole:
return self.bold_font return self.bold_font
@ -642,9 +654,19 @@ class TagTreeItem(object): # {{{
''' '''
set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES
''' '''
if self.type == self.TAG:
if set_to is None: 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: else:
self.tag.state = set_to self.tag.state = set_to
@ -677,7 +699,8 @@ class TagsModel(QAbstractItemModel): # {{{
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags'] self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
self.drag_drop_finished = drag_drop_finished 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.db = db
self.tags_view = parent self.tags_view = parent
self.hidden_categories = hidden_categories self.hidden_categories = hidden_categories
@ -691,26 +714,33 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.get_node_tree(config['sort_tags_by']) data = self.get_node_tree(config['sort_tags_by'])
gst = db.prefs.get('grouped_search_terms', {}) gst = db.prefs.get('grouped_search_terms', {})
self.root_item = TagTreeItem() self.root_item = TagTreeItem(icon_map=self.icon_state_map)
self.category_nodes = [] self.category_nodes = []
last_category_node = None last_category_node = None
category_node_map = {} category_node_map = {}
self.category_node_tree = {} self.category_node_tree = {}
for i, r in enumerate(self.row_map): for i, key in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories: 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 continue
is_gst = False is_gst = False
if r.startswith('@') and r[1:] in gst: if key.startswith('@') and key[1:] in gst:
tt = _(u'The grouped search term name is "{0}"').format(r[1:]) tt = _(u'The grouped search term name is "{0}"').format(key[1:])
is_gst = True is_gst = True
elif r == 'news': elif key == 'news':
tt = '' tt = ''
else: else:
tt = _(u'The lookup/search name is "{0}"').format(r) tt = _(u'The lookup/search name is "{0}"').format(key)
if r.startswith('@'): if key.startswith('@'):
path_parts = [p for p in r.split('.')] path_parts = [p for p in key.split('.')]
path = '' path = ''
last_category_node = self.root_item last_category_node = self.root_item
tree_root = self.category_node_tree tree_root = self.category_node_tree
@ -719,9 +749,10 @@ class TagsModel(QAbstractItemModel): # {{{
if path not in category_node_map: if path not in category_node_map:
node = TagTreeItem(parent=last_category_node, node = TagTreeItem(parent=last_category_node,
data=p[1:] if i == 0 else p, data=p[1:] if i == 0 else p,
category_icon=self.category_icon_map[r], category_icon=self.category_icon_map[key],
tooltip=tt if path == r else path, tooltip=tt if path == key else path,
category_key=path) category_key=path,
icon_map=self.icon_state_map)
last_category_node = node last_category_node = node
category_node_map[path] = node category_node_map[path] = node
self.category_nodes.append(node) self.category_nodes.append(node)
@ -736,11 +767,12 @@ class TagsModel(QAbstractItemModel): # {{{
path += '.' path += '.'
else: else:
node = TagTreeItem(parent=self.root_item, node = TagTreeItem(parent=self.root_item,
data=self.categories[i], data=self.categories[key],
category_icon=self.category_icon_map[r], category_icon=self.category_icon_map[key],
tooltip=tt, category_key=r) tooltip=tt, category_key=key,
icon_map=self.icon_state_map)
node.is_gst = False node.is_gst = False
category_node_map[r] = node category_node_map[key] = node
last_category_node = node last_category_node = node
self.category_nodes.append(node) self.category_nodes.append(node)
self.refresh(data=data) self.refresh(data=data)
@ -1015,7 +1047,7 @@ class TagsModel(QAbstractItemModel): # {{{
def get_node_tree(self, sort): def get_node_tree(self, sort):
old_row_map = self.row_map[:] old_row_map = self.row_map[:]
self.row_map = [] self.row_map = []
self.categories = [] self.categories = {}
# Get the categories # Get the categories
if self.search_restriction: if self.search_restriction:
@ -1062,7 +1094,7 @@ class TagsModel(QAbstractItemModel): # {{{
for category in tb_categories: for category in tb_categories:
if category in data: # The search category can come and go if category in data: # The search category can come and go
self.row_map.append(category) 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): 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 # A category has been added or removed. We must force a rebuild of
@ -1163,7 +1195,8 @@ class TagsModel(QAbstractItemModel): # {{{
sub_cat = TagTreeItem(parent=category, data = name, sub_cat = TagTreeItem(parent=category, data = name,
tooltip = None, temporary=True, tooltip = None, temporary=True,
category_icon = category_node.icon, 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() self.endInsertRows()
else: # by 'first letter' else: # by 'first letter'
cl = cl_list[idx] cl = cl_list[idx]
@ -1173,7 +1206,8 @@ class TagsModel(QAbstractItemModel): # {{{
data = collapse_letter, data = collapse_letter,
category_icon = category_node.icon, category_icon = category_node.icon,
tooltip = None, temporary=True, 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 node_parent = sub_cat
else: else:
node_parent = category node_parent = category
@ -1284,16 +1318,19 @@ class TagsModel(QAbstractItemModel): # {{{
return False return False
user_cats = self.db.prefs.get('user_categories', {}) user_cats = self.db.prefs.get('user_categories', {})
user_cat_keys_lower = [icu_lower(k) for k in user_cats]
ckey = item.category_key[1:] ckey = item.category_key[1:]
ckey_lower = icu_lower(ckey)
dotpos = ckey.rfind('.') dotpos = ckey.rfind('.')
if dotpos < 0: if dotpos < 0:
nkey = val nkey = val
else: else:
nkey = ckey[:dotpos+1] + val nkey = ckey[:dotpos+1] + val
for c in user_cats: nkey_lower = icu_lower(nkey)
if c.startswith(ckey): for c in sorted(user_cats.keys(), key=sort_key):
if icu_lower(c).startswith(ckey_lower):
if len(c) == len(ckey): if len(c) == len(ckey):
if nkey in user_cats: if nkey_lower in user_cat_keys_lower:
error_dialog(self.tags_view, _('Rename user category'), error_dialog(self.tags_view, _('Rename user category'),
_('The name %s is already used')%nkey, show=True) _('The name %s is already used')%nkey, show=True)
return False return False
@ -1301,7 +1338,7 @@ class TagsModel(QAbstractItemModel): # {{{
del user_cats[ckey] del user_cats[ckey]
elif c[len(ckey)] == '.': elif c[len(ckey)] == '.':
rest = c[len(ckey):] rest = c[len(ckey):]
if (nkey + rest) in user_cats: if icu_lower(nkey + rest) in user_cat_keys_lower:
error_dialog(self.tags_view, _('Rename user category'), error_dialog(self.tags_view, _('Rename user category'),
_('The name %s is already used')%(nkey+rest), show=True) _('The name %s is already used')%(nkey+rest), show=True)
return False return False
@ -1477,7 +1514,6 @@ class TagsModel(QAbstractItemModel): # {{{
def reset_all_states(self, except_=None): def reset_all_states(self, except_=None):
update_list = [] update_list = []
def process_tag(tag_item): def process_tag(tag_item):
if tag_item.type != TagTreeItem.CATEGORY:
tag = tag_item.tag tag = tag_item.tag
if tag is except_: if tag is except_:
tag_index = self.createIndex(tag_item.row(), 0, tag_item) tag_index = self.createIndex(tag_item.row(), 0, tag_item)
@ -1503,13 +1539,11 @@ class TagsModel(QAbstractItemModel): # {{{
''' '''
if not index.isValid(): return False if not index.isValid(): return False
item = index.internalPointer() item = index.internalPointer()
if item.type == TagTreeItem.TAG:
item.toggle(set_to=set_to) item.toggle(set_to=set_to)
if exclusive: if exclusive:
self.reset_all_states(except_=item.tag) self.reset_all_states(except_=item.tag)
self.dataChanged.emit(index, index) self.dataChanged.emit(index, index)
return True return True
return False
def tokens(self): def tokens(self):
ans = [] ans = []
@ -1523,19 +1557,31 @@ class TagsModel(QAbstractItemModel): # {{{
# into the search string only once. The nodes_seen set helps us do that # into the search string only once. The nodes_seen set helps us do that
nodes_seen = set() 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: 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 key = node.category_key
for tag_item in node.child_tags(): for tag_item in node.child_tags():
tag = tag_item.tag tag = tag_item.tag
if tag.state != TAG_SEARCH_STATES['clear']: if tag.state != TAG_SEARCH_STATES['clear']:
prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \ if tag.state == TAG_SEARCH_STATES['mark_minus'] or \
else '' tag.state == TAG_SEARCH_STATES['mark_minusminus']:
prefix = ' not '
else:
prefix = ''
category = tag.category if key != 'news' else 'tag' category = tag.category if key != 'news' else 'tag'
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating 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))) ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
else: else:
name = original_name(tag) 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 category == 'tags':
if name in tags_seen: if name in tags_seen:
continue continue

View File

@ -419,28 +419,23 @@ class ResultCache(SearchQueryParser): # {{{
def get_user_category_matches(self, location, query, candidates): def get_user_category_matches(self, location, query, candidates):
res = set([]) res = set([])
if self.db_prefs is None: if self.db_prefs is None or len(query) < 2:
return res return res
user_cats = self.db_prefs.get('user_categories', []) user_cats = self.db_prefs.get('user_categories', [])
c = set(candidates) c = set(candidates)
l = location.rfind('.')
if l > 0: if query.startswith('.'):
alt_loc = location[0:l] check_subcats = True
alt_item = location[l+1:] query = query[1:]
else: else:
alt_loc = None check_subcats = False
for key in user_cats: 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]: for (item, category, ign) in user_cats[key]:
s = self.get_matches(category, '=' + item, candidates=c) s = self.get_matches(category, '=' + item, candidates=c)
c -= s c -= s
res |= 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': if query == 'false':
return candidates - res return candidates - res
return res return res