diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index 6c3ebb22d5..cee9eb42b9 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -58,10 +58,12 @@ class TagListEditor(QDialog, Ui_TagListEditor): self.to_rename = {} self.to_delete = set([]) + self.original_names = {} self.all_tags = {} for k,v in data: self.all_tags[v] = k + self.original_names[k] = v for tag in sorted(self.all_tags.keys(), key=key): item = ListWidgetItem(tag) item.setData(Qt.UserRole, self.all_tags[tag]) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index d5b62f8efc..06f01a1649 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -25,7 +25,7 @@ from calibre.utils.config import tweaks from calibre.utils.icu import sort_key, upper, lower, strcmp from calibre.utils.search_query_parser import saved_searches 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.tag_categories import TagCategories 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): # {{{ - refresh_required = pyqtSignal() - tags_marked = pyqtSignal(object) - user_category_edit = pyqtSignal(object) - add_subcategory = pyqtSignal(object) - tag_list_edit = pyqtSignal(object, object) - saved_search_edit = pyqtSignal(object) - author_sort_edit = pyqtSignal(object, object) - tag_item_renamed = pyqtSignal() - search_item_renamed = pyqtSignal() - drag_drop_finished = pyqtSignal(object, object) + refresh_required = pyqtSignal() + tags_marked = pyqtSignal(object) + edit_user_category = pyqtSignal(object) + delete_user_category = pyqtSignal(object) + del_item_from_user_cat = pyqtSignal(object, object, object) + add_item_to_user_cat = pyqtSignal(object, object, object) + add_subcategory = pyqtSignal(object) + tag_list_edit = pyqtSignal(object, object) + saved_search_edit = pyqtSignal(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): QTreeView.__init__(self, parent=None) @@ -105,6 +108,7 @@ class TagsView(QTreeView): # {{{ else: self.collapse_model = gprefs['tags_browser_partition_method'] self.search_icon = QIcon(I('search.png')) + self.user_category_icon = QIcon(I('tb_folder.png')) def set_pane_is_visible(self, to_what): pv = self.pane_is_visible @@ -218,17 +222,29 @@ class TagsView(QTreeView): # {{{ self.tag_list_edit.emit(category, key) return if action == 'manage_categories': - self.user_category_edit.emit(category) - return - if action == 'add_subcategory': - self.add_subcategory.emit(category) + self.edit_user_category.emit(category) return if action == 'search': self._toggle(index, set_to=search_state) 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': self.tags_marked.emit(key + ':' + search_state) 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': self.saved_search_edit.emit(category) return @@ -259,14 +275,11 @@ class TagsView(QTreeView): # {{{ if index.isValid(): item = index.internalPointer() - tag_name = '' + tag = None if item.type == TagTreeItem.TAG: - tag_item = item - t = item.tag - tag_name = t.name - tag_id = t.id - can_edit = getattr(t, 'can_edit', True) + tag = item.tag + can_edit = getattr(tag, 'can_edit', True) while item.type != TagTreeItem.CATEGORY: item = item.parent @@ -281,42 +294,70 @@ class TagsView(QTreeView): # {{{ return True # Did the user click on a leaf node? - if tag_name: + if tag: # If the user right-clicked on an editable item, then offer # the possibility of renaming that item. - if can_edit and \ - key in ['authors', 'tags', 'series', 'publisher', 'search'] or \ - (self.db.field_metadata[key]['is_custom'] and \ - self.db.field_metadata[key]['datatype'] != 'rating'): + if can_edit: # 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', - category=tag_item, index=index)) + index=index)) 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, - 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 self.context_menu.addAction(self.search_icon, - _('Search for %s')%tag_name, + _('Search for %s')%tag.name, partial(self.context_menu_handler, action='search', search_state=TAG_SEARCH_STATES['mark_plus'], index=index)) 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', search_state=TAG_SEARCH_STATES['mark_minus'], index=index)) self.context_menu.addSeparator() - elif key.startswith('@'): + elif key.startswith('@') and not item.is_gst: 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', - category=key, index=index)) - self.context_menu.addAction(self.search_icon, - _('Add sub-category to %s')%key[1:], + index=index)) + self.context_menu.addAction(self.user_category_icon, + _('Add sub-category to %s')%item.py_name, 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() # Hide/Show/Restore categories if not key.startswith('@') or key.find('.') < 0: @@ -345,14 +386,15 @@ class TagsView(QTreeView): # {{{ self.db.field_metadata[key]['is_custom']: self.context_menu.addAction(_('Manage %s')%category, 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': self.context_menu.addAction(_('Manage %s')%category, partial(self.context_menu_handler, action='edit_author_sort')) elif key == 'search': self.context_menu.addAction(_('Manage Saved 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 self.context_menu.addSeparator() @@ -405,7 +447,7 @@ class TagsView(QTreeView): # {{{ if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled: self.setDropIndicatorShown(not src_is_tb) 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) if fm_dest['kind'] == 'user': if src_is_tb: @@ -413,7 +455,8 @@ class TagsView(QTreeView): # {{{ data = str(event.mimeData().data('application/calibre+from_tag_browser')) src = cPickle.loads(data) 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 self.setDropIndicatorShown(True) return @@ -534,6 +577,8 @@ class TagTreeItem(object): # {{{ def category_data(self, role): if role == Qt.DisplayRole: return QVariant(self.py_name + ' [%d]'%len(self.child_tags())) + if role == Qt.EditRole: + return QVariant(self.py_name) if role == Qt.DecorationRole: return self.icon if role == Qt.FontRole: @@ -634,11 +679,14 @@ class TagsModel(QAbstractItemModel): # {{{ 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: continue + is_gst = False if r.startswith('@') and r[1:] in gst: tt = _(u'The grouped search term name is "{0}"').format(r[1:]) + is_gst = True elif r == 'news': tt = '' else: @@ -648,6 +696,7 @@ class TagsModel(QAbstractItemModel): # {{{ path_parts = [p.strip() for p in r.split('.') if p.strip()] path = '' last_category_node = self.root_item + tree_root = self.category_node_tree for i,p in enumerate(path_parts): path += p if path not in category_node_map: @@ -659,15 +708,21 @@ class TagsModel(QAbstractItemModel): # {{{ last_category_node = node category_node_map[path] = 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: last_category_node = category_node_map[path] + tree_root = tree_root[p] path += '.' else: node = TagTreeItem(parent=self.root_item, data=self.categories[i], category_icon=self.category_icon_map[r], tooltip=tt, category_key=r) + node.is_gst = False category_node_map[r] = node last_category_node = node self.category_nodes.append(node) @@ -693,7 +748,7 @@ class TagsModel(QAbstractItemModel): # {{{ p = node while p.type != TagTreeItem.CATEGORY: 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) data.append(d) else: @@ -727,10 +782,23 @@ class TagsModel(QAbstractItemModel): # {{{ for s in src: if s[0] != TagTreeItem.TAG: 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', {}) parent_node = None + copied_node = None 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 if src_parent.startswith('@'): is_uc = True @@ -742,12 +810,16 @@ class TagsModel(QAbstractItemModel): # {{{ continue new_cat = [] # 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]: if src_name == tup[0] and src_cat == tup[1]: continue new_cat.append(list(tup)) user_cats[src_parent] = new_cat + else: + copied_node = (src_parent, src_name) + # Now add the item to the destination user category add_it = True if not is_uc and src_cat == 'news': @@ -757,12 +829,17 @@ class TagsModel(QAbstractItemModel): # {{{ add_it = False if add_it: user_cats[dest_key].append([src_name, src_cat, 0]) + self.db.prefs.set('user_categories', user_cats) - self.tags_view.set_new_model() + self.tags_view.recount() + if parent_node is not None: - # Must work with the new model here 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) self.tags_view.setExpanded(idx, True) m.show_item_at_index(idx) @@ -1031,15 +1108,17 @@ class TagsModel(QAbstractItemModel): # {{{ node_parent = category 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\ len(components) == 1 or \ - fm['kind'] == 'user' or \ - fm['datatype'] not in ['text', 'series', 'enumeration']: + fm['kind'] == 'user': self.beginInsertRows(category_index, 999999, 1) TagTreeItem(parent=node_parent, data=tag, tooltip=tt, icon_map=self.icon_state_map) self.endInsertRows() + tag.can_edit = key != 'formats' and (key == 'news' or \ + self.db.field_metadata[tag.category]['datatype'] in \ + ['text', 'series', 'enumeration']) else: for i,comp in enumerate(components): 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: node_parent = child_map[comp] node_parent.tag.count += tag.count + node_parent.tag.use_prefix = True else: if i < len(components)-1: t = copy.copy(tag) @@ -1137,10 +1217,9 @@ class TagsModel(QAbstractItemModel): # {{{ p = self.tags_view.model().find_category_node('@' + nkey) self.tags_view.model().show_item_at_path(p) return True - itm = item.parent - while itm.type != TagTreeItem.CATEGORY: - itm = itm.parent - key = itm.category_key + + key = item.tag.category + name = getattr(item.tag, 'original_name', item.tag.name) # make certain we know about the item's category if key not in self.db.field_metadata: return False @@ -1171,10 +1250,54 @@ class TagsModel(QAbstractItemModel): # {{{ label=self.db.field_metadata[key]['label']) self.tags_view.tag_item_renamed.emit() 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.show_item_at_path(path) 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): return NONE @@ -1329,19 +1452,20 @@ class TagsModel(QAbstractItemModel): # {{{ name.replace(r'"', r'\"'))) 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 - the key (exact case-insensitive match) and txt (contains case- - insensitive 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 + the key (exact case-insensitive match) and txt (not equals_match => + case-insensitive contains match; equals_match => case_insensitive + 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 is changed subsequent to calling this method, the path can easily refer to a different node or no node at all. ''' if not txt: return None - txt = lower(txt) + txt = lower(txt) if not equals_match else txt self.path_found = None if start_path is None: start_path = [] @@ -1353,9 +1477,14 @@ class TagsModel(QAbstractItemModel): # {{{ tag = tag_item.tag if tag is None: 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 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 def process_level(depth, category_index, start_path): @@ -1365,15 +1494,14 @@ class TagsModel(QAbstractItemModel): # {{{ return False if path[depth] > start_path[depth]: start_path = path - if key and strcmp(category_index.internalPointer().category_key, key) != 0: - return False + my_key = category_index.internalPointer().category_key for j in xrange(self.rowCount(category_index)): tag_index = self.index(j, 0, category_index) tag_item = tag_index.internalPointer() if tag_item.type == TagTreeItem.CATEGORY: if process_level(depth+1, tag_index, start_path): return True - else: + elif not key or strcmp(key, my_key) == 0: if process_tag(depth+1, tag_index, tag_item, start_path): return True return False @@ -1456,26 +1584,37 @@ class TagBrowserMixin(object): # {{{ 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.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_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.author_sort_edit.connect(self.do_author_sort_edit) 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.drag_drop_finished.connect(self.drag_drop_finished) 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 user_cats = db.prefs.get('user_categories', {}) # Ensure that the temporary name we will use is not already there 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 while True: - new_cat = on_category[1:] + '.' + n + new_cat = on_category_key[1:] + '.' + n if new_cat not in user_cats: break i += 1 @@ -1488,9 +1627,13 @@ class TagBrowserMixin(object): # {{{ idx = m.index_for_path(m.find_category_node('@' + new_cat)) m.show_item_at_index(idx) # 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 d = TagCategories(self, db, on_category) if d.exec_() == d.Accepted: @@ -1500,9 +1643,89 @@ class TagBrowserMixin(object): # {{{ db.field_metadata.add_user_category('@' + k, k) db.data.change_search_locations(db.field_metadata.get_search_terms()) 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): + ''' + 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 if category == 'tags': result = db.get_tags_with_ids() @@ -1527,6 +1750,8 @@ class TagBrowserMixin(object): # {{{ if d.result() == d.Accepted: to_rename = d.to_rename # dict of new text to old id to_delete = d.to_delete # list of ids + orig_name = d.original_names # dict of id: name + rename_func = None if category == 'tags': rename_func = db.rename_tag @@ -1540,15 +1765,19 @@ class TagBrowserMixin(object): # {{{ else: rename_func = partial(db.rename_custom_item, label=cc_label) delete_func = partial(db.delete_custom_item_using_id, label=cc_label) + m = self.tags_view.model() if rename_func: for item in to_delete: delete_func(item) + m.delete_item_from_all_user_categories(orig_name[item], category) for old_id in to_rename: 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 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): # Clean up library view and search @@ -1564,6 +1793,9 @@ class TagBrowserMixin(object): # {{{ # refreshing the tags view happens at the emit()/call() site def do_author_sort_edit(self, parent, id): + ''' + Open the manage authors dialog + ''' db = self.library_view.model().db editor = EditAuthorsDialog(parent, db, id) d = editor.exec_() diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 4f5a034222..e626d446d2 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -128,7 +128,8 @@ def _match(query, value, matchkind): if query[0] == '.': if t.startswith(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: return True elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I)) or ### search unanchored