From 848027856d6036d38932bb0bea21eaacf8c916bb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:37:03 +0100 Subject: [PATCH] Allow refreshing a novel model node --- novelwriter/core/index.py | 31 ++++++++------ novelwriter/core/novelmodel.py | 78 ++++++++++++++++++++++++++++------ 2 files changed, 84 insertions(+), 25 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 550fe3f6..e3c5517c 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -178,9 +178,9 @@ class Index: return self._rootChange.get(rootHandle, self._indexChange) > float(checkTime) return False - def refreshNovelModel(self, tHandle: str) -> None: + def refreshNovelModel(self, tHandle: str | None) -> None: """Refresh a novel model.""" - if model := self.getNovelModel(tHandle): + if tHandle and (model := self.getNovelModel(tHandle)): logger.info("Refreshing novel model '%s'", tHandle) model.beginResetModel() model.clear() @@ -188,6 +188,19 @@ class Index: model.endResetModel() return + def updateNovelModelData(self, nwItem: NWItem) -> bool: + """Refresh a novel model.""" + if ( + (rHandle := nwItem.itemRoot) + and (model := self._novelModels.get(rHandle)) + and (node := self._itemIndex[nwItem.itemHandle]) + and node.item.isDocumentLayout() + and node.item.isActive + ): + logger.info("Updating novel model data '%s'", nwItem.itemHandle) + return model.refresh(node) + return False + ## # Load and Save Index to/from File ## @@ -285,7 +298,6 @@ class Index: # Keep a record of existing tags, and create a new item entry itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), False) - itemHeadings = self._itemIndex.headingCount(tHandle) self._itemIndex.add(tHandle, tItem) # Run word counter for the whole text @@ -309,10 +321,9 @@ class Index: else: self._scanActive(tHandle, tItem, text, itemTags) - if itemHeadings == self._itemIndex.headingCount(tHandle): - pass - else: - tItem.notifyNovelStructureChange() + if tItem.itemClass == nwItemClass.NOVEL and not blockSignal: + if not self.updateNovelModelData(tItem): + self.refreshNovelModel(tItem.itemRoot) # Update timestamps for index changes nowTime = time() @@ -915,12 +926,6 @@ class ItemIndex: self._items[tHandle] = IndexNode(tHandle, nwItem) return - def headingCount(self, tHandle: str) -> int: - """Return the number of headings in this item.""" - if tHandle in self._items: - return len(self._items[tHandle]) - return 0 - def allItemTags(self, tHandle: str) -> list[str]: """Get all tags set for headings of an item.""" if tHandle in self._items: diff --git a/novelwriter/core/novelmodel.py b/novelwriter/core/novelmodel.py index 8d5a644f..a5f425d8 100644 --- a/novelwriter/core/novelmodel.py +++ b/novelwriter/core/novelmodel.py @@ -30,7 +30,8 @@ from PyQt6.QtGui import QIcon, QPixmap from novelwriter import SHARED from novelwriter.constants import nwStyles -from novelwriter.core.indexdata import IndexNode +from novelwriter.core.indexdata import IndexHeading, IndexNode +from novelwriter.error import logException from novelwriter.types import QtAlignRight logger = logging.getLogger(__name__) @@ -121,15 +122,68 @@ class NovelModel(QAbstractTableModel): handle = node.handle for key, head in node.items(): if key != "T0000": - iLevel = nwStyles.H_LEVEL.get(head.level, 0) - more = self._columns - 1 - data = {} - data[C_FACTOR*0 | R_TEXT] = head.title - data[C_FACTOR*0 | R_ICON] = SHARED.theme.getHeaderDecoration(iLevel) - data[C_FACTOR*1 | R_TEXT] = f"{head.mainCount:n}" - data[C_FACTOR*1 | R_ALIGN] = QtAlignRight - data[C_FACTOR*more | R_ICON] = self._more - data[R_HANDLE] = handle - data[R_KEY] = key - self._rows.append(data) + self._rows.append(self._generateEntry(handle, key, head)) return + + def refresh(self, node: IndexNode) -> bool: + """Refresh an index node.""" + handle = node.handle + current = [] + for i, row in enumerate(self._rows): + if row.get(R_HANDLE) == handle: + current.append(i) + + if current == []: + return False + + cols = self._columns - 1 + + first = current[0] + last = current[-1] + remains = [] + try: + for key, head in node.items(): + if key != "T0000": + if current: + j = current.pop(0) + self._rows[j] = self._generateEntry(handle, key, head) + else: + remains.append((key, head)) + + self.dataChanged.emit(self.createIndex(first, 0), self.createIndex(last, cols)) + + if remains: + self.beginInsertRows(QModelIndex(), last, last + len(remains) - 1) + for k, (key, head) in enumerate(remains, last + 1): + self._rows.insert(k, self._generateEntry(handle, key, head)) + self.endInsertRows() + elif current: + self.beginRemoveRows(QModelIndex(), current[0], current[-1]) + del self._rows[current[0]:current[-1] + 1] + self.endRemoveRows() + + except Exception: + # This is faster than to check for index boundaries. + # We definitely don't want to cause a crash. + logException() + return False + + return True + + ## + # Internal Functions + ## + + def _generateEntry(self, handle: str, key: str, heading: IndexHeading) -> dict: + """Generate a cache entry.""" + iLevel = nwStyles.H_LEVEL.get(heading.level, 0) + more = self._columns - 1 + data = {} + data[C_FACTOR*0 | R_TEXT] = heading.title + data[C_FACTOR*0 | R_ICON] = SHARED.theme.getHeaderDecoration(iLevel) + data[C_FACTOR*1 | R_TEXT] = f"{heading.mainCount:n}" + data[C_FACTOR*1 | R_ALIGN] = QtAlignRight + data[C_FACTOR*more | R_ICON] = self._more + data[R_HANDLE] = handle + data[R_KEY] = key + return data