mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
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:
parent
1f88b1f1b5
commit
f5f1c87cd5
@ -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
|
||||
@ -73,6 +73,8 @@ class TagsView(QTreeView): # {{{
|
||||
refresh_required = pyqtSignal()
|
||||
tags_marked = pyqtSignal(object)
|
||||
user_category_edit = pyqtSignal(object)
|
||||
user_category_delete= pyqtSignal(object)
|
||||
del_user_cat_item = pyqtSignal(object, object, object)
|
||||
add_subcategory = pyqtSignal(object)
|
||||
tag_list_edit = pyqtSignal(object, object)
|
||||
saved_search_edit = pyqtSignal(object)
|
||||
@ -105,6 +107,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
|
||||
@ -220,15 +223,21 @@ class TagsView(QTreeView): # {{{
|
||||
if action == 'manage_categories':
|
||||
self.user_category_edit.emit(category)
|
||||
return
|
||||
if action == 'add_subcategory':
|
||||
self.add_subcategory.emit(category)
|
||||
return
|
||||
if action == 'search':
|
||||
self._toggle(index, set_to=search_state)
|
||||
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.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':
|
||||
self.saved_search_edit.emit(category)
|
||||
return
|
||||
@ -259,14 +268,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 +287,50 @@ 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))
|
||||
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
|
||||
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('@'):
|
||||
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 +359,14 @@ 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=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()
|
||||
@ -534,6 +548,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:
|
||||
@ -727,8 +743,19 @@ 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, 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', {})
|
||||
parent_node = None
|
||||
copied_node = None
|
||||
for s in src:
|
||||
src_parent, src_name, src_cat = s[1:4]
|
||||
parent_node = src_parent
|
||||
@ -748,6 +775,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
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 +787,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 +1066,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 \
|
||||
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
|
||||
@ -1137,10 +1174,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 = item.tag.name
|
||||
# make certain we know about the item's category
|
||||
if key not in self.db.field_metadata:
|
||||
return False
|
||||
@ -1171,6 +1207,17 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
label=self.db.field_metadata[key]['label'])
|
||||
self.tags_view.tag_item_renamed.emit()
|
||||
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.show_item_at_path(path)
|
||||
return True
|
||||
@ -1329,19 +1376,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,7 +1401,9 @@ 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
|
||||
return False
|
||||
@ -1365,15 +1415,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
|
||||
@ -1457,6 +1506,8 @@ class TagBrowserMixin(object): # {{{
|
||||
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.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.saved_search_edit.connect(self.do_saved_search_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.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
|
||||
user_cats = db.prefs.get('user_categories', {})
|
||||
|
||||
@ -1475,7 +1548,7 @@ class TagBrowserMixin(object): # {{{
|
||||
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
|
||||
@ -1500,7 +1573,39 @@ 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_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):
|
||||
db=self.library_view.model().db
|
||||
@ -1548,7 +1653,7 @@ class TagBrowserMixin(object): # {{{
|
||||
|
||||
# Clean up the library view
|
||||
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):
|
||||
# Clean up library view and search
|
||||
|
Loading…
x
Reference in New Issue
Block a user