From 669eabc7e88696a31339d30ea5252a294d96823e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 13:46:54 +0200 Subject: [PATCH 01/12] Handle actions in the project tree internally in the class --- novelwriter/gui/projtree.py | 98 ++++++++++++++++++++++++------------- novelwriter/guimain.py | 83 +++++++++++++------------------ 2 files changed, 99 insertions(+), 82 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index e0a8ef62..4a069f42 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -27,6 +27,7 @@ along with this program. If not, see . import logging import novelwriter +from enum import Enum from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot @@ -37,7 +38,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.core import NWDoc -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert +from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.common import minmax from novelwriter.dialogs.itemeditor import GuiItemEditor @@ -51,11 +52,16 @@ class GuiProjectTree(QTreeWidget): C_EXPORT = 2 C_STATUS = 3 + # Signals triggered when the meta data values of items change treeItemChanged = pyqtSignal(str) novelItemChanged = pyqtSignal(str) rootFolderChanged = pyqtSignal(str) wordCountsChanged = pyqtSignal() + # Signals for user interaction with the project tree + selectedItemChanged = pyqtSignal(str) + openDocumentRequest = pyqtSignal(str, Enum) + def __init__(self, theParent): QTreeWidget.__init__(self, theParent) @@ -127,6 +133,10 @@ class GuiProjectTree(QTreeWidget): # The last column should just auto-scale self.resizeColumnToContents(self.C_STATUS) + # Connect signals + self.itemDoubleClicked.connect(self._treeDoubleClick) + self.itemSelectionChanged.connect(self._treeSelectionChange) + # Set custom settings self.initTree() @@ -223,9 +233,9 @@ class GuiProjectTree(QTreeWidget): if tHandle is None: # pragma: no cover return True - # Add the new item to the tree + # Add the new item to the tree and open the editor dialog self.revealNewTreeItem(tHandle, nHandle) - self.theParent.editItem(tHandle) + self.editTreeItem(tHandle) # Handle new file creation nwItem = self.theProject.tree[tHandle] @@ -249,10 +259,7 @@ class GuiProjectTree(QTreeWidget): pIndex.scanText(tHandle, newText) # Get Word Counts - cC, wC, pC = pIndex.getCounts(tHandle) - nwItem.setCharCount(cC) - nwItem.setWordCount(wC) - nwItem.setParaCount(pC) + _, wC, _ = pIndex.getCounts(tHandle) self.propagateCount(tHandle, wC) self.wordCountsChanged.emit() @@ -280,8 +287,8 @@ class GuiProjectTree(QTreeWidget): return True def moveTreeItem(self, nStep): - """Move an item up or down in the tree, but only if the treeView - has focus. This also applies when the menu is used. + """Move an item up or down in the tree, but only if the project + tree has focus. This also applies when the menu is used. """ if not self.theParent.hasProject: logger.error("No project open") @@ -321,13 +328,9 @@ class GuiProjectTree(QTreeWidget): return True - def editTreeItem(self, tHandle=None): + def editTreeItem(self, tHandle): """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 @@ -357,8 +360,8 @@ class GuiProjectTree(QTreeWidget): return True def getTreeFromHandle(self, tHandle): - """Recursively return all the children items starting from a - given item handle. + """Recursively return all the child items starting from a given + item handle. """ theList = [] theItem = self._getTreeItem(tHandle) @@ -366,14 +369,6 @@ class GuiProjectTree(QTreeWidget): theList = self._scanChildren(theList, theItem, 0) return theList - def toggleExpanded(self, tHandle): - """Expand an item based on its handle. - """ - trItem = self._getTreeItem(tHandle) - if trItem is not None: - trItem.setExpanded(not trItem.isExpanded()) - return - def getColumnSizes(self): """Return the column widths for the tree columns. """ @@ -719,9 +714,52 @@ class GuiProjectTree(QTreeWidget): return self._timeChanged > checkTime ## - # Slots + # Public Solts ## + @pyqtSlot(str, int, int, int) + def doUpdateCounts(self, tHandle, cCount, wCount, pCount): + """Slot for updating the word count of a specific item. + """ + self.propagateCount(tHandle, wCount, countChildren=True) + self.wordCountsChanged.emit() + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _treeSelectionChange(self): + """The user changed which item is selected. + """ + tHandle = self.getSelectedHandle() + if tHandle is not None: + self.selectedItemChanged.emit(tHandle) + return + + @pyqtSlot("QTreeWidgetItem*", int) + def _treeDoubleClick(self, tItem, colNo): + """Capture a double-click event and either request the document + for editing if it is a file, or expand/close the node it is not. + """ + tHandle = self.getSelectedHandle() + if tHandle is None: + return + + tItem = self.theProject.tree[tHandle] + if tItem is None: + return + + if tItem.itemType == nwItemType.FILE: + self.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + else: + trItem = self._getTreeItem(tHandle) + if trItem is not None: + trItem.setExpanded(not trItem.isExpanded()) + + return + @pyqtSlot("QPoint") def _rightClickMenu(self, clickPos): """The user right clicked an element in the project tree, so we @@ -739,14 +777,6 @@ class GuiProjectTree(QTreeWidget): return - @pyqtSlot(str, int, int, int) - def doUpdateCounts(self, tHandle, cCount, wCount, pCount): - """Slot for updating the word count of a specific item. - """ - self.propagateCount(tHandle, wCount, countChildren=True) - self.wordCountsChanged.emit() - return - ## # Events ## @@ -774,7 +804,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.theParent.viewDocument(tHandle) + self.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6a056dca..354edbf4 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -115,28 +115,6 @@ class GuiMain(QMainWindow): self.mainMenu = GuiMainMenu(self) self.viewsBar = GuiViewsBar(self) - # Connect Signals Between Main Elements - self.viewsBar.viewChangeRequested.connect(self._changeView) - - self.treeView.itemSelectionChanged.connect(self._treeSingleClick) - self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) - self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) - self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - 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) - self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts) - self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts) - self.docEditor.loadDocumentTagRequest.connect(self._followTag) - - self.docViewer.loadDocumentTagRequest.connect(self._followTag) - - self.projView.loadDocumentTagRequest.connect(self._followTag) - # Project Tree Stack self.projStack = QStackedWidget() self.projStack.addWidget(self.treeView) @@ -214,6 +192,30 @@ class GuiMain(QMainWindow): self.setStatusBar(self.statusBar) self.addToolBar(Qt.LeftToolBarArea, self.viewsBar) + # Connect Signals + # =============== + + self.viewsBar.viewChangeRequested.connect(self._changeView) + + self.treeView.selectedItemChanged.connect(self.treeMeta.updateViewBox) + self.treeView.openDocumentRequest.connect(self._openDocument) + self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) + self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) + 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) + self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts) + self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts) + self.docEditor.loadDocumentTagRequest.connect(self._followTag) + + self.docViewer.loadDocumentTagRequest.connect(self._followTag) + + self.projView.loadDocumentTagRequest.connect(self._followTag) + # Finalise Initialisation # ======================= @@ -1468,6 +1470,17 @@ class GuiMain(QMainWindow): self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}") return + @pyqtSlot(str, Enum) + def _openDocument(self, tHandle, tMode): + """Handle an open document request. + """ + if tHandle is not None: + if tMode == nwDocMode.EDIT: + self.openDocument(tHandle, changeFocus=False) + elif tMode == nwDocMode.VIEW: + self.viewDocument(tHandle=tHandle) + return + @pyqtSlot(nwView) def _changeView(self, view): """Handle the requested change of view from the GuiViewBar. @@ -1531,32 +1544,6 @@ class GuiMain(QMainWindow): return - @pyqtSlot() - def _treeSingleClick(self): - """Single click on a project tree item just updates the details - panel below the tree. - """ - tHandle = self.treeView.getSelectedHandle() - if tHandle is not None: - self.treeMeta.updateViewBox(tHandle) - return - - @pyqtSlot("QTreeWidgetItem*", int) - def _treeDoubleClick(self, tItem, colNo): - """The user double-clicked an item in the tree. If it is a file, - we open it. Otherwise, we toggle the expanded status. - """ - tHandle = self.treeView.getSelectedHandle() - if tHandle is not None: - tItem = self.theProject.tree[tHandle] - if tItem is None: - return - if tItem.itemType == nwItemType.FILE: - self.openDocument(tHandle, changeFocus=False, doScroll=False) - else: - self.treeView.toggleExpanded(tHandle) - return - @pyqtSlot() def _treeNovelItemChanged(self): """Triggered when there is a change to a novel item in the From b88cdb767727b6868e9b144920f79bbfc04e4069 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 14:11:59 +0200 Subject: [PATCH 02/12] Make the project tree columns fixed --- novelwriter/gui/projtree.py | 47 ++++++++++--------------------------- novelwriter/guimain.py | 1 - 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 4a069f42..c6d077b8 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -34,7 +34,7 @@ from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, - QDialog + QDialog, QHeaderView ) from novelwriter.core import NWDoc @@ -88,28 +88,24 @@ class GuiProjectTree(QTreeWidget): # Tree Settings iPx = self.theTheme.baseIconSize + cMg = self.mainConf.pxInt(6) self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) self.setExpandsOnDoubleClick(False) + self.setHeaderHidden(True) self.setIndentation(iPx) self.setColumnCount(4) - self.setHeaderLabels([ - self.tr("Project Tree"), self.tr("Words"), "", "" - ]) - treeHeadItem = self.headerItem() - treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) - treeHeadItem.setToolTip(self.C_NAME, self.tr("Item label")) - treeHeadItem.setToolTip(self.C_COUNT, self.tr("Word count")) - treeHeadItem.setToolTip(self.C_EXPORT, self.tr("Include in build")) - treeHeadItem.setToolTip(self.C_STATUS, self.tr("Item status")) - - # Let the last column stretch, and set the minimum size to the - # size of the icon as the default Qt font metrics approach fails - # for some fonts like the Ubuntu font. + # Lock the column sizes treeHeader = self.header() - treeHeader.setStretchLastSection(True) - treeHeader.setMinimumSectionSize(iPx + 6) + treeHeader.setStretchLastSection(False) + treeHeader.setMinimumSectionSize(iPx + cMg) + treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.Stretch) + treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_EXPORT, QHeaderView.Fixed) + treeHeader.setSectionResizeMode(self.C_STATUS, QHeaderView.Fixed) + treeHeader.resizeSection(self.C_EXPORT, iPx + cMg) + treeHeader.resizeSection(self.C_STATUS, iPx + cMg) # Allow Move by Drag & Drop self.setDragEnabled(True) @@ -124,15 +120,6 @@ class GuiProjectTree(QTreeWidget): # self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) - # Get user's column width preferences for NAME and COUNT - treeColWidth = self.mainConf.getTreeColWidths() - if len(treeColWidth) <= 4: - for colN, colW in enumerate(treeColWidth): - self.setColumnWidth(colN, colW) - - # The last column should just auto-scale - self.resizeColumnToContents(self.C_STATUS) - # Connect signals self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._treeSelectionChange) @@ -369,16 +356,6 @@ class GuiProjectTree(QTreeWidget): theList = self._scanChildren(theList, theItem, 0) return theList - def getColumnSizes(self): - """Return the column widths for the tree columns. - """ - retVals = [ - self.columnWidth(0), - self.columnWidth(1), - self.columnWidth(2), - ] - return retVals - def emptyTrash(self): """Permanently delete all documents in the Trash folder. This function only asks for confirmation once, and calls the regular diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 354edbf4..7afd1675 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1161,7 +1161,6 @@ class GuiMain(QMainWindow): self.mainConf.setViewPanePos(self.splitView.sizes()) self.mainConf.setShowRefPanel(self.viewMeta.isVisible()) - self.mainConf.setTreeColWidths(self.treeView.getColumnSizes()) self.mainConf.setNovelColWidths(self.novelView.getColumnSizes()) if not self.mainConf.isFullScreen: self.mainConf.setWinSize(self.width(), self.height()) From d07f5dc520162c6228de07f726f5041e47665d02 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 15:50:46 +0200 Subject: [PATCH 03/12] Wrap project tree in an outer widget with a toolbar --- novelwriter/dialogs/preferences.py | 2 +- novelwriter/gui/__init__.py | 4 +- novelwriter/gui/itemdetails.py | 2 +- novelwriter/gui/outline.py | 3 + novelwriter/gui/projtree.py | 185 ++++++++++++++++++---- novelwriter/guimain.py | 20 +-- tests/test_dialogs/test_dlg_docmerge.py | 28 ++-- tests/test_dialogs/test_dlg_docsplit.py | 20 +-- tests/test_dialogs/test_dlg_itemeditor.py | 10 +- tests/test_gui/test_gui_docviewer.py | 6 +- tests/test_gui/test_gui_guimain.py | 49 +++--- tests/test_gui/test_gui_mainmenu.py | 6 +- tests/test_gui/test_gui_projtree.py | 20 +-- 13 files changed, 238 insertions(+), 117 deletions(-) diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index d38a4ab6..755b9198 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -107,7 +107,7 @@ class GuiPreferences(PagedDialog): ), nwAlert.INFO) if refreshTree: - self.theParent.treeView.buildTree() + self.theParent.treeView.populateTree() self._saveWindowSize() self.accept() diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index e3560a99..0ae23024 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -25,7 +25,7 @@ from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.mainmenu import GuiMainMenu from novelwriter.gui.noveltree import GuiNovelTree from novelwriter.gui.outline import GuiOutline -from novelwriter.gui.projtree import GuiProjectTree +from novelwriter.gui.projtree import GuiProjectWiew from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.theme import GuiTheme from novelwriter.gui.viewsbar import GuiViewsBar @@ -39,7 +39,7 @@ __all__ = [ "GuiMainStatus", "GuiNovelTree", "GuiOutline", - "GuiProjectTree", + "GuiProjectWiew", "GuiTheme", "GuiViewsBar", ] diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 643479a4..cf9e8017 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -296,7 +296,7 @@ class GuiItemDetails(QWidget): return @pyqtSlot(str, int, int, int) - def doUpdateCounts(self, tHandle, cC, wC, pC): + def updateCounts(self, tHandle, cC, wC, pC): """Update the counts if the handle is the same as the one we're already showing. Otherwise, do nothing. """ diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 34cb35da..65bffdbd 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -62,6 +62,7 @@ class GuiOutline(QWidget): self.mainConf = novelwriter.CONFIG self.theParent = theParent + # Build GUI self.outlineBar = GuiOutlineToolBar(self) self.outlineView = GuiOutlineView(self) self.outlineData = GuiOutlineDetails(self) @@ -225,6 +226,8 @@ class GuiOutlineToolBar(QToolBar): logger.debug("GuiOutlineToolBar initialisation complete") + return + ## # Methods ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index c6d077b8..7286f87f 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -6,6 +6,8 @@ GUI classes for the main window project tree File History: Created: 2018-09-29 [0.0.1] GuiProjectTree Created: 2020-06-04 [0.7] GuiProjectTreeMenu +Created: 2022-06-06 [1.7b1] GuiProjectWiew +Created: 2022-06-06 [1.7b1] GuiProjectToolBar This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -34,7 +36,8 @@ from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, - QDialog, QHeaderView + QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, QToolButton, + QSizePolicy ) from novelwriter.core import NWDoc @@ -45,12 +48,11 @@ from novelwriter.dialogs.itemeditor import GuiItemEditor logger = logging.getLogger(__name__) -class GuiProjectTree(QTreeWidget): - - C_NAME = 0 - C_COUNT = 1 - C_EXPORT = 2 - C_STATUS = 3 +class GuiProjectWiew(QWidget): + """This is a wrapper class holding all the elements of the project + tree. The core object is the project tree itself. Most methods + available are mapped through to the project tree class. + """ # Signals triggered when the meta data values of items change treeItemChanged = pyqtSignal(str) @@ -63,14 +65,143 @@ class GuiProjectTree(QTreeWidget): openDocumentRequest = pyqtSignal(str, Enum) def __init__(self, theParent): - QTreeWidget.__init__(self, theParent) + QWidget.__init__(self, theParent) + + self.theParent = theParent + + # Build GUI + self.projBar = GuiProjectToolBar(self) + self.projTree = GuiProjectTree(self) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.projBar) + self.outerBox.addWidget(self.projTree) + self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + # Function Mappings + self.newTreeItem = self.projTree.newTreeItem + self.revealNewTreeItem = self.projTree.revealNewTreeItem + self.moveTreeItem = self.projTree.moveTreeItem + self.editTreeItem = self.projTree.editTreeItem + self.getTreeFromHandle = self.projTree.getTreeFromHandle + self.emptyTrash = self.projTree.emptyTrash + self.deleteItem = self.projTree.deleteItem + self.setTreeItemValues = self.projTree.setTreeItemValues + self.propagateCount = self.projTree.propagateCount + self.undoLastMove = self.projTree.undoLastMove + self.getSelectedHandle = self.projTree.getSelectedHandle + self.setSelectedHandle = self.projTree.setSelectedHandle + self.changedSince = self.projTree.changedSince + + return + + ## + # Methods + ## + + def initSettings(self): + self.projTree.initSettings() + return + + def clearProject(self): + self.projTree.clearTree() + return + + def saveProjectTree(self): + self.projTree.saveTreeOrder() + return + + def populateTree(self): + self.projTree.buildTree() + return + + def treeFocus(self): + return self.projTree.hasFocus() + + ## + # Public Solts + ## + + @pyqtSlot(str, int, int, int) + def updateCounts(self, tHandle, cCount, wCount, pCount): + """Slot for updating the word count of a specific item. + """ + self.projTree.propagateCount(tHandle, wCount, countChildren=True) + self.wordCountsChanged.emit() + return + +# END Class GuiProjectWiew + + +class GuiProjectToolBar(QToolBar): + + def __init__(self, theWidget): + QTreeWidget.__init__(self, theWidget) + + logger.debug("Initialising GuiProjectToolBar ...") + + self.mainConf = novelwriter.CONFIG + self.theParent = theWidget.theParent + self.theProject = theWidget.theParent.theProject + self.theTheme = theWidget.theParent.theTheme + + iPx = self.theTheme.baseIconSize + mPx = self.mainConf.pxInt(12) + + self.setMovable(False) + self.setIconSize(QSize(iPx, iPx)) + self.setContentsMargins(0, 0, 0, 0) + self.setStyleSheet("QToolBar {border: 0px;}") + + # Novel Selector + self.projLabel = QLabel(self.tr("Project")) + self.projLabel.setContentsMargins(0, 0, mPx, 0) + self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Itemss Menu + self.tbItems = QToolButton(self) + self.tbItems.setIcon(self.theTheme.getIcon("add")) + self.tbItems.setPopupMode(QToolButton.InstantPopup) + + # Settings Menu + self.tbSettings = QToolButton(self) + self.tbSettings.setIcon(self.theTheme.getIcon("menu")) + self.tbSettings.setPopupMode(QToolButton.InstantPopup) + + # Assemble + self.addWidget(self.projLabel) + self.addSeparator() + self.addWidget(self.tbItems) + self.addWidget(self.tbSettings) + + logger.debug("GuiProjectToolBar initialisation complete") + + return + +# END Class GuiProjectToolBar + + +class GuiProjectTree(QTreeWidget): + + C_NAME = 0 + C_COUNT = 1 + C_EXPORT = 2 + C_STATUS = 3 + + def __init__(self, theWidget): + QTreeWidget.__init__(self, theWidget) logger.debug("Initialising GuiProjectTree ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.theWidget = theWidget + self.theParent = theWidget.theParent + self.theTheme = theWidget.theParent.theTheme + self.theProject = theWidget.theParent.theProject # Internal Variables self._treeMap = {} @@ -125,13 +256,13 @@ class GuiProjectTree(QTreeWidget): self.itemSelectionChanged.connect(self._treeSelectionChange) # Set custom settings - self.initTree() + self.initSettings() logger.debug("GuiProjectTree initialisation complete") return - def initTree(self): + def initSettings(self): """Set or update tree widget settings. """ # Scroll bars @@ -248,7 +379,7 @@ class GuiProjectTree(QTreeWidget): # Get Word Counts _, wC, _ = pIndex.getCounts(tHandle) self.propagateCount(tHandle, wC) - self.wordCountsChanged.emit() + self.theWidget.wordCountsChanged.emit() return True @@ -494,7 +625,7 @@ class GuiProjectTree(QTreeWidget): self._deleteTreeItem(dHandle) self._alertTreeChange(tHandle=tHandle, flush=autoFlush) - self.wordCountsChanged.emit() + self.theWidget.wordCountsChanged.emit() else: # The item is not already in the trash folder, so we @@ -690,18 +821,6 @@ class GuiProjectTree(QTreeWidget): """ return self._timeChanged > checkTime - ## - # Public Solts - ## - - @pyqtSlot(str, int, int, int) - def doUpdateCounts(self, tHandle, cCount, wCount, pCount): - """Slot for updating the word count of a specific item. - """ - self.propagateCount(tHandle, wCount, countChildren=True) - self.wordCountsChanged.emit() - return - ## # Private Slots ## @@ -712,7 +831,7 @@ class GuiProjectTree(QTreeWidget): """ tHandle = self.getSelectedHandle() if tHandle is not None: - self.selectedItemChanged.emit(tHandle) + self.theWidget.selectedItemChanged.emit(tHandle) return @pyqtSlot("QTreeWidgetItem*", int) @@ -729,7 +848,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + self.theWidget.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) else: trItem = self._getTreeItem(tHandle) if trItem is not None: @@ -781,7 +900,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + self.theWidget.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) return @@ -988,11 +1107,11 @@ class GuiProjectTree(QTreeWidget): itemType = tItem.itemType if itemType == nwItemType.ROOT: - self.rootFolderChanged.emit(tHandle) + self.theWidget.rootFolderChanged.emit(tHandle) elif itemType == nwItemType.FILE and tItem.isNovelLike(): - self.novelItemChanged.emit(tHandle) + self.theWidget.novelItemChanged.emit(tHandle) - self.treeItemChanged.emit(tHandle) + self.theWidget.treeItemChanged.emit(tHandle) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 7afd1675..d18d4d17 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, - GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectTree, GuiTheme, + GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectWiew, GuiTheme, GuiViewsBar ) from novelwriter.dialogs import ( @@ -105,7 +105,7 @@ class GuiMain(QMainWindow): # Main GUI Elements self.statusBar = GuiMainStatus(self) - self.treeView = GuiProjectTree(self) + self.treeView = GuiProjectWiew(self) self.novelView = GuiNovelTree(self) self.docEditor = GuiDocEditor(self) self.viewMeta = GuiDocViewDetails(self) @@ -208,8 +208,8 @@ class GuiMain(QMainWindow): self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) - self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts) - self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts) + self.docEditor.docCountsChanged.connect(self.treeMeta.updateCounts) + self.docEditor.docCountsChanged.connect(self.treeView.updateCounts) self.docEditor.loadDocumentTagRequest.connect(self._followTag) self.docViewer.loadDocumentTagRequest.connect(self._followTag) @@ -291,7 +291,7 @@ class GuiMain(QMainWindow): """Wrapper function to clear all sub-elements of the main GUI. """ # Project Area - self.treeView.clearTree() + self.treeView.clearProject() self.novelView.clearTree() self.treeMeta.clearDetails() @@ -541,7 +541,7 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - self.treeView.saveTreeOrder() + self.treeView.saveProjectTree() if self.theProject.saveProject(autoSave=autoSave): self.theProject.index.saveIndex() @@ -789,7 +789,7 @@ class GuiMain(QMainWindow): tHandle = None tLine = None - if self.treeView.hasFocus(): + if self.treeView.treeFocus(): tHandle = self.treeView.getSelectedHandle() elif self.novelView.hasFocus(): tHandle, tLine = self.novelView.getSelectedHandle() @@ -824,7 +824,7 @@ class GuiMain(QMainWindow): def rebuildTrees(self): """Rebuild the project tree. """ - self.treeView.buildTree() + self.treeView.populateTree() self.novelView.refreshTree() return @@ -847,7 +847,7 @@ class GuiMain(QMainWindow): qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) tStart = time() - self.treeView.saveTreeOrder() + self.treeView.saveProjectTree() self.theProject.index.clearIndex() for tItem in self.theProject.tree: @@ -919,7 +919,7 @@ class GuiMain(QMainWindow): self.saveDocument() self.docEditor.initEditor() self.docViewer.initViewer() - self.treeView.initTree() + self.treeView.initSettings() self.novelView.initTree() self.projView.initOutline() self._updateStatusWordCount() diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index a82c77bd..ce5c68d4 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -56,11 +56,11 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Add Project Content monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE) - nwGUI.treeView.newTreeItem(nwItemType.FILE) - nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -82,8 +82,8 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Open the Merge tool nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None) nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger) @@ -101,27 +101,27 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwMerge.listBox.count() == 0 # No item selected - nwGUI.treeView.clearSelection() + nwGUI.treeView.projTree.clearSelection() assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Non-existing item with monkeypatch.context() as mp: mp.setattr(NWTree, "__getitem__", lambda *a: None) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Select a non-folder - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterOne).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterOne).setSelected(True) assert nwMerge._populateList() is False assert nwMerge.listBox.count() == 0 # Select the chapter folder - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwMerge._populateList() is True assert nwMerge.listBox.count() == 5 diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 90e3375d..0e3174c7 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -60,9 +60,9 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Add Project Content monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: QDialog.Accepted) nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hNovelRoot).setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -89,8 +89,8 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Open the Split tool nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hToSplit).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hToSplit).setSelected(True) monkeypatch.setattr(GuiDocSplit, "exec_", lambda *a: None) nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger) @@ -109,7 +109,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # No item selected nwSplit.sourceItem = None - nwGUI.treeView.clearSelection() + nwGUI.treeView.projTree.clearSelection() assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 @@ -117,15 +117,15 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): with monkeypatch.context() as mp: mp.setattr(NWTree, "__getitem__", lambda *a: None) nwSplit.sourceItem = None - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hToSplit).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hToSplit).setSelected(True) assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 # Select a non-file nwSplit.sourceItem = None - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem(hChapterDir).setSelected(True) assert nwSplit._populateList() is False assert nwSplit.listBox.count() == 0 diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 95b1ee71..501d1e3b 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -25,10 +25,10 @@ from tools import getGuiItem, buildTestProject from PyQt5.QtWidgets import QAction, QDialog, QMessageBox -from novelwriter.gui import GuiProjectTree from novelwriter.enum import nwItemLayout, nwItemType from novelwriter.dialogs import GuiItemEditor from novelwriter.core.tree import NWTree +from novelwriter.gui.projtree import GuiProjectTree statusKeys = ["s000000", "s000001", "s000002", "s000003"] importKeys = ["i000004", "i000005", "i000006", "i000007"] @@ -52,7 +52,7 @@ def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): tHandle = "000000000000f" # No Selection - nwGUI.treeView.clearSelection() + nwGUI.treeView.projTree.clearSelection() assert nwGUI.editItem() is False # Force opening from editor @@ -163,9 +163,9 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.theProject.importItems.name(importKeys[1]) == "Minor" # Create Note - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) # Open Note assert nwGUI.openDocument("0000000000010") diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 1fa6b2c7..d1d8797a 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -54,9 +54,9 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.treeView.setSelectedHandle("88243afbe5ed8") # Middle-click the selected item - theItem = nwGUI.treeView._getTreeItem("88243afbe5ed8") - theRect = nwGUI.treeView.visualItemRect(theItem) - qtbot.mouseClick(nwGUI.treeView.viewport(), Qt.MidButton, pos=theRect.center()) + theItem = nwGUI.treeView.projTree._getTreeItem("88243afbe5ed8") + theRect = nwGUI.treeView.projTree.visualItemRect(theItem) + qtbot.mouseClick(nwGUI.treeView.projTree.viewport(), Qt.MidButton, pos=theRect.center()) assert nwGUI.docViewer.docHandle() == "88243afbe5ed8" # Reload the text diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 5f49d51d..78c2af66 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -28,11 +28,10 @@ from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QDialog -from novelwriter.gui import ( - GuiDocEditor, GuiProjectTree, GuiNovelTree, GuiOutline -) +from novelwriter.gui import GuiDocEditor, GuiNovelTree, GuiOutline from novelwriter.enum import nwItemType, nwWidget from novelwriter.tools import GuiProjectWizard +from novelwriter.gui.projtree import GuiProjectTree from novelwriter.dialogs.itemeditor import GuiItemEditor keyDelay = 2 @@ -126,7 +125,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): with monkeypatch.context() as mp: mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - nwGUI.treeView._getTreeItem(sHandle).setSelected(True) + nwGUI.treeView.projTree._getTreeItem(sHandle).setSelected(True) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True @@ -220,14 +219,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.spellCheck is False # Check that tree items have been created - assert nwGUI.treeView._getTreeItem("0000000000008") is not None - assert nwGUI.treeView._getTreeItem("0000000000009") is not None - assert nwGUI.treeView._getTreeItem("000000000000a") is not None - assert nwGUI.treeView._getTreeItem("000000000000b") is not None - assert nwGUI.treeView._getTreeItem("000000000000c") is not None - assert nwGUI.treeView._getTreeItem("000000000000d") is not None - assert nwGUI.treeView._getTreeItem("000000000000e") is not None - assert nwGUI.treeView._getTreeItem("000000000000f") is not None + assert nwGUI.treeView.projTree._getTreeItem("0000000000008") is not None + assert nwGUI.treeView.projTree._getTreeItem("0000000000009") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000a") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000b") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000c") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000d") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000e") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000f") is not None nwGUI.mainMenu.aSpellCheck.setChecked(True) assert nwGUI.mainMenu._toggleSpellCheck() @@ -240,9 +239,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a Character File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() # Type something into the document @@ -262,9 +261,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a Plot File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("0000000000009").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("0000000000009").setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() # Type something into the document @@ -284,9 +283,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Add a World File nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("000000000000b").setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("000000000000b").setSelected(True) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() # Add Some Text @@ -315,10 +314,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Select the 'New Scene' file nwGUI.switchFocus(nwWidget.TREE) - nwGUI.treeView.clearSelection() - nwGUI.treeView._getTreeItem("0000000000008").setExpanded(True) - nwGUI.treeView._getTreeItem("000000000000d").setExpanded(True) - nwGUI.treeView._getTreeItem("000000000000f").setSelected(True) + nwGUI.treeView.projTree.clearSelection() + nwGUI.treeView.projTree._getTreeItem("0000000000008").setExpanded(True) + nwGUI.treeView.projTree._getTreeItem("000000000000d").setExpanded(True) + nwGUI.treeView.projTree._getTreeItem("000000000000f").setSelected(True) assert nwGUI.openSelectedItem() # Type something into the document diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 801abd68..274746d7 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -467,7 +467,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): buildTestProject(nwGUI, fncProj) - assert nwGUI.treeView._getTreeItem("000000000000f") is not None + assert nwGUI.treeView.projTree._getTreeItem("000000000000f") is not None assert nwGUI.openDocument("000000000000f") is True nwGUI.docEditor.clear() @@ -476,10 +476,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): assert nwGUI.docEditor.getText() == "hello world" nwGUI.docEditor.clear() - assert not nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) + assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False assert nwGUI.docEditor.isEmpty() - assert not nwGUI.docEditor.insertText(None) + assert nwGUI.docEditor.insertText(None) is False assert nwGUI.docEditor.isEmpty() # qtbot.stopForInteraction() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 258ad150..7b66f305 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -52,7 +52,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) buildTestProject(nwGUI, prjDir) # No itemType set - nwTree.clearSelection() + nwTree.projTree.clearSelection() assert nwTree.newTreeItem(None) is False # Root Items @@ -69,7 +69,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # ================= # No location selected for new item - nwTree.clearSelection() + nwTree.projTree.clearSelection() caplog.clear() assert nwTree.newTreeItem(nwItemType.FILE) is False assert nwTree.newTreeItem(nwItemType.FOLDER) is False @@ -116,7 +116,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) nwGUI.theProject.tree["0000000000013"].setParent("0000000000011") # Get the trash folder - nwTree._addTrashRoot() + nwTree.projTree._addTrashRoot() trashHandle = nwGUI.theProject.trashFolder() nwTree.setSelectedHandle(trashHandle) assert nwTree.newTreeItem(nwItemType.FILE) is False @@ -182,7 +182,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # Move with no selections - nwTree.clearSelection() + nwTree.projTree.clearSelection() assert nwTree.moveTreeItem(1) is False # Move second item up twice (should give same result) @@ -304,13 +304,13 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # No selection made - nwTree.clearSelection() + nwTree.projTree.clearSelection() caplog.clear() assert nwTree.deleteItem() is False assert "no item to delete" in caplog.text # Not a valid handle - nwTree.clearSelection() + nwTree.projTree.clearSelection() caplog.clear() assert nwTree.deleteItem("0000000000000") is False assert "Could not find tree item" in caplog.text @@ -326,10 +326,10 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # =========== # Block adding trash folder - funcPointer = nwTree._addTrashRoot - nwTree._addTrashRoot = lambda *a: None + funcPointer = nwTree.projTree._addTrashRoot + nwTree.projTree._addTrashRoot = lambda *a: None assert nwTree.deleteItem("0000000000012") is False - nwTree._addTrashRoot = funcPointer + nwTree.projTree._addTrashRoot = funcPointer # Delete last two documents, which also adds the trash folder assert nwTree.deleteItem("0000000000012") is True @@ -441,7 +441,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) # Delete proper - assert nwTree._deleteTreeItem("000000000000e") is True + assert nwTree.projTree._deleteTreeItem("000000000000e") is True assert not os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) # Clean up From 12a7ec7fe388030c18884e4064edb1b9b14d267c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 22:02:44 +0200 Subject: [PATCH 04/12] Allow the project class to create new files --- novelwriter/core/project.py | 32 ++++++++++++++- tests/test_core/test_core_project.py | 58 +++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 460fb91f..cce4603d 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -45,7 +45,7 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.error import logException from novelwriter.common import ( checkString, checkBool, checkInt, isHandle, formatTimeStamp, - makeFileNameSafe, hexToInt, simplified + makeFileNameSafe, hexToInt, minmax, simplified ) from novelwriter.constants import trConst, nwFiles, nwLabels @@ -154,6 +154,8 @@ class NWProject(): def newFolder(self, label, pHandle): """Add a new folder with a given label and parent item. """ + if pHandle not in self._projTree: + return None newItem = NWItem(self) newItem.setName(label) newItem.setType(nwItemType.FOLDER) @@ -164,6 +166,8 @@ class NWProject(): def newFile(self, label, pHandle): """Add a new file with a given label and parent item. """ + if pHandle not in self._projTree: + return None newItem = NWItem(self) newItem.setName(label) newItem.setType(nwItemType.FILE) @@ -171,6 +175,32 @@ class NWProject(): self._projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle + def writeNewFile(self, tHandle, hLevel, isDocument): + """Write content to a new document after it is created. This + will not run if the file exists and is not empty. + """ + tItem = self._projTree[tHandle] + if tItem is None: + return False + if tItem.itemType != nwItemType.FILE: + return False + + newDoc = NWDoc(self, tHandle) + if newDoc.readDocument().strip(): + return False + + hshText = "#"*minmax(hLevel, 1, 4) + newText = f"{hshText} {tItem.itemName}\n\n" + if tItem.isNovelLike() and isDocument: + tItem.setLayout(nwItemLayout.DOCUMENT) + else: + tItem.setLayout(nwItemLayout.NOTE) + + newDoc.writeDocument(newText) + self._projIndex.scanText(tHandle, newText) + + return True + def trashFolder(self): """Add the special trash root folder to the project. """ diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 005277c6..9454b0fc 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -29,10 +29,14 @@ from lxml import etree from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE from mock import causeOSError -from novelwriter.core.project import NWProject from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.common import formatTimeStamp from novelwriter.constants import nwFiles +from novelwriter.core.tree import NWTree +from novelwriter.core.index import NWIndex +from novelwriter.core.project import NWProject +from novelwriter.core.options import OptionState +from novelwriter.core.document import NWDoc @pytest.mark.core @@ -280,12 +284,12 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd): @pytest.mark.core -def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): +def testCoreProject_NewFileFolder(fncDir, outDir, refDir, mockGUI, mockRnd): """Check that new files can be added to the project. """ projFile = os.path.join(fncDir, "nwProject.nwx") - testFile = os.path.join(outDir, "coreProject_NewFile_nwProject.nwx") - compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx") + testFile = os.path.join(outDir, "coreProject_NewFileFolder_nwProject.nwx") + compFile = os.path.join(refDir, "coreProject_NewFileFolder_nwProject.nwx") theProject = NWProject(mockGUI) buildTestProject(theProject, fncDir) @@ -295,9 +299,33 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.closeProject() is True assert theProject.openProject(projFile) is True - assert isinstance(theProject.newFile("Hello", "31489056e0916"), str) - assert isinstance(theProject.newFile("Jane", "71ee45a3c0db9"), str) - assert theProject.projChanged + # Invalid call + assert theProject.newFolder("New Folder", "1234567890abc") is None + assert theProject.newFile("New File", "1234567890abc") is None + + # Add files properly + assert theProject.newFolder("Stuff", "0000000000015") == "0000000000028" + assert theProject.newFile("Hello", "0000000000015") == "0000000000029" + assert theProject.newFile("Jane", "0000000000012") == "000000000002a" + + assert "0000000000028" in theProject.tree + assert "0000000000029" in theProject.tree + assert "000000000002a" in theProject.tree + + # Write to file, failed + assert theProject.writeNewFile("blabla", 1, True) is False # Not a handle + assert theProject.writeNewFile("0000000000028", 1, True) is False # Not a file + assert theProject.writeNewFile("0000000000014", 1, True) is False # Already has content + + # Write to file, success + assert theProject.writeNewFile("0000000000029", 2, True) is True + assert NWDoc(theProject, "0000000000029").readDocument() == "## Hello\n\n" + + assert theProject.writeNewFile("000000000002a", 1, False) is True + assert NWDoc(theProject, "000000000002a").readDocument() == "# Jane\n\n" + + # Save, close and check + assert theProject.projChanged is True assert theProject.saveProject() is True assert theProject.closeProject() is True @@ -305,7 +333,7 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI, mockRnd): assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert theProject.projChanged is False -# END Test testCoreProject_NewFile +# END Test testCoreProject_NewFileFolder @pytest.mark.core @@ -613,6 +641,11 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): theProject = NWProject(mockGUI) theProject.openProject(nwMinimal) + # Storage Objects + assert isinstance(theProject.index, NWIndex) + assert isinstance(theProject.tree, NWTree) + assert isinstance(theProject.options, OptionState) + # Move Novel ROOT to after its files oldOrder = [ "a508bb932959c", # ROOT: Novel @@ -722,7 +755,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): # Change Importance # ================= - fHandle = theProject.newFile("Jane Doe", "8b9d2e465e150") + fHandle = theProject.newFile("Jane Doe", "0000000000012") theProject.tree[fHandle].setImport("Main") assert theProject.tree[fHandle].itemImport == importKeys[3] @@ -894,6 +927,10 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.setProjectLang("en_GB") is True assert theProject.projLang == "en_GB" + # Language Lookup + assert theProject.localLookup(1) == "One" + assert theProject.localLookup(10) == "Ten" + # Automatic outline update theProject.projChanged = False assert theProject.setAutoOutline(True) @@ -1007,7 +1044,8 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): # Add a file with non-existent parent # This file will be renoved from the project on open - assert theProject.newFile("Oops", "0000000000000") + oHandle = theProject.newFile("Oops", "b3643d0f92e32") + theProject.tree[oHandle].setParent("1234567890abc") # Save and close assert theProject.saveProject() is True From 46bb8e5bccf2704f3db3055d609a81b2b1f4827e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 22:03:42 +0200 Subject: [PATCH 05/12] Populate the add button menu in the project widget --- novelwriter/core/index.py | 5 - novelwriter/gui/projtree.py | 158 +++++++++++++----- sample/nwProject.nwx | 10 +- ...> coreProject_NewFileFolder_nwProject.nwx} | 30 ++-- tests/test_core/test_core_index.py | 3 - tests/test_dialogs/test_dlg_docmerge.py | 3 +- tests/test_dialogs/test_dlg_docsplit.py | 3 +- tests/test_dialogs/test_dlg_itemeditor.py | 5 +- tests/test_gui/test_gui_guimain.py | 9 +- tests/test_gui/test_gui_projtree.py | 40 +++-- 10 files changed, 180 insertions(+), 86 deletions(-) rename tests/reference/{coreProject_NewFile_nwProject.nwx => coreProject_NewFileFolder_nwProject.nwx} (76%) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index f86de8c2..ee9ea089 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -502,11 +502,6 @@ class NWIndex: """ return self._itemIndex.mainItemHeader(tHandle) - def getHandleHeaderIntLevel(self, tHandle): - """Get the integer header level of the first header of a handle. - """ - return H_LEVEL.get(self._itemIndex.mainItemHeader(tHandle), 0) - def getTableOfContents(self, maxDepth, skipExcl=True): """Generate a table of contents up to a maximum depth. """ diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 7286f87f..2e58a61d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -37,13 +37,13 @@ from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, QToolButton, - QSizePolicy + QSizePolicy, QInputDialog ) from novelwriter.core import NWDoc from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.common import minmax from novelwriter.dialogs.itemeditor import GuiItemEditor +from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) @@ -82,6 +82,8 @@ class GuiProjectWiew(QWidget): self.setLayout(self.outerBox) + # Connect Signals + # Function Mappings self.newTreeItem = self.projTree.newTreeItem self.revealNewTreeItem = self.projTree.revealNewTreeItem @@ -139,12 +141,19 @@ class GuiProjectWiew(QWidget): class GuiProjectToolBar(QToolBar): + ADD_PLAIN = 0 + ADD_CHAP = 1 + ADD_SCENE = 2 + ADD_NOTE = 3 + ADD_FOLDER = 4 + def __init__(self, theWidget): QTreeWidget.__init__(self, theWidget) logger.debug("Initialising GuiProjectToolBar ...") self.mainConf = novelwriter.CONFIG + self.theWidget = theWidget self.theParent = theWidget.theParent self.theProject = theWidget.theParent.theProject self.theTheme = theWidget.theParent.theTheme @@ -157,14 +166,49 @@ class GuiProjectToolBar(QToolBar): self.setContentsMargins(0, 0, 0, 0) self.setStyleSheet("QToolBar {border: 0px;}") - # Novel Selector + # Tree Label self.projLabel = QLabel(self.tr("Project")) self.projLabel.setContentsMargins(0, 0, mPx, 0) self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - # Itemss Menu + # Items Menu + self.mItems = QMenu() + + self.aAddEmpty = self.mItems.addAction(self.tr("Plain Document")) + self.aAddEmpty.setIcon(self.theTheme.getIcon("proj_document")) + self.aAddEmpty.triggered.connect(lambda: self._forwardNewItem(self.ADD_PLAIN)) + + self.aAddChap = self.mItems.addAction(self.tr("Chapter Document")) + self.aAddChap.setIcon(self.theTheme.getIcon("proj_chapter")) + self.aAddChap.triggered.connect(lambda: self._forwardNewItem(self.ADD_CHAP)) + + self.aAddScene = self.mItems.addAction(self.tr("Scene Document")) + self.aAddScene.setIcon(self.theTheme.getIcon("proj_scene")) + self.aAddScene.triggered.connect(lambda: self._forwardNewItem(self.ADD_SCENE)) + + self.aAddNote = self.mItems.addAction(self.tr("Project Note")) + self.aAddNote.setIcon(self.theTheme.getIcon("proj_note")) + self.aAddNote.triggered.connect(lambda: self._forwardNewItem(self.ADD_NOTE)) + + self.aAddFolder = self.mItems.addAction(self.tr("Folder")) + self.aAddFolder.setIcon(self.theTheme.getIcon("proj_folder")) + self.aAddFolder.triggered.connect(lambda: self._forwardNewItem(self.ADD_FOLDER)) + + self.mAddRoot = self.mItems.addMenu(self.tr("Root Folder")) + self._addRootFolderEntry(nwItemClass.NOVEL) + self._addRootFolderEntry(nwItemClass.ARCHIVE) + self.mAddRoot.addSeparator() + self._addRootFolderEntry(nwItemClass.PLOT) + self._addRootFolderEntry(nwItemClass.CHARACTER) + self._addRootFolderEntry(nwItemClass.WORLD) + self._addRootFolderEntry(nwItemClass.ARCHIVE) + self._addRootFolderEntry(nwItemClass.OBJECT) + self._addRootFolderEntry(nwItemClass.ENTITY) + self._addRootFolderEntry(nwItemClass.CUSTOM) + self.tbItems = QToolButton(self) self.tbItems.setIcon(self.theTheme.getIcon("add")) + self.tbItems.setMenu(self.mItems) self.tbItems.setPopupMode(QToolButton.InstantPopup) # Settings Menu @@ -182,6 +226,45 @@ class GuiProjectToolBar(QToolBar): return + ## + # Private Slots + ## + + @pyqtSlot(Enum) + def _forwardNewRootFolder(self, itemClass): + """Forward the request for a new root folder to the tree. + """ + self.theWidget.projTree.newTreeItem(nwItemType.ROOT, itemClass) + return + + @pyqtSlot(int) + def _forwardNewItem(self, type): + """Forward the request for a new item of a given type. + """ + if type == self.ADD_PLAIN: + self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) + elif type == self.ADD_CHAP: + self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) + elif type == self.ADD_SCENE: + self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) + elif type == self.ADD_NOTE: + self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + elif type == self.ADD_FOLDER: + self.theWidget.projTree.newTreeItem(nwItemType.FOLDER) + return + + ## + # Internal Functions + ## + + def _addRootFolderEntry(self, itemClass): + """Add a menu entry for a root folder of a given class. + """ + aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) + aNew.setIcon(self.theTheme.getIcon(nwLabels.CLASS_ICON[itemClass])) + aNew.triggered.connect(lambda: self._forwardNewRootFolder(itemClass)) + self.mAddRoot.addAction(aNew) + # END Class GuiProjectToolBar @@ -291,7 +374,7 @@ class GuiProjectTree(QTreeWidget): self._timeChanged = 0 return - def newTreeItem(self, itemType, itemClass=None): + def newTreeItem(self, itemType, itemClass=None, hLevel=1, isNote=False): """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 @@ -334,52 +417,49 @@ class GuiProjectTree(QTreeWidget): ), nwAlert.ERROR) return False + # Ask for label + if itemType == nwItemType.FILE: + if isNote: + newLabel = self.tr("New Note") + elif hLevel == 2: + newLabel = self.tr("New Chapter") + elif hLevel == 3: + newLabel = self.tr("New Scene") + else: + newLabel = self.tr("New Document") + else: + newLabel = self.tr("New Folder") + + newLabel, dlgOk = QInputDialog.getText(self, "", self.tr("Label:"), text=newLabel) + if not dlgOk: + logger.info("New item creation cancelled by user") + return False + # Add the file or folder if itemType == nwItemType.FILE: - if pItem.isNovelLike(): - tHandle = self.theProject.newFile(self.tr("New Document"), sHandle) - else: - tHandle = self.theProject.newFile(self.tr("New Note"), sHandle) - elif itemType == nwItemType.FOLDER: - tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle) + tHandle = self.theProject.newFile(newLabel, sHandle) + else: + tHandle = self.theProject.newFolder(newLabel, sHandle) else: logger.error("Failed to add new item") return False - # If there is no handle set, return here. This is a bug + # If there is no handle set, return here. This is a bug. if tHandle is None: # pragma: no cover + logger.error("Internal error") return True - # Add the new item to the tree and open the editor dialog - self.revealNewTreeItem(tHandle, nHandle) - self.editTreeItem(tHandle) - # Handle new file creation - nwItem = self.theProject.tree[tHandle] - if nwItem.itemType != nwItemType.FILE: - return True + if itemType == nwItemType.FILE and hLevel > 0: + if self.theProject.writeNewFile(tHandle, hLevel, not isNote): + # If successful, update word count + wC = self.theProject.index.getCounts(tHandle)[1] + self.propagateCount(tHandle, wC) + self.theWidget.wordCountsChanged.emit() - # This is a new file, so let's add some content - newDoc = NWDoc(self.theProject, tHandle) - if not newDoc.readDocument(): - if nwItem.itemLayout == nwItemLayout.DOCUMENT: - iLvl = self.theProject.index.getHandleHeaderIntLevel(sHandle) - hLvl = "#"*minmax(iLvl + 1, 2, 4) - newText = f"{hLvl} {nwItem.itemName}\n\n" - else: - newText = f"# {nwItem.itemName}\n\n" - - pIndex = self.theProject.index - - # Save the text and index it - newDoc.writeDocument(newText) - pIndex.scanText(tHandle, newText) - - # Get Word Counts - _, wC, _ = pIndex.getCounts(tHandle) - self.propagateCount(tHandle, wC) - self.theWidget.wordCountsChanged.emit() + # Add the new item to the project tree + self.revealNewTreeItem(tHandle, nHandle) return True diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 9f3a6587..4dd6396a 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1334 - 225 - 67746 + 1342 + 227 + 68090 False @@ -15,7 +15,7 @@ True None True - a520879ca0b45 + 636b6aa9b697b 636b6aa9b697b 1363 954 diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx similarity index 76% rename from tests/reference/coreProject_NewFile_nwProject.nwx rename to tests/reference/coreProject_NewFileFolder_nwProject.nwx index 19a2f4bf..4235e9bb 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,9 +16,9 @@ True None None - 0 - 0 - 0 + 2 + 1 + 1 %title% @@ -28,19 +28,19 @@
- New + New Note Draft Finished - New + New Minor Major Main
- + Novel @@ -73,13 +73,17 @@ New Scene - - - Hello + + + Stuff - - - Jane + + + Hello + + + + Jane
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 3d40a9df..8f126e56 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -232,9 +232,6 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): assert theIndex.getHandleHeaderLevel(cHandle) == "H1" assert theIndex.getHandleHeaderLevel(nHandle) == "H1" - assert theIndex.getHandleHeaderIntLevel(cHandle) == 1 - assert theIndex.getHandleHeaderIntLevel(nHandle) == 1 - assert theIndex.getHandleHeaderIntLevel("stuff") == 0 # Zero Items assert theIndex.checkThese([], cItem) == [] diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index ce5c68d4..f796f864 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -25,7 +25,7 @@ import pytest from mock import causeOSError from tools import getGuiItem, readFile, writeFile, buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from PyQt5.QtWidgets import QAction, QMessageBox, QDialog, QInputDialog from novelwriter.enum import nwItemType, nwWidget from novelwriter.dialogs import GuiDocMerge, GuiItemEditor @@ -39,6 +39,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) # Create a new project buildTestProject(nwGUI, fncProj) diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 0e3174c7..080be8ab 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -25,7 +25,7 @@ import pytest from mock import causeOSError from tools import getGuiItem, readFile, writeFile, buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from PyQt5.QtWidgets import QAction, QMessageBox, QDialog, QInputDialog from novelwriter.enum import nwItemType, nwWidget from novelwriter.dialogs import GuiDocSplit, GuiItemEditor @@ -40,6 +40,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Ok) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) # Create a new project buildTestProject(nwGUI, fncProj) diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 501d1e3b..53426df7 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -23,7 +23,7 @@ import pytest from tools import getGuiItem, buildTestProject -from PyQt5.QtWidgets import QAction, QDialog, QMessageBox +from PyQt5.QtWidgets import QAction, QDialog, QMessageBox, QInputDialog from novelwriter.enum import nwItemLayout, nwItemType from novelwriter.dialogs import GuiItemEditor @@ -154,6 +154,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) # Create Project and Open Document buildTestProject(nwGUI, fncProj) @@ -165,7 +166,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): # Create Note nwGUI.treeView.projTree.clearSelection() nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) # Open Note assert nwGUI.openDocument("0000000000010") diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 78c2af66..6085c59b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -26,7 +26,7 @@ from shutil import copyfile from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox, QDialog +from PyQt5.QtWidgets import QMessageBox, QDialog, QInputDialog from novelwriter.gui import GuiDocEditor, GuiNovelTree, GuiOutline from novelwriter.enum import nwItemType, nwWidget @@ -173,6 +173,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock monkeypatch.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) # Create new, save, close project buildTestProject(nwGUI, fncProj) @@ -241,7 +242,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.projTree.clearSelection() nwGUI.treeView.projTree._getTreeItem("000000000000a").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -263,7 +264,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.projTree.clearSelection() nwGUI.treeView.projTree._getTreeItem("0000000000009").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -285,7 +286,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.projTree.clearSelection() nwGUI.treeView.projTree._getTreeItem("000000000000b").setSelected(True) - nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True) assert nwGUI.openSelectedItem() # Add Some Text diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 7b66f305..81c9a801 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,9 +24,8 @@ import os from tools import buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox +from PyQt5.QtWidgets import QAction, QMessageBox, QInputDialog -from novelwriter.guimain import GuiMain from novelwriter.gui.projtree import GuiProjectTree from novelwriter.enum import nwItemType, nwItemClass @@ -40,7 +39,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) nwTree = nwGUI.treeView @@ -89,22 +88,31 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) assert nwGUI.theProject.tree["0000000000012"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL - # Add a new file next to the other new file + # Add a new chapter next to the other new file nwTree.setSelectedHandle("0000000000012") - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE, hLevel=2) is True assert nwGUI.theProject.tree["0000000000013"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL assert nwGUI.openDocument("0000000000013") - assert nwGUI.docEditor.getText() == "## New Document\n\n" + assert nwGUI.docEditor.getText() == "## New Chapter\n\n" + + # Add a new scene next to the other new file + nwTree.setSelectedHandle("0000000000012") + assert nwTree.newTreeItem(nwItemType.FILE, hLevel=3) is True + assert nwGUI.theProject.tree["0000000000014"].itemParent == "0000000000011" + assert nwGUI.theProject.tree["0000000000014"].itemRoot == "0000000000008" + assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument("0000000000014") + assert nwGUI.docEditor.getText() == "### New Scene\n\n" # Add a new file to the characters folder nwTree.setSelectedHandle("000000000000a") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwGUI.theProject.tree["0000000000014"].itemParent == "000000000000a" - assert nwGUI.theProject.tree["0000000000014"].itemRoot == "000000000000a" - assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.CHARACTER - assert nwGUI.openDocument("0000000000014") + assert nwTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True + assert nwGUI.theProject.tree["0000000000015"].itemParent == "000000000000a" + assert nwGUI.theProject.tree["0000000000015"].itemRoot == "000000000000a" + assert nwGUI.theProject.tree["0000000000015"].itemClass == nwItemClass.CHARACTER + assert nwGUI.openDocument("0000000000015") assert nwGUI.docEditor.getText() == "# New Note\n\n" # Make sure the sibling folder bug trap works @@ -115,6 +123,12 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) assert "Internal error" in caplog.text nwGUI.theProject.tree["0000000000013"].setParent("0000000000011") + # Cancel during creation + with monkeypatch.context() as mp: + mp.setattr(QInputDialog, "getText", lambda *a, **k: ("", False)) + nwTree.setSelectedHandle("0000000000013") + assert nwTree.newTreeItem(nwItemType.FILE) is False + # Get the trash folder nwTree.projTree._addTrashRoot() trashHandle = nwGUI.theProject.trashFolder() @@ -148,7 +162,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) nwTree = nwGUI.treeView @@ -272,7 +286,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) nwTree = nwGUI.treeView From 272e7f6e2461b849abd9bc662b32b4b45a9644de Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 6 Jun 2022 22:17:24 +0200 Subject: [PATCH 06/12] Remove redundant menu entries --- novelwriter/gui/mainmenu.py | 29 +---------------------------- novelwriter/gui/projtree.py | 31 +------------------------------ 2 files changed, 2 insertions(+), 58 deletions(-) diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index bd8d1edd..1996f151 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction -from novelwriter.enum import nwItemType, nwItemClass, nwDocAction, nwDocInsert, nwWidget +from novelwriter.enum import nwItemType, nwDocAction, nwDocInsert, nwWidget from novelwriter.constants import trConst, nwKeyWords, nwLabels, nwUnicode logger = logging.getLogger(__name__) @@ -164,33 +164,6 @@ class GuiMainMenu(QMenuBar): # Project > Separator self.projMenu.addSeparator() - # Project > New Root - self.rootMenu = self.projMenu.addMenu(self.tr("Create Root Folder")) - self.rootItems = {} - self.rootItems[nwItemClass.NOVEL] = QAction(self.tr("Novel Root"), self.rootMenu) - self.rootItems[nwItemClass.PLOT] = QAction(self.tr("Plot Root"), self.rootMenu) - self.rootItems[nwItemClass.CHARACTER] = QAction(self.tr("Character Root"), self.rootMenu) - self.rootItems[nwItemClass.WORLD] = QAction(self.tr("Location Root"), self.rootMenu) - self.rootItems[nwItemClass.TIMELINE] = QAction(self.tr("Timeline Root"), self.rootMenu) - self.rootItems[nwItemClass.OBJECT] = QAction(self.tr("Object Root"), self.rootMenu) - self.rootItems[nwItemClass.ENTITY] = QAction(self.tr("Entity Root"), self.rootMenu) - self.rootItems[nwItemClass.CUSTOM] = QAction(self.tr("Custom Root"), self.rootMenu) - self.rootItems[nwItemClass.ARCHIVE] = QAction(self.tr("Archive Root"), self.rootMenu) - for n, itemClass in enumerate(self.rootItems.keys()): - self.rootItems[itemClass].triggered.connect( - lambda n, itemClass=itemClass: self._newTreeItem(nwItemType.ROOT, itemClass) - ) - self.rootMenu.addAction(self.rootItems[itemClass]) - - # Project > New Folder - self.aCreateFolder = QAction(self.tr("Create Folder"), self) - self.aCreateFolder.setShortcut("Ctrl+Shift+N") - self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER)) - self.projMenu.addAction(self.aCreateFolder) - - # Project > Separator - self.projMenu.addSeparator() - # Project > Edit self.aEditItem = QAction(self.tr("Edit Item"), self) self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 2e58a61d..f519fb96 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -167,7 +167,7 @@ class GuiProjectToolBar(QToolBar): self.setStyleSheet("QToolBar {border: 0px;}") # Tree Label - self.projLabel = QLabel(self.tr("Project")) + self.projLabel = QLabel("%s" % self.tr("Project Content")) self.projLabel.setContentsMargins(0, 0, mPx, 0) self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -1235,14 +1235,6 @@ class GuiProjectTreeMenu(QMenu): self.toggleExp.triggered.connect(self._doToggleExported) self.addAction(self.toggleExp) - self.newFile = QAction(self.tr("New File"), self) - self.newFile.triggered.connect(self._doMakeFile) - self.addAction(self.newFile) - - self.newFolder = QAction(self.tr("New Folder"), self) - self.newFolder.triggered.connect(self._doMakeFolder) - self.addAction(self.newFolder) - self.deleteItem = QAction(self.tr("Delete Item"), self) self.deleteItem.triggered.connect(self._doDeleteItem) self.addAction(self.deleteItem) @@ -1273,18 +1265,13 @@ class GuiProjectTreeMenu(QMenu): trashHandle = self.theTree.theProject.tree.trashRoot() - inTrash = self.theTree.theProject.tree.isTrash(theItem.itemHandle) isTrash = theItem.itemHandle == trashHandle and trashHandle is not None isFile = theItem.itemType == nwItemType.FILE - allowNew = not (isTrash or inTrash) - self.editItem.setVisible(not isTrash) self.openItem.setVisible(isFile) self.viewItem.setVisible(isFile) self.toggleExp.setVisible(isFile) - self.newFile.setVisible(allowNew) - self.newFolder.setVisible(allowNew) self.deleteItem.setVisible(not isTrash) self.emptyTrash.setVisible(isTrash) @@ -1318,22 +1305,6 @@ class GuiProjectTreeMenu(QMenu): self.theTree.theParent.editItem() return - @pyqtSlot() - def _doMakeFile(self): - """Forward the new file call to the project tree. - """ - if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FILE) - return - - @pyqtSlot() - def _doMakeFolder(self): - """Forward the new folder call to the project tree. - """ - if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FOLDER) - return - @pyqtSlot() def _doToggleExported(self): """Flip the isExported flag of the current item. From 5a6ef5efd3804a4778257d43c2afea3879dac836 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 7 Jun 2022 00:11:42 +0200 Subject: [PATCH 07/12] Add move buttons to project tree toolbar --- docs/source/usage_shortcuts.rst | 3 +- novelwriter/gui/__init__.py | 4 +- novelwriter/gui/mainmenu.py | 22 +---------- novelwriter/gui/projtree.py | 58 ++++++++++++++++++++++++----- novelwriter/guimain.py | 4 +- tests/test_gui/test_gui_projtree.py | 45 +++++++++++----------- 6 files changed, 78 insertions(+), 58 deletions(-) diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst index 69c2e773..fa761a3e 100644 --- a/docs/source/usage_shortcuts.rst +++ b/docs/source/usage_shortcuts.rst @@ -48,7 +48,7 @@ The main shorcuts are as follows: ":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)" ":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)." ":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`." - ":kbd:`Ctrl`:kbd:`N`", "Create new document." + ":kbd:`Ctrl`:kbd:`N`", "Create new project item." ":kbd:`Ctrl`:kbd:`O`", "Open selected document." ":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter." ":kbd:`Ctrl`:kbd:`R`", "If in the project tree, open a document for viewing. If the editor has focus, open current document for viewing." @@ -72,7 +72,6 @@ The main shorcuts are as follows: ":kbd:`Ctrl`:kbd:`Shift`:kbd:`A`", "Select all text in current paragraph." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`G`", "Find previous occurrence of search word in current document." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`I`", "Import text to the current document from a text file." - ":kbd:`Ctrl`:kbd:`Shift`:kbd:`N`", "Create new folder." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`O`", "Open a project." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project." diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index 0ae23024..9560df1b 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -25,7 +25,7 @@ from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.mainmenu import GuiMainMenu from novelwriter.gui.noveltree import GuiNovelTree from novelwriter.gui.outline import GuiOutline -from novelwriter.gui.projtree import GuiProjectWiew +from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.theme import GuiTheme from novelwriter.gui.viewsbar import GuiViewsBar @@ -39,7 +39,7 @@ __all__ = [ "GuiMainStatus", "GuiNovelTree", "GuiOutline", - "GuiProjectWiew", + "GuiProjectView", "GuiTheme", "GuiViewsBar", ] diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 1996f151..37f1f75c 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction -from novelwriter.enum import nwItemType, nwDocAction, nwDocInsert, nwWidget +from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget from novelwriter.constants import trConst, nwKeyWords, nwLabels, nwUnicode logger = logging.getLogger(__name__) @@ -62,8 +62,6 @@ class GuiMainMenu(QMenuBar): # Function Pointers self._docAction = self.theParent.passDocumentAction - self._moveTreeItem = self.theParent.treeView.moveTreeItem - self._newTreeItem = self.theParent.treeView.newTreeItem self._docInsert = self.theParent.docEditor.insertText self._insertKeyWord = self.theParent.docEditor.insertKeyWord @@ -176,18 +174,6 @@ class GuiMainMenu(QMenuBar): self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) - # Project > Move Up - self.aMoveUp = QAction(self.tr("Move Item Up"), self) - self.aMoveUp.setShortcut("Ctrl+Up") - self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1)) - self.projMenu.addAction(self.aMoveUp) - - # Project > Move Down - self.aMoveDown = QAction(self.tr("Move Item Down"), self) - self.aMoveDown.setShortcut("Ctrl+Down") - self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1)) - self.projMenu.addAction(self.aMoveDown) - # Project > Undo Last Action self.aMoveUndo = QAction(self.tr("Undo Last Move"), self) self.aMoveUndo.setShortcut("Ctrl+Shift+Z") @@ -217,12 +203,6 @@ class GuiMainMenu(QMenuBar): # Document self.docuMenu = self.addMenu(self.tr("&Document")) - # Document > New - self.aNewDoc = QAction(self.tr("New Document"), self) - self.aNewDoc.setShortcut("Ctrl+N") - self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE)) - self.docuMenu.addAction(self.aNewDoc) - # Document > Open self.aOpenDoc = QAction(self.tr("Open Document"), self) self.aOpenDoc.setShortcut("Ctrl+O") diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index f519fb96..fcd2cd83 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -6,7 +6,7 @@ GUI classes for the main window project tree File History: Created: 2018-09-29 [0.0.1] GuiProjectTree Created: 2020-06-04 [0.7] GuiProjectTreeMenu -Created: 2022-06-06 [1.7b1] GuiProjectWiew +Created: 2022-06-06 [1.7b1] GuiProjectView Created: 2022-06-06 [1.7b1] GuiProjectToolBar This file is a part of novelWriter @@ -35,9 +35,9 @@ 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, - QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, QToolButton, - QSizePolicy, QInputDialog + qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, + QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, + QToolButton, QSizePolicy, QInputDialog ) from novelwriter.core import NWDoc @@ -48,7 +48,7 @@ from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) -class GuiProjectWiew(QWidget): +class GuiProjectView(QWidget): """This is a wrapper class holding all the elements of the project tree. The core object is the project tree itself. Most methods available are mapped through to the project tree class. @@ -87,7 +87,6 @@ class GuiProjectWiew(QWidget): # Function Mappings self.newTreeItem = self.projTree.newTreeItem self.revealNewTreeItem = self.projTree.revealNewTreeItem - self.moveTreeItem = self.projTree.moveTreeItem self.editTreeItem = self.projTree.editTreeItem self.getTreeFromHandle = self.projTree.getTreeFromHandle self.emptyTrash = self.projTree.emptyTrash @@ -121,9 +120,26 @@ class GuiProjectWiew(QWidget): self.projTree.buildTree() return + def setFocus(self): + """Forward the set focus call to the tree widget. + """ + self.projTree.setFocus() + return + def treeFocus(self): + """Check if the project tree has focus. + """ return self.projTree.hasFocus() + def anyFocus(self): + """Check if any widget or child widget has focus. + """ + if self.hasFocus(): + return True + if self.isAncestorOf(qApp.focusWidget()): + return True + return False + ## # Public Solts ## @@ -136,7 +152,7 @@ class GuiProjectWiew(QWidget): self.wordCountsChanged.emit() return -# END Class GuiProjectWiew +# END Class GuiProjectView class GuiProjectToolBar(QToolBar): @@ -171,6 +187,19 @@ class GuiProjectToolBar(QToolBar): self.projLabel.setContentsMargins(0, 0, mPx, 0) self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # Move Buttons + self.tbMoveU = QToolButton(self) + self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up")) + self.tbMoveU.setShortcut("Ctrl+Up") + self.tbMoveU.setIcon(self.theTheme.getIcon("up")) + self.tbMoveU.clicked.connect(lambda: self._forwardMoveItem(-1)) + + self.tbMoveD = QToolButton(self) + self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) + self.tbMoveD.setShortcut("Ctrl+Down") + self.tbMoveD.setIcon(self.theTheme.getIcon("down")) + self.tbMoveD.clicked.connect(lambda: self._forwardMoveItem(1)) + # Items Menu self.mItems = QMenu() @@ -207,6 +236,8 @@ class GuiProjectToolBar(QToolBar): self._addRootFolderEntry(nwItemClass.CUSTOM) self.tbItems = QToolButton(self) + self.tbItems.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) + self.tbItems.setShortcut("Ctrl+N") self.tbItems.setIcon(self.theTheme.getIcon("add")) self.tbItems.setMenu(self.mItems) self.tbItems.setPopupMode(QToolButton.InstantPopup) @@ -219,6 +250,8 @@ class GuiProjectToolBar(QToolBar): # Assemble self.addWidget(self.projLabel) self.addSeparator() + self.addWidget(self.tbMoveU) + self.addWidget(self.tbMoveD) self.addWidget(self.tbItems) self.addWidget(self.tbSettings) @@ -253,6 +286,13 @@ class GuiProjectToolBar(QToolBar): self.theWidget.projTree.newTreeItem(nwItemType.FOLDER) return + @pyqtSlot(int) + def _forwardMoveItem(self, steps): + """Forward the request to move an item up or down. + """ + self.theWidget.projTree.moveTreeItem(steps) + return + ## # Internal Functions ## @@ -492,7 +532,7 @@ class GuiProjectTree(QTreeWidget): logger.error("No project open") return False - if not self.hasFocus(): + if not self.theWidget.anyFocus(): return False tHandle = self.getSelectedHandle() @@ -831,7 +871,7 @@ class GuiProjectTree(QTreeWidget): dstItem = self._lastMove.get("parent", None) dstIndex = self._lastMove.get("index", None) - if not self.hasFocus(): + if not self.theWidget.anyFocus(): return False if srcItem is None or dstItem is None or dstIndex is None: diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index d18d4d17..2716213e 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, - GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectWiew, GuiTheme, + GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectView, GuiTheme, GuiViewsBar ) from novelwriter.dialogs import ( @@ -105,7 +105,7 @@ class GuiMain(QMainWindow): # Main GUI Elements self.statusBar = GuiMainStatus(self) - self.treeView = GuiProjectWiew(self) + self.treeView = GuiProjectView(self) self.novelView = GuiNovelTree(self) self.docEditor = GuiDocEditor(self) self.viewMeta = GuiDocViewDetails(self) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 81c9a801..031e7680 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -26,7 +26,7 @@ from tools import buildTestProject from PyQt5.QtWidgets import QAction, QMessageBox, QInputDialog -from novelwriter.gui.projtree import GuiProjectTree +from novelwriter.gui.projtree import GuiProjectView, GuiProjectTree from novelwriter.enum import nwItemType, nwItemClass @@ -163,11 +163,12 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(GuiProjectView, "anyFocus", lambda *a: True) nwTree = nwGUI.treeView # Try to move item with no project - assert nwTree.moveTreeItem(1) is False + assert nwTree.projTree.moveTreeItem(1) is False # Create a project prjDir = os.path.join(fncDir, "project") @@ -187,33 +188,33 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): ] # Move item without focus - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - assert nwTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle("000000000000d") == [ - "000000000000d", "000000000000e", "000000000000f", - "0000000000010", "0000000000011", "0000000000012", - ] - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) + with monkeypatch.context() as mp: + mp.setattr(GuiProjectView, "anyFocus", lambda *a: False) + assert nwTree.projTree.moveTreeItem(1) is False + assert nwTree.getTreeFromHandle("000000000000d") == [ + "000000000000d", "000000000000e", "000000000000f", + "0000000000010", "0000000000011", "0000000000012", + ] # Move with no selections nwTree.projTree.clearSelection() - assert nwTree.moveTreeItem(1) is False + assert nwTree.projTree.moveTreeItem(1) is False # Move second item up twice (should give same result) nwTree.setSelectedHandle("000000000000f") - assert nwTree.moveTreeItem(-1) is True + assert nwTree.projTree.moveTreeItem(-1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000f", "000000000000e", "0000000000010", "0000000000011", "0000000000012", ] - assert nwTree.moveTreeItem(-1) is False + assert nwTree.projTree.moveTreeItem(-1) is False assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000f", "000000000000e", "0000000000010", "0000000000011", "0000000000012", ] - # Restore via menu entry - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + # Restore + assert nwTree.projTree.moveTreeItem(1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -221,19 +222,19 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Move fifth item down twice (should give same result) nwTree.setSelectedHandle("0000000000011") - assert nwTree.moveTreeItem(1) is True + assert nwTree.projTree.moveTreeItem(1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", ] - assert nwTree.moveTreeItem(1) is False + assert nwTree.projTree.moveTreeItem(1) is False assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", ] - # Restore via menu entry - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + # Restore + assert nwTree.projTree.moveTreeItem(-1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -241,7 +242,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Move down again, and restore via undo nwTree.setSelectedHandle("0000000000011") - assert nwTree.moveTreeItem(1) is True + assert nwTree.projTree.moveTreeItem(1) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", @@ -259,15 +260,15 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder up - assert nwTree.moveTreeItem(-1) is False + assert nwTree.projTree.moveTreeItem(-1) is False assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Move novel folder down - assert nwTree.moveTreeItem(1) is True + assert nwTree.projTree.moveTreeItem(1) is True assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1 # Move novel folder up again - assert nwTree.moveTreeItem(-1) is True + assert nwTree.projTree.moveTreeItem(-1) is True assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 # Clean up From ef270bc0698f272df542321ff9ddf5549aef0319 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 7 Jun 2022 22:38:02 +0200 Subject: [PATCH 08/12] Tweak project toolbar look, and fix shortcuts --- novelwriter/gui/projtree.py | 165 +++++++++++++--------------- tests/test_gui/test_gui_projtree.py | 9 -- 2 files changed, 79 insertions(+), 95 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index fcd2cd83..bc5b78b6 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -33,11 +33,11 @@ from enum import Enum from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, - QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QToolBar, QLabel, - QToolButton, QSizePolicy, QInputDialog + QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QLabel, QToolButton, + QSizePolicy, QInputDialog, QHBoxLayout, QShortcut ) from novelwriter.core import NWDoc @@ -75,13 +75,24 @@ class GuiProjectView(QWidget): # Assemble self.outerBox = QVBoxLayout() - self.outerBox.addWidget(self.projBar) - self.outerBox.addWidget(self.projTree) + self.outerBox.addWidget(self.projBar, 0) + self.outerBox.addWidget(self.projTree, 1) self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setSpacing(0) self.setLayout(self.outerBox) + # Keyboard Shortcuts + self.keyCtrlUp = QShortcut(self.projTree) + self.keyCtrlUp.setKey("Ctrl+Up") + self.keyCtrlUp.setContext(Qt.WidgetShortcut) + self.keyCtrlUp.activated.connect(lambda: self.projTree.moveTreeItem(-1)) + + self.keyCtrlDown = QShortcut(self.projTree) + self.keyCtrlDown.setKey("Ctrl+Down") + self.keyCtrlDown.setContext(Qt.WidgetShortcut) + self.keyCtrlDown.activated.connect(lambda: self.projTree.moveTreeItem(1)) + # Connect Signals # Function Mappings @@ -155,7 +166,7 @@ class GuiProjectView(QWidget): # END Class GuiProjectView -class GuiProjectToolBar(QToolBar): +class GuiProjectToolBar(QWidget): ADD_PLAIN = 0 ADD_CHAP = 1 @@ -163,42 +174,52 @@ class GuiProjectToolBar(QToolBar): ADD_NOTE = 3 ADD_FOLDER = 4 - def __init__(self, theWidget): - QTreeWidget.__init__(self, theWidget) + def __init__(self, projView): + QTreeWidget.__init__(self, projView) logger.debug("Initialising GuiProjectToolBar ...") self.mainConf = novelwriter.CONFIG - self.theWidget = theWidget - self.theParent = theWidget.theParent - self.theProject = theWidget.theParent.theProject - self.theTheme = theWidget.theParent.theTheme + self.projView = projView + self.theParent = projView.theParent + self.theProject = projView.theParent.theProject + self.theTheme = projView.theParent.theTheme iPx = self.theTheme.baseIconSize - mPx = self.mainConf.pxInt(12) + mPx = self.mainConf.pxInt(4) - self.setMovable(False) - self.setIconSize(QSize(iPx, iPx)) self.setContentsMargins(0, 0, 0, 0) - self.setStyleSheet("QToolBar {border: 0px;}") + self.setAutoFillBackground(True) + + qPalette = self.palette() + qPalette.setBrush(QPalette.Window, qPalette.base()) + self.setPalette(qPalette) + + fadeCol = qPalette.text().color() + buttonStyle = ( + "QToolButton {{border: none; background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" + ).format(fadeCol.red(), fadeCol.green(), fadeCol.blue()) # Tree Label self.projLabel = QLabel("%s" % self.tr("Project Content")) - self.projLabel.setContentsMargins(0, 0, mPx, 0) + self.projLabel.setContentsMargins(0, 0, 0, 0) self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Move Buttons self.tbMoveU = QToolButton(self) self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up")) - self.tbMoveU.setShortcut("Ctrl+Up") self.tbMoveU.setIcon(self.theTheme.getIcon("up")) - self.tbMoveU.clicked.connect(lambda: self._forwardMoveItem(-1)) + self.tbMoveU.setIconSize(QSize(iPx, iPx)) + self.tbMoveU.setStyleSheet(buttonStyle) + self.tbMoveU.clicked.connect(lambda: self.projView.projTree.moveTreeItem(-1)) self.tbMoveD = QToolButton(self) self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) - self.tbMoveD.setShortcut("Ctrl+Down") self.tbMoveD.setIcon(self.theTheme.getIcon("down")) - self.tbMoveD.clicked.connect(lambda: self._forwardMoveItem(1)) + self.tbMoveD.setIconSize(QSize(iPx, iPx)) + self.tbMoveD.setStyleSheet(buttonStyle) + self.tbMoveD.clicked.connect(lambda: self.projView.projTree.moveTreeItem(1)) # Items Menu self.mItems = QMenu() @@ -239,21 +260,29 @@ class GuiProjectToolBar(QToolBar): self.tbItems.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) self.tbItems.setShortcut("Ctrl+N") self.tbItems.setIcon(self.theTheme.getIcon("add")) + self.tbItems.setIconSize(QSize(iPx, iPx)) + self.tbItems.setStyleSheet(buttonStyle) self.tbItems.setMenu(self.mItems) self.tbItems.setPopupMode(QToolButton.InstantPopup) # Settings Menu self.tbSettings = QToolButton(self) self.tbSettings.setIcon(self.theTheme.getIcon("menu")) + self.tbSettings.setIconSize(QSize(iPx, iPx)) + self.tbSettings.setStyleSheet(buttonStyle) self.tbSettings.setPopupMode(QToolButton.InstantPopup) # Assemble - self.addWidget(self.projLabel) - self.addSeparator() - self.addWidget(self.tbMoveU) - self.addWidget(self.tbMoveD) - self.addWidget(self.tbItems) - self.addWidget(self.tbSettings) + self.outerBox = QHBoxLayout() + self.outerBox.addWidget(self.projLabel) + self.outerBox.addWidget(self.tbMoveU) + self.outerBox.addWidget(self.tbMoveD) + self.outerBox.addWidget(self.tbItems) + self.outerBox.addWidget(self.tbSettings) + self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) + self.outerBox.setSpacing(mPx) + + self.setLayout(self.outerBox) logger.debug("GuiProjectToolBar initialisation complete") @@ -267,7 +296,7 @@ class GuiProjectToolBar(QToolBar): def _forwardNewRootFolder(self, itemClass): """Forward the request for a new root folder to the tree. """ - self.theWidget.projTree.newTreeItem(nwItemType.ROOT, itemClass) + self.projView.projTree.newTreeItem(nwItemType.ROOT, itemClass) return @pyqtSlot(int) @@ -275,22 +304,15 @@ class GuiProjectToolBar(QToolBar): """Forward the request for a new item of a given type. """ if type == self.ADD_PLAIN: - self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) + self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) elif type == self.ADD_CHAP: - self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) + self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) elif type == self.ADD_SCENE: - self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) + self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) elif type == self.ADD_NOTE: - self.theWidget.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) elif type == self.ADD_FOLDER: - self.theWidget.projTree.newTreeItem(nwItemType.FOLDER) - return - - @pyqtSlot(int) - def _forwardMoveItem(self, steps): - """Forward the request to move an item up or down. - """ - self.theWidget.projTree.moveTreeItem(steps) + self.projView.projTree.newTreeItem(nwItemType.FOLDER) return ## @@ -315,16 +337,16 @@ class GuiProjectTree(QTreeWidget): C_EXPORT = 2 C_STATUS = 3 - def __init__(self, theWidget): - QTreeWidget.__init__(self, theWidget) + def __init__(self, projView): + QTreeWidget.__init__(self, projView) logger.debug("Initialising GuiProjectTree ...") self.mainConf = novelwriter.CONFIG - self.theWidget = theWidget - self.theParent = theWidget.theParent - self.theTheme = theWidget.theParent.theTheme - self.theProject = theWidget.theParent.theProject + self.projView = projView + self.theParent = projView.theParent + self.theTheme = projView.theParent.theTheme + self.theProject = projView.theParent.theProject # Internal Variables self._treeMap = {} @@ -496,7 +518,7 @@ class GuiProjectTree(QTreeWidget): # If successful, update word count wC = self.theProject.index.getCounts(tHandle)[1] self.propagateCount(tHandle, wC) - self.theWidget.wordCountsChanged.emit() + self.projView.wordCountsChanged.emit() # Add the new item to the project tree self.revealNewTreeItem(tHandle, nHandle) @@ -525,19 +547,12 @@ class GuiProjectTree(QTreeWidget): return True def moveTreeItem(self, nStep): - """Move an item up or down in the tree, but only if the project - tree has focus. This also applies when the menu is used. + """Move an item up or down in the tree. """ - if not self.theParent.hasProject: - logger.error("No project open") - return False - - if not self.theWidget.anyFocus(): - return False - tHandle = self.getSelectedHandle() tItem = self._getTreeItem(tHandle) if tItem is None: + logger.verbose("No item selected") return False pItem = tItem.parent() @@ -745,7 +760,7 @@ class GuiProjectTree(QTreeWidget): self._deleteTreeItem(dHandle) self._alertTreeChange(tHandle=tHandle, flush=autoFlush) - self.theWidget.wordCountsChanged.emit() + self.projView.wordCountsChanged.emit() else: # The item is not already in the trash folder, so we @@ -871,7 +886,7 @@ class GuiProjectTree(QTreeWidget): dstItem = self._lastMove.get("parent", None) dstIndex = self._lastMove.get("index", None) - if not self.theWidget.anyFocus(): + if not self.projView.anyFocus(): return False if srcItem is None or dstItem is None or dstIndex is None: @@ -951,7 +966,7 @@ class GuiProjectTree(QTreeWidget): """ tHandle = self.getSelectedHandle() if tHandle is not None: - self.theWidget.selectedItemChanged.emit(tHandle) + self.projView.selectedItemChanged.emit(tHandle) return @pyqtSlot("QTreeWidgetItem*", int) @@ -968,7 +983,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.theWidget.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) else: trItem = self._getTreeItem(tHandle) if trItem is not None: @@ -1020,7 +1035,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.theWidget.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) return @@ -1227,11 +1242,11 @@ class GuiProjectTree(QTreeWidget): itemType = tItem.itemType if itemType == nwItemType.ROOT: - self.theWidget.rootFolderChanged.emit(tHandle) + self.projView.rootFolderChanged.emit(tHandle) elif itemType == nwItemType.FILE and tItem.isNovelLike(): - self.theWidget.novelItemChanged.emit(tHandle) + self.projView.novelItemChanged.emit(tHandle) - self.theWidget.treeItemChanged.emit(tHandle) + self.projView.treeItemChanged.emit(tHandle) return @@ -1283,14 +1298,6 @@ class GuiProjectTreeMenu(QMenu): self.emptyTrash.triggered.connect(self._doEmptyTrash) self.addAction(self.emptyTrash) - self.moveUp = QAction(self.tr("Move Item Up"), self) - self.moveUp.triggered.connect(self._doMoveUp) - self.addAction(self.moveUp) - - self.moveDown = QAction(self.tr("Move Item Down"), self) - self.moveDown.triggered.connect(self._doMoveDown) - self.addAction(self.moveDown) - return def filterActions(self, theItem): @@ -1369,18 +1376,4 @@ class GuiProjectTreeMenu(QMenu): self.theTree.emptyTrash() return - @pyqtSlot() - def _doMoveUp(self): - """Forward the move item call to the project tree. - """ - self.theTree.moveTreeItem(-1) - return - - @pyqtSlot() - def _doMoveDown(self): - """Forward the move item call to the project tree. - """ - self.theTree.moveTreeItem(1) - return - # END Class GuiProjectTreeMenu diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 031e7680..c0189a66 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -187,15 +187,6 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): "0000000000010", "0000000000011", "0000000000012", ] - # Move item without focus - with monkeypatch.context() as mp: - mp.setattr(GuiProjectView, "anyFocus", lambda *a: False) - assert nwTree.projTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle("000000000000d") == [ - "000000000000d", "000000000000e", "000000000000f", - "0000000000010", "0000000000011", "0000000000012", - ] - # Move with no selections nwTree.projTree.clearSelection() assert nwTree.projTree.moveTreeItem(1) is False From af39fb98a13f0a3a8e09dcc0d3e5bc731f5263ad Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 7 Jun 2022 23:38:17 +0200 Subject: [PATCH 09/12] Clean up project tree code and add undo action to the menu --- novelwriter/gui/mainmenu.py | 6 -- novelwriter/gui/projtree.py | 153 +++++++++++++--------------- tests/test_gui/test_gui_guimain.py | 2 +- tests/test_gui/test_gui_projtree.py | 52 +++++----- 4 files changed, 95 insertions(+), 118 deletions(-) diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 37f1f75c..33b43cfb 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -174,12 +174,6 @@ class GuiMainMenu(QMenuBar): self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) - # Project > Undo Last Action - self.aMoveUndo = QAction(self.tr("Undo Last Move"), self) - self.aMoveUndo.setShortcut("Ctrl+Shift+Z") - self.aMoveUndo.triggered.connect(lambda: self.theParent.treeView.undoLastMove()) - self.projMenu.addAction(self.aMoveUndo) - # Project > Empty Trash self.aEmptyTrash = QAction(self.tr("Empty Trash"), self) self.aEmptyTrash.triggered.connect(lambda: self.theParent.treeView.emptyTrash()) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index bc5b78b6..6ad4d501 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -70,8 +70,8 @@ class GuiProjectView(QWidget): self.theParent = theParent # Build GUI - self.projBar = GuiProjectToolBar(self) self.projTree = GuiProjectTree(self) + self.projBar = GuiProjectToolBar(self) # Assemble self.outerBox = QVBoxLayout() @@ -83,20 +83,22 @@ class GuiProjectView(QWidget): self.setLayout(self.outerBox) # Keyboard Shortcuts - self.keyCtrlUp = QShortcut(self.projTree) - self.keyCtrlUp.setKey("Ctrl+Up") - self.keyCtrlUp.setContext(Qt.WidgetShortcut) - self.keyCtrlUp.activated.connect(lambda: self.projTree.moveTreeItem(-1)) + self.keyMoveUp = QShortcut(self.projTree) + self.keyMoveUp.setKey("Ctrl+Up") + self.keyMoveUp.setContext(Qt.WidgetShortcut) + self.keyMoveUp.activated.connect(lambda: self.projTree.moveTreeItem(-1)) - self.keyCtrlDown = QShortcut(self.projTree) - self.keyCtrlDown.setKey("Ctrl+Down") - self.keyCtrlDown.setContext(Qt.WidgetShortcut) - self.keyCtrlDown.activated.connect(lambda: self.projTree.moveTreeItem(1)) + self.keyMoveDn = QShortcut(self.projTree) + self.keyMoveDn.setKey("Ctrl+Down") + self.keyMoveDn.setContext(Qt.WidgetShortcut) + self.keyMoveDn.activated.connect(lambda: self.projTree.moveTreeItem(1)) - # Connect Signals + self.keyUndoMv = QShortcut(self.projTree) + self.keyUndoMv.setKey("Ctrl+Shift+Z") + self.keyUndoMv.setContext(Qt.WidgetShortcut) + self.keyUndoMv.activated.connect(lambda: self.projTree.undoLastMove()) # Function Mappings - self.newTreeItem = self.projTree.newTreeItem self.revealNewTreeItem = self.projTree.revealNewTreeItem self.editTreeItem = self.projTree.editTreeItem self.getTreeFromHandle = self.projTree.getTreeFromHandle @@ -104,7 +106,6 @@ class GuiProjectView(QWidget): self.deleteItem = self.projTree.deleteItem self.setTreeItemValues = self.projTree.setTreeItemValues self.propagateCount = self.projTree.propagateCount - self.undoLastMove = self.projTree.undoLastMove self.getSelectedHandle = self.projTree.getSelectedHandle self.setSelectedHandle = self.projTree.setSelectedHandle self.changedSince = self.projTree.changedSince @@ -152,7 +153,7 @@ class GuiProjectView(QWidget): return False ## - # Public Solts + # Public Slots ## @pyqtSlot(str, int, int, int) @@ -168,12 +169,6 @@ class GuiProjectView(QWidget): class GuiProjectToolBar(QWidget): - ADD_PLAIN = 0 - ADD_CHAP = 1 - ADD_SCENE = 2 - ADD_NOTE = 3 - ADD_FOLDER = 4 - def __init__(self, projView): QTreeWidget.__init__(self, projView) @@ -181,6 +176,7 @@ class GuiProjectToolBar(QWidget): self.mainConf = novelwriter.CONFIG self.projView = projView + self.projTree = projView.projTree self.theParent = projView.theParent self.theProject = projView.theParent.theProject self.theTheme = projView.theParent.theTheme @@ -197,9 +193,9 @@ class GuiProjectToolBar(QWidget): fadeCol = qPalette.text().color() buttonStyle = ( - "QToolButton {{border: none; background: transparent;}} " - "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(fadeCol.red(), fadeCol.green(), fadeCol.blue()) + "QToolButton {{padding: {0}px; border: none; background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" + ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue()) # Tree Label self.projLabel = QLabel("%s" % self.tr("Project Content")) @@ -212,39 +208,49 @@ class GuiProjectToolBar(QWidget): self.tbMoveU.setIcon(self.theTheme.getIcon("up")) self.tbMoveU.setIconSize(QSize(iPx, iPx)) self.tbMoveU.setStyleSheet(buttonStyle) - self.tbMoveU.clicked.connect(lambda: self.projView.projTree.moveTreeItem(-1)) + self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1)) self.tbMoveD = QToolButton(self) self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) self.tbMoveD.setIcon(self.theTheme.getIcon("down")) self.tbMoveD.setIconSize(QSize(iPx, iPx)) self.tbMoveD.setStyleSheet(buttonStyle) - self.tbMoveD.clicked.connect(lambda: self.projView.projTree.moveTreeItem(1)) + self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1)) - # Items Menu - self.mItems = QMenu() + # Add Item Menu + self.mAdd = QMenu() - self.aAddEmpty = self.mItems.addAction(self.tr("Plain Document")) + self.aAddEmpty = self.mAdd.addAction(self.tr("Plain Document")) self.aAddEmpty.setIcon(self.theTheme.getIcon("proj_document")) - self.aAddEmpty.triggered.connect(lambda: self._forwardNewItem(self.ADD_PLAIN)) + self.aAddEmpty.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) + ) - self.aAddChap = self.mItems.addAction(self.tr("Chapter Document")) + self.aAddChap = self.mAdd.addAction(self.tr("Chapter Document")) self.aAddChap.setIcon(self.theTheme.getIcon("proj_chapter")) - self.aAddChap.triggered.connect(lambda: self._forwardNewItem(self.ADD_CHAP)) + self.aAddChap.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) + ) - self.aAddScene = self.mItems.addAction(self.tr("Scene Document")) + self.aAddScene = self.mAdd.addAction(self.tr("Scene Document")) self.aAddScene.setIcon(self.theTheme.getIcon("proj_scene")) - self.aAddScene.triggered.connect(lambda: self._forwardNewItem(self.ADD_SCENE)) + self.aAddScene.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) + ) - self.aAddNote = self.mItems.addAction(self.tr("Project Note")) + self.aAddNote = self.mAdd.addAction(self.tr("Project Note")) self.aAddNote.setIcon(self.theTheme.getIcon("proj_note")) - self.aAddNote.triggered.connect(lambda: self._forwardNewItem(self.ADD_NOTE)) + self.aAddNote.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + ) - self.aAddFolder = self.mItems.addAction(self.tr("Folder")) + self.aAddFolder = self.mAdd.addAction(self.tr("Folder")) self.aAddFolder.setIcon(self.theTheme.getIcon("proj_folder")) - self.aAddFolder.triggered.connect(lambda: self._forwardNewItem(self.ADD_FOLDER)) + self.aAddFolder.triggered.connect( + lambda: self.projTree.newTreeItem(nwItemType.FOLDER) + ) - self.mAddRoot = self.mItems.addMenu(self.tr("Root Folder")) + self.mAddRoot = self.mAdd.addMenu(self.tr("Root Folder")) self._addRootFolderEntry(nwItemClass.NOVEL) self._addRootFolderEntry(nwItemClass.ARCHIVE) self.mAddRoot.addSeparator() @@ -256,31 +262,38 @@ class GuiProjectToolBar(QWidget): self._addRootFolderEntry(nwItemClass.ENTITY) self._addRootFolderEntry(nwItemClass.CUSTOM) - self.tbItems = QToolButton(self) - self.tbItems.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) - self.tbItems.setShortcut("Ctrl+N") - self.tbItems.setIcon(self.theTheme.getIcon("add")) - self.tbItems.setIconSize(QSize(iPx, iPx)) - self.tbItems.setStyleSheet(buttonStyle) - self.tbItems.setMenu(self.mItems) - self.tbItems.setPopupMode(QToolButton.InstantPopup) + self.tbAdd = QToolButton(self) + self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) + self.tbAdd.setShortcut("Ctrl+N") + self.tbAdd.setIcon(self.theTheme.getIcon("add")) + self.tbAdd.setIconSize(QSize(iPx, iPx)) + self.tbAdd.setStyleSheet(buttonStyle) + self.tbAdd.setMenu(self.mAdd) + self.tbAdd.setPopupMode(QToolButton.InstantPopup) - # Settings Menu - self.tbSettings = QToolButton(self) - self.tbSettings.setIcon(self.theTheme.getIcon("menu")) - self.tbSettings.setIconSize(QSize(iPx, iPx)) - self.tbSettings.setStyleSheet(buttonStyle) - self.tbSettings.setPopupMode(QToolButton.InstantPopup) + # More Options Menu + self.mMore = QMenu() + + self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move")) + self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove()) + + self.tbMore = QToolButton(self) + self.tbMore.setToolTip(self.tr("More Options")) + self.tbMore.setIcon(self.theTheme.getIcon("menu")) + self.tbMore.setIconSize(QSize(iPx, iPx)) + self.tbMore.setStyleSheet(buttonStyle) + self.tbMore.setMenu(self.mMore) + self.tbMore.setPopupMode(QToolButton.InstantPopup) # Assemble self.outerBox = QHBoxLayout() self.outerBox.addWidget(self.projLabel) self.outerBox.addWidget(self.tbMoveU) self.outerBox.addWidget(self.tbMoveD) - self.outerBox.addWidget(self.tbItems) - self.outerBox.addWidget(self.tbSettings) + self.outerBox.addWidget(self.tbAdd) + self.outerBox.addWidget(self.tbMore) self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) - self.outerBox.setSpacing(mPx) + self.outerBox.setSpacing(0) self.setLayout(self.outerBox) @@ -288,33 +301,6 @@ class GuiProjectToolBar(QWidget): return - ## - # Private Slots - ## - - @pyqtSlot(Enum) - def _forwardNewRootFolder(self, itemClass): - """Forward the request for a new root folder to the tree. - """ - self.projView.projTree.newTreeItem(nwItemType.ROOT, itemClass) - return - - @pyqtSlot(int) - def _forwardNewItem(self, type): - """Forward the request for a new item of a given type. - """ - if type == self.ADD_PLAIN: - self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) - elif type == self.ADD_CHAP: - self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) - elif type == self.ADD_SCENE: - self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) - elif type == self.ADD_NOTE: - self.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) - elif type == self.ADD_FOLDER: - self.projView.projTree.newTreeItem(nwItemType.FOLDER) - return - ## # Internal Functions ## @@ -324,7 +310,7 @@ class GuiProjectToolBar(QWidget): """ aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) aNew.setIcon(self.theTheme.getIcon(nwLabels.CLASS_ICON[itemClass])) - aNew.triggered.connect(lambda: self._forwardNewRootFolder(itemClass)) + aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass)) self.mAddRoot.addAction(aNew) # END Class GuiProjectToolBar @@ -886,9 +872,6 @@ class GuiProjectTree(QTreeWidget): dstItem = self._lastMove.get("parent", None) dstIndex = self._lastMove.get("index", None) - if not self.projView.anyFocus(): - return False - if srcItem is None or dstItem is None or dstIndex is None: logger.verbose("No tree move to undo") return False diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 6085c59b..ab3a1141 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -461,7 +461,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock qtbot.wait(stepDelay) # Check a Quick Create and Delete - assert nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + assert nwGUI.treeView.projTree.newTreeItem(nwItemType.FILE, None) newHandle = nwGUI.treeView.getSelectedHandle() assert nwGUI.theProject.tree["0000000000020"] is not None assert nwGUI.treeView.deleteItem() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index c0189a66..fa904d34 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,7 +24,7 @@ import os from tools import buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox, QInputDialog +from PyQt5.QtWidgets import QMessageBox, QInputDialog from novelwriter.gui.projtree import GuiProjectView, GuiProjectTree from novelwriter.enum import nwItemType, nwItemClass @@ -44,7 +44,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) nwTree = nwGUI.treeView # Try to add item with no project - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False # Create a project prjDir = os.path.join(fncDir, "project") @@ -52,16 +52,16 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # No itemType set nwTree.projTree.clearSelection() - assert nwTree.newTreeItem(None) is False + assert nwTree.projTree.newTreeItem(None) is False # Root Items # ========== # No class set - assert nwTree.newTreeItem(nwItemType.ROOT) is False + assert nwTree.projTree.newTreeItem(nwItemType.ROOT) is False # Create root item - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True + assert nwTree.projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True assert "0000000000010" in nwGUI.theProject.tree # File/Folder Items @@ -70,27 +70,27 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # No location selected for new item nwTree.projTree.clearSelection() caplog.clear() - assert nwTree.newTreeItem(nwItemType.FILE) is False - assert nwTree.newTreeItem(nwItemType.FOLDER) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is False assert "Did not find anywhere" in caplog.text # Create new folder as child of Novel folder nwTree.setSelectedHandle("0000000000008") - assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True assert nwGUI.theProject.tree["0000000000011"].itemParent == "0000000000008" assert nwGUI.theProject.tree["0000000000011"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000011"].itemClass == nwItemClass.NOVEL # Add a new file in the new folder nwTree.setSelectedHandle("0000000000011") - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert nwGUI.theProject.tree["0000000000012"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000012"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000012"].itemClass == nwItemClass.NOVEL # Add a new chapter next to the other new file nwTree.setSelectedHandle("0000000000012") - assert nwTree.newTreeItem(nwItemType.FILE, hLevel=2) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True assert nwGUI.theProject.tree["0000000000013"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL @@ -99,7 +99,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # Add a new scene next to the other new file nwTree.setSelectedHandle("0000000000012") - assert nwTree.newTreeItem(nwItemType.FILE, hLevel=3) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True assert nwGUI.theProject.tree["0000000000014"].itemParent == "0000000000011" assert nwGUI.theProject.tree["0000000000014"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000014"].itemClass == nwItemClass.NOVEL @@ -108,7 +108,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # Add a new file to the characters folder nwTree.setSelectedHandle("000000000000a") - assert nwTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True assert nwGUI.theProject.tree["0000000000015"].itemParent == "000000000000a" assert nwGUI.theProject.tree["0000000000015"].itemRoot == "000000000000a" assert nwGUI.theProject.tree["0000000000015"].itemClass == nwItemClass.CHARACTER @@ -119,7 +119,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) nwTree.setSelectedHandle("0000000000013") nwGUI.theProject.tree["0000000000013"].setParent(None) # This should not happen caplog.clear() - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False assert "Internal error" in caplog.text nwGUI.theProject.tree["0000000000013"].setParent("0000000000011") @@ -127,13 +127,13 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) with monkeypatch.context() as mp: mp.setattr(QInputDialog, "getText", lambda *a, **k: ("", False)) nwTree.setSelectedHandle("0000000000013") - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False # Get the trash folder nwTree.projTree._addTrashRoot() trashHandle = nwGUI.theProject.trashFolder() nwTree.setSelectedHandle(trashHandle) - assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False assert "Cannot add new files or folders to the Trash folder" in caplog.text # Other Checks @@ -179,9 +179,9 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Add some files nwTree.setSelectedHandle("000000000000d") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -238,7 +238,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000012", "0000000000011", ] - nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) + assert nwTree.projTree.undoLastMove() is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -295,9 +295,9 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Add some files nwTree.setSelectedHandle("000000000000d") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -379,10 +379,10 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Add a folder with two files nwTree.setSelectedHandle("0000000000009") - assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True nwTree.setSelectedHandle("0000000000014") - assert nwTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000015.nwd")) assert os.path.isfile(os.path.join(fncDir, "project", "content", "0000000000016.nwd")) @@ -405,7 +405,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR # Add an empty folder, which can be deleted with no further restrictions nwTree.setSelectedHandle("0000000000009") - assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True assert nwTree.getTreeFromHandle("0000000000009") == ["0000000000009", "0000000000017"] nwTree.setSelectedHandle("0000000000017") From 77746f712f4dc35623b572c05062823de6d67dd7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 12:33:37 +0200 Subject: [PATCH 10/12] Fix inconsistend expand/collapse when tree items are moved --- novelwriter/gui/projtree.py | 33 +++++++++++++---------------- tests/test_gui/test_gui_projtree.py | 3 +-- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 6ad4d501..dc52ffc2 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -35,9 +35,9 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( - qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, - QFrame, QDialog, QHeaderView, QWidget, QVBoxLayout, QLabel, QToolButton, - QSizePolicy, QInputDialog, QHBoxLayout, QShortcut + QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView, + QInputDialog, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, + QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter.core import NWDoc @@ -143,15 +143,6 @@ class GuiProjectView(QWidget): """ return self.projTree.hasFocus() - def anyFocus(self): - """Check if any widget or child widget has focus. - """ - if self.hasFocus(): - return True - if self.isAncestorOf(qApp.focusWidget()): - return True - return False - ## # Public Slots ## @@ -536,34 +527,40 @@ class GuiProjectTree(QTreeWidget): """Move an item up or down in the tree. """ tHandle = self.getSelectedHandle() - tItem = self._getTreeItem(tHandle) - if tItem is None: + trItem = self._getTreeItem(tHandle) + if trItem is None: logger.verbose("No item selected") return False - pItem = tItem.parent() + pItem = trItem.parent() + isExp = trItem.isExpanded() if pItem is None: - tIndex = self.indexOfTopLevelItem(tItem) + tIndex = self.indexOfTopLevelItem(trItem) nChild = self.topLevelItemCount() + nIndex = tIndex + nStep if nIndex < 0 or nIndex >= nChild: return False + cItem = self.takeTopLevelItem(tIndex) self.insertTopLevelItem(nIndex, cItem) else: - tIndex = pItem.indexOfChild(tItem) + tIndex = pItem.indexOfChild(trItem) nChild = pItem.childCount() + nIndex = tIndex + nStep if nIndex < 0 or nIndex >= nChild: return False + cItem = pItem.takeChild(tIndex) pItem.insertChild(nIndex, cItem) self._recordLastMove(cItem, pItem, tIndex) self._alertTreeChange(tHandle=tHandle, flush=True) self.clearSelection() - cItem.setSelected(True) + trItem.setSelected(True) + trItem.setExpanded(isExp) return True diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index fa904d34..ee1a1105 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -26,7 +26,7 @@ from tools import buildTestProject from PyQt5.QtWidgets import QMessageBox, QInputDialog -from novelwriter.gui.projtree import GuiProjectView, GuiProjectTree +from novelwriter.gui.projtree import GuiProjectTree from novelwriter.enum import nwItemType, nwItemClass @@ -163,7 +163,6 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) - monkeypatch.setattr(GuiProjectView, "anyFocus", lambda *a: True) nwTree = nwGUI.treeView From f0bef18b63911c6139165aaf1586e9be2ac78b43 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 13:44:45 +0200 Subject: [PATCH 11/12] Re-implement the tree context menu --- novelwriter/gui/projtree.py | 277 +++++++++++++++++++----------------- sample/nwProject.nwx | 24 ++-- 2 files changed, 158 insertions(+), 143 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index dc52ffc2..65614e05 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -5,7 +5,6 @@ GUI classes for the main window project tree File History: Created: 2018-09-29 [0.0.1] GuiProjectTree -Created: 2020-06-04 [0.7] GuiProjectTreeMenu Created: 2022-06-06 [1.7b1] GuiProjectView Created: 2022-06-06 [1.7b1] GuiProjectToolBar @@ -35,9 +34,9 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( - QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView, - QInputDialog, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, - QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QInputDialog, + QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, + QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter.core import NWDoc @@ -211,37 +210,37 @@ class GuiProjectToolBar(QWidget): # Add Item Menu self.mAdd = QMenu() - self.aAddEmpty = self.mAdd.addAction(self.tr("Plain Document")) + self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"])) self.aAddEmpty.setIcon(self.theTheme.getIcon("proj_document")) self.aAddEmpty.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) ) - self.aAddChap = self.mAdd.addAction(self.tr("Chapter Document")) + self.aAddChap = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"])) self.aAddChap.setIcon(self.theTheme.getIcon("proj_chapter")) self.aAddChap.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) ) - self.aAddScene = self.mAdd.addAction(self.tr("Scene Document")) + self.aAddScene = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"])) self.aAddScene.setIcon(self.theTheme.getIcon("proj_scene")) self.aAddScene.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) ) - self.aAddNote = self.mAdd.addAction(self.tr("Project Note")) + self.aAddNote = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["note"])) self.aAddNote.setIcon(self.theTheme.getIcon("proj_note")) self.aAddNote.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) ) - self.aAddFolder = self.mAdd.addAction(self.tr("Folder")) + self.aAddFolder = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["folder"])) self.aAddFolder.setIcon(self.theTheme.getIcon("proj_folder")) self.aAddFolder.triggered.connect( lambda: self.projTree.newTreeItem(nwItemType.FOLDER) ) - self.mAddRoot = self.mAdd.addMenu(self.tr("Root Folder")) + self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"])) self._addRootFolderEntry(nwItemClass.NOVEL) self._addRootFolderEntry(nwItemClass.ARCHIVE) self.mAddRoot.addSeparator() @@ -268,6 +267,9 @@ class GuiProjectToolBar(QWidget): self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move")) self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove()) + self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash")) + self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash()) + self.tbMore = QToolButton(self) self.tbMore.setToolTip(self.tr("More Options")) self.tbMore.setIcon(self.theTheme.getIcon("menu")) @@ -335,9 +337,8 @@ class GuiProjectTree(QTreeWidget): ## # Context Menu - self.ctxMenu = GuiProjectTreeMenu(self) self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self._rightClickMenu) + self.customContextMenuRequested.connect(self._openContextMenu) # Tree Settings iPx = self.theTheme.baseIconSize @@ -972,19 +973,106 @@ class GuiProjectTree(QTreeWidget): return @pyqtSlot("QPoint") - def _rightClickMenu(self, clickPos): + def _openContextMenu(self, clickPos): """The user right clicked an element in the project tree, so we open a context menu in-place. """ + tItem = None selItem = self.itemAt(clickPos) if isinstance(selItem, QTreeWidgetItem): tHandle = selItem.data(self.C_NAME, Qt.UserRole) - self.setSelectedHandle(tHandle) # Just to be safe tItem = self.theProject.tree[tHandle] - if tItem is not None: - if self.ctxMenu.filterActions(tItem): - # Only open menu if any actions remain after filter - self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) + + if tItem is None: + logger.debug("No item found") + + ctxMenu = QMenu() + + # Trash Folder + # ============ + + trashHandle = self.theProject.tree.trashRoot() + if tItem.itemHandle == trashHandle and trashHandle is not None: + # The trash folder only has one option + ctxMenu.addAction( + self.tr("Empty Trash"), lambda: self.emptyTrash() + ) + ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) + return + + # Document Actions + # ================ + + isFile = tItem.itemType == nwItemType.FILE + if isFile: + ctxMenu.addAction( + self.tr("Open Document"), + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + ) + ctxMenu.addAction( + self.tr("View Document"), + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + ) + ctxMenu.addSeparator() + + # Edit Item Settings + # ================== + + if isFile: + ctxMenu.addAction( + self.tr("Toggle Exported"), lambda: self._toggleItemExported(tHandle) + ) + + if tItem.isNovelLike(): + mStatus = ctxMenu.addMenu(self.tr("Change Status")) + for n, (key, entry) in enumerate(self.theProject.statusItems.items()): + aStatus = mStatus.addAction(entry["icon"], entry["name"]) + aStatus.triggered.connect( + lambda n, key=key: self._changeItemStatus(tHandle, key) + ) + else: + mImport = ctxMenu.addMenu(self.tr("Change Importance")) + for n, (key, entry) in enumerate(self.theProject.importItems.items()): + aImport = mImport.addAction(entry["icon"], entry["name"]) + aImport.triggered.connect( + lambda n, key=key: self._changeItemImport(tHandle, key) + ) + + if isFile and tItem.documentAllowed(): + if tItem.itemLayout == nwItemLayout.NOTE: + ctxMenu.addAction( + self.tr("Change to {0}").format( + trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT]) + ), + lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT) + ) + else: + ctxMenu.addAction( + self.tr("Change to {0}").format( + trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE]) + ), + lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE) + ) + + ctxMenu.addSeparator() + + # Major Item Actions + # ================== + + ctxMenu.addAction( + self.tr("Edit Item Settings"), lambda: self.editTreeItem(tHandle) + ) + + if tItem.itemClass == nwItemClass.TRASH or tItem.itemType == nwItemType.ROOT: + ctxMenu.addAction( + self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle) + ) + else: + ctxMenu.addAction( + self.tr("Move to Trash"), lambda: self.deleteItem(tHandle) + ) + + ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) return @@ -1121,6 +1209,46 @@ class GuiProjectTree(QTreeWidget): return True + def _toggleItemExported(self, tHandle): + """Toggle the exported status of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + tItem.setExported(not tItem.isExported) + self.setTreeItemValues(tItem.itemHandle) + return + + def _changeItemStatus(self, tHandle, tStatus): + """Set a new status value of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + tItem.setStatus(tStatus) + self.setTreeItemValues(tItem.itemHandle) + return + + def _changeItemImport(self, tHandle, tImport): + """Set a new importance value of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + tItem.setImport(tImport) + self.setTreeItemValues(tItem.itemHandle) + return + + def _changeItemLayout(self, tHandle, itemLayout): + """Set a new item layout value of an item. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None: + if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): + tItem.setLayout(nwItemLayout.DOCUMENT) + self.setTreeItemValues(tItem.itemHandle) + elif itemLayout == nwItemLayout.NOTE: + tItem.setLayout(nwItemLayout.NOTE) + self.setTreeItemValues(tItem.itemHandle) + return + def _scanChildren(self, theList, tItem, tIndex): """This is a recursive function returning all items in a tree starting at a given QTreeWidgetItem. @@ -1244,116 +1372,3 @@ class GuiProjectTree(QTreeWidget): return # END Class GuiProjectTree - - -class GuiProjectTreeMenu(QMenu): - - def __init__(self, theTree): - QMenu.__init__(self, theTree) - - self.theTree = theTree - self.theItem = None - - self.editItem = QAction(self.tr("Edit Project Item"), self) - self.editItem.triggered.connect(self._doEditItem) - self.addAction(self.editItem) - - self.openItem = QAction(self.tr("Open Document"), self) - self.openItem.triggered.connect(self._doOpenItem) - self.addAction(self.openItem) - - self.viewItem = QAction(self.tr("View Document"), self) - self.viewItem.triggered.connect(self._doViewItem) - self.addAction(self.viewItem) - - self.toggleExp = QAction(self.tr("Toggle Included Flag"), self) - self.toggleExp.triggered.connect(self._doToggleExported) - self.addAction(self.toggleExp) - - self.deleteItem = QAction(self.tr("Delete Item"), self) - self.deleteItem.triggered.connect(self._doDeleteItem) - self.addAction(self.deleteItem) - - self.emptyTrash = QAction(self.tr("Empty Trash"), self) - self.emptyTrash.triggered.connect(self._doEmptyTrash) - self.addAction(self.emptyTrash) - - return - - def filterActions(self, theItem): - """Filter the menu entries available based on the properties of - the item the menu was activated on. - """ - self.theItem = theItem - - if theItem is None: - logger.error("Failed to extract information to build tree context menu") - return False - - trashHandle = self.theTree.theProject.tree.trashRoot() - - isTrash = theItem.itemHandle == trashHandle and trashHandle is not None - isFile = theItem.itemType == nwItemType.FILE - - self.editItem.setVisible(not isTrash) - self.openItem.setVisible(isFile) - self.viewItem.setVisible(isFile) - self.toggleExp.setVisible(isFile) - self.deleteItem.setVisible(not isTrash) - self.emptyTrash.setVisible(isTrash) - - return True - - ## - # Slots - ## - - @pyqtSlot() - def _doOpenItem(self): - """Forward the open document call to the main GUI window. - """ - if self.theItem is not None: - self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False) - return - - @pyqtSlot() - def _doViewItem(self): - """Forward the view document call to the main GUI window. - """ - if self.theItem is not None: - self.theTree.theParent.viewDocument(self.theItem.itemHandle) - return - - @pyqtSlot() - def _doEditItem(self): - """Forward the edit item call to the main GUI window. - """ - if self.theItem is not None: - self.theTree.theParent.editItem() - return - - @pyqtSlot() - def _doToggleExported(self): - """Flip the isExported flag of the current item. - """ - if self.theItem is not None: - self.theItem.setExported(not self.theItem.isExported) - self.theTree.setTreeItemValues(self.theItem.itemHandle) - return - - @pyqtSlot() - def _doDeleteItem(self): - """Forward the delete item call to the project tree. - """ - if self.theItem is not None: - self.theTree.deleteItem() - return - - @pyqtSlot() - def _doEmptyTrash(self): - """Forward the empty trash call to the project tree. - """ - self.theTree.emptyTrash() - return - -# END Class GuiProjectTreeMenu diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 4dd6396a..e8ee5c59 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1342 - 227 - 68090 + 1345 + 229 + 68286 False @@ -33,13 +33,13 @@
- New - Notes + New + Notes Started - 1st Draft - 2nd Draft + 1st Draft + 2nd Draft 3rd Draft - Finished + Finished None @@ -63,7 +63,7 @@ - Part One + Part One @@ -79,11 +79,11 @@ - Interlude + Interlude - A Note on Structure + A Note on Structure From 025463a28e0b65404002a9244d0c52f3526e19a3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Jun 2022 14:40:03 +0200 Subject: [PATCH 12/12] Improve project tree test coverage --- novelwriter/gui/projtree.py | 5 +- tests/test_gui/test_gui_projtree.py | 90 ++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 65614e05..372a2d84 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -985,6 +985,7 @@ class GuiProjectTree(QTreeWidget): if tItem is None: logger.debug("No item found") + return False ctxMenu = QMenu() @@ -998,7 +999,7 @@ class GuiProjectTree(QTreeWidget): self.tr("Empty Trash"), lambda: self.emptyTrash() ) ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) - return + return True # Document Actions # ================ @@ -1074,7 +1075,7 @@ class GuiProjectTree(QTreeWidget): ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) - return + return True ## # Events diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index ee1a1105..7b6d2f42 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,10 +24,10 @@ import os from tools import buildTestProject -from PyQt5.QtWidgets import QMessageBox, QInputDialog +from PyQt5.QtWidgets import QMessageBox, QInputDialog, QMenu from novelwriter.gui.projtree import GuiProjectTree -from novelwriter.enum import nwItemType, nwItemClass +from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass @pytest.mark.gui @@ -454,3 +454,89 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR nwGUI.closeProject() # END Test testGuiProjTree_DeleteItems + + +@pytest.mark.gui +def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): + """Test the building of the project tree context menu. All this does + is test that the menu builds. It doesn't open the actual menu, + """ + # Block message box + monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) + monkeypatch.setattr(QMenu, "exec_", lambda *a: None) + + # Create a project + prjDir = os.path.join(fncDir, "project") + buildTestProject(nwGUI, prjDir) + + # Handles for new objects + hNovelRoot = "0000000000008" + hTitlePage = "000000000000c" + hChapterDir = "000000000000d" + hChapterFile = "000000000000e" + hCharRoot = "000000000000a" + hCharNote = "0000000000011" + hNovelNote = "0000000000012" + + projTree = nwGUI.treeView.projTree + projTree._getTreeItem(hNovelRoot).setExpanded(True) + projTree._getTreeItem(hChapterDir).setExpanded(True) + + projTree._addTrashRoot() + hTrashRoot = projTree.theProject.tree.trashRoot() + + projTree.setSelectedHandle(hCharRoot) + projTree.newTreeItem(nwItemType.FILE) + projTree.setSelectedHandle(hNovelRoot) + projTree.newTreeItem(nwItemType.FILE, isNote=True) + + def itemPos(tHandle): + return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center() + + # No item under menu + assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False + + # Generate the possible menu combinarions + assert projTree._openContextMenu(itemPos(hTrashRoot)) is True + assert projTree._openContextMenu(itemPos(hNovelRoot)) is True + assert projTree._openContextMenu(itemPos(hNovelNote)) is True + assert projTree._openContextMenu(itemPos(hTitlePage)) is True + assert projTree._openContextMenu(itemPos(hChapterDir)) is True + assert projTree._openContextMenu(itemPos(hChapterFile)) is True + assert projTree._openContextMenu(itemPos(hCharRoot)) is True + assert projTree._openContextMenu(itemPos(hCharNote)) is True + + # Direct Edit Functions + # ===================== + # Trigger the dedicated functions the menu entries connect to + nwItem = projTree.theProject.tree[hNovelNote] + + # Toggle exported flag + assert nwItem.isExported is True + projTree._toggleItemExported(hNovelNote) + assert nwItem.isExported is False + + # Change item status + assert nwItem.itemStatus == "s000000" + projTree._changeItemStatus(hNovelNote, "s000001") + assert nwItem.itemStatus == "s000001" + + # Change item importance + assert nwItem.itemImport == "i000004" + projTree._changeItemImport(hNovelNote, "i000005") + assert nwItem.itemImport == "i000005" + + # Change item layout + assert nwItem.itemLayout == nwItemLayout.NOTE + projTree._changeItemLayout(hNovelNote, nwItemLayout.DOCUMENT) + assert nwItem.itemLayout == nwItemLayout.DOCUMENT + projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE) + assert nwItem.itemLayout == nwItemLayout.NOTE + + # qtbot.stop() + +# END Test testGuiProjTree_ContextMenu