mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
...
This commit is contained in:
commit
4e940f330d
@ -171,6 +171,13 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
cat_name = unicode(self.input_box.text()).strip()
|
||||
if cat_name == '':
|
||||
return False
|
||||
comps = [c.strip() for c in cat_name.split('.') if c.strip()]
|
||||
if len(comps) == 0 or '.'.join(comps) != cat_name:
|
||||
error_dialog(self, _('Invalid name'),
|
||||
_('That name contains leading or trailing periods, '
|
||||
'multiple periods in a row or spaces before '
|
||||
'or after periods.')).exec_()
|
||||
return False
|
||||
for c in self.categories:
|
||||
if strcmp(c, cat_name) == 0:
|
||||
error_dialog(self, _('Name already used'),
|
||||
@ -193,6 +200,14 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
return False
|
||||
if not self.current_cat_name:
|
||||
return False
|
||||
comps = [c.strip() for c in cat_name.split('.') if c.strip()]
|
||||
if len(comps) == 0 or '.'.join(comps) != cat_name:
|
||||
error_dialog(self, _('Invalid name'),
|
||||
_('That name contains leading or trailing periods, '
|
||||
'multiple periods in a row or spaces before '
|
||||
'or after periods.')).exec_()
|
||||
return False
|
||||
|
||||
for c in self.categories:
|
||||
if strcmp(c, cat_name) == 0:
|
||||
error_dialog(self, _('Name already used'),
|
||||
|
@ -9,7 +9,7 @@ Browsing book collection by tags.
|
||||
|
||||
import traceback, copy, cPickle
|
||||
|
||||
from itertools import izip
|
||||
from itertools import izip, repeat
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
|
||||
@ -22,7 +22,7 @@ from calibre.ebooks.metadata import title_sort
|
||||
from calibre.gui2 import config, NONE, gprefs
|
||||
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.utils.icu import sort_key, upper, lower, strcmp
|
||||
from calibre.utils.icu import sort_key, lower, strcmp
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.utils.formatter import eval_formatter
|
||||
from calibre.gui2 import error_dialog, question_dialog
|
||||
@ -32,6 +32,9 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
||||
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
|
||||
from calibre.gui2.widgets import HistoryLineEdit
|
||||
|
||||
def original_name(t):
|
||||
return getattr(t, 'original_name', t.name)
|
||||
|
||||
class TagDelegate(QItemDelegate): # {{{
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
@ -228,9 +231,13 @@ class TagsView(QTreeView): # {{{
|
||||
self._toggle(index, set_to=search_state)
|
||||
return
|
||||
if action == 'add_to_category':
|
||||
self.add_item_to_user_cat.emit(category,
|
||||
getattr(index, 'original_name', index.name),
|
||||
index.category)
|
||||
tag = index.tag
|
||||
if len(index.children) > 0:
|
||||
for c in index.children:
|
||||
self.add_item_to_user_cat.emit(category, original_name(c.tag),
|
||||
c.tag.category)
|
||||
self.add_item_to_user_cat.emit(category, original_name(tag),
|
||||
tag.category)
|
||||
return
|
||||
if action == 'add_subcategory':
|
||||
self.add_subcategory.emit(key)
|
||||
@ -242,8 +249,12 @@ class TagsView(QTreeView): # {{{
|
||||
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)
|
||||
tag = index.tag
|
||||
if len(index.children) > 0:
|
||||
for c in index.children:
|
||||
self.del_item_from_user_cat.emit(key, original_name(c.tag),
|
||||
c.tag.category)
|
||||
self.del_item_from_user_cat.emit(key, original_name(tag), tag.category)
|
||||
return
|
||||
if action == 'manage_searches':
|
||||
self.saved_search_edit.emit(category)
|
||||
@ -278,8 +289,8 @@ class TagsView(QTreeView): # {{{
|
||||
tag = None
|
||||
|
||||
if item.type == TagTreeItem.TAG:
|
||||
tag_item = item
|
||||
tag = item.tag
|
||||
can_edit = getattr(tag, 'can_edit', True)
|
||||
while item.type != TagTreeItem.CATEGORY:
|
||||
item = item.parent
|
||||
|
||||
@ -297,7 +308,7 @@ class TagsView(QTreeView): # {{{
|
||||
if tag:
|
||||
# If the user right-clicked on an editable item, then offer
|
||||
# the possibility of renaming that item.
|
||||
if can_edit:
|
||||
if tag.is_editable:
|
||||
# Add the 'rename' items
|
||||
self.context_menu.addAction(_('Rename %s')%tag.name,
|
||||
partial(self.context_menu_handler, action='edit_item',
|
||||
@ -317,8 +328,7 @@ class TagsView(QTreeView): # {{{
|
||||
m.addAction(self.user_category_icon, n,
|
||||
partial(self.context_menu_handler,
|
||||
'add_to_category',
|
||||
category='.'.join(p),
|
||||
index=tag))
|
||||
category='.'.join(p), index=tag_item))
|
||||
if len(tree_dict[k]):
|
||||
tm = m.addMenu(self.user_category_icon,
|
||||
_('Children of %s')%n)
|
||||
@ -331,7 +341,7 @@ class TagsView(QTreeView): # {{{
|
||||
_('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))
|
||||
key = key, index = tag_item))
|
||||
# Add the search for value items
|
||||
self.context_menu.addAction(self.search_icon,
|
||||
_('Search for %s')%tag.name,
|
||||
@ -345,7 +355,7 @@ class TagsView(QTreeView): # {{{
|
||||
index=index))
|
||||
self.context_menu.addSeparator()
|
||||
elif key.startswith('@') and not item.is_gst:
|
||||
if item.can_edit:
|
||||
if item.can_be_edited:
|
||||
self.context_menu.addAction(self.user_category_icon,
|
||||
_('Rename %s')%item.py_name,
|
||||
partial(self.context_menu_handler, action='edit_item',
|
||||
@ -386,8 +396,8 @@ 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=getattr(tag, 'original_name', tag.name)
|
||||
if tag else None, key=key))
|
||||
category=original_name(tag) 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'))
|
||||
@ -524,9 +534,11 @@ class TagTreeItem(object): # {{{
|
||||
ROOT = 2
|
||||
|
||||
def __init__(self, data=None, category_icon=None, icon_map=None,
|
||||
parent=None, tooltip=None, category_key=None):
|
||||
parent=None, tooltip=None, category_key=None, temporary=False):
|
||||
self.parent = parent
|
||||
self.children = []
|
||||
self.id_set = set()
|
||||
self.is_gst = False
|
||||
self.boxed = False
|
||||
if self.parent is not None:
|
||||
self.parent.append(self)
|
||||
@ -541,6 +553,7 @@ class TagTreeItem(object): # {{{
|
||||
self.bold_font.setBold(True)
|
||||
self.bold_font = QVariant(self.bold_font)
|
||||
self.category_key = category_key
|
||||
self.temporary = temporary
|
||||
elif self.type == self.TAG:
|
||||
icon_map[0] = data.icon
|
||||
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
|
||||
@ -597,18 +610,20 @@ class TagTreeItem(object): # {{{
|
||||
p = self
|
||||
while p.parent.type != self.ROOT:
|
||||
p = p.parent
|
||||
if p.category_key.startswith('@'):
|
||||
name = getattr(tag, 'original_name', tag.name)
|
||||
if not tag.is_hierarchical:
|
||||
name = original_name(tag)
|
||||
else:
|
||||
name = tag.name
|
||||
tt_author = False
|
||||
if role == Qt.DisplayRole:
|
||||
if tag.count == 0:
|
||||
count = len(self.id_set)
|
||||
count = count if count > 0 else tag.count
|
||||
if count == 0:
|
||||
return QVariant('%s'%(name))
|
||||
else:
|
||||
return QVariant('[%d] %s'%(tag.count, name))
|
||||
return QVariant('[%d] %s'%(count, name))
|
||||
if role == Qt.EditRole:
|
||||
return QVariant(getattr(tag, 'original_name', tag.name))
|
||||
return QVariant(original_name(tag))
|
||||
if role == Qt.DecorationRole:
|
||||
return self.icon_state_map[tag.state]
|
||||
if role == Qt.ToolTipRole:
|
||||
@ -671,7 +686,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
self.filter_categories_by = filter_categories_by
|
||||
self.collapse_model = collapse_model
|
||||
|
||||
# get_node_tree cannot return None here, because row_map is empty
|
||||
# get_node_tree cannot return None here, because row_map is empty. Note
|
||||
# that get_node_tree can indirectly change the user_categories dict.
|
||||
|
||||
data = self.get_node_tree(config['sort_tags_by'])
|
||||
gst = db.prefs.get('grouped_search_terms', {})
|
||||
self.root_item = TagTreeItem()
|
||||
@ -693,7 +710,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
tt = _(u'The lookup/search name is "{0}"').format(r)
|
||||
|
||||
if r.startswith('@'):
|
||||
path_parts = [p.strip() for p in r.split('.') if p.strip()]
|
||||
path_parts = [p for p in r.split('.')]
|
||||
path = ''
|
||||
last_category_node = self.root_item
|
||||
tree_root = self.category_node_tree
|
||||
@ -708,7 +725,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
last_category_node = node
|
||||
category_node_map[path] = node
|
||||
self.category_nodes.append(node)
|
||||
node.can_edit = (not is_gst) and (i == (len(path_parts)-1))
|
||||
node.can_be_edited = (not is_gst) and (i == (len(path_parts)-1))
|
||||
node.is_gst = is_gst
|
||||
if not is_gst:
|
||||
tree_root[p] = {}
|
||||
@ -741,6 +758,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
if idx.isValid():
|
||||
# get some useful serializable data
|
||||
node = idx.internalPointer()
|
||||
path = self.path_for_index(idx)
|
||||
if node.type == TagTreeItem.CATEGORY:
|
||||
d = (node.type, node.py_name, node.category_key)
|
||||
else:
|
||||
@ -748,8 +766,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
p = node
|
||||
while p.type != TagTreeItem.CATEGORY:
|
||||
p = p.parent
|
||||
d = (node.type, p.category_key, p.is_gst,
|
||||
getattr(t, 'original_name', t.name), t.category, t.id)
|
||||
d = (node.type, p.category_key, p.is_gst, original_name(t),
|
||||
t.category, path)
|
||||
data.append(d)
|
||||
else:
|
||||
data.append(None)
|
||||
@ -788,37 +806,30 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
'''
|
||||
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.
|
||||
full name, category key, path to node)
|
||||
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_parent_is_gst, src_name, src_cat = s[1:5]
|
||||
parent_node = src_parent
|
||||
if src_parent.startswith('@'):
|
||||
is_uc = True
|
||||
src_parent = src_parent[1:]
|
||||
else:
|
||||
is_uc = False
|
||||
dest_key = dest.category_key[1:]
|
||||
if dest_key not in user_cats:
|
||||
continue
|
||||
new_cat = []
|
||||
def process_source_node(user_cats, src_parent, src_parent_is_gst,
|
||||
is_uc, dest_key, node):
|
||||
'''
|
||||
Copy/move an item and all its children to the destination
|
||||
'''
|
||||
copied = False
|
||||
src_name = original_name(node.tag)
|
||||
src_cat = node.tag.category
|
||||
# delete the item if the source is a user category and action is move
|
||||
if is_uc and not src_parent_is_gst and src_parent in user_cats and \
|
||||
action == Qt.MoveAction:
|
||||
new_cat = []
|
||||
for tup in user_cats[src_parent]:
|
||||
if src_name == tup[0] and src_cat == tup[1]:
|
||||
continue
|
||||
new_cat.append(list(tup))
|
||||
user_cats[src_parent] = new_cat
|
||||
else:
|
||||
copied_node = (src_parent, src_name)
|
||||
copied = True
|
||||
|
||||
# Now add the item to the destination user category
|
||||
add_it = True
|
||||
@ -830,18 +841,53 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
if add_it:
|
||||
user_cats[dest_key].append([src_name, src_cat, 0])
|
||||
|
||||
for c in node.children:
|
||||
copied = process_source_node(user_cats, src_parent, src_parent_is_gst,
|
||||
is_uc, dest_key, c)
|
||||
return copied
|
||||
|
||||
user_cats = self.db.prefs.get('user_categories', {})
|
||||
parent_node = None
|
||||
copied = False
|
||||
path = None
|
||||
for s in src:
|
||||
src_parent, src_parent_is_gst = s[1:3]
|
||||
path = s[5]
|
||||
parent_node = src_parent
|
||||
|
||||
if src_parent.startswith('@'):
|
||||
is_uc = True
|
||||
src_parent = src_parent[1:]
|
||||
else:
|
||||
is_uc = False
|
||||
dest_key = dest.category_key[1:]
|
||||
|
||||
if dest_key not in user_cats:
|
||||
continue
|
||||
|
||||
node = self.index_for_path(path)
|
||||
if node:
|
||||
copied = process_source_node(user_cats, src_parent, src_parent_is_gst,
|
||||
is_uc, dest_key, node.internalPointer())
|
||||
|
||||
self.db.prefs.set('user_categories', user_cats)
|
||||
self.tags_view.recount()
|
||||
|
||||
# Scroll to the item copied. If it was moved, scroll to the parent
|
||||
if parent_node is not None:
|
||||
self.clear_boxed()
|
||||
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:
|
||||
if not copied:
|
||||
p = path[-1]
|
||||
if p == 0:
|
||||
path = m.find_category_node(parent_node)
|
||||
else:
|
||||
path[-1] = p - 1
|
||||
idx = m.index_for_path(path)
|
||||
self.tags_view.setExpanded(idx, True)
|
||||
if idx.internalPointer().type == TagTreeItem.TAG:
|
||||
m.show_item_at_index(idx, box=True)
|
||||
else:
|
||||
m.show_item_at_index(idx)
|
||||
return True
|
||||
|
||||
@ -1044,24 +1090,60 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
else:
|
||||
collapse_model = 'partition'
|
||||
collapse_template = tweaks['categories_collapsed_popularity_template']
|
||||
collapse_letter = collapse_letter_sk = None
|
||||
|
||||
def process_one_node(category, state_map, collapse_letter, collapse_letter_sk):
|
||||
def process_one_node(category, state_map):
|
||||
collapse_letter = None
|
||||
category_index = self.createIndex(category.row(), 0, category)
|
||||
category_node = category_index.internalPointer()
|
||||
key = category_node.category_key
|
||||
if key not in data:
|
||||
return ((collapse_letter, collapse_letter_sk))
|
||||
return
|
||||
cat_len = len(data[key])
|
||||
if cat_len <= 0:
|
||||
return ((collapse_letter, collapse_letter_sk))
|
||||
return
|
||||
|
||||
category_child_map = {}
|
||||
fm = self.db.field_metadata[key]
|
||||
clear_rating = True if key not in self.categories_with_ratings and \
|
||||
not fm['is_custom'] and \
|
||||
not fm['kind'] == 'user' \
|
||||
else False
|
||||
tt = key if fm['kind'] == 'user' else None
|
||||
|
||||
if collapse_model == 'first letter':
|
||||
# Build a list of 'equal' first letters by looking for
|
||||
# overlapping ranges. If a range overlaps another, then the
|
||||
# letters are assumed to be equivalent. ICU collating is complex
|
||||
# beyond belief. This mechanism lets us determine the logical
|
||||
# first character from ICU's standpoint.
|
||||
chardict = {}
|
||||
for idx,tag in enumerate(data[key]):
|
||||
if not tag.sort:
|
||||
c = ' '
|
||||
else:
|
||||
c = icu_upper(tag.sort[0])
|
||||
if c not in chardict:
|
||||
chardict[c] = [idx, idx]
|
||||
else:
|
||||
chardict[c][1] = idx
|
||||
|
||||
# sort the ranges to facilitate detecting overlap
|
||||
ranges = sorted([(v[0], v[1], c) for c,v in chardict.items()])
|
||||
|
||||
# Create a list of 'first letters' to use for each item in
|
||||
# the category. The list is generated using the ranges. Overlaps
|
||||
# are filled with the character that first occurs.
|
||||
cl_list = list(repeat(None, len(data[key])))
|
||||
for t in ranges:
|
||||
start = t[0]
|
||||
c = t[2]
|
||||
if cl_list[start] is None:
|
||||
nc = c
|
||||
else:
|
||||
nc = cl_list[start]
|
||||
for i in range(start, t[1]+1):
|
||||
cl_list[i] = nc
|
||||
|
||||
for idx,tag in enumerate(data[key]):
|
||||
if clear_rating:
|
||||
tag.avg_rating = None
|
||||
@ -1078,72 +1160,77 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
name = eval_formatter.safe_format(collapse_template,
|
||||
d, 'TAG_VIEW', None)
|
||||
self.beginInsertRows(category_index, 999999, 1) #len(data[key])-1)
|
||||
sub_cat = TagTreeItem(parent=category,
|
||||
data = name, tooltip = None,
|
||||
sub_cat = TagTreeItem(parent=category, data = name,
|
||||
tooltip = None, temporary=True,
|
||||
category_icon = category_node.icon,
|
||||
category_key=category_node.category_key)
|
||||
self.endInsertRows()
|
||||
else:
|
||||
ts = tag.sort
|
||||
if not ts:
|
||||
ts = ' '
|
||||
try:
|
||||
sk = sort_key(ts)[0]
|
||||
except:
|
||||
sk = ts[0]
|
||||
|
||||
if sk != collapse_letter_sk:
|
||||
collapse_letter = upper(ts[0])
|
||||
try:
|
||||
collapse_letter_sk = sort_key(collapse_letter)[0]
|
||||
except:
|
||||
collapse_letter_sk = collapse_letter
|
||||
else: # by 'first letter'
|
||||
cl = cl_list[idx]
|
||||
if cl != collapse_letter:
|
||||
collapse_letter = cl
|
||||
sub_cat = TagTreeItem(parent=category,
|
||||
data = collapse_letter,
|
||||
category_icon = category_node.icon,
|
||||
tooltip = None,
|
||||
tooltip = None, temporary=True,
|
||||
category_key=category_node.category_key)
|
||||
node_parent = sub_cat
|
||||
else:
|
||||
node_parent = category
|
||||
|
||||
components = [t for t in tag.name.split('.')]
|
||||
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':
|
||||
# category display order is important here. The following works
|
||||
# only of all the non-user categories are displayed before the
|
||||
# user categories
|
||||
components = [t.strip() for t in original_name(tag).split('.')
|
||||
if t.strip()]
|
||||
if len(components) == 0 or '.'.join(components) != original_name(tag):
|
||||
components = [original_name(tag)]
|
||||
in_uc = fm['kind'] == 'user'
|
||||
if (not tag.is_hierarchical) and (in_uc or
|
||||
key in ['authors', 'publisher', 'news', 'formats', 'rating'] or
|
||||
key not in self.db.prefs.get('categories_using_hierarchy', []) or
|
||||
len(components) == 1):
|
||||
self.beginInsertRows(category_index, 999999, 1)
|
||||
TagTreeItem(parent=node_parent, data=tag, tooltip=tt,
|
||||
n = TagTreeItem(parent=node_parent, data=tag, tooltip=tt,
|
||||
icon_map=self.icon_state_map)
|
||||
if tag.id_set is not None:
|
||||
n.id_set |= tag.id_set
|
||||
category_child_map[tag.name, tag.category] = n
|
||||
self.endInsertRows()
|
||||
tag.can_edit = key != 'formats' and (key == 'news' or \
|
||||
tag.is_editable = key != 'formats' and (key == 'news' or \
|
||||
self.db.field_metadata[tag.category]['datatype'] in \
|
||||
['text', 'series', 'enumeration'])
|
||||
else:
|
||||
for i,comp in enumerate(components):
|
||||
child_map = dict([(t.tag.name, t) for t in node_parent.children
|
||||
if i == 0:
|
||||
child_map = category_child_map
|
||||
else:
|
||||
child_map = dict([((t.tag.name, t.tag.category), t)
|
||||
for t in node_parent.children
|
||||
if t.type != TagTreeItem.CATEGORY])
|
||||
if comp in child_map:
|
||||
node_parent = child_map[comp]
|
||||
node_parent.tag.count += tag.count
|
||||
node_parent.tag.use_prefix = True
|
||||
if (comp,tag.category) in child_map:
|
||||
node_parent = child_map[(comp,tag.category)]
|
||||
node_parent.tag.is_hierarchical = True
|
||||
else:
|
||||
if i < len(components)-1:
|
||||
t = copy.copy(tag)
|
||||
t.original_name = '.'.join(components[:i+1])
|
||||
t.can_edit = False
|
||||
t.is_editable = False
|
||||
else:
|
||||
t = tag
|
||||
if not in_uc:
|
||||
t.original_name = t.name
|
||||
t.can_edit = True
|
||||
t.use_prefix = True
|
||||
t.is_editable = True
|
||||
t.is_hierarchical = True
|
||||
t.name = comp
|
||||
self.beginInsertRows(category_index, 999999, 1)
|
||||
node_parent = TagTreeItem(parent=node_parent, data=t,
|
||||
tooltip=tt, icon_map=self.icon_state_map)
|
||||
child_map[(comp,tag.category)] = node_parent
|
||||
self.endInsertRows()
|
||||
|
||||
return ((collapse_letter, collapse_letter_sk))
|
||||
# This id_set must not be None
|
||||
node_parent.id_set |= tag.id_set
|
||||
return
|
||||
|
||||
for category in self.category_nodes:
|
||||
if len(category.children) > 0:
|
||||
@ -1151,7 +1238,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
states = [c.tag.state for c in category.child_tags()]
|
||||
names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
|
||||
state_map = dict(izip(names, states))
|
||||
ctags = [c for c in child_map if c.type == TagTreeItem.CATEGORY]
|
||||
# temporary sub-categories (the partitioning ones) must follow
|
||||
# the permanent sub-categories. This will happen naturally if
|
||||
# the temp ones are added by process_node
|
||||
ctags = [c for c in child_map if
|
||||
c.type == TagTreeItem.CATEGORY and not c.temporary]
|
||||
start = len(ctags)
|
||||
self.beginRemoveRows(self.createIndex(category.row(), 0, category),
|
||||
start, len(child_map)-1)
|
||||
@ -1160,8 +1251,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
else:
|
||||
state_map = {}
|
||||
|
||||
collapse_letter, collapse_letter_sk = process_one_node(category,
|
||||
state_map, collapse_letter, collapse_letter_sk)
|
||||
process_one_node(category, state_map)
|
||||
return True
|
||||
|
||||
def columnCount(self, parent):
|
||||
@ -1180,13 +1270,19 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
# working with the last item and that item is deleted, in which case
|
||||
# we position at the parent label
|
||||
path = index.model().path_for_index(index)
|
||||
val = unicode(value.toString())
|
||||
val = unicode(value.toString()).strip()
|
||||
if not val:
|
||||
error_dialog(self.tags_view, _('Item is blank'),
|
||||
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
||||
return False
|
||||
item = index.internalPointer()
|
||||
if item.type == TagTreeItem.CATEGORY and item.category_key.startswith('@'):
|
||||
if val.find('.') >= 0:
|
||||
error_dialog(self.tags_view, _('Rename user category'),
|
||||
_('You cannot use periods in the name when '
|
||||
'renaming user categories'), show=True)
|
||||
return False
|
||||
|
||||
user_cats = self.db.prefs.get('user_categories', {})
|
||||
ckey = item.category_key[1:]
|
||||
dotpos = ckey.rfind('.')
|
||||
@ -1199,7 +1295,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
if len(c) == len(ckey):
|
||||
if nkey in user_cats:
|
||||
error_dialog(self.tags_view, _('Rename user category'),
|
||||
_('The name %s is already used'%nkey), show=True)
|
||||
_('The name %s is already used')%nkey, show=True)
|
||||
return False
|
||||
user_cats[nkey] = user_cats[ckey]
|
||||
del user_cats[ckey]
|
||||
@ -1219,7 +1315,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
return True
|
||||
|
||||
key = item.tag.category
|
||||
name = getattr(item.tag, 'original_name', item.tag.name)
|
||||
name = original_name(item.tag)
|
||||
# make certain we know about the item's category
|
||||
if key not in self.db.field_metadata:
|
||||
return False
|
||||
@ -1306,7 +1402,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
if index.isValid():
|
||||
node = self.data(index, Qt.UserRole)
|
||||
if node.type == TagTreeItem.TAG:
|
||||
if getattr(node.tag, 'can_edit', True):
|
||||
if node.tag.is_editable:
|
||||
ans |= Qt.ItemIsDragEnabled
|
||||
fm = self.db.metadata_for_field(node.tag.category)
|
||||
if node.tag.category in \
|
||||
@ -1438,8 +1534,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
|
||||
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
|
||||
else:
|
||||
name = getattr(tag, 'original_name', tag.name)
|
||||
use_prefix = getattr(tag, 'use_prefix', False)
|
||||
name = original_name(tag)
|
||||
use_prefix = tag.is_hierarchical
|
||||
if category == 'tags':
|
||||
if name in tags_seen:
|
||||
continue
|
||||
@ -1477,7 +1573,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
tag = tag_item.tag
|
||||
if tag is None:
|
||||
return False
|
||||
name = getattr(tag, 'original_name', tag.name)
|
||||
name = original_name(tag)
|
||||
if (equals_match and strcmp(name, txt) == 0) or \
|
||||
(not equals_match and lower(name).find(txt) >= 0):
|
||||
self.path_found = path
|
||||
@ -1559,11 +1655,16 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
if tag_item.boxed:
|
||||
tag_item.boxed = False
|
||||
self.dataChanged.emit(tag_index, tag_index)
|
||||
for i,c in enumerate(tag_item.children):
|
||||
process_tag(self.index(i, 0, tag_index), c)
|
||||
|
||||
def process_level(category_index):
|
||||
for j in xrange(self.rowCount(category_index)):
|
||||
tag_index = self.index(j, 0, category_index)
|
||||
tag_item = tag_index.internalPointer()
|
||||
if tag_item.boxed:
|
||||
tag_item.boxed = False
|
||||
self.dataChanged.emit(tag_index, tag_index)
|
||||
if tag_item.type == TagTreeItem.CATEGORY:
|
||||
process_level(tag_index)
|
||||
else:
|
||||
@ -1703,8 +1804,9 @@ class TagBrowserMixin(object): # {{{
|
||||
db = self.library_view.model().db
|
||||
user_cats = db.prefs.get('user_categories', {})
|
||||
|
||||
if dest_category.startswith('@'):
|
||||
if dest_category and 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)
|
||||
|
@ -46,11 +46,14 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||
class Tag(object):
|
||||
|
||||
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
|
||||
tooltip=None, icon=None, category=None):
|
||||
tooltip=None, icon=None, category=None, id_set=None):
|
||||
self.name = name
|
||||
self.id = id
|
||||
self.count = count
|
||||
self.state = state
|
||||
self.is_hierarchical = False
|
||||
self.is_editable = True
|
||||
self.id_set = id_set
|
||||
self.avg_rating = avg/2.0 if avg is not None else 0
|
||||
self.sort = sort
|
||||
if self.avg_rating > 0:
|
||||
@ -1160,6 +1163,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.n = name
|
||||
self.s = sort
|
||||
self.c = 0
|
||||
self.id_set = set()
|
||||
self.rt = 0
|
||||
self.rc = 0
|
||||
self.id = None
|
||||
@ -1177,6 +1181,22 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
return 'n=%s s=%s c=%d rt=%d rc=%d id=%s'%\
|
||||
(self.n, self.s, self.c, self.rt, self.rc, self.id)
|
||||
|
||||
def clean_user_categories(self):
|
||||
user_cats = self.prefs.get('user_categories', {})
|
||||
new_cats = {}
|
||||
for k in user_cats:
|
||||
comps = [c.strip() for c in k.split('.') if c.strip()]
|
||||
if len(comps) == 0:
|
||||
i = 1
|
||||
while True:
|
||||
if unicode(i) not in user_cats:
|
||||
new_cats[unicode(i)] = user_cats[k]
|
||||
break
|
||||
i += 1
|
||||
else:
|
||||
new_cats['.'.join(comps)] = user_cats[k]
|
||||
self.prefs.set('user_categories', new_cats)
|
||||
return new_cats
|
||||
|
||||
def get_categories(self, sort='name', ids=None, icon_map=None):
|
||||
#start = last = time.clock()
|
||||
@ -1264,6 +1284,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
item = tag_class(val, sort_val)
|
||||
tcategories[cat][val] = item
|
||||
item.c += 1
|
||||
item.id_set.add(book[0])
|
||||
item.id = item_id
|
||||
if rating > 0:
|
||||
item.rt += rating
|
||||
@ -1281,6 +1302,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
item = tag_class(val, sort_val)
|
||||
tcategories[cat][val] = item
|
||||
item.c += 1
|
||||
item.id_set.add(book[0])
|
||||
item.id = item_id
|
||||
if rating > 0:
|
||||
item.rt += rating
|
||||
@ -1368,7 +1390,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
|
||||
categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id,
|
||||
avg=avgr(r), sort=r.s, icon=icon,
|
||||
tooltip=tooltip, category=category)
|
||||
tooltip=tooltip, category=category,
|
||||
id_set=r.id_set)
|
||||
for r in items]
|
||||
|
||||
#print 'end phase "tags list":', time.clock() - last, 'seconds'
|
||||
@ -1377,6 +1400,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
# Needed for legacy databases that have multiple ratings that
|
||||
# map to n stars
|
||||
for r in categories['rating']:
|
||||
r.id_set = None
|
||||
for x in categories['rating']:
|
||||
if r.name == x.name and r.id != x.id:
|
||||
r.count = r.count + x.count
|
||||
@ -1413,7 +1437,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
categories['formats'].sort(key = lambda x:x.name)
|
||||
|
||||
#### Now do the user-defined categories. ####
|
||||
user_categories = dict.copy(self.prefs['user_categories'])
|
||||
user_categories = dict.copy(self.clean_user_categories())
|
||||
|
||||
# We want to use same node in the user category as in the source
|
||||
# category. To do that, we need to find the original Tag node. There is
|
||||
|
@ -342,6 +342,7 @@ class BrowseServer(object):
|
||||
return category_meta[x]['name'].lower()
|
||||
|
||||
displayed_custom_fields = custom_fields_to_display(self.db)
|
||||
uc_displayed = set()
|
||||
for category in sorted(categories, key=lambda x: sort_key(getter(x))):
|
||||
if len(categories[category]) == 0:
|
||||
continue
|
||||
@ -361,6 +362,18 @@ class BrowseServer(object):
|
||||
icon = category_icon_map['user:']
|
||||
else:
|
||||
icon = 'blank.png'
|
||||
|
||||
if meta['kind'] == 'user':
|
||||
dot = category.find('.')
|
||||
if dot > 0:
|
||||
cat = category[:dot]
|
||||
if cat not in uc_displayed:
|
||||
cats.append((meta['name'][:dot-1], cat, icon))
|
||||
uc_displayed.add(cat)
|
||||
else:
|
||||
cats.append((meta['name'], category, icon))
|
||||
uc_displayed.add(category)
|
||||
else:
|
||||
cats.append((meta['name'], category, icon))
|
||||
|
||||
cats = [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}"> </a>'
|
||||
@ -394,13 +407,59 @@ class BrowseServer(object):
|
||||
category_name = category_meta[category]['name']
|
||||
datatype = category_meta[category]['datatype']
|
||||
|
||||
# See if we have any sub-categories to display. As we find them, add
|
||||
# them to the displayed set to avoid showing the same item twice
|
||||
uc_displayed = set()
|
||||
cats = []
|
||||
for ucat in sorted(categories.keys(), key=sort_key):
|
||||
if len(categories[ucat]) == 0:
|
||||
continue
|
||||
if category == 'formats':
|
||||
continue
|
||||
meta = category_meta.get(ucat, None)
|
||||
if meta is None:
|
||||
continue
|
||||
if meta['kind'] != 'user':
|
||||
continue
|
||||
cat_len = len(category)
|
||||
if not (len(ucat) > cat_len and ucat.startswith(category+'.')):
|
||||
continue
|
||||
icon = category_icon_map['user:']
|
||||
# we have a subcategory. Find any further dots (further subcats)
|
||||
cat_len += 1
|
||||
cat = ucat[cat_len:]
|
||||
dot = cat.find('.')
|
||||
if dot > 0:
|
||||
# More subcats
|
||||
cat = cat[:dot]
|
||||
if cat not in uc_displayed:
|
||||
cats.append((cat, ucat[:cat_len+dot], icon))
|
||||
uc_displayed.add(cat)
|
||||
else:
|
||||
# This is the end of the chain
|
||||
cats.append((cat, ucat, icon))
|
||||
uc_displayed.add(cat)
|
||||
|
||||
cats = u'\n\n'.join(
|
||||
[(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}"> </a>'
|
||||
u'<img src="{3}{src}" alt="{0}" />'
|
||||
u'<span class="label">{0}</span>'
|
||||
u'</li>')
|
||||
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
|
||||
self.opts.url_prefix, src='/browse/icon/'+z)
|
||||
for x, y, z in cats])
|
||||
if cats:
|
||||
cats = (u'\n<div class="toplevel">\n'
|
||||
'{0}</div>').format(cats)
|
||||
script = 'toplevel();'
|
||||
else:
|
||||
script = 'true'
|
||||
|
||||
# Now do the category items
|
||||
items = categories[category]
|
||||
sort = self.browse_sort_categories(items, sort)
|
||||
|
||||
script = 'true'
|
||||
|
||||
if len(items) == 1:
|
||||
if not cats and len(items) == 1:
|
||||
# Only one item in category, go directly to book list
|
||||
prefix = '' if self.is_wsgi else self.opts.url_prefix
|
||||
html = get_category_items(category, items,
|
||||
@ -443,6 +502,9 @@ class BrowseServer(object):
|
||||
|
||||
|
||||
|
||||
if cats:
|
||||
script = 'toplevel();category(%s);'%script
|
||||
else:
|
||||
script = 'category(%s);'%script
|
||||
|
||||
main = u'''
|
||||
@ -453,7 +515,7 @@ class BrowseServer(object):
|
||||
{1}
|
||||
</div>
|
||||
'''.format(
|
||||
xml(_('Browsing by')+': ' + category_name), items,
|
||||
xml(_('Browsing by')+': ' + category_name), cats + items,
|
||||
xml(_('Up'), True), self.opts.url_prefix)
|
||||
|
||||
return self.browse_template(sort).format(title=category_name,
|
||||
|
Loading…
x
Reference in New Issue
Block a user