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/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/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/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..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 GuiProjectTree +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", - "GuiProjectTree", + "GuiProjectView", "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/mainmenu.py b/novelwriter/gui/mainmenu.py index bd8d1edd..33b43cfb 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 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 @@ -164,33 +162,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"]) @@ -203,24 +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") - 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()) @@ -244,12 +197,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/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 e0a8ef62..372a2d84 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -5,7 +5,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] GuiProjectView +Created: 2022-06-06 [1.7b1] GuiProjectToolBar This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -27,23 +28,287 @@ 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 -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame, - QDialog + QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QInputDialog, + QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, + QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter.core import NWDoc -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.common import minmax +from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.dialogs.itemeditor import GuiItemEditor +from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) +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. + """ + + # 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): + QWidget.__init__(self, theParent) + + self.theParent = theParent + + # Build GUI + self.projTree = GuiProjectTree(self) + self.projBar = GuiProjectToolBar(self) + + # Assemble + self.outerBox = QVBoxLayout() + 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.keyMoveUp = QShortcut(self.projTree) + self.keyMoveUp.setKey("Ctrl+Up") + self.keyMoveUp.setContext(Qt.WidgetShortcut) + self.keyMoveUp.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)) + + 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.revealNewTreeItem = self.projTree.revealNewTreeItem + 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.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 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() + + ## + # Public Slots + ## + + @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 GuiProjectView + + +class GuiProjectToolBar(QWidget): + + def __init__(self, projView): + QTreeWidget.__init__(self, projView) + + logger.debug("Initialising GuiProjectToolBar ...") + + 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 + + iPx = self.theTheme.baseIconSize + mPx = self.mainConf.pxInt(4) + + self.setContentsMargins(0, 0, 0, 0) + self.setAutoFillBackground(True) + + qPalette = self.palette() + qPalette.setBrush(QPalette.Window, qPalette.base()) + self.setPalette(qPalette) + + fadeCol = qPalette.text().color() + buttonStyle = ( + "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")) + 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.setIcon(self.theTheme.getIcon("up")) + self.tbMoveU.setIconSize(QSize(iPx, iPx)) + self.tbMoveU.setStyleSheet(buttonStyle) + 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.projTree.moveTreeItem(1)) + + # Add Item Menu + self.mAdd = QMenu() + + 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(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(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(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(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(trConst(nwLabels.ITEM_DESCRIPTION["root"])) + 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.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) + + # More Options Menu + self.mMore = QMenu() + + 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")) + 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.tbAdd) + self.outerBox.addWidget(self.tbMore) + self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + logger.debug("GuiProjectToolBar initialisation complete") + + 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.projTree.newTreeItem(nwItemType.ROOT, itemClass)) + self.mAddRoot.addAction(aNew) + +# END Class GuiProjectToolBar + + class GuiProjectTree(QTreeWidget): C_NAME = 0 @@ -51,20 +316,16 @@ class GuiProjectTree(QTreeWidget): C_EXPORT = 2 C_STATUS = 3 - treeItemChanged = pyqtSignal(str) - novelItemChanged = pyqtSignal(str) - rootFolderChanged = pyqtSignal(str) - wordCountsChanged = pyqtSignal() - - def __init__(self, theParent): - QTreeWidget.__init__(self, theParent) + def __init__(self, projView): + QTreeWidget.__init__(self, projView) logger.debug("Initialising GuiProjectTree ...") self.mainConf = novelwriter.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.theProject = theParent.theProject + self.projView = projView + self.theParent = projView.theParent + self.theTheme = projView.theParent.theTheme + self.theProject = projView.theParent.theProject # Internal Variables self._treeMap = {} @@ -76,34 +337,29 @@ 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 + 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) @@ -118,23 +374,18 @@ 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) # 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 @@ -163,7 +414,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 @@ -206,55 +457,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 - self.revealNewTreeItem(tHandle, nHandle) - self.theParent.editItem(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.projView.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 - cC, wC, pC = pIndex.getCounts(tHandle) - nwItem.setCharCount(cC) - nwItem.setWordCount(wC) - nwItem.setParaCount(pC) - self.propagateCount(tHandle, wC) - self.wordCountsChanged.emit() + # Add the new item to the project tree + self.revealNewTreeItem(tHandle, nHandle) return True @@ -280,54 +525,49 @@ 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. """ - if not self.theParent.hasProject: - logger.error("No project open") - return False - - if not self.hasFocus(): - return False - 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 - 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 +597,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,24 +606,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. - """ - 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 @@ -522,7 +744,7 @@ class GuiProjectTree(QTreeWidget): self._deleteTreeItem(dHandle) self._alertTreeChange(tHandle=tHandle, flush=autoFlush) - self.wordCountsChanged.emit() + self.projView.wordCountsChanged.emit() else: # The item is not already in the trash folder, so we @@ -648,9 +870,6 @@ class GuiProjectTree(QTreeWidget): dstItem = self._lastMove.get("parent", None) dstIndex = self._lastMove.get("index", None) - if not self.hasFocus(): - return False - if srcItem is None or dstItem is None or dstIndex is None: logger.verbose("No tree move to undo") return False @@ -719,33 +938,144 @@ class GuiProjectTree(QTreeWidget): return self._timeChanged > checkTime ## - # Slots + # Private Slots ## + @pyqtSlot() + def _treeSelectionChange(self): + """The user changed which item is selected. + """ + tHandle = self.getSelectedHandle() + if tHandle is not None: + self.projView.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.projView.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): + 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)) - return + if tItem is None: + logger.debug("No item found") + return False - @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 + 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 True + + # 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 True ## # Events @@ -774,7 +1104,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.theParent.viewDocument(tHandle) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) return @@ -880,6 +1210,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. @@ -981,11 +1351,11 @@ class GuiProjectTree(QTreeWidget): itemType = tItem.itemType if itemType == nwItemType.ROOT: - self.rootFolderChanged.emit(tHandle) + self.projView.rootFolderChanged.emit(tHandle) elif itemType == nwItemType.FILE and tItem.isNovelLike(): - self.novelItemChanged.emit(tHandle) + self.projView.novelItemChanged.emit(tHandle) - self.treeItemChanged.emit(tHandle) + self.projView.treeItemChanged.emit(tHandle) return @@ -1003,167 +1373,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.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) - - self.emptyTrash = QAction(self.tr("Empty Trash"), self) - 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): - """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() - - 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) - - 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 _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. - """ - 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 - - @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/novelwriter/guimain.py b/novelwriter/guimain.py index 6a056dca..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, GuiProjectTree, 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 = GuiProjectTree(self) + self.treeView = GuiProjectView(self) self.novelView = GuiNovelTree(self) self.docEditor = GuiDocEditor(self) self.viewMeta = GuiDocViewDetails(self) @@ -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.updateCounts) + self.docEditor.docCountsChanged.connect(self.treeView.updateCounts) + self.docEditor.loadDocumentTagRequest.connect(self._followTag) + + self.docViewer.loadDocumentTagRequest.connect(self._followTag) + + self.projView.loadDocumentTagRequest.connect(self._followTag) + # Finalise Initialisation # ======================= @@ -289,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() @@ -539,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() @@ -787,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() @@ -822,7 +824,7 @@ class GuiMain(QMainWindow): def rebuildTrees(self): """Rebuild the project tree. """ - self.treeView.buildTree() + self.treeView.populateTree() self.novelView.refreshTree() return @@ -845,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: @@ -917,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() @@ -1159,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()) @@ -1468,6 +1469,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 +1543,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 diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 9f3a6587..e8ee5c59 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1334 - 225 - 67746 + 1345 + 229 + 68286 False @@ -15,7 +15,7 @@ True None True - a520879ca0b45 + 636b6aa9b697b 636b6aa9b697b 1363 954 @@ -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 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_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 diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index a82c77bd..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) @@ -56,11 +57,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 +83,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 +102,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..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) @@ -60,9 +61,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 +90,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 +110,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 +118,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..53426df7 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -23,12 +23,12 @@ import pytest from tools import getGuiItem, buildTestProject -from PyQt5.QtWidgets import QAction, QDialog, QMessageBox +from PyQt5.QtWidgets import QAction, QDialog, QMessageBox, QInputDialog -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 @@ -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) @@ -163,9 +164,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, isNote=True) # 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..ab3a1141 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -26,13 +26,12 @@ 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, 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 @@ -174,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) @@ -220,14 +220,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 +240,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, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -262,9 +262,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, isNote=True) assert nwGUI.openSelectedItem() # Type something into the document @@ -284,9 +284,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, isNote=True) assert nwGUI.openSelectedItem() # Add Some Text @@ -315,10 +315,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 @@ -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_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..7b6d2f42 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -24,11 +24,10 @@ import os from tools import buildTestProject -from PyQt5.QtWidgets import QAction, QMessageBox +from PyQt5.QtWidgets import QMessageBox, QInputDialog, QMenu -from novelwriter.guimain import GuiMain from novelwriter.gui.projtree import GuiProjectTree -from novelwriter.enum import nwItemType, nwItemClass +from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass @pytest.mark.gui @@ -40,86 +39,101 @@ 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 # 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") buildTestProject(nwGUI, prjDir) # No itemType set - nwTree.clearSelection() - assert nwTree.newTreeItem(None) is False + nwTree.projTree.clearSelection() + 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 # ================= # 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 + 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 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.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 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.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 + 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.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 + assert nwGUI.openDocument("0000000000015") assert nwGUI.docEditor.getText() == "# New Note\n\n" # Make sure the sibling folder bug trap works 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") + # Cancel during creation + with monkeypatch.context() as mp: + mp.setattr(QInputDialog, "getText", lambda *a, **k: ("", False)) + nwTree.setSelectedHandle("0000000000013") + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is False + # Get the trash folder - nwTree._addTrashRoot() + 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 @@ -148,12 +162,12 @@ 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 # 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") @@ -164,42 +178,33 @@ 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", ] - # 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) - # Move with no selections - nwTree.clearSelection() - assert nwTree.moveTreeItem(1) is False + nwTree.projTree.clearSelection() + 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", @@ -207,19 +212,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", @@ -227,12 +232,12 @@ 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", ] - nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) + assert nwTree.projTree.undoLastMove() is True assert nwTree.getTreeFromHandle("000000000000d") == [ "000000000000d", "000000000000e", "000000000000f", "0000000000010", "0000000000011", "0000000000012", @@ -245,15 +250,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 @@ -272,7 +277,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 @@ -289,9 +294,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", @@ -304,13 +309,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 +331,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 @@ -373,10 +378,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")) @@ -399,7 +404,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") @@ -441,7 +446,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 @@ -449,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