From 2252d7f088b006e3944fb0572b34fcc8387a3b5b Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Sat, 25 Dec 2021 13:54:33 +0000 Subject: [PATCH] Bug #1955732: Hierarchical entries in user category may not merge correctly in tag browser Note that this fix works with grouped search terms. 'Real' user categories will never be merged because the source category is part of the 'name' --- src/calibre/db/categories.py | 8 +++-- src/calibre/gui2/tag_browser/model.py | 50 +++++++++++++++++---------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/calibre/db/categories.py b/src/calibre/db/categories.py index b3ec3e1a9f..0fd61aeb56 100644 --- a/src/calibre/db/categories.py +++ b/src/calibre/db/categories.py @@ -44,7 +44,8 @@ def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None, @property def string_representation(self): - return '%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, self.category) + return '%s:%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, + self.category, self.original_categories) def __str__(self): return self.string_representation @@ -207,10 +208,11 @@ def get_metadata(book_id): for c in gst: if c not in muc: continue - user_categories[c] = [] + uc = [] for sc in gst[c]: for t in categories.get(sc, ()): - user_categories[c].append([t.name, sc, 0]) + uc.append([t.name, sc, 0]) + user_categories[c] = uc if user_categories: # We want to use same node in the user category as in the source diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 125553dcf2..4d6d1fdcf0 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -56,13 +56,14 @@ class TagTreeItem: # {{{ file_icon_provider = None def __init__(self, data=None, is_category=False, icon_map=None, - parent=None, tooltip=None, category_key=None, temporary=False): + parent=None, tooltip=None, category_key=None, temporary=False, + is_gst=False): if self.file_icon_provider is None: self.file_icon_provider = TagTreeItem.file_icon_provider = file_icon_provider().icon_from_ext self.parent = parent self.children = [] self.blank = QIcon() - self.is_gst = False + self.is_gst = is_gst self.boxed = False self.temporary = False self.can_be_edited = False @@ -103,6 +104,12 @@ def break_cycles(self): del self.parent del self.children + def root_node(self): + p = self + while p.parent.type != self.ROOT: + p = p.parent + return p + def ensure_icon(self): if self.icon_state_map[0] is not None: return @@ -111,7 +118,10 @@ def ensure_icon(self): fmt = self.tag.original_name.replace('ORIGINAL_', '') cc = self.file_icon_provider(fmt) else: - cc = self.category_custom_icons.get(self.tag.category, None) + if self.is_gst: + cc = self.category_custom_icons.get(self.root_node().category_key, None) + else: + cc = self.category_custom_icons.get(self.tag.category, None) elif self.type == self.CATEGORY: cc = self.category_custom_icons.get(self.category_key, None) self.icon_state_map[0] = cc or QIcon() @@ -461,6 +471,7 @@ def _rebuild_node_tree(self, state_map): node = self.create_node(parent=last_category_node, data=p[1:] if i == 0 else p, is_category=True, + is_gst = is_gst, tooltip=tt if path == key else path, category_key=path, icon_map=self.icon_state_map) @@ -468,7 +479,6 @@ def _rebuild_node_tree(self, state_map): category_node_map[path] = node self.category_nodes.append(node) node.can_be_edited = (not is_gst) and (i == (len(path_parts)-1)) - node.is_gst = is_gst if not is_gst: node.tag.is_hierarchical = '5state' tree_root[p] = {} @@ -481,9 +491,9 @@ def _rebuild_node_tree(self, state_map): node = self.create_node(parent=self.root_item, data=self.categories[key], is_category=True, + is_gst = False, tooltip=tt, category_key=key, icon_map=self.icon_state_map) - node.is_gst = False category_node_map[key] = node last_category_node = node self.category_nodes.append(node) @@ -663,10 +673,10 @@ def process_one_node(category, collapse_model, book_rating_map, state_map): # { sub_cat = self.create_node(parent=category, data=name, tooltip=None, temporary=True, is_category=True, + is_gst=is_gst, category_key=category.category_key, icon_map=self.icon_state_map) sub_cat.tag.is_searchable = False - sub_cat.is_gst = is_gst node_parent = sub_cat last_idx = idx # remember where we last partitioned else: @@ -678,10 +688,10 @@ def process_one_node(category, collapse_model, book_rating_map, state_map): # { sub_cat = self.create_node(parent=category, data=collapse_letter, is_category=True, + is_gst=is_gst, tooltip=None, temporary=True, category_key=category.category_key, icon_map=self.icon_state_map) - sub_cat.is_gst = is_gst node_parent = sub_cat else: node_parent = category @@ -698,28 +708,29 @@ def process_one_node(category, collapse_model, book_rating_map, state_map): # { (fm['is_custom'] and fm['display'].get('is_names', False)) or not category_is_hierarchical or len(components) == 1): n = self.create_node(parent=node_parent, data=tag, tooltip=tt, - icon_map=self.icon_state_map) + is_gst=is_gst, icon_map=self.icon_state_map) category_child_map[tag.name, tag.category] = n else: + child_key = key if is_gst else tag.category for i,comp in enumerate(components): if i == 0: child_map = category_child_map top_level_component = comp else: - child_map = {(t.tag.name, t.tag.category): t - for t in node_parent.children + child_map = {(t.tag.name, key if is_gst else t.tag.category): + t for t in node_parent.children if t.type != TagTreeItem.CATEGORY} - if (comp,tag.category) in child_map: - node_parent = child_map[(comp,tag.category)] + if (comp,child_key) in child_map: + node_parent = child_map[(comp,child_key)] t = node_parent.tag t.is_hierarchical = '5state' if tag.category != 'search' else '3state' if tag.id_set is not None and t.id_set is not None: t.id_set = t.id_set | tag.id_set - intermediate_nodes[t.original_name, t.category] = t + intermediate_nodes[t.original_name,child_key] = t else: if i < len(components)-1: original_name = '.'.join(components[:i+1]) - t = intermediate_nodes.get((original_name, tag.category), None) + t = intermediate_nodes.get((original_name, child_key), None) if t is None: t = copy.copy(tag) t.original_name = original_name @@ -730,18 +741,19 @@ def process_one_node(category, collapse_model, book_rating_map, state_map): # { t.is_editable = False else: t.is_searchable = t.is_editable = False - intermediate_nodes[original_name, tag.category] = t + intermediate_nodes[original_name,child_key] = t else: t = tag if not in_uc: t.original_name = t.name - intermediate_nodes[t.original_name, t.category] = t + intermediate_nodes[t.original_name,child_key] = t t.is_hierarchical = \ '5state' if t.category != 'search' else '3state' t.name = comp - node_parent = self.create_node(parent=node_parent, data=t, - tooltip=tt, icon_map=self.icon_state_map) - child_map[(comp,tag.category)] = node_parent + node_parent = self.create_node(parent=node_parent, + data=t, is_gst=is_gst, tooltip=tt, + icon_map=self.icon_state_map) + child_map[(comp, child_key)] = node_parent # Correct the average rating for the node total = count = 0