Several things:

1) can remove an item from a user category
2) can remove a user category
3) renaming an item will also rename the item in all user categories
4) make find_item_node work in the face of hierarchies (user cats and items)
5) change set_new_model() to recount() where possible to avoid closing expanded trees
This commit is contained in:
Charles Haley 2011-02-24 09:03:10 +00:00
parent 1f88b1f1b5
commit f5f1c87cd5

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
@ -73,6 +73,8 @@ class TagsView(QTreeView): # {{{
refresh_required = pyqtSignal() refresh_required = pyqtSignal()
tags_marked = pyqtSignal(object) tags_marked = pyqtSignal(object)
user_category_edit = pyqtSignal(object) user_category_edit = pyqtSignal(object)
user_category_delete= pyqtSignal(object)
del_user_cat_item = pyqtSignal(object, object, object)
add_subcategory = pyqtSignal(object) add_subcategory = pyqtSignal(object)
tag_list_edit = pyqtSignal(object, object) tag_list_edit = pyqtSignal(object, object)
saved_search_edit = pyqtSignal(object) saved_search_edit = pyqtSignal(object)
@ -105,6 +107,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
@ -220,15 +223,21 @@ class TagsView(QTreeView): # {{{
if action == 'manage_categories': if action == 'manage_categories':
self.user_category_edit.emit(category) self.user_category_edit.emit(category)
return return
if action == 'add_subcategory':
self.add_subcategory.emit(category)
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_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.user_category_delete.emit(key)
return
if action == 'delete_item_from_user_category':
self.del_user_cat_item.emit(key, 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 +268,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 +287,50 @@ 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))
if key.startswith('@'):
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('@'):
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 +359,14 @@ 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=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()
@ -534,6 +548,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:
@ -727,8 +743,19 @@ 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, full name, category key, id)
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_name, src_cat = s[1:4]
parent_node = src_parent parent_node = src_parent
@ -748,6 +775,9 @@ class TagsModel(QAbstractItemModel): # {{{
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,11 +787,16 @@ 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()
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) 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)
@ -1031,15 +1066,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 \
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
@ -1137,10 +1174,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 = 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,6 +1207,17 @@ 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
# rename the item in any user categories
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] == name and tup[1] == key:
new_contents.append([val, key, 0])
else:
new_contents.append(tup)
user_cats[k] = new_contents
self.db.prefs.set('user_categories', user_cats)
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
@ -1329,19 +1376,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,7 +1401,9 @@ 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
return False return False
@ -1365,15 +1415,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
@ -1457,6 +1506,8 @@ class TagBrowserMixin(object): # {{{
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.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.user_category_delete.connect(self.do_user_category_delete)
self.tags_view.del_user_cat_item.connect(self.do_del_user_cat_item)
self.tags_view.add_subcategory.connect(self.do_add_subcategory) self.tags_view.add_subcategory.connect(self.do_add_subcategory)
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)
@ -1466,7 +1517,29 @@ class TagBrowserMixin(object): # {{{
self.edit_categories.clicked.connect(lambda x: self.edit_categories.clicked.connect(lambda x:
self.do_user_categories_edit()) self.do_user_categories_edit())
def do_add_subcategory(self, on_category=None): def do_del_user_cat_item(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
new_contents = []
for tup in user_cats[user_cat]:
if tup[0] != item_name or tup[1] != item_category:
new_contents.append(tup)
user_cats[user_cat] = new_contents
db.prefs.set('user_categories', user_cats)
self.tags_view.recount()
def do_add_subcategory(self, on_category_key=None):
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', {})
@ -1475,7 +1548,7 @@ class TagBrowserMixin(object): # {{{
new_name = _('New Category').replace('.', '') 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
@ -1500,7 +1573,39 @@ 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_user_category_delete(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_tags_list_edit(self, tag, category): def do_tags_list_edit(self, tag, category):
db=self.library_view.model().db db=self.library_view.model().db
@ -1548,7 +1653,7 @@ class TagBrowserMixin(object): # {{{
# 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.set_new_model()
def do_tag_item_renamed(self): def do_tag_item_renamed(self):
# Clean up library view and search # Clean up library view and search