diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index c3f17105dc..730c91d37f 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -149,7 +149,8 @@ class TagsView(QTreeView): # {{{ hidden_categories=self.hidden_categories, search_restriction=None, drag_drop_finished=self.drag_drop_finished, - collapse_model=self.collapse_model) + collapse_model=self.collapse_model, + state_map={}) self.pane_is_visible = True # because TagsModel.init did a recount self.sort_by = sort_by self.tag_match = tag_match @@ -173,6 +174,7 @@ class TagsView(QTreeView): # {{{ self.made_connections = True self.refresh_signal_processed = True db.add_listener(self.database_changed) + self.expanded.connect(self.item_expanded) def database_changed(self, event, ids): if self.refresh_signal_processed: @@ -541,6 +543,10 @@ class TagsView(QTreeView): # {{{ return self.isExpanded(idx) def recount(self, *args): + ''' + Rebuild the category tree, expand any categories that were expanded, + reset the search states, and reselect the current node. + ''' if self.disable_recounting or not self.pane_is_visible: return self.refresh_signal_processed = True @@ -548,18 +554,23 @@ class TagsView(QTreeView): # {{{ if not ci.isValid(): ci = self.indexAt(QPoint(10, 10)) path = self.model().path_for_index(ci) if self.is_visible(ci) else None - try: - if not self.model().refresh(): # categories changed! - self.set_new_model() - path = None - except: #Database connection could be closed if an integrity check is happening - pass + expanded_categories, state_map = self.model().get_state() + self.set_new_model(state_map=state_map) + for category in expanded_categories: + self.expand(self.model().index_for_category(category)) self._model.show_item_at_path(path) - # If the number of user categories changed, if custom columns have come or - # gone, or if columns have been hidden or restored, we must rebuild the - # model. Reason: it is much easier than reconstructing the browser tree. - def set_new_model(self, filter_categories_by=None): + def item_expanded(self, idx): + ''' + Called by the expanded signal + ''' + self.setCurrentIndex(idx) + + def set_new_model(self, filter_categories_by=None, state_map={}): + ''' + There are cases where we need to rebuild the category tree without + attempting to reposition the current node. + ''' try: old = getattr(self, '_model', None) if old is not None: @@ -569,7 +580,8 @@ class TagsView(QTreeView): # {{{ search_restriction=self.search_restriction, drag_drop_finished=self.drag_drop_finished, filter_categories_by=filter_categories_by, - collapse_model=self.collapse_model) + collapse_model=self.collapse_model, + state_map=state_map) self.setModel(self._model) except: # The DB must be gone. Set the model to None and hope that someone @@ -752,7 +764,8 @@ class TagsModel(QAbstractItemModel): # {{{ def __init__(self, db, parent, hidden_categories=None, search_restriction=None, drag_drop_finished=None, - filter_categories_by=None, collapse_model='disable'): + filter_categories_by=None, collapse_model='disable', + state_map={}): QAbstractItemModel.__init__(self, parent) # must do this here because 'QPixmap: Must construct a QApplication @@ -776,10 +789,10 @@ 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. Note - # that get_node_tree can indirectly change the user_categories dict. + # Note that _get_category_nodes can indirectly change the + # user_categories dict. - data = self.get_node_tree(config['sort_tags_by']) + data = self._get_category_nodes(config['sort_tags_by']) gst = db.prefs.get('grouped_search_terms', {}) self.root_item = TagTreeItem(icon_map=self.icon_state_map) self.category_nodes = [] @@ -844,7 +857,7 @@ class TagsModel(QAbstractItemModel): # {{{ category_node_map[key] = node last_category_node = node self.category_nodes.append(node) - self.refresh(data=data) + self._create_node_tree(data, state_map) def break_cycles(self): self.root_item.break_cycles() @@ -1121,8 +1134,10 @@ class TagsModel(QAbstractItemModel): # {{{ def set_search_restriction(self, s): self.search_restriction = s - def get_node_tree(self, sort): - old_row_map_len = len(self.row_map) + def _get_category_nodes(self, sort): + ''' + Called by __init__. Do not directly call this method. + ''' self.row_map = [] self.categories = {} @@ -1176,19 +1191,27 @@ class TagsModel(QAbstractItemModel): # {{{ if category in data: # The search category can come and go self.row_map.append(category) self.categories[category] = tb_categories[category]['name'] - - if old_row_map_len != 0 and old_row_map_len != len(self.row_map): - # A category has been added or removed. We must force a rebuild of - # the model - return None return data def refresh(self, data=None): + ''' + Here to trap usages of refresh in the old architecture. Can eventually + be removed. + ''' + print 'TagsModel: refresh called!' + traceback.print_stack() + return False + + def _create_node_tree(self, data, state_map): + ''' + Called by __init__. Do not directly call this method. + ''' sort_by = config['sort_tags_by'] + if data is None: - data = self.get_node_tree(sort_by) # get category data - if data is None: - return False + print '_create_node_tree: no data!' + traceback.print_stack() + return collapse = gprefs['tags_browser_collapse_at'] collapse_model = self.collapse_model @@ -1354,29 +1377,23 @@ class TagsModel(QAbstractItemModel): # {{{ # }}} for category in self.category_nodes: - if len(category.children) > 0: - child_map = category.children - 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)) - # 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) - category.children = ctags - for i in range(start, len(child_map)): - child_map[i].break_cycles() - child_map = None - self.endRemoveRows() - else: - state_map = {} + process_one_node(category, state_map.get(category.py_name, {})) - process_one_node(category, state_map) - return True + def get_state(self): + state_map = {} + expanded_categories = [] + for row, category in enumerate(self.category_nodes): + if self.tags_view.isExpanded(self.index(row, 0, QModelIndex())): + expanded_categories.append(category.py_name) + 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[category.py_name] = dict(izip(names, states)) + return expanded_categories, state_map + + def index_for_category(self, name): + for row, category in enumerate(self.category_nodes): + if category.py_name == name: + return self.index(row, 0, QModelIndex()) def columnCount(self, parent): return 1 @@ -1476,7 +1493,7 @@ class TagsModel(QAbstractItemModel): # {{{ self.tags_view.tag_item_renamed.emit() 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_required.emit() self.show_item_at_path(path) return True @@ -1789,19 +1806,22 @@ class TagsModel(QAbstractItemModel): # {{{ return v return None - def show_item_at_path(self, path, box=False): + def show_item_at_path(self, path, box=False, + position=QTreeView.PositionAtCenter): ''' Scroll the browser and open categories to show the item referenced by path. If possible, the item is placed in the center. If box=True, a box is drawn around the item. ''' if path: - self.show_item_at_index(self.index_for_path(path), box) + self.show_item_at_index(self.index_for_path(path), box=box, + position=position) - def show_item_at_index(self, idx, box=False): + def show_item_at_index(self, idx, box=False, + position=QTreeView.PositionAtCenter): if idx.isValid(): self.tags_view.setCurrentIndex(idx) - self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter) + self.tags_view.scrollTo(idx, position) if box: tag_item = idx.internalPointer() tag_item.boxed = True