mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
...
This commit is contained in:
commit
a28e0b6f1d
@ -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])
|
||||||
|
@ -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_()
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user