mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-31 14:33:54 -04:00
Tag browser: Improve handling of hierarchical tags
Renaming a tag now renames it and all its children. Drag and drop of hierarchical tags alters their hierarchy. Deleting a tag now deletes all its children. Fixes #1880264 [[Enhancement] Additional functionality for tag hierarchies](https://bugs.launchpad.net/calibre/+bug/1880264) Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
0ec36b22b2
@ -59,6 +59,8 @@ class TagTreeItem(object): # {{{
|
|||||||
self.blank = QIcon()
|
self.blank = QIcon()
|
||||||
self.is_gst = False
|
self.is_gst = False
|
||||||
self.boxed = False
|
self.boxed = False
|
||||||
|
self.temporary = False
|
||||||
|
self.can_be_edited = False
|
||||||
self.icon_state_map = list(icon_map)
|
self.icon_state_map = list(icon_map)
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
self.parent.append(self)
|
self.parent.append(self)
|
||||||
@ -113,9 +115,9 @@ class TagTreeItem(object): # {{{
|
|||||||
if self.type == self.ROOT:
|
if self.type == self.ROOT:
|
||||||
return 'ROOT'
|
return 'ROOT'
|
||||||
if self.type == self.CATEGORY:
|
if self.type == self.CATEGORY:
|
||||||
return 'CATEGORY(category_key={!r}, name={!r}, num_children={!r})'.format(
|
return 'CATEGORY(category_key={!r}, name={!r}, num_children={!r}, temp={!r})'.format(
|
||||||
self.category_key, self.name, len(self.children))
|
self.category_key, self.name, len(self.children), self.temporary)
|
||||||
return 'TAG(name=%r)'%self.tag.name
|
return 'TAG(name={!r}), temp={!r})'.format(self.tag.name, self.temporary)
|
||||||
|
|
||||||
def row(self):
|
def row(self):
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
@ -390,6 +392,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
del node # Clear reference to node in the current frame
|
del node # Clear reference to node in the current frame
|
||||||
self.node_map.clear()
|
self.node_map.clear()
|
||||||
self.category_nodes = []
|
self.category_nodes = []
|
||||||
|
self.hierarchical_categories = {}
|
||||||
self.root_item = self.create_node(icon_map=self.icon_state_map)
|
self.root_item = self.create_node(icon_map=self.icon_state_map)
|
||||||
self._rebuild_node_tree(state_map=state_map)
|
self._rebuild_node_tree(state_map=state_map)
|
||||||
|
|
||||||
@ -538,10 +541,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
top_level_component = 'z' + data[key][0].original_name
|
top_level_component = 'z' + data[key][0].original_name
|
||||||
|
|
||||||
last_idx = -collapse
|
last_idx = -collapse
|
||||||
category_is_hierarchical = not (
|
category_is_hierarchical = self.is_key_a_hierarchical_category(key)
|
||||||
key in ['authors', 'publisher', 'news', 'formats', 'rating'] or
|
|
||||||
key not in self.db.prefs.get('categories_using_hierarchy', []) or
|
|
||||||
config['sort_tags_by'] != 'name')
|
|
||||||
|
|
||||||
for idx,tag in enumerate(data[key]):
|
for idx,tag in enumerate(data[key]):
|
||||||
components = None
|
components = None
|
||||||
@ -573,7 +573,13 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
d['first'] = ct2
|
d['first'] = ct2
|
||||||
else:
|
else:
|
||||||
d = {'first': tag}
|
d = {'first': tag}
|
||||||
|
# Some nodes like formats and identifiers don't
|
||||||
|
# have sort set. Fix that so the template will work
|
||||||
|
if d['first'].sort is None:
|
||||||
|
d['first'].sort = tag.name
|
||||||
d['last'] = data[key][last]
|
d['last'] = data[key][last]
|
||||||
|
if d['last'].sort is None:
|
||||||
|
d['last'].sort = data[key][last].name
|
||||||
|
|
||||||
name = eval_formatter.safe_format(collapse_template,
|
name = eval_formatter.safe_format(collapse_template,
|
||||||
d, '##TAG_VIEW##', None)
|
d, '##TAG_VIEW##', None)
|
||||||
@ -716,6 +722,22 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
p = p.parent
|
p = p.parent
|
||||||
return p.tag.category.startswith('@')
|
return p.tag.category.startswith('@')
|
||||||
|
|
||||||
|
def is_key_a_hierarchical_category(self, key):
|
||||||
|
result = self.hierarchical_categories.get(key)
|
||||||
|
if result is None:
|
||||||
|
result = not (
|
||||||
|
key in ['authors', 'publisher', 'news', 'formats', 'rating'] or
|
||||||
|
key not in self.db.prefs.get('categories_using_hierarchy', []) or
|
||||||
|
config['sort_tags_by'] != 'name')
|
||||||
|
self.hierarchical_categories[key] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
def is_index_on_a_hierarchical_category(self, index):
|
||||||
|
if not index.isValid():
|
||||||
|
return False
|
||||||
|
p = self.get_node(index)
|
||||||
|
return self.is_key_a_hierarchical_category(p.tag.category)
|
||||||
|
|
||||||
# Drag'n Drop {{{
|
# Drag'n Drop {{{
|
||||||
def mimeTypes(self):
|
def mimeTypes(self):
|
||||||
return ["application/calibre+from_library",
|
return ["application/calibre+from_library",
|
||||||
@ -760,15 +782,46 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if not parent.isValid():
|
if not parent.isValid():
|
||||||
return False
|
return False
|
||||||
dest = self.get_node(parent)
|
dest = self.get_node(parent)
|
||||||
if dest.type != TagTreeItem.CATEGORY:
|
|
||||||
return False
|
|
||||||
if not md.hasFormat('application/calibre+from_tag_browser'):
|
if not md.hasFormat('application/calibre+from_tag_browser'):
|
||||||
return False
|
return False
|
||||||
data = bytes(md.data('application/calibre+from_tag_browser'))
|
data = bytes(md.data('application/calibre+from_tag_browser'))
|
||||||
src = json_loads(data)
|
src = json_loads(data)
|
||||||
for s in src:
|
if len(src) == 1:
|
||||||
|
# Check to see if this is a hierarchical rename
|
||||||
|
s = src[0]
|
||||||
|
# This check works for both hierarchical and user categories.
|
||||||
|
# We can drag only tag items.
|
||||||
if s[0] != TagTreeItem.TAG:
|
if s[0] != TagTreeItem.TAG:
|
||||||
return False
|
return False
|
||||||
|
src_index = self.index_for_path(s[5])
|
||||||
|
if src_index == parent:
|
||||||
|
# dropped on itself
|
||||||
|
return False
|
||||||
|
src_item = self.get_node(src_index)
|
||||||
|
dest_item = parent.data(Qt.UserRole)
|
||||||
|
# Here we do the real work. If src is a tag, src == dest, and src
|
||||||
|
# is hierarchical then we can do a rename.
|
||||||
|
if (src_item.type == TagTreeItem.TAG and
|
||||||
|
src_item.tag.category == dest_item.tag.category and
|
||||||
|
self.is_key_a_hierarchical_category(src_item.tag.category)):
|
||||||
|
key = s[1]
|
||||||
|
# work out the part of the source name to use in the rename
|
||||||
|
# It isn't necessarily a simple name but might be the remaining
|
||||||
|
# levels of the hierarchy
|
||||||
|
part = src_item.tag.original_name.rpartition('.')
|
||||||
|
src_simple_name = part[2]
|
||||||
|
# work out the new prefix, the destination node name
|
||||||
|
if dest.type == TagTreeItem.TAG:
|
||||||
|
new_name = dest_item.tag.original_name + '.' + src_simple_name
|
||||||
|
else:
|
||||||
|
new_name = src_simple_name
|
||||||
|
# In d&d renames always use the vl. This might be controversial.
|
||||||
|
src_item.use_vl = True
|
||||||
|
self.rename_item(src_item, key, new_name)
|
||||||
|
return True
|
||||||
|
# Should be working with a user category
|
||||||
|
if dest.type != TagTreeItem.CATEGORY:
|
||||||
|
return False
|
||||||
return self.move_or_copy_item_to_user_category(src, dest, action)
|
return self.move_or_copy_item_to_user_category(src, dest, action)
|
||||||
|
|
||||||
def move_or_copy_item_to_user_category(self, src, dest, action):
|
def move_or_copy_item_to_user_category(self, src, dest, action):
|
||||||
@ -1134,7 +1187,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
key = item.tag.category
|
key = item.tag.category
|
||||||
name = item.tag.original_name
|
|
||||||
# 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
|
||||||
@ -1153,19 +1205,46 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
item.tag.name = val
|
item.tag.name = val
|
||||||
self.search_item_renamed.emit() # Does a refresh
|
self.search_item_renamed.emit() # Does a refresh
|
||||||
else:
|
else:
|
||||||
self.use_position_based_index_on_next_recount = True
|
self.rename_item(item, key, val)
|
||||||
restrict_to_book_ids=self.get_book_ids_to_use() if item.use_vl else None
|
|
||||||
self.db.new_api.rename_items(key, {item.tag.id: val},
|
|
||||||
restrict_to_book_ids=restrict_to_book_ids)
|
|
||||||
self.tag_item_renamed.emit()
|
|
||||||
item.tag.name = val
|
|
||||||
item.tag.state = TAG_SEARCH_STATES['clear']
|
|
||||||
self.use_position_based_index_on_next_recount = True
|
|
||||||
if not restrict_to_book_ids:
|
|
||||||
self.rename_item_in_all_user_categories(name, key, val)
|
|
||||||
self.refresh_required.emit()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def rename_item(self, item, key, to_what):
|
||||||
|
def do_one_item(lookup_key, an_item, original_name, new_name, restrict_to_books):
|
||||||
|
self.use_position_based_index_on_next_recount = True
|
||||||
|
self.db.new_api.rename_items(lookup_key, {an_item.tag.id: new_name},
|
||||||
|
restrict_to_book_ids=restrict_to_books)
|
||||||
|
self.tag_item_renamed.emit()
|
||||||
|
an_item.tag.name = new_name
|
||||||
|
an_item.tag.state = TAG_SEARCH_STATES['clear']
|
||||||
|
self.use_position_based_index_on_next_recount = True
|
||||||
|
if not restrict_to_books:
|
||||||
|
self.rename_item_in_all_user_categories(original_name,
|
||||||
|
lookup_key, new_name)
|
||||||
|
|
||||||
|
children = item.all_children()
|
||||||
|
restrict_to_book_ids=self.get_book_ids_to_use() if item.use_vl else None
|
||||||
|
if item.tag.is_editable and len(children) == 0:
|
||||||
|
# Leaf node, just do it.
|
||||||
|
do_one_item(key, item, item.tag.original_name, to_what, restrict_to_book_ids)
|
||||||
|
else:
|
||||||
|
# Middle node of a hierarchy
|
||||||
|
search_name = item.tag.original_name
|
||||||
|
# Clear any search icons on the original tag
|
||||||
|
if item.parent.type == TagTreeItem.TAG:
|
||||||
|
item.parent.tag.state = TAG_SEARCH_STATES['clear']
|
||||||
|
# It might also be a leaf
|
||||||
|
if item.tag.is_editable:
|
||||||
|
do_one_item(key, item, item.tag.original_name, to_what, restrict_to_book_ids)
|
||||||
|
# Now do the children
|
||||||
|
for child_item in children:
|
||||||
|
from calibre.utils.icu import startswith
|
||||||
|
if (child_item.tag.is_editable and
|
||||||
|
startswith(child_item.tag.original_name, search_name)):
|
||||||
|
new_name = to_what + child_item.tag.original_name[len(search_name):]
|
||||||
|
do_one_item(key, child_item, child_item.tag.original_name,
|
||||||
|
new_name, restrict_to_book_ids)
|
||||||
|
self.refresh_required.emit()
|
||||||
|
|
||||||
def rename_item_in_all_user_categories(self, item_name, item_category, new_name):
|
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
|
Search all User categories for items named item_name with category
|
||||||
@ -1217,7 +1296,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if index.isValid():
|
if index.isValid():
|
||||||
node = self.data(index, Qt.UserRole)
|
node = self.data(index, Qt.UserRole)
|
||||||
if node.type == TagTreeItem.TAG:
|
if node.type == TagTreeItem.TAG:
|
||||||
if node.tag.is_editable:
|
if node.tag.is_editable or node.tag.is_hierarchical:
|
||||||
ans |= Qt.ItemIsDragEnabled
|
ans |= Qt.ItemIsDragEnabled
|
||||||
fm = self.db.metadata_for_field(node.tag.category)
|
fm = self.db.metadata_for_field(node.tag.category)
|
||||||
if node.tag.category in \
|
if node.tag.category in \
|
||||||
|
@ -278,21 +278,46 @@ class TagBrowserMixin(object): # {{{
|
|||||||
self.do_tag_item_renamed()
|
self.do_tag_item_renamed()
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
|
|
||||||
def do_tag_item_delete(self, category, item_id, orig_name, restrict_to_book_ids=None):
|
def do_tag_item_delete(self, category, item_id, orig_name,
|
||||||
|
restrict_to_book_ids=None, children=[]):
|
||||||
'''
|
'''
|
||||||
Delete an item from some category.
|
Delete an item from some category.
|
||||||
'''
|
'''
|
||||||
if restrict_to_book_ids:
|
tag_names = []
|
||||||
msg = _('%s will be deleted from books in the Virtual library. Are you sure?')%orig_name
|
for child in children:
|
||||||
|
if child.tag.is_editable:
|
||||||
|
tag_names.append(child.tag.original_name)
|
||||||
|
n = '\n '.join(tag_names)
|
||||||
|
if n:
|
||||||
|
n = '%s:\n %s\n%s:\n %s'%(_('Item'), orig_name, _('Children'), n)
|
||||||
|
if n:
|
||||||
|
if restrict_to_book_ids:
|
||||||
|
msg = _('%s and its children will be deleted from books '
|
||||||
|
'in the Virtual library. Are you sure?')%orig_name
|
||||||
|
else:
|
||||||
|
msg = _('%s and its children will be deleted from all books. '
|
||||||
|
'Are you sure?')%orig_name
|
||||||
else:
|
else:
|
||||||
msg = _('%s will be deleted from all books. Are you sure?')%orig_name
|
if restrict_to_book_ids:
|
||||||
|
msg = _('%s will be deleted from books in the Virtual library. Are you sure?')%orig_name
|
||||||
|
else:
|
||||||
|
msg = _('%s will be deleted from all books. Are you sure?')%orig_name
|
||||||
|
|
||||||
if not question_dialog(self.tags_view,
|
if not question_dialog(self.tags_view,
|
||||||
title=_('Delete item'),
|
title=_('Delete item'),
|
||||||
msg='<p>'+ msg,
|
msg='<p>'+ msg,
|
||||||
skip_dialog_name='tag_item_delete',
|
det_msg=n,
|
||||||
|
# Change the skip name because functionality has greatly changed
|
||||||
|
skip_dialog_name='tag_item_delete_hierarchical',
|
||||||
skip_dialog_msg=_('Show this confirmation again')):
|
skip_dialog_msg=_('Show this confirmation again')):
|
||||||
return
|
return
|
||||||
self.current_db.new_api.remove_items(category, (item_id,), restrict_to_book_ids=restrict_to_book_ids)
|
ids_to_remove = [item_id]
|
||||||
|
for child in children:
|
||||||
|
if child.tag.is_editable:
|
||||||
|
ids_to_remove.append(child.tag.id)
|
||||||
|
|
||||||
|
self.current_db.new_api.remove_items(category, ids_to_remove,
|
||||||
|
restrict_to_book_ids=restrict_to_book_ids)
|
||||||
if restrict_to_book_ids is None:
|
if restrict_to_book_ids is None:
|
||||||
m = self.tags_view.model()
|
m = self.tags_view.model()
|
||||||
m.delete_item_from_all_user_categories(orig_name, category)
|
m.delete_item_from_all_user_categories(orig_name, category)
|
||||||
|
@ -153,7 +153,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
search_item_renamed = pyqtSignal()
|
search_item_renamed = pyqtSignal()
|
||||||
drag_drop_finished = pyqtSignal(object)
|
drag_drop_finished = pyqtSignal(object)
|
||||||
restriction_error = pyqtSignal()
|
restriction_error = pyqtSignal()
|
||||||
tag_item_delete = pyqtSignal(object, object, object, object)
|
tag_item_delete = pyqtSignal(object, object, object, object, object)
|
||||||
apply_tag_to_selected = pyqtSignal(object, object, object)
|
apply_tag_to_selected = pyqtSignal(object, object, object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@ -320,6 +320,11 @@ class TagsView(QTreeView): # {{{
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.buttons() & Qt.LeftButton:
|
||||||
|
self.possible_drag_start = event.pos()
|
||||||
|
return QTreeView.mousePressEvent(self, event)
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
def mouseMoveEvent(self, event):
|
||||||
dex = self.indexAt(event.pos())
|
dex = self.indexAt(event.pos())
|
||||||
if dex.isValid():
|
if dex.isValid():
|
||||||
@ -331,6 +336,11 @@ class TagsView(QTreeView): # {{{
|
|||||||
if self.in_drag_drop or not dex.isValid():
|
if self.in_drag_drop or not dex.isValid():
|
||||||
QTreeView.mouseMoveEvent(self, event)
|
QTreeView.mouseMoveEvent(self, event)
|
||||||
return
|
return
|
||||||
|
# don't start drag/drop until the mouse has moved a bit.
|
||||||
|
if ((event.pos() - self.possible_drag_start).manhattanLength() <
|
||||||
|
QApplication.startDragDistance()):
|
||||||
|
QTreeView.mouseMoveEvent(self, event)
|
||||||
|
return
|
||||||
# Must deal with odd case where the node being dragged is 'virtual',
|
# Must deal with odd case where the node being dragged is 'virtual',
|
||||||
# created to form a hierarchy. We can't really drag this node, but in
|
# created to form a hierarchy. We can't really drag this node, but in
|
||||||
# addition we can't allow drag recognition to notice going over some
|
# addition we can't allow drag recognition to notice going over some
|
||||||
@ -345,7 +355,14 @@ class TagsView(QTreeView): # {{{
|
|||||||
drag = QDrag(self)
|
drag = QDrag(self)
|
||||||
drag.setPixmap(pixmap)
|
drag.setPixmap(pixmap)
|
||||||
drag.setMimeData(md)
|
drag.setMimeData(md)
|
||||||
if self._model.is_in_user_category(dex):
|
if (self._model.is_in_user_category(dex) or
|
||||||
|
self._model.is_index_on_a_hierarchical_category(dex)):
|
||||||
|
'''
|
||||||
|
Things break if we specify MoveAction as the default, which is
|
||||||
|
what we want for drag on hierarchical categories. Dragging user
|
||||||
|
categories stops working. Don't know why. To avoid the problem
|
||||||
|
we fix the action in dragMoveEvent.
|
||||||
|
'''
|
||||||
drag.exec_(Qt.CopyAction|Qt.MoveAction, Qt.CopyAction)
|
drag.exec_(Qt.CopyAction|Qt.MoveAction, Qt.CopyAction)
|
||||||
else:
|
else:
|
||||||
drag.exec_(Qt.CopyAction)
|
drag.exec_(Qt.CopyAction)
|
||||||
@ -440,11 +457,17 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.edit(index)
|
self.edit(index)
|
||||||
return
|
return
|
||||||
if action == 'delete_item_in_vl':
|
if action == 'delete_item_in_vl':
|
||||||
self.tag_item_delete.emit(key, index.id, index.original_name,
|
tag = index.tag
|
||||||
self.model().get_book_ids_to_use())
|
children = index.child_tags()
|
||||||
|
self.tag_item_delete.emit(key, tag.id, tag.original_name,
|
||||||
|
self.model().get_book_ids_to_use(),
|
||||||
|
children)
|
||||||
return
|
return
|
||||||
if action == 'delete_item_no_vl':
|
if action == 'delete_item_no_vl':
|
||||||
self.tag_item_delete.emit(key, index.id, index.original_name, None)
|
tag = index.tag
|
||||||
|
children = index.child_tags()
|
||||||
|
self.tag_item_delete.emit(key, tag.id, tag.original_name,
|
||||||
|
None, children)
|
||||||
return
|
return
|
||||||
if action == 'open_editor':
|
if action == 'open_editor':
|
||||||
self.tags_list_edit.emit(category, key, is_first_letter)
|
self.tags_list_edit.emit(category, key, is_first_letter)
|
||||||
@ -550,6 +573,8 @@ class TagsView(QTreeView): # {{{
|
|||||||
if len(n) > 45:
|
if len(n) > 45:
|
||||||
n = n[:45] + '...'
|
n = n[:45] + '...'
|
||||||
ans = "'" + n + "'"
|
ans = "'" + n + "'"
|
||||||
|
elif tag.is_hierarchical and not tag.is_editable:
|
||||||
|
ans = tag.original_name
|
||||||
if ans:
|
if ans:
|
||||||
ans = ans.replace('&', '&&')
|
ans = ans.replace('&', '&&')
|
||||||
return ans
|
return ans
|
||||||
@ -582,8 +607,8 @@ class TagsView(QTreeView): # {{{
|
|||||||
if tag:
|
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 tag.is_editable:
|
if tag.is_editable or tag.is_hierarchical:
|
||||||
# Add the 'rename' items
|
# Add the 'rename' items to both interior and leaf nodes
|
||||||
if self.model().get_in_vl():
|
if self.model().get_in_vl():
|
||||||
self.context_menu.addAction(self.rename_icon,
|
self.context_menu.addAction(self.rename_icon,
|
||||||
_('Rename %s in Virtual library')%display_name(tag),
|
_('Rename %s in Virtual library')%display_name(tag),
|
||||||
@ -593,18 +618,19 @@ class TagsView(QTreeView): # {{{
|
|||||||
_('Rename %s')%display_name(tag),
|
_('Rename %s')%display_name(tag),
|
||||||
partial(self.context_menu_handler, action='edit_item_no_vl',
|
partial(self.context_menu_handler, action='edit_item_no_vl',
|
||||||
index=index, category=key))
|
index=index, category=key))
|
||||||
|
if tag.is_editable:
|
||||||
if key in ('tags', 'series', 'publisher') or \
|
if key in ('tags', 'series', 'publisher') or \
|
||||||
self._model.db.field_metadata.is_custom_field(key):
|
self._model.db.field_metadata.is_custom_field(key):
|
||||||
if self.model().get_in_vl():
|
if self.model().get_in_vl():
|
||||||
self.context_menu.addAction(self.delete_icon,
|
self.context_menu.addAction(self.delete_icon,
|
||||||
_('Delete %s in Virtual library')%display_name(tag),
|
_('Delete %s in Virtual library')%display_name(tag),
|
||||||
partial(self.context_menu_handler, action='delete_item_in_vl',
|
partial(self.context_menu_handler, action='delete_item_in_vl',
|
||||||
key=key, index=tag))
|
key=key, index=tag_item))
|
||||||
|
|
||||||
self.context_menu.addAction(self.delete_icon,
|
self.context_menu.addAction(self.delete_icon,
|
||||||
_('Delete %s')%display_name(tag),
|
_('Delete %s')%display_name(tag),
|
||||||
partial(self.context_menu_handler, action='delete_item_no_vl',
|
partial(self.context_menu_handler, action='delete_item_no_vl',
|
||||||
key=key, index=tag))
|
key=key, index=tag_item))
|
||||||
if key == 'authors':
|
if key == 'authors':
|
||||||
self.context_menu.addAction(_('Edit sort for %s')%display_name(tag),
|
self.context_menu.addAction(_('Edit sort for %s')%display_name(tag),
|
||||||
partial(self.context_menu_handler,
|
partial(self.context_menu_handler,
|
||||||
@ -841,8 +867,20 @@ class TagsView(QTreeView): # {{{
|
|||||||
item = index.data(Qt.UserRole)
|
item = index.data(Qt.UserRole)
|
||||||
if item.type == TagTreeItem.ROOT:
|
if item.type == TagTreeItem.ROOT:
|
||||||
return
|
return
|
||||||
flags = self._model.flags(index)
|
|
||||||
if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled:
|
if src_is_tb:
|
||||||
|
src = json_loads(bytes(event.mimeData().data('application/calibre+from_tag_browser')))
|
||||||
|
if len(src) == 1:
|
||||||
|
src_item = self._model.get_node(self._model.index_for_path(src[0][5]))
|
||||||
|
if (src_item.type == TagTreeItem.TAG and
|
||||||
|
src_item.tag.category == item.tag.category and
|
||||||
|
not item.temporary and
|
||||||
|
self._model.is_key_a_hierarchical_category(src_item.tag.category)):
|
||||||
|
event.setDropAction(Qt.MoveAction)
|
||||||
|
self.setDropIndicatorShown(True)
|
||||||
|
return
|
||||||
|
if item.type == TagTreeItem.TAG and self._model.flags(index) & Qt.ItemIsDropEnabled:
|
||||||
|
event.setDropAction(Qt.CopyAction)
|
||||||
self.setDropIndicatorShown(not src_is_tb)
|
self.setDropIndicatorShown(not src_is_tb)
|
||||||
return
|
return
|
||||||
if item.type == TagTreeItem.CATEGORY and not item.is_gst:
|
if item.type == TagTreeItem.CATEGORY and not item.is_gst:
|
||||||
@ -850,8 +888,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
if fm_dest['kind'] == 'user':
|
if fm_dest['kind'] == 'user':
|
||||||
if src_is_tb:
|
if src_is_tb:
|
||||||
if event.dropAction() == Qt.MoveAction:
|
if event.dropAction() == Qt.MoveAction:
|
||||||
data = bytes(event.mimeData().data('application/calibre+from_tag_browser'))
|
# src is initialized above
|
||||||
src = json_loads(data)
|
|
||||||
for s in src:
|
for s in src:
|
||||||
if s[0] == TagTreeItem.TAG and \
|
if s[0] == TagTreeItem.TAG and \
|
||||||
(not s[1].startswith('@') or s[2]):
|
(not s[1].startswith('@') or s[2]):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user