diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 9c080c96..5ede17f1 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -569,16 +569,6 @@ class GuiDocEditor(QTextEdit): return - def updateDocInfo(self, tHandle): - """Called when an item label is changed to check if the document - title bar needs updating, - """ - if tHandle == self._docHandle: - self.docHeader.setTitleFromHandle(self._docHandle) - self.docFooter.updateInfo() - self.updateDocMargins() - return - ## # Properties ## @@ -1068,7 +1058,22 @@ class GuiDocEditor(QTextEdit): return ## - # Slots + # Public Slots + ## + + @pyqtSlot(str) + def updateDocInfo(self, tHandle): + """Called when an item label is changed to check if the document + title bar needs updating, + """ + if tHandle == self._docHandle: + self.docHeader.setTitleFromHandle(self._docHandle) + self.docFooter.updateInfo() + self.updateDocMargins() + return + + ## + # Private Slots ## @pyqtSlot(int, int, int) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 313cce0d..89ae8ea2 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -322,15 +322,6 @@ class GuiDocViewer(QTextBrowser): return - def updateDocInfo(self, tHandle): - """Called when an item label is changed to check if the document - title bar needs updating, - """ - if tHandle == self._docHandle: - self.docHeader.setTitleFromHandle(self._docHandle) - self.updateDocMargins() - return - ## # Properties ## @@ -389,7 +380,21 @@ class GuiDocViewer(QTextBrowser): return 0 ## - # Slots + # Public Slots + ## + + @pyqtSlot(str) + def updateDocInfo(self, tHandle): + """Called when an item label is changed to check if the document + title bar needs updating, + """ + if tHandle == self._docHandle: + self.docHeader.setTitleFromHandle(self._docHandle) + self.updateDocMargins() + return + + ## + # Private Slots ## @pyqtSlot("QUrl") diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 89a38b76..643479a4 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -220,6 +220,11 @@ class GuiItemDetails(QWidget): """ self.updateViewBox(self._itemHandle) + ## + # Public Slots + ## + + @pyqtSlot(str) def updateViewBox(self, tHandle): """Populate the details box from a given handle. """ @@ -290,10 +295,6 @@ class GuiItemDetails(QWidget): return - ## - # Slots - ## - @pyqtSlot(str, int, int, int) def doUpdateCounts(self, tHandle, cC, wC, pC): """Update the counts if the handle is the same as the one we're diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index e5ad816e..9ac2ea2b 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -146,7 +146,6 @@ 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: diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index ea919615..20464485 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -126,17 +126,21 @@ class GuiOutline(QWidget): return self.outlineView.setFocus() ## - # Slots + # Public Slots ## - @pyqtSlot() - def projectUpdated(self): - """Should be called whenever the number of root folders change. + @pyqtSlot(str) + def updateRootItem(self, tHandle): + """Should be called whenever a root folders changes. """ self.outlineBar.populateNovelList() self.outlineData.updateClasses() return + ## + # Private Slots + ## + @pyqtSlot() def _updateMenuColumns(self): """Trigger an update of the toggled state of the column menu diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 833b8b87..b4f99421 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -32,11 +32,13 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame + QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, + QDialog ) from novelwriter.core import NWDoc from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert +from novelwriter.dialogs.itemeditor import GuiItemEditor logger = logging.getLogger(__name__) @@ -48,10 +50,10 @@ class GuiProjectTree(QTreeWidget): C_EXPORT = 2 C_STATUS = 3 - novelItemChanged = pyqtSignal() - noteItemChanged = pyqtSignal() + treeItemChanged = pyqtSignal(str) + novelItemChanged = pyqtSignal(str) + rootFolderChanged = pyqtSignal(str) wordCountsChanged = pyqtSignal() - rootFoldersChanged = pyqtSignal() def __init__(self, theParent): QTreeWidget.__init__(self, theParent) @@ -64,10 +66,9 @@ class GuiProjectTree(QTreeWidget): self.theProject = theParent.theProject # Internal Variables - self._treeMap = {} - self._treeChanged = False + self._treeMap = {} + self._lastMove = {} self._timeChanged = 0 - self._lastMove = {} ## # Build GUI @@ -157,15 +158,15 @@ class GuiProjectTree(QTreeWidget): """ self.clear() self._treeMap = {} - self._treeChanged = False + self._lastMove = {} self._timeChanged = 0 return def newTreeItem(self, itemType, itemClass=None): """Add new item to the tree, with a given itemType (and - itemClass if Root), and attach it to the selected handle. Also make - sure the item is added in a place it can be added, and that other - meta data is set correctly to ensure a valid project tree. + itemClass if Root), and attach it to the selected handle. Also + make sure the item is added in a place it can be added, and that + other meta data is set correctly to ensure a valid project tree. """ if not self.theParent.hasProject: logger.error("No project open") @@ -177,7 +178,6 @@ class GuiProjectTree(QTreeWidget): if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass): tHandle = self.theProject.newRoot(itemClass) - self.rootFoldersChanged.emit() elif itemType in (nwItemType.FILE, nwItemType.FOLDER): @@ -223,9 +223,9 @@ class GuiProjectTree(QTreeWidget): # Add the new item to the tree self.revealNewTreeItem(tHandle, nHandle) self.theParent.editItem(tHandle) - nwItem = self.theProject.tree[tHandle] - # If this is a folder, return here + # Handle new file creation + nwItem = self.theProject.tree[tHandle] if nwItem.itemType != nwItemType.FILE: return True @@ -268,7 +268,7 @@ class GuiProjectTree(QTreeWidget): if pHandle is not None and pHandle in self._treeMap: self._treeMap[pHandle].setExpanded(True) - self._emitItemChange(tHandle) + self._alertTreeChange(tHandle=tHandle, flush=True) self.clearSelection() trItem.setSelected(True) @@ -310,10 +310,31 @@ class GuiProjectTree(QTreeWidget): pItem.insertChild(nIndex, cItem) self._recordLastMove(cItem, pItem, tIndex) + self._alertTreeChange(tHandle=tHandle, flush=True) self.clearSelection() cItem.setSelected(True) - self._setTreeChanged(True) - self._emitItemChange(tHandle) + + return True + + def editTreeItem(self, tHandle=None): + """Open the edit item dialog. + """ + if tHandle is None: + logger.warning("No item selected") + return False + + tItem = self.theProject.tree[tHandle] + if tItem is None: + return False + if tItem.itemType == nwItemType.NO_TYPE: + return False + + logger.verbose("Requesting change to item '%s'", tHandle) + dlgProj = GuiItemEditor(self, tHandle) + dlgProj.exec_() + if dlgProj.result() == QDialog.Accepted: + self.setTreeItemValues(tHandle) + self._alertTreeChange(tHandle=tHandle, flush=False) return True @@ -330,16 +351,6 @@ class GuiProjectTree(QTreeWidget): self.theProject.setTreeOrder(theList) return True - def flushTreeOrder(self): - """Calls saveTreeOrder if there are unsaved changes, otherwise - does nothing. - """ - if self._treeChanged: - logger.verbose("Flushing project tree to project class") - self.saveTreeOrder() - self._setTreeChanged(False) - return - def getTreeFromHandle(self, tHandle): """Recursively return all the children items starting from a given item handle. @@ -411,7 +422,7 @@ class GuiProjectTree(QTreeWidget): self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True) if nTrash > 0: - self._setTreeChanged(True) + self._alertTreeChange(tHandle=trashHandle, flush=True) return True @@ -445,6 +456,7 @@ class GuiProjectTree(QTreeWidget): return False wCount = self._getItemWordCount(tHandle) + autoFlush = not bulkAction if nwItemS.itemType == nwItemType.ROOT: # Only an empty ROOT folder can be deleted logger.debug("User requested a root folder '%s' deleted", tHandle) @@ -452,8 +464,7 @@ class GuiProjectTree(QTreeWidget): if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) self._deleteTreeItem(tHandle) - self._setTreeChanged(True) - self.rootFoldersChanged.emit() + self._alertTreeChange(tHandle=tHandle, flush=True) else: self.theParent.makeAlert(self.tr( "Cannot delete root folder. It is not empty. " @@ -469,7 +480,7 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemP.takeChild(tIndex) self._deleteTreeItem(tHandle) - self._setTreeChanged(True) + self._alertTreeChange(tHandle=tHandle, flush=autoFlush) else: # A populated FOLDER or a FILE requires confirmtation @@ -505,7 +516,7 @@ class GuiProjectTree(QTreeWidget): self.theParent.closeDocument() self._deleteTreeItem(dHandle) - self._setTreeChanged(True) + self._alertTreeChange(tHandle=tHandle, flush=autoFlush) self.wordCountsChanged.emit() else: @@ -524,7 +535,7 @@ class GuiProjectTree(QTreeWidget): trItemT.addChild(trItemC) self._postItemMove(tHandle, wCount) self._recordLastMove(trItemS, trItemP, tIndex) - self._setTreeChanged(True) + self._alertTreeChange(tHandle=tHandle, flush=autoFlush) return True @@ -660,6 +671,7 @@ class GuiProjectTree(QTreeWidget): dstItem.insertChild(dstIndex, movItem) self._postItemMove(sHandle, wCount) + self._alertTreeChange(tHandle=sHandle, flush=True) self.clearSelection() movItem.setSelected(True) @@ -788,6 +800,7 @@ class GuiProjectTree(QTreeWidget): QTreeWidget.dropEvent(self, theEvent) self._postItemMove(sHandle, wCount) self._recordLastMove(sItem, pItem, pIndex) + self._alertTreeChange(tHandle=sHandle, flush=True) sItem.setExpanded(isExpanded) return @@ -829,8 +842,6 @@ class GuiProjectTree(QTreeWidget): # Trigger dependent updates self.propagateCount(tHandle, wCount) - self._setTreeChanged(True) - self._emitItemChange(tHandle) return True @@ -931,8 +942,6 @@ class GuiProjectTree(QTreeWidget): self.setTreeItemValues(tHandle) newItem.setExpanded(nwItem.isExpanded) - self._setTreeChanged(True) - return newItem def _addTrashRoot(self): @@ -945,33 +954,34 @@ class GuiProjectTree(QTreeWidget): trItem = self._getTreeItem(trashHandle) if trItem is None: - trItem = self._addTreeItem( - self.theProject.tree[trashHandle] - ) + trItem = self._addTreeItem(self.theProject.tree[trashHandle]) if trItem is not None: trItem.setExpanded(True) - self._setTreeChanged(True) + self._alertTreeChange(tHandle=trashHandle, flush=True) return trItem - def _setTreeChanged(self, theState): - """Set the tree change flag, and propagate to the project. + def _alertTreeChange(self, tHandle=None, flush=True): + """Update information on tree change state, and emit necessary + signals. """ - self._treeChanged = theState - if theState: - self._timeChanged = time() - self.theProject.setProjectChanged(True) - return + self._timeChanged = time() + self.theProject.setProjectChanged(True) + if flush: + self.saveTreeOrder() + + tItem = self.theProject.tree[tHandle] + if tItem is None: + return + + itemType = tItem.itemType + if itemType == nwItemType.ROOT: + self.rootFolderChanged.emit(tHandle) + elif itemType == nwItemType.FILE and tItem.isNovelLike(): + self.novelItemChanged.emit(tHandle) + + self.treeItemChanged.emit(tHandle) - def _emitItemChange(self, tHandle): - """Emit an item change signal for a given handle. - """ - if self.theProject.tree.checkType(tHandle, nwItemType.FILE): - nwItem = self.theProject.tree[tHandle] - if nwItem.isNovelLike(): - self.novelItemChanged.emit() - else: - self.noteItemChanged.emit() return def _recordLastMove(self, srcItem, parItem, parIndex): diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ad6dd742..c2051c6e 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -44,9 +44,8 @@ from novelwriter.gui import ( GuiViewsBar ) from novelwriter.dialogs import ( - GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences, - GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, GuiUpdates, - GuiWordList + GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails, + GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList ) from novelwriter.tools import ( GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats @@ -123,7 +122,10 @@ class GuiMain(QMainWindow): self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated) + self.treeView.treeItemChanged.connect(self.docEditor.updateDocInfo) + self.treeView.treeItemChanged.connect(self.docViewer.updateDocInfo) + self.treeView.treeItemChanged.connect(self.treeMeta.updateViewBox) + self.treeView.rootFolderChanged.connect(self.projView.updateRootItem) self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) @@ -357,7 +359,7 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.saveProject() self.docEditor.setDictionaries() - self.projView.projectUpdated() + self.projView.updateRootItem(None) self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -504,7 +506,7 @@ class GuiMain(QMainWindow): self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.statusBar.setRefTime(self.theProject.projOpened) - self.projView.projectUpdated() + self.projView.updateRootItem(None) self._updateStatusWordCount() # Restore previously open documents, if any @@ -596,7 +598,6 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - self.treeView.flushTreeOrder() nHandle = None # The next handle after tHandle fHandle = None # The first file handle we encounter foundIt = False # We've found tHandle, pick the next we see @@ -813,27 +814,10 @@ class GuiMain(QMainWindow): tHandle = self.docEditor.docHandle() else: tHandle = self.treeView.getSelectedHandle() + if tHandle: + return self.treeView.editTreeItem(tHandle) - if tHandle is None: - logger.warning("No item selected") - return False - - tItem = self.theProject.tree[tHandle] - if tItem is None: - return False - if tItem.itemType == nwItemType.NO_TYPE: - return False - - logger.verbose("Requesting change to item '%s'", tHandle) - dlgProj = GuiItemEditor(self, tHandle) - dlgProj.exec_() - if dlgProj.result() == QDialog.Accepted: - self.treeView.setTreeItemValues(tHandle) - self.treeMeta.updateViewBox(tHandle) - self.docEditor.updateDocInfo(tHandle) - self.docViewer.updateDocInfo(tHandle) - - return True + return False def rebuildTrees(self): """Rebuild the project tree. @@ -966,8 +950,6 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - self.treeView.flushTreeOrder() - dlgDetails = getGuiItem("GuiProjectDetails") if dlgDetails is None: dlgDetails = GuiProjectDetails(self) @@ -1583,7 +1565,6 @@ class GuiMain(QMainWindow): if self.mainStack.currentIndex() == self.idxOutlineView: logger.verbose("Novel tree changed while Outline tab active") if self.hasProject: - self.treeView.flushTreeOrder() self.projView.refreshView(novelChanged=True) return diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 06849ee9..66f203bd 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -709,7 +709,6 @@ class GuiBuildNovel(QDialog): bldObj.initDocument() # Make sure the project and document is up to date - self.theParent.treeView.flushTreeOrder() self.theParent.saveDocument() self.buildProgress.setMaximum(len(self.theProject.tree)) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 39e0b4dc..96e04da4 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -246,17 +246,14 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Move novel folder up assert nwTree.moveTreeItem(-1) is False - nwTree.flushTreeOrder() assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder down assert nwTree.moveTreeItem(1) is True - nwTree.flushTreeOrder() assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1 # Move novel folder up again assert nwTree.moveTreeItem(-1) is True - nwTree.flushTreeOrder() assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Clean up @@ -432,10 +429,8 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert nwTree.emptyTrash() is False # Empty the trash proper - nwTree._setTreeChanged(False) assert nwTree.emptyTrash() is True assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] - assert nwTree._treeChanged is True # Try to delete a file, but block the underlying deletion of the file on disk assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))