This commit is contained in:
Kovid Goyal 2011-02-24 08:19:01 -07:00
commit a28e0b6f1d
3 changed files with 310 additions and 75 deletions

View File

@ -58,10 +58,12 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.to_rename = {} self.to_rename = {}
self.to_delete = set([]) self.to_delete = set([])
self.original_names = {}
self.all_tags = {} self.all_tags = {}
for k,v in data: for k,v in data:
self.all_tags[v] = k self.all_tags[v] = k
self.original_names[k] = v
for tag in sorted(self.all_tags.keys(), key=key): for tag in sorted(self.all_tags.keys(), key=key):
item = ListWidgetItem(tag) item = ListWidgetItem(tag)
item.setData(Qt.UserRole, self.all_tags[tag]) item.setData(Qt.UserRole, self.all_tags[tag])

View File

@ -25,7 +25,7 @@ from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key, upper, lower, strcmp from calibre.utils.icu import sort_key, upper, lower, strcmp
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.utils.formatter import eval_formatter from calibre.utils.formatter import eval_formatter
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog, question_dialog
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.dialogs.tag_list_editor import TagListEditor
@ -70,16 +70,19 @@ TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_minus': 2}
class TagsView(QTreeView): # {{{ class TagsView(QTreeView): # {{{
refresh_required = pyqtSignal() refresh_required = pyqtSignal()
tags_marked = pyqtSignal(object) tags_marked = pyqtSignal(object)
user_category_edit = pyqtSignal(object) edit_user_category = pyqtSignal(object)
add_subcategory = pyqtSignal(object) delete_user_category = pyqtSignal(object)
tag_list_edit = pyqtSignal(object, object) del_item_from_user_cat = pyqtSignal(object, object, object)
saved_search_edit = pyqtSignal(object) add_item_to_user_cat = pyqtSignal(object, object, object)
author_sort_edit = pyqtSignal(object, object) add_subcategory = pyqtSignal(object)
tag_item_renamed = pyqtSignal() tag_list_edit = pyqtSignal(object, object)
search_item_renamed = pyqtSignal() saved_search_edit = pyqtSignal(object)
drag_drop_finished = pyqtSignal(object, object) author_sort_edit = pyqtSignal(object, object)
tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal()
drag_drop_finished = pyqtSignal(object, object)
def __init__(self, parent=None): def __init__(self, parent=None):
QTreeView.__init__(self, parent=None) QTreeView.__init__(self, parent=None)
@ -105,6 +108,7 @@ class TagsView(QTreeView): # {{{
else: else:
self.collapse_model = gprefs['tags_browser_partition_method'] self.collapse_model = gprefs['tags_browser_partition_method']
self.search_icon = QIcon(I('search.png')) self.search_icon = QIcon(I('search.png'))
self.user_category_icon = QIcon(I('tb_folder.png'))
def set_pane_is_visible(self, to_what): def set_pane_is_visible(self, to_what):
pv = self.pane_is_visible pv = self.pane_is_visible
@ -218,17 +222,29 @@ class TagsView(QTreeView): # {{{
self.tag_list_edit.emit(category, key) self.tag_list_edit.emit(category, key)
return return
if action == 'manage_categories': if action == 'manage_categories':
self.user_category_edit.emit(category) self.edit_user_category.emit(category)
return
if action == 'add_subcategory':
self.add_subcategory.emit(category)
return return
if action == 'search': if action == 'search':
self._toggle(index, set_to=search_state) self._toggle(index, set_to=search_state)
return return
if action == 'add_to_category':
self.add_item_to_user_cat.emit(category,
getattr(index, 'original_name', index.name),
index.category)
return
if action == 'add_subcategory':
self.add_subcategory.emit(key)
return
if action == 'search_category': if action == 'search_category':
self.tags_marked.emit(key + ':' + search_state) self.tags_marked.emit(key + ':' + search_state)
return return
if action == 'delete_user_category':
self.delete_user_category.emit(key)
return
if action == 'delete_item_from_user_category':
self.del_item_from_user_cat.emit(key,
getattr(index, 'original_name', index.name), index.category)
return
if action == 'manage_searches': if action == 'manage_searches':
self.saved_search_edit.emit(category) self.saved_search_edit.emit(category)
return return
@ -259,14 +275,11 @@ class TagsView(QTreeView): # {{{
if index.isValid(): if index.isValid():
item = index.internalPointer() item = index.internalPointer()
tag_name = '' tag = None
if item.type == TagTreeItem.TAG: if item.type == TagTreeItem.TAG:
tag_item = item tag = item.tag
t = item.tag can_edit = getattr(tag, 'can_edit', True)
tag_name = t.name
tag_id = t.id
can_edit = getattr(t, 'can_edit', True)
while item.type != TagTreeItem.CATEGORY: while item.type != TagTreeItem.CATEGORY:
item = item.parent item = item.parent
@ -281,42 +294,70 @@ class TagsView(QTreeView): # {{{
return True return True
# Did the user click on a leaf node? # Did the user click on a leaf node?
if tag_name: if tag:
# If the user right-clicked on an editable item, then offer # If the user right-clicked on an editable item, then offer
# the possibility of renaming that item. # the possibility of renaming that item.
if can_edit and \ if can_edit:
key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
(self.db.field_metadata[key]['is_custom'] and \
self.db.field_metadata[key]['datatype'] != 'rating'):
# Add the 'rename' items # Add the 'rename' items
self.context_menu.addAction(_('Rename %s')%tag_name, self.context_menu.addAction(_('Rename %s')%tag.name,
partial(self.context_menu_handler, action='edit_item', partial(self.context_menu_handler, action='edit_item',
category=tag_item, index=index)) index=index))
if key == 'authors': if key == 'authors':
self.context_menu.addAction(_('Edit sort for %s')%tag_name, self.context_menu.addAction(_('Edit sort for %s')%tag.name,
partial(self.context_menu_handler, partial(self.context_menu_handler,
action='edit_author_sort', index=tag_id)) action='edit_author_sort', index=tag.id))
m = self.context_menu.addMenu(self.user_category_icon,
_('Add %s to user category')%tag.name)
nt = self.model().category_node_tree
def add_node_tree(tree_dict, m, path):
p = path[:]
for k in sorted(tree_dict.keys(), key=sort_key):
p.append(k)
n = k[1:] if k.startswith('@') else k
m.addAction(self.user_category_icon, n,
partial(self.context_menu_handler,
'add_to_category',
category='.'.join(p),
index=tag))
if len(tree_dict[k]):
tm = m.addMenu(self.user_category_icon,
_('Children of %s')%n)
add_node_tree(tree_dict[k], tm, p)
p.pop()
add_node_tree(nt, m, [])
if key.startswith('@') and not item.is_gst:
self.context_menu.addAction(self.user_category_icon,
_('Remove %s from category %s')%(tag.name, item.py_name),
partial(self.context_menu_handler,
action='delete_item_from_user_category',
key = key, index = tag))
# Add the search for value items # Add the search for value items
self.context_menu.addAction(self.search_icon, self.context_menu.addAction(self.search_icon,
_('Search for %s')%tag_name, _('Search for %s')%tag.name,
partial(self.context_menu_handler, action='search', partial(self.context_menu_handler, action='search',
search_state=TAG_SEARCH_STATES['mark_plus'], search_state=TAG_SEARCH_STATES['mark_plus'],
index=index)) index=index))
self.context_menu.addAction(self.search_icon, self.context_menu.addAction(self.search_icon,
_('Search for everything but %s')%tag_name, _('Search for everything but %s')%tag.name,
partial(self.context_menu_handler, action='search', partial(self.context_menu_handler, action='search',
search_state=TAG_SEARCH_STATES['mark_minus'], search_state=TAG_SEARCH_STATES['mark_minus'],
index=index)) index=index))
self.context_menu.addSeparator() self.context_menu.addSeparator()
elif key.startswith('@'): elif key.startswith('@') and not item.is_gst:
if item.can_edit: if item.can_edit:
self.context_menu.addAction(_('Rename %s')%key[1:], self.context_menu.addAction(self.user_category_icon,
_('Rename %s')%item.py_name,
partial(self.context_menu_handler, action='edit_item', partial(self.context_menu_handler, action='edit_item',
category=key, index=index)) index=index))
self.context_menu.addAction(self.search_icon, self.context_menu.addAction(self.user_category_icon,
_('Add sub-category to %s')%key[1:], _('Add sub-category to %s')%item.py_name,
partial(self.context_menu_handler, partial(self.context_menu_handler,
action='add_subcategory', category=key)) action='add_subcategory', key=key))
self.context_menu.addAction(self.user_category_icon,
_('Delete user category %s')%item.py_name,
partial(self.context_menu_handler,
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:
@ -345,14 +386,15 @@ class TagsView(QTreeView): # {{{
self.db.field_metadata[key]['is_custom']: self.db.field_metadata[key]['is_custom']:
self.context_menu.addAction(_('Manage %s')%category, self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='open_editor', partial(self.context_menu_handler, action='open_editor',
category=tag_name, key=key)) category=getattr(tag, 'original_name', tag.name)
if tag else None, key=key))
elif key == 'authors': elif key == 'authors':
self.context_menu.addAction(_('Manage %s')%category, self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='edit_author_sort')) partial(self.context_menu_handler, action='edit_author_sort'))
elif key == 'search': elif key == 'search':
self.context_menu.addAction(_('Manage Saved Searches'), self.context_menu.addAction(_('Manage Saved Searches'),
partial(self.context_menu_handler, action='manage_searches', partial(self.context_menu_handler, action='manage_searches',
category=tag_name)) category=tag.name if tag else None))
# Always show the user categories editor # Always show the user categories editor
self.context_menu.addSeparator() self.context_menu.addSeparator()
@ -405,7 +447,7 @@ class TagsView(QTreeView): # {{{
if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled: if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled:
self.setDropIndicatorShown(not src_is_tb) self.setDropIndicatorShown(not src_is_tb)
return return
if item.type == TagTreeItem.CATEGORY: if item.type == TagTreeItem.CATEGORY and not item.is_gst:
fm_dest = self.db.metadata_for_field(item.category_key) fm_dest = self.db.metadata_for_field(item.category_key)
if fm_dest['kind'] == 'user': if fm_dest['kind'] == 'user':
if src_is_tb: if src_is_tb:
@ -413,7 +455,8 @@ class TagsView(QTreeView): # {{{
data = str(event.mimeData().data('application/calibre+from_tag_browser')) data = str(event.mimeData().data('application/calibre+from_tag_browser'))
src = cPickle.loads(data) src = cPickle.loads(data)
for s in src: for s in src:
if s[0] == TagTreeItem.TAG and not s[1].startswith('@'): if s[0] == TagTreeItem.TAG and \
(not s[1].startswith('@') or s[2]):
return return
self.setDropIndicatorShown(True) self.setDropIndicatorShown(True)
return return
@ -534,6 +577,8 @@ class TagTreeItem(object): # {{{
def category_data(self, role): def category_data(self, role):
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
return QVariant(self.py_name + ' [%d]'%len(self.child_tags())) return QVariant(self.py_name + ' [%d]'%len(self.child_tags()))
if role == Qt.EditRole:
return QVariant(self.py_name)
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
return self.icon return self.icon
if role == Qt.FontRole: if role == Qt.FontRole:
@ -634,11 +679,14 @@ class TagsModel(QAbstractItemModel): # {{{
last_category_node = None last_category_node = None
category_node_map = {} category_node_map = {}
self.category_node_tree = {}
for i, r in enumerate(self.row_map): for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories: if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue continue
is_gst = False
if r.startswith('@') and r[1:] in gst: if r.startswith('@') and r[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(r[1:])
is_gst = True
elif r == 'news': elif r == 'news':
tt = '' tt = ''
else: else:
@ -648,6 +696,7 @@ class TagsModel(QAbstractItemModel): # {{{
path_parts = [p.strip() for p in r.split('.') if p.strip()] path_parts = [p.strip() for p in r.split('.') if p.strip()]
path = '' path = ''
last_category_node = self.root_item last_category_node = self.root_item
tree_root = self.category_node_tree
for i,p in enumerate(path_parts): for i,p in enumerate(path_parts):
path += p path += p
if path not in category_node_map: if path not in category_node_map:
@ -659,15 +708,21 @@ class TagsModel(QAbstractItemModel): # {{{
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)
node.can_edit = i == (len(path_parts) - 1) node.can_edit = (not is_gst) and (i == (len(path_parts)-1))
node.is_gst = is_gst
if not is_gst:
tree_root[p] = {}
tree_root = tree_root[p]
else: else:
last_category_node = category_node_map[path] last_category_node = category_node_map[path]
tree_root = tree_root[p]
path += '.' path += '.'
else: else:
node = TagTreeItem(parent=self.root_item, node = TagTreeItem(parent=self.root_item,
data=self.categories[i], data=self.categories[i],
category_icon=self.category_icon_map[r], category_icon=self.category_icon_map[r],
tooltip=tt, category_key=r) tooltip=tt, category_key=r)
node.is_gst = False
category_node_map[r] = node category_node_map[r] = node
last_category_node = node last_category_node = node
self.category_nodes.append(node) self.category_nodes.append(node)
@ -693,7 +748,7 @@ class TagsModel(QAbstractItemModel): # {{{
p = node p = node
while p.type != TagTreeItem.CATEGORY: while p.type != TagTreeItem.CATEGORY:
p = p.parent p = p.parent
d = (node.type, p.category_key, d = (node.type, p.category_key, p.is_gst,
getattr(t, 'original_name', t.name), t.category, t.id) getattr(t, 'original_name', t.name), t.category, t.id)
data.append(d) data.append(d)
else: else:
@ -727,10 +782,23 @@ class TagsModel(QAbstractItemModel): # {{{
for s in src: for s in src:
if s[0] != TagTreeItem.TAG: if s[0] != TagTreeItem.TAG:
return False return False
return self.move_or_copy_item_to_user_category(src, dest, action)
def move_or_copy_item_to_user_category(self, src, dest, action):
'''
src is a list of tuples representing items to copy. The tuple is
(type, containing category key, category key is global search term,
full name, category key, id)
The 'id' member is ignored, and can be None.
The type must be TagTreeItem.TAG
dest is the TagTreeItem node to receive the items
action is Qt.CopyAction or Qt.MoveAction
'''
user_cats = self.db.prefs.get('user_categories', {}) user_cats = self.db.prefs.get('user_categories', {})
parent_node = None parent_node = None
copied_node = None
for s in src: for s in src:
src_parent, src_name, src_cat = s[1:4] src_parent, src_parent_is_gst, src_name, src_cat = s[1:5]
parent_node = src_parent parent_node = src_parent
if src_parent.startswith('@'): if src_parent.startswith('@'):
is_uc = True is_uc = True
@ -742,12 +810,16 @@ class TagsModel(QAbstractItemModel): # {{{
continue continue
new_cat = [] new_cat = []
# delete the item if the source is a user category and action is move # delete the item if the source is a user category and action is move
if is_uc and src_parent in user_cats and action == Qt.MoveAction: if is_uc and not src_parent_is_gst and src_parent in user_cats and \
action == Qt.MoveAction:
for tup in user_cats[src_parent]: for tup in user_cats[src_parent]:
if src_name == tup[0] and src_cat == tup[1]: if src_name == tup[0] and src_cat == tup[1]:
continue continue
new_cat.append(list(tup)) new_cat.append(list(tup))
user_cats[src_parent] = new_cat user_cats[src_parent] = new_cat
else:
copied_node = (src_parent, src_name)
# Now add the item to the destination user category # Now add the item to the destination user category
add_it = True add_it = True
if not is_uc and src_cat == 'news': if not is_uc and src_cat == 'news':
@ -757,12 +829,17 @@ class TagsModel(QAbstractItemModel): # {{{
add_it = False add_it = False
if add_it: if add_it:
user_cats[dest_key].append([src_name, src_cat, 0]) user_cats[dest_key].append([src_name, src_cat, 0])
self.db.prefs.set('user_categories', user_cats) self.db.prefs.set('user_categories', user_cats)
self.tags_view.set_new_model() self.tags_view.recount()
if parent_node is not None: if parent_node is not None:
# Must work with the new model here
m = self.tags_view.model() m = self.tags_view.model()
path = m.find_category_node(parent_node) if copied_node is not None:
path = m.find_item_node(parent_node, copied_node[1], None,
equals_match=True)
else:
path = m.find_category_node(parent_node)
idx = m.index_for_path(path) idx = m.index_for_path(path)
self.tags_view.setExpanded(idx, True) self.tags_view.setExpanded(idx, True)
m.show_item_at_index(idx) m.show_item_at_index(idx)
@ -1031,15 +1108,17 @@ class TagsModel(QAbstractItemModel): # {{{
node_parent = category node_parent = category
components = [t for t in tag.name.split('.')] components = [t for t in tag.name.split('.')]
if key in ['authors', 'publisher', 'news', 'formats'] or \ if key in ['authors', 'publisher', 'news', 'formats', 'rating'] or \
key not in self.db.prefs.get('categories_using_hierarchy', []) or\ key not in self.db.prefs.get('categories_using_hierarchy', []) or\
len(components) == 1 or \ len(components) == 1 or \
fm['kind'] == 'user' or \ fm['kind'] == 'user':
fm['datatype'] not in ['text', 'series', 'enumeration']:
self.beginInsertRows(category_index, 999999, 1) self.beginInsertRows(category_index, 999999, 1)
TagTreeItem(parent=node_parent, data=tag, tooltip=tt, TagTreeItem(parent=node_parent, data=tag, tooltip=tt,
icon_map=self.icon_state_map) icon_map=self.icon_state_map)
self.endInsertRows() self.endInsertRows()
tag.can_edit = key != 'formats' and (key == 'news' or \
self.db.field_metadata[tag.category]['datatype'] in \
['text', 'series', 'enumeration'])
else: else:
for i,comp in enumerate(components): for i,comp in enumerate(components):
child_map = dict([(t.tag.name, t) for t in node_parent.children child_map = dict([(t.tag.name, t) for t in node_parent.children
@ -1047,6 +1126,7 @@ class TagsModel(QAbstractItemModel): # {{{
if comp in child_map: if comp in child_map:
node_parent = child_map[comp] node_parent = child_map[comp]
node_parent.tag.count += tag.count node_parent.tag.count += tag.count
node_parent.tag.use_prefix = True
else: else:
if i < len(components)-1: if i < len(components)-1:
t = copy.copy(tag) t = copy.copy(tag)
@ -1137,10 +1217,9 @@ class TagsModel(QAbstractItemModel): # {{{
p = self.tags_view.model().find_category_node('@' + nkey) p = self.tags_view.model().find_category_node('@' + nkey)
self.tags_view.model().show_item_at_path(p) self.tags_view.model().show_item_at_path(p)
return True return True
itm = item.parent
while itm.type != TagTreeItem.CATEGORY: key = item.tag.category
itm = itm.parent name = getattr(item.tag, 'original_name', item.tag.name)
key = itm.category_key
# make certain we know about the item's category # make certain we know about the item's category
if key not in self.db.field_metadata: if key not in self.db.field_metadata:
return False return False
@ -1171,10 +1250,54 @@ class TagsModel(QAbstractItemModel): # {{{
label=self.db.field_metadata[key]['label']) label=self.db.field_metadata[key]['label'])
self.tags_view.tag_item_renamed.emit() self.tags_view.tag_item_renamed.emit()
item.tag.name = val item.tag.name = val
self.rename_item_in_all_user_categories(name, key, val)
self.refresh() # Should work, because no categories can have disappeared self.refresh() # Should work, because no categories can have disappeared
self.show_item_at_path(path) self.show_item_at_path(path)
return True return True
def rename_item_in_all_user_categories(self, item_name, item_category, new_name):
'''
Search all user categories for items named item_name with category
item_category and rename them to new_name. The caller must arrange to
redisplay the tree as appropriate (recount or set_new_model)
'''
user_cats = self.db.prefs.get('user_categories', {})
for k in user_cats.keys():
new_contents = []
for tup in user_cats[k]:
if tup[0] == item_name and tup[1] == item_category:
new_contents.append([new_name, item_category, 0])
else:
new_contents.append(tup)
user_cats[k] = new_contents
self.db.prefs.set('user_categories', user_cats)
def delete_item_from_all_user_categories(self, item_name, item_category):
'''
Search all user categories for items named item_name with category
item_category and delete them. The caller must arrange to redisplay the
tree as appropriate (recount or set_new_model)
'''
user_cats = self.db.prefs.get('user_categories', {})
for cat in user_cats.keys():
self.delete_item_from_user_category(cat, item_name, item_category,
user_categories=user_cats)
self.db.prefs.set('user_categories', user_cats)
def delete_item_from_user_category(self, category, item_name, item_category,
user_categories=None):
if user_categories is not None:
user_cats = user_categories
else:
user_cats = self.db.prefs.get('user_categories', {})
new_contents = []
for tup in user_cats[category]:
if tup[0] != item_name or tup[1] != item_category:
new_contents.append(tup)
user_cats[category] = new_contents
if user_categories is None:
self.db.prefs.set('user_categories', user_cats)
def headerData(self, *args): def headerData(self, *args):
return NONE return NONE
@ -1329,19 +1452,20 @@ class TagsModel(QAbstractItemModel): # {{{
name.replace(r'"', r'\"'))) name.replace(r'"', r'\"')))
return ans return ans
def find_item_node(self, key, txt, start_path): def find_item_node(self, key, txt, start_path, equals_match=False):
''' '''
Search for an item (a node) in the tags browser list that matches both Search for an item (a node) in the tags browser list that matches both
the key (exact case-insensitive match) and txt (contains case- the key (exact case-insensitive match) and txt (not equals_match =>
insensitive match). Returns the path to the node. Note that paths are to case-insensitive contains match; equals_match => case_insensitive
a location (second item, fourth item, 25 item), not to a node. If equal match). Returns the path to the node. Note that paths are to a
location (second item, fourth item, 25 item), not to a node. If
start_path is None, the search starts with the topmost node. If the tree start_path is None, the search starts with the topmost node. If the tree
is changed subsequent to calling this method, the path can easily refer is changed subsequent to calling this method, the path can easily refer
to a different node or no node at all. to a different node or no node at all.
''' '''
if not txt: if not txt:
return None return None
txt = lower(txt) txt = lower(txt) if not equals_match else txt
self.path_found = None self.path_found = None
if start_path is None: if start_path is None:
start_path = [] start_path = []
@ -1353,9 +1477,14 @@ class TagsModel(QAbstractItemModel): # {{{
tag = tag_item.tag tag = tag_item.tag
if tag is None: if tag is None:
return False return False
if lower(tag.name).find(txt) >= 0: name = getattr(tag, 'original_name', tag.name)
if (equals_match and strcmp(name, txt) == 0) or \
(not equals_match and lower(name).find(txt) >= 0):
self.path_found = path self.path_found = path
return True return True
for i,c in enumerate(tag_item.children):
if process_tag(depth+1, self.createIndex(i, 0, c), c, start_path):
return True
return False return False
def process_level(depth, category_index, start_path): def process_level(depth, category_index, start_path):
@ -1365,15 +1494,14 @@ class TagsModel(QAbstractItemModel): # {{{
return False return False
if path[depth] > start_path[depth]: if path[depth] > start_path[depth]:
start_path = path start_path = path
if key and strcmp(category_index.internalPointer().category_key, key) != 0: my_key = category_index.internalPointer().category_key
return False
for j in xrange(self.rowCount(category_index)): for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index) tag_index = self.index(j, 0, category_index)
tag_item = tag_index.internalPointer() tag_item = tag_index.internalPointer()
if tag_item.type == TagTreeItem.CATEGORY: if tag_item.type == TagTreeItem.CATEGORY:
if process_level(depth+1, tag_index, start_path): if process_level(depth+1, tag_index, start_path):
return True return True
else: elif not key or strcmp(key, my_key) == 0:
if process_tag(depth+1, tag_index, tag_item, start_path): if process_tag(depth+1, tag_index, tag_item, start_path):
return True return True
return False return False
@ -1456,26 +1584,37 @@ class TagBrowserMixin(object): # {{{
self.tags_view.set_database(db, self.tag_match, self.sort_by) self.tags_view.set_database(db, self.tag_match, self.sort_by)
self.tags_view.tags_marked.connect(self.search.set_search_string) self.tags_view.tags_marked.connect(self.search.set_search_string)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit) self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit) self.tags_view.edit_user_category.connect(self.do_edit_user_categories)
self.tags_view.delete_user_category.connect(self.do_delete_user_category)
self.tags_view.del_item_from_user_cat.connect(self.do_del_item_from_user_cat)
self.tags_view.add_subcategory.connect(self.do_add_subcategory) self.tags_view.add_subcategory.connect(self.do_add_subcategory)
self.tags_view.add_item_to_user_cat.connect(self.do_add_item_to_user_cat)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.tags_view.author_sort_edit.connect(self.do_author_sort_edit) self.tags_view.author_sort_edit.connect(self.do_author_sort_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed) self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_searches_changed) self.tags_view.search_item_renamed.connect(self.saved_searches_changed)
self.tags_view.drag_drop_finished.connect(self.drag_drop_finished) self.tags_view.drag_drop_finished.connect(self.drag_drop_finished)
self.edit_categories.clicked.connect(lambda x: self.edit_categories.clicked.connect(lambda x:
self.do_user_categories_edit()) self.do_edit_user_categories())
def do_add_subcategory(self, on_category=None): def do_add_subcategory(self, on_category_key, new_category_name=None):
'''
Add a subcategory to the category 'on_category'. If new_category_name is
None, then a default name is shown and the user is offered the
opportunity to edit the name.
'''
db = self.library_view.model().db db = self.library_view.model().db
user_cats = db.prefs.get('user_categories', {}) user_cats = db.prefs.get('user_categories', {})
# Ensure that the temporary name we will use is not already there # Ensure that the temporary name we will use is not already there
i = 0 i = 0
new_name = _('New Category').replace('.', '') if new_category_name is not None:
new_name = new_category_name.replace('.', '')
else:
new_name = _('New Category').replace('.', '')
n = new_name n = new_name
while True: while True:
new_cat = on_category[1:] + '.' + n new_cat = on_category_key[1:] + '.' + n
if new_cat not in user_cats: if new_cat not in user_cats:
break break
i += 1 i += 1
@ -1488,9 +1627,13 @@ class TagBrowserMixin(object): # {{{
idx = m.index_for_path(m.find_category_node('@' + new_cat)) idx = m.index_for_path(m.find_category_node('@' + new_cat))
m.show_item_at_index(idx) m.show_item_at_index(idx)
# Open the editor on the new item to rename it # Open the editor on the new item to rename it
self.tags_view.edit(idx) if new_category_name is None:
self.tags_view.edit(idx)
def do_user_categories_edit(self, on_category=None): def do_edit_user_categories(self, on_category=None):
'''
Open the user categories editor.
'''
db = self.library_view.model().db db = self.library_view.model().db
d = TagCategories(self, db, on_category) d = TagCategories(self, db, on_category)
if d.exec_() == d.Accepted: if d.exec_() == d.Accepted:
@ -1500,9 +1643,89 @@ class TagBrowserMixin(object): # {{{
db.field_metadata.add_user_category('@' + k, k) db.field_metadata.add_user_category('@' + k, k)
db.data.change_search_locations(db.field_metadata.get_search_terms()) db.data.change_search_locations(db.field_metadata.get_search_terms())
self.tags_view.set_new_model() self.tags_view.set_new_model()
self.tags_view.recount()
def do_delete_user_category(self, category_name):
'''
Delete the user category named category_name. Any leading '@' is removed
'''
if category_name.startswith('@'):
category_name = category_name[1:]
db = self.library_view.model().db
user_cats = db.prefs.get('user_categories', {})
cat_keys = sorted(user_cats.keys(), key=sort_key)
has_children = False
found = False
for k in cat_keys:
if k == category_name:
found = True
has_children = len(user_cats[k])
elif k.startswith(category_name + '.'):
has_children = True
if not found:
return error_dialog(self.tags_view, _('Delete user category'),
_('%s is not a user category')%category_name, show=True)
if has_children:
if not question_dialog(self.tags_view, _('Delete user category'),
_('%s contains items. Do you really '
'want to delete it?')%category_name):
return
for k in cat_keys:
if k == category_name:
del user_cats[k]
elif k.startswith(category_name + '.'):
del user_cats[k]
db.prefs.set('user_categories', user_cats)
self.tags_view.set_new_model()
def do_del_item_from_user_cat(self, user_cat, item_name, item_category):
'''
Delete the item (item_name, item_category) from the user category with
key user_cat. Any leading '@' characters are removed
'''
if user_cat.startswith('@'):
user_cat = user_cat[1:]
db = self.library_view.model().db
user_cats = db.prefs.get('user_categories', {})
if user_cat not in user_cats:
error_dialog(self.tags_view, _('Remove category'),
_('User category %s does not exist')%user_cat,
show=True)
return
self.tags_view.model().delete_item_from_user_category(user_cat,
item_name, item_category)
self.tags_view.recount()
def do_add_item_to_user_cat(self, dest_category, src_name, src_category):
'''
Add the item src_name in src_category to the user category
dest_category. Any leading '@' is removed
'''
db = self.library_view.model().db
user_cats = db.prefs.get('user_categories', {})
if dest_category.startswith('@'):
dest_category = dest_category[1:]
if dest_category not in user_cats:
return error_dialog(self.tags_view, _('Add to user category'),
_('A user category %s does not exist')%dest_category, show=True)
# Now add the item to the destination user category
add_it = True
if src_category == 'news':
src_category = 'tags'
for tup in user_cats[dest_category]:
if src_name == tup[0] and src_category == tup[1]:
add_it = False
if add_it:
user_cats[dest_category].append([src_name, src_category, 0])
db.prefs.set('user_categories', user_cats)
self.tags_view.recount()
def do_tags_list_edit(self, tag, category): def do_tags_list_edit(self, tag, category):
'''
Open the 'manage_X' dialog where X == category. If tag is not None, the
dialog will position the editor on that item.
'''
db=self.library_view.model().db db=self.library_view.model().db
if category == 'tags': if category == 'tags':
result = db.get_tags_with_ids() result = db.get_tags_with_ids()
@ -1527,6 +1750,8 @@ class TagBrowserMixin(object): # {{{
if d.result() == d.Accepted: if d.result() == d.Accepted:
to_rename = d.to_rename # dict of new text to old id to_rename = d.to_rename # dict of new text to old id
to_delete = d.to_delete # list of ids to_delete = d.to_delete # list of ids
orig_name = d.original_names # dict of id: name
rename_func = None rename_func = None
if category == 'tags': if category == 'tags':
rename_func = db.rename_tag rename_func = db.rename_tag
@ -1540,15 +1765,19 @@ class TagBrowserMixin(object): # {{{
else: else:
rename_func = partial(db.rename_custom_item, label=cc_label) rename_func = partial(db.rename_custom_item, label=cc_label)
delete_func = partial(db.delete_custom_item_using_id, label=cc_label) delete_func = partial(db.delete_custom_item_using_id, label=cc_label)
m = self.tags_view.model()
if rename_func: if rename_func:
for item in to_delete: for item in to_delete:
delete_func(item) delete_func(item)
m.delete_item_from_all_user_categories(orig_name[item], category)
for old_id in to_rename: for old_id in to_rename:
rename_func(old_id, new_name=unicode(to_rename[old_id])) rename_func(old_id, new_name=unicode(to_rename[old_id]))
m.rename_item_in_all_user_categories(orig_name[old_id],
category, unicode(to_rename[old_id]))
# Clean up the library view # Clean up the library view
self.do_tag_item_renamed() self.do_tag_item_renamed()
self.tags_view.set_new_model() # does a refresh for free self.tags_view.recount()
def do_tag_item_renamed(self): def do_tag_item_renamed(self):
# Clean up library view and search # Clean up library view and search
@ -1564,6 +1793,9 @@ class TagBrowserMixin(object): # {{{
# refreshing the tags view happens at the emit()/call() site # refreshing the tags view happens at the emit()/call() site
def do_author_sort_edit(self, parent, id): def do_author_sort_edit(self, parent, id):
'''
Open the manage authors dialog
'''
db = self.library_view.model().db db = self.library_view.model().db
editor = EditAuthorsDialog(parent, db, id) editor = EditAuthorsDialog(parent, db, id)
d = editor.exec_() d = editor.exec_()

View File

@ -128,7 +128,8 @@ def _match(query, value, matchkind):
if query[0] == '.': if query[0] == '.':
if t.startswith(query[1:]): if t.startswith(query[1:]):
ql = len(query) - 1 ql = len(query) - 1
return (len(t) == ql) or (t[ql:ql+1] == '.') if (len(t) == ql) or (t[ql:ql+1] == '.'):
return True
elif query == t: elif query == t:
return True return True
elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I)) or ### search unanchored elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I)) or ### search unanchored