diff --git a/nw/core/index.py b/nw/core/index.py index cb0ac376..d56429d3 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -51,18 +51,16 @@ class NWIndex(): self.indexBroken = False # Indices - self.tagIndex = None - self.refIndex = None - self.novelIndex = None - self.noteIndex = None - self.textCounts = None + self.tagIndex = {} + self.refIndex = {} + self.novelIndex = {} + self.noteIndex = {} + self.textCounts = {} # TimeStamps - self.timeNovel = 0 - self.timeNote = 0 - self.timeIndex = 0 - - self.clearIndex() + self._timeNovel = 0 + self._timeNotes = 0 + self._timeIndex = 0 return @@ -78,9 +76,9 @@ class NWIndex(): self.novelIndex = {} self.noteIndex = {} self.textCounts = {} - self.timeNovel = 0 - self.timeNote = 0 - self.timeIndex = 0 + self._timeNovel = 0 + self._timeNotes = 0 + self._timeIndex = 0 return def deleteHandle(self, tHandle): @@ -123,6 +121,21 @@ class NWIndex(): return True + def novelChangedSince(self, checkTime): + """Check if the novel index has changed since a given time. + """ + return self._timeNovel > checkTime + + def notesChangedSince(self, checkTime): + """Check if the notes index has changed since a given time. + """ + return self._timeNotes > checkTime + + def indexChangedSince(self, checkTime): + """Check if the index has changed since a given time. + """ + return self._timeIndex > checkTime + ## # Load and Save Index to/from File ## @@ -155,9 +168,9 @@ class NWIndex(): self.textCounts = theData["textCounts"] nowTime = round(time()) - self.timeNovel = nowTime - self.timeNote = nowTime - self.timeIndex = nowTime + self._timeNovel = nowTime + self._timeNotes = nowTime + self._timeIndex = nowTime self.checkIndex() @@ -334,11 +347,11 @@ class NWIndex(): # Update timestamps for index changes nowTime = round(time()) - self.timeIndex = nowTime + self._timeIndex = nowTime if isNovel: - self.timeNovel = nowTime + self._timeNovel = nowTime else: - self.timeNote = nowTime + self._timeNotes = nowTime return True diff --git a/nw/core/project.py b/nw/core/project.py index 1af00534..f14894cc 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1025,7 +1025,7 @@ class NWProject(): by drag-and-drop. Forwarded to the NWTree class. """ if len(self.projTree) != len(newOrder): - logger.warning("Size of new and old tree order do not match") + logger.warning("Sizes of new and old tree order do not match") self.projTree.setOrder(newOrder) self.setProjectChanged(True) return True @@ -1341,7 +1341,7 @@ class NWProject(): if oLayout is None: oLayout = nwItemLayout.NOTE - if oParent is None or not self.projTree.handleExists(oParent): + if oParent is None or oParent not in self.projTree: oParent = self.projTree.findRoot(oClass) if oParent is None: oParent = self.projTree.findRoot(nwItemClass.NOVEL) diff --git a/nw/core/tree.py b/nw/core/tree.py index 0c59fd7f..43678b73 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -294,11 +294,6 @@ class NWTree(): tTree.append(tHandle) return tTree - def handleExists(self, tHandle): - """Check if a handle exists in the project. - """ - return tHandle in self._treeOrder - ## # Setters ## diff --git a/nw/gui/noveltree.py b/nw/gui/noveltree.py index bbb3948f..163889d9 100644 --- a/nw/gui/noveltree.py +++ b/nw/gui/noveltree.py @@ -54,9 +54,9 @@ class GuiNovelTree(QTreeWidget): self.theProject = theParent.theProject self.theIndex = theParent.theIndex - # Tree State - self.lastBuild = 0 - self.treeMap = {} + # Internal Variables + self._treeMap = {} + self._lastBuild = 0 # Build GUI iPx = self.theTheme.baseIconSize @@ -124,14 +124,17 @@ class GuiNovelTree(QTreeWidget): """Clear the GUI content and the related maps. """ self.clear() - self.treeMap = {} + self._treeMap = {} + self._lastBuild = 0 return def refreshTree(self, overRide=False): """Called whenever the Novel tab is activated. """ - if self.lastBuild >= self.theIndex.timeNovel: - logger.verbose("Novel tree more recent than the novel index: not updating") + treeChanged = self.theParent.treeView.changedSince(self._lastBuild) + indexChanged = self.theIndex.novelChangedSince(self._lastBuild) + if not (treeChanged or indexChanged): + logger.verbose("No changes made to the novel") return selItem = self.selectedItems() @@ -139,10 +142,11 @@ class GuiNovelTree(QTreeWidget): if selItem: titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2] + self.theParent.treeView.flushTreeOrder() self._populateTree() - if titleKey is not None and titleKey in self.treeMap: - self.treeMap[titleKey].setSelected(True) + if titleKey is not None and titleKey in self._treeMap: + self._treeMap[titleKey].setSelected(True) return @@ -233,7 +237,7 @@ class GuiNovelTree(QTreeWidget): def _populateTree(self): """Build the tree based on the project index. """ - self.clear() + self.clearTree() for titleKey in self.theIndex.getNovelStructure(skipExcluded=True): @@ -250,7 +254,7 @@ class GuiNovelTree(QTreeWidget): tLevel = self.theIndex.novelIndex[tHandle][sTitle]["level"] tItem = self._createTreeItem(tHandle, sTitle, tLevel, titleKey) - self.treeMap[titleKey] = tItem + self._treeMap[titleKey] = tItem if tLevel == "H1": currTitle = tItem @@ -284,7 +288,7 @@ class GuiNovelTree(QTreeWidget): tItem.setExpanded(True) - self.lastBuild = time() + self._lastBuild = time() return diff --git a/nw/gui/outline.py b/nw/gui/outline.py index 75aa9dc2..cb259565 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -179,11 +179,8 @@ class GuiOutline(QTreeWidget): # If the novel index has changed since the tree was last built, # we rebuild the tree from the updated index. - lastChange = self.theParent.theIndex.timeNovel - logger.verbose("Last outline build: %.3f" % self.lastBuild) - logger.verbose("Novel index change: %.3f" % lastChange) - - doBuild = lastChange > self.lastBuild and self.theProject.autoOutline + idxChanged = self.theParent.theIndex.novelChangedSince(self.lastBuild) + doBuild = idxChanged and self.theProject.autoOutline if doBuild or overRide: logger.debug("Rebuilding Project Outline") self._populateTree() diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index bc5bbbe2..a74493ff 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -29,6 +29,8 @@ import nw import logging +from time import time + from PyQt5.QtCore import Qt, QSize from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( @@ -60,22 +62,27 @@ class GuiProjectTree(QTreeWidget): self.theProject = theParent.theProject self.theIndex = theParent.theIndex - # Tree Settings - self.theMap = {} - self.treeChanged = False + # Internal Variables + self._treeMap = {} + self._treeChanged = False + self._timeChanged = 0 + ## + # Build GUI + ## + + # Context Menu self.ctxMenu = GuiProjectTreeMenu(self) - self.clearTree() + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._rightClickMenu) - # Build GUI + # Tree Settings iPx = self.theTheme.baseIconSize self.setIconSize(QSize(iPx, iPx)) self.setExpandsOnDoubleClick(True) self.setIndentation(iPx) self.setColumnCount(4) self.setHeaderLabels(["Label", "Words", "Inc", "Flags"]) - self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self._rightClickMenu) treeHeadItem = self.headerItem() treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) @@ -102,7 +109,7 @@ class GuiProjectTree(QTreeWidget): # Set Multiple Selection by CTRL # Disabled for now, until the merge files option has been added # self.setSelectionMode(QAbstractItemView.ExtendedSelection) - # self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setSelectionBehavior(QAbstractItemView.SelectRows) # Get user's column width preferences for NAME and COUNT treeColWidth = self.mainConf.getTreeColWidths() @@ -116,10 +123,11 @@ class GuiProjectTree(QTreeWidget): # Set custom settings self.initTree() - logger.debug("GuiProjectTree initialisation complete") + # Internal Function Mapping + self.makeAlert = self.theParent.makeAlert + self.askQuestion = self.theParent.askQuestion - # Internal Mapping - self.makeAlert = self.theParent.makeAlert + logger.debug("GuiProjectTree initialisation complete") return @@ -147,8 +155,9 @@ class GuiProjectTree(QTreeWidget): """Clear the GUI content and the related map. """ self.clear() - self.theMap = {} - self.treeChanged = False + self._treeMap = {} + self._treeChanged = False + self._timeChanged = 0 return def newTreeItem(self, itemType, itemClass): @@ -274,8 +283,8 @@ class GuiProjectTree(QTreeWidget): return False pHandle = nwItem.itemParent - if pHandle is not None and pHandle in self.theMap: - self.theMap[pHandle].setExpanded(True) + if pHandle is not None and pHandle in self._treeMap: + self._treeMap[pHandle].setExpanded(True) self.clearSelection() trItem.setSelected(True) return True @@ -338,7 +347,7 @@ class GuiProjectTree(QTreeWidget): """Calls saveTreeOrder if there are unsaved changes, otherwise does nothing. """ - if self.treeChanged: + if self._treeChanged: logger.verbose("Flushing project tree to project class") self.saveTreeOrder() self._setTreeChanged(False) @@ -391,7 +400,7 @@ class GuiProjectTree(QTreeWidget): self.makeAlert("The Trash folder is already empty.", nwAlert.INFO) return False - msgYes = self.theParent.askQuestion( + msgYes = self.askQuestion( "Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash ) if not msgYes: @@ -446,7 +455,7 @@ class GuiProjectTree(QTreeWidget): # user if they want to permanently delete the file. doPermanent = False if not alreadyAsked: - msgYes = self.theParent.askQuestion( + msgYes = self.askQuestion( "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName ) if msgYes: @@ -474,7 +483,7 @@ class GuiProjectTree(QTreeWidget): # move it there. doTrash = False if askForTrash: - msgYes = self.theParent.askQuestion( + msgYes = self.askQuestion( "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName ) if msgYes: @@ -533,7 +542,9 @@ class GuiProjectTree(QTreeWidget): return True def setTreeItemValues(self, tHandle): - """Set the name and flag values for a tree item. + """Set the name and flag values for a tree item from a handle in + the project tree. Does not trigger a tree change as the data is + already coming from the project tree. """ trItem = self._getTreeItem(tHandle) nwItem = self.theProject.projTree[tHandle] @@ -622,9 +633,9 @@ class GuiProjectTree(QTreeWidget): sent first. """ logger.debug("Building the project tree ...") - self.clear() - iCount = 0 + self.clearTree() + iCount = 0 for nwItem in self.theProject.getProjectItems(): iCount += 1 self._addTreeItem(nwItem) @@ -642,21 +653,10 @@ class GuiProjectTree(QTreeWidget): return None - def getSelectedHandles(self): - """Return a list of all currently selected item handles. - """ - selItems = self.selectedItems() - selHandles = [] - for n in range(len(selItems)): - if isinstance(selItems[n], QTreeWidgetItem): - selHandles.append(selItems[n].data(self.C_NAME, Qt.UserRole)) - - return selHandles - def setSelectedHandle(self, tHandle, doScroll=False): """Set a specific handle as the selected item. """ - if tHandle not in self.theMap: + if tHandle not in self._treeMap: return False tItem = self._getTreeItem(tHandle) @@ -664,7 +664,7 @@ class GuiProjectTree(QTreeWidget): return False self.clearSelection() - self.theMap[tHandle].setSelected(True) + self._treeMap[tHandle].setSelected(True) selItems = self.selectedIndexes() if selItems and doScroll: @@ -672,6 +672,11 @@ class GuiProjectTree(QTreeWidget): return True + def changedSince(self, checkTime): + """Check if the tree has changed since a given time. + """ + return self._timeChanged > checkTime + ## # Slots ## @@ -797,7 +802,7 @@ class GuiProjectTree(QTreeWidget): def _getTreeItem(self, tHandle): """Returns the QTreeWidgetItem of a given item handle. """ - return self.theMap.get(tHandle, None) + return self._treeMap.get(tHandle, None) def _scanChildren(self, theList, theItem, theIndex): """This is a recursive function returning all items in a tree @@ -834,7 +839,7 @@ class GuiProjectTree(QTreeWidget): newItem.setData(self.C_NAME, Qt.UserRole, tHandle) newItem.setData(self.C_COUNT, Qt.UserRole, 0) - self.theMap[tHandle] = newItem + self._treeMap[tHandle] = newItem if pHandle is None: if nwItem.itemType == nwItemType.ROOT: self.addTopLevelItem(newItem) @@ -845,20 +850,20 @@ class GuiProjectTree(QTreeWidget): self.makeAlert( "There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR ) - del self.theMap[tHandle] + del self._treeMap[tHandle] return None else: byIndex = -1 - if nHandle is not None and nHandle in self.theMap: + if nHandle is not None and nHandle in self._treeMap: try: - byIndex = self.theMap[pHandle].indexOfChild(self.theMap[nHandle]) + byIndex = self._treeMap[pHandle].indexOfChild(self._treeMap[nHandle]) except Exception: logger.error("Failed to get index of item with handle %s" % nHandle) if byIndex >= 0: - self.theMap[pHandle].insertChild(byIndex+1, newItem) + self._treeMap[pHandle].insertChild(byIndex+1, newItem) else: - self.theMap[pHandle].addChild(newItem) + self._treeMap[pHandle].addChild(newItem) self.propagateCount(tHandle, nwItem.wordCount) self.setTreeItemValues(tHandle) @@ -919,8 +924,9 @@ class GuiProjectTree(QTreeWidget): def _setTreeChanged(self, theState): """Set the tree change flag, and propagate to the project. """ - self.treeChanged = theState + self._treeChanged = theState if theState: + self._timeChanged = time() self.theProject.setProjectChanged(True) return diff --git a/nw/guimain.py b/nw/guimain.py index 18ebfd4e..56185e89 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -761,9 +761,7 @@ class GuiMain(QMainWindow): """ self._makeStatusIcons() self._makeImportIcons() - self.treeView.clearTree() self.treeView.buildTree() - self.novelView.clearTree() self.novelView.refreshTree() return diff --git a/tests/test_core_index.py b/tests/test_core_index.py index ad87c7f3..8fd22d73 100644 --- a/tests/test_core_index.py +++ b/tests/test_core_index.py @@ -205,6 +205,10 @@ def testCoreIndex_CheckThese(nwMinimal, dummyGUI): nItem = theProject.projTree[nHandle] cItem = theProject.projTree[cHandle] + assert not theIndex.novelChangedSince(0) + assert not theIndex.notesChangedSince(0) + assert not theIndex.indexChangedSince(0) + assert theIndex.scanText(cHandle, ( "# Jane Smith\n" "@tag: Jane" @@ -216,6 +220,10 @@ def testCoreIndex_CheckThese(nwMinimal, dummyGUI): assert theIndex.tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]} assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" + assert theIndex.novelChangedSince(0) + assert theIndex.notesChangedSince(0) + assert theIndex.indexChangedSince(0) + assert theIndex.checkThese([], cItem) == [] assert theIndex.checkThese(["@tag", "Jane"], cItem) == [True, True] assert theIndex.checkThese(["@tag", "John"], cItem) == [True, True]