From 9eda085a3be233da45f8a84691ebab22f4e1c90d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 24 Feb 2011 21:22:59 +0000 Subject: [PATCH 1/9] 1) refactor hierarchy code to build the category child map once. 2) make hierarchical items display hierarchically in user categories 3) make add item to category also copy child nodes 4) make remove node also remove child nodes 5) some basic code cleanups --- src/calibre/gui2/tag_view.py | 107 +++++++++++++++++++------------ src/calibre/library/database2.py | 2 + 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 06f01a1649..de82321124 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -32,6 +32,9 @@ 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 @@ def context_menu_handler(self, action=None, category=None, 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 @@ def context_menu_handler(self, action=None, category=None, 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 @@ def show_context_menu(self, point): 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 @@ def show_context_menu(self, point): 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 @@ def add_node_tree(tree_dict, m, path): 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 @@ def add_node_tree(tree_dict, m, path): _('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 @@ def add_node_tree(tree_dict, m, path): 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 @@ def add_node_tree(tree_dict, m, path): 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')) @@ -597,8 +607,8 @@ def tag_data(self, role): 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 @@ -608,7 +618,7 @@ def tag_data(self, role): else: return QVariant('[%d] %s'%(tag.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: @@ -708,7 +718,7 @@ def __init__(self, db, parent, hidden_categories=None, 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] = {} @@ -748,8 +758,8 @@ def mimeData(self, indexes): 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, t.id) data.append(d) else: data.append(None) @@ -794,6 +804,7 @@ def move_or_copy_item_to_user_category(self, src, dest, action): dest is the TagTreeItem node to receive the items action is Qt.CopyAction or Qt.MoveAction ''' +##### TODO: must handle children of item being copied user_cats = self.db.prefs.get('user_categories', {}) parent_node = None copied_node = None @@ -1056,6 +1067,7 @@ def process_one_node(category, state_map, collapse_letter, collapse_letter_sk): if cat_len <= 0: return ((collapse_letter, collapse_letter_sk)) + 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 \ @@ -1107,40 +1119,52 @@ def process_one_node(category, state_map, collapse_letter, collapse_letter_sk): 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 for t in original_name(tag).split('.')] + 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) + 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)] + if not in_uc: + node_parent.tag.count += tag.count + 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 - t.original_name = t.name - t.can_edit = True - t.use_prefix = True + if not in_uc: + t.original_name = t.name + 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)) @@ -1219,7 +1243,7 @@ def setData(self, index, value, role=Qt.EditRole): 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 +1330,7 @@ def flags(self, index, *args): 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 +1462,8 @@ def tokens(self): 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 +1501,7 @@ def process_tag(depth, tag_index, tag_item, start_path): 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 @@ -1703,8 +1727,9 @@ def do_add_item_to_user_cat(self, dest_category, src_name, src_category): 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) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index dce0b34aef..03fff3a5cd 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -51,6 +51,8 @@ def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None, self.id = id self.count = count self.state = state + self.is_hierarchical = False + self.is_editable = True self.avg_rating = avg/2.0 if avg is not None else 0 self.sort = sort if self.avg_rating > 0: From 2b24a487faa3f8232c20f1d8e3593f6830adf5d6 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 25 Feb 2011 09:23:30 +0000 Subject: [PATCH 2/9] 1) Performance improvements when using hierarchical categories 2) ensure correct counts when using sub-categories 3) make drag & drop copy a node and its children 4) make tb searching work better with subcategories and hierarchies --- src/calibre/gui2/tag_view.py | 98 ++++++++++++++++++++++---------- src/calibre/library/database2.py | 10 +++- 2 files changed, 76 insertions(+), 32 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index de82321124..32171fd5f4 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -537,6 +537,7 @@ def __init__(self, data=None, category_icon=None, icon_map=None, parent=None, tooltip=None, category_key=None): self.parent = parent self.children = [] + self.id_set = set() self.boxed = False if self.parent is not None: self.parent.append(self) @@ -613,10 +614,12 @@ def tag_data(self, role): 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(original_name(tag)) if role == Qt.DecorationRole: @@ -751,6 +754,7 @@ def mimeData(self, indexes): 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: @@ -759,7 +763,7 @@ def mimeData(self, indexes): while p.type != TagTreeItem.CATEGORY: p = p.parent d = (node.type, p.category_key, p.is_gst, original_name(t), - t.category, t.id) + t.category, path) data.append(d) else: data.append(None) @@ -798,38 +802,30 @@ 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. + 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 ''' -##### TODO: must handle children of item being copied - 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 @@ -841,19 +837,54 @@ def move_or_copy_item_to_user_category(self, src, dest, action): 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: - path = m.find_category_node(parent_node) + 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) - m.show_item_at_index(idx) + if idx.internalPointer().type == TagTreeItem.TAG: + m.show_item_at_index(idx, boxed=True) + else: + m.show_item_at_index(idx) return True def do_drop_from_library(self, md, action, row, column, parent): @@ -1131,6 +1162,8 @@ def process_one_node(category, state_map, collapse_letter, collapse_letter_sk): self.beginInsertRows(category_index, 999999, 1) 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.is_editable = key != 'formats' and (key == 'news' or \ @@ -1146,8 +1179,6 @@ def process_one_node(category, state_map, collapse_letter, collapse_letter_sk): if t.type != TagTreeItem.CATEGORY]) if (comp,tag.category) in child_map: node_parent = child_map[(comp,tag.category)] - if not in_uc: - node_parent.tag.count += tag.count node_parent.tag.is_hierarchical = True else: if i < len(components)-1: @@ -1166,6 +1197,8 @@ def process_one_node(category, state_map, collapse_letter, collapse_letter_sk): tooltip=tt, icon_map=self.icon_state_map) child_map[(comp,tag.category)] = node_parent self.endInsertRows() + # This id_set must not be None + node_parent.id_set |= tag.id_set return ((collapse_letter, collapse_letter_sk)) @@ -1583,11 +1616,16 @@ def process_tag(tag_index, tag_item): 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: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 03fff3a5cd..a48e7a5d73 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -46,13 +46,14 @@ 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: @@ -1162,6 +1163,7 @@ def __init__(self, name, sort): self.n = name self.s = sort self.c = 0 + self.id_set = set() self.rt = 0 self.rc = 0 self.id = None @@ -1266,6 +1268,7 @@ def get_categories(self, sort='name', ids=None, icon_map=None): 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 @@ -1283,6 +1286,7 @@ def get_categories(self, sort='name', ids=None, icon_map=None): 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 @@ -1370,7 +1374,8 @@ def get_categories(self, sort='name', ids=None, icon_map=None): 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' @@ -1379,6 +1384,7 @@ def get_categories(self, sort='name', ids=None, icon_map=None): # 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 From 976acd25a700501851bbdde01648522daca1abe0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 25 Feb 2011 10:52:43 +0000 Subject: [PATCH 3/9] Attempt at content server with subcategories. --- src/calibre/library/server/browse.py | 65 +++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 5415cfe8bb..5963b918d3 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -342,6 +342,7 @@ def getter(x): 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,7 +362,19 @@ def getter(x): icon = category_icon_map['user:'] else: icon = 'blank.png' - cats.append((meta['name'], category, icon)) + + 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'