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