From 52683d5a21202a6e85a15b029262027afa747889 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 12 Jun 2022 17:47:32 +0200
Subject: [PATCH] Add novel tree toolbar and connect actions
---
novelwriter/core/project.py | 16 +++
novelwriter/gui/noveltree.py | 224 +++++++++++++++++++++++++++--------
novelwriter/gui/projtree.py | 26 ++--
novelwriter/guimain.py | 18 ++-
sample/nwProject.nwx | 16 +--
5 files changed, 224 insertions(+), 76 deletions(-)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index c2683cc7..43f3297e 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -102,6 +102,8 @@ class NWProject():
self.importItems = None # Note file importance values
self.lastEdited = None # The handle of the last file to be edited
self.lastViewed = None # The handle of the last file to be viewed
+ self.lastNovel = None # The handle of the last novel root viewed
+ self.lastOutline = None # The handle of the last outline root viewed
self.lastWCount = 0 # The project word count from last session
self.lastNovelWC = 0 # The novel files word count from last session
self.lastNotesWC = 0 # The note files word count from last session
@@ -612,6 +614,10 @@ class NWProject():
self.lastEdited = checkString(xItem.text, None, True)
elif xItem.tag == "lastViewed":
self.lastViewed = checkString(xItem.text, None, True)
+ elif xItem.tag == "lastNovel":
+ self.lastNovel = checkString(xItem.text, None, True)
+ elif xItem.tag == "lastOutline":
+ self.lastOutline = checkString(xItem.text, None, True)
elif xItem.tag == "lastWordCount":
self.lastWCount = checkInt(xItem.text, 0, False)
elif xItem.tag == "novelWordCount":
@@ -732,6 +738,8 @@ class NWProject():
self._packProjectValue(xSettings, "autoOutline", self.autoOutline)
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
+ self._packProjectValue(xSettings, "lastNovel", self.lastNovel)
+ self._packProjectValue(xSettings, "lastOutline", self.lastOutline)
self._packProjectValue(xSettings, "lastWordCount", self.currWCount)
self._packProjectValue(xSettings, "novelWordCount", self.currNovelWC)
self._packProjectValue(xSettings, "notesWordCount", self.currNotesWC)
@@ -1123,6 +1131,14 @@ class NWProject():
self.setProjectChanged(True)
return True
+ def setLastNovelViewed(self, tHandle):
+ """Set last viewed novel root in the novel tree.
+ """
+ if self.lastNovel != tHandle:
+ self.lastNovel = tHandle
+ self.setProjectChanged(True)
+ return True
+
def setStatusColours(self, newCols, delCols):
"""Update the list of novel file status flags.
"""
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index 85773478..8cec9fdb 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -27,25 +27,34 @@ import logging
import novelwriter
from time import time
+from enum import Enum
-from PyQt5.QtCore import Qt, QSize
+from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal
+from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import (
- QAbstractItemView, QFrame, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
+ QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel,
+ QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
QWidget
)
+from novelwriter.enum import nwDocMode, nwItemClass
from novelwriter.common import checkInt
-from novelwriter.constants import nwKeyWords
+from novelwriter.constants import nwKeyWords, nwLabels
logger = logging.getLogger(__name__)
class GuiNovelView(QWidget):
+ # Signals for user interaction with the novel tree
+ selectedItemChanged = pyqtSignal(str)
+ openDocumentRequest = pyqtSignal(str, Enum, int, str)
+
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
- self.mainGui = mainGui
+ self.mainGui = mainGui
+ self.theProject = mainGui.theProject
# Build GUI
self.novelTree = GuiNovelTree(self)
@@ -60,6 +69,11 @@ class GuiNovelView(QWidget):
self.setLayout(self.outerBox)
+ # Connect Signals
+ self.novelBar.rootFolderSelectionChanged.connect(
+ lambda tHandle: self.novelTree.refreshTree(rootHandle=tHandle, overRide=True)
+ )
+
# Function Mappings
self.refreshTree = self.novelTree.refreshTree
self.updateWordCounts = self.novelTree.updateWordCounts
@@ -79,6 +93,21 @@ class GuiNovelView(QWidget):
self.novelTree.clearTree()
return
+ def openProjectTasks(self):
+ """Run tasks related to opening a project.
+ """
+ lastNovel = self.theProject.lastNovel
+ if lastNovel is None:
+ lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL)
+
+ logger.debug("Setting novel tree to root item '%s'", lastNovel)
+
+ self.clearProject()
+ self.novelBar.rebuildNovelRootMenu(selHandle=lastNovel)
+ self.novelTree.refreshTree(rootHandle=lastNovel, overRide=True)
+
+ return
+
def setFocus(self):
"""Forward the set focus call to the tree widget.
"""
@@ -90,16 +119,114 @@ class GuiNovelView(QWidget):
"""
return self.novelTree.hasFocus()
+ ##
+ # Public Slots
+ ##
+
+ @pyqtSlot(str)
+ def updateRootItem(self, tHandle):
+ """Should be called whenever a root folders changes.
+ """
+ self.novelBar.rebuildNovelRootMenu()
+ return
+
# END Class GuiNovelView
class GuiNovelToolBar(QWidget):
+ rootFolderSelectionChanged = pyqtSignal(str)
+
def __init__(self, novelView):
QTreeWidget.__init__(self, novelView)
- self.mainConf = novelwriter.CONFIG
- self.novelView = novelView
+ logger.debug("Initialising GuiNovelToolBar ...")
+
+ self.mainConf = novelwriter.CONFIG
+ self.novelView = novelView
+ self.theProject = novelView.mainGui.theProject
+ self.mainTheme = novelView.mainGui.mainTheme
+
+ iPx = self.mainTheme.baseIconSize
+ mPx = self.mainConf.pxInt(4)
+
+ self.setContentsMargins(0, 0, 0, 0)
+ self.setAutoFillBackground(True)
+
+ qPalette = self.palette()
+ qPalette.setBrush(QPalette.Window, qPalette.base())
+ self.setPalette(qPalette)
+
+ fadeCol = qPalette.text().color()
+ buttonStyle = (
+ "QToolButton {{padding: {0}px; border: none; background: transparent;}} "
+ "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
+ ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue())
+
+ # Widget Label
+ self.viewLabel = QLabel("%s" % self.tr("Novel Outline"))
+ self.viewLabel.setContentsMargins(0, 0, 0, 0)
+ self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+
+ # Novel Root Menu
+ self.mRoot = QMenu()
+
+ self.tbRoot = QToolButton(self)
+ self.tbRoot.setToolTip(self.tr("Novel Root"))
+ self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]))
+ self.tbRoot.setIconSize(QSize(iPx, iPx))
+ self.tbRoot.setStyleSheet(buttonStyle)
+ self.tbRoot.setMenu(self.mRoot)
+ self.tbRoot.setPopupMode(QToolButton.InstantPopup)
+
+ # More Options Menu
+ self.mMore = QMenu()
+
+ self.tbMore = QToolButton(self)
+ self.tbMore.setToolTip(self.tr("More Options"))
+ self.tbMore.setIcon(self.mainTheme.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.viewLabel)
+ self.outerBox.addWidget(self.tbRoot)
+ self.outerBox.addWidget(self.tbMore)
+ self.outerBox.setContentsMargins(mPx, mPx, 0, mPx)
+ self.outerBox.setSpacing(0)
+
+ self.setLayout(self.outerBox)
+
+ logger.debug("GuiNovelToolBar initialisation complete")
+
+ return
+
+ ##
+ # Methods
+ ##
+
+ def rebuildNovelRootMenu(self, selHandle=None):
+ """Build the novel root menu.
+ """
+ self.mRoot.clear()
+
+ agRoot = QActionGroup(self.mRoot)
+ for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(nwItemClass.NOVEL)):
+ aRoot = self.mRoot.addAction(nwItem.itemName)
+ aRoot.setData(tHandle)
+ aRoot.setCheckable(True)
+ aRoot.triggered.connect(
+ lambda n, tHandle=tHandle: self.rootFolderSelectionChanged.emit(tHandle)
+ )
+ agRoot.addAction(aRoot)
+
+ if n == 0:
+ aRoot.setChecked(True)
+ if selHandle == tHandle:
+ aRoot.setChecked(True)
return
@@ -128,41 +255,32 @@ class GuiNovelTree(QTreeWidget):
self._lastBuild = 0
# Build GUI
+ # =========
+
iPx = self.mainTheme.baseIconSize
- self.setFrameStyle(QFrame.NoFrame)
+ cMg = self.mainConf.pxInt(6)
+
self.setIconSize(QSize(iPx, iPx))
+ self.setFrameStyle(QFrame.NoFrame)
+ self.setHeaderHidden(True)
self.setIndentation(iPx)
self.setColumnCount(3)
- self.setHeaderLabels([
- self.tr("Novel Outline"),
- self.tr("Words"),
- self.tr("POV")
- ])
- self.itemDoubleClicked.connect(self._treeDoubleClick)
- self.itemSelectionChanged.connect(self._itemSelected)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setExpandsOnDoubleClick(False)
self.setDragEnabled(False)
- treeHeadItem = self.headerItem()
- treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
- treeHeadItem.setToolTip(self.C_TITLE, self.tr("Section title"))
- treeHeadItem.setToolTip(self.C_WORDS, self.tr("Word count"))
- treeHeadItem.setToolTip(self.C_POV, self.tr("Point-of-view character"))
-
+ # 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_TITLE, QHeaderView.Stretch)
+ treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents)
+ treeHeader.setSectionResizeMode(self.C_POV, QHeaderView.ResizeToContents)
- # Get user's column width preferences for NAME and COUNT
- treeColWidth = self.mainConf.getNovelColWidths()
- if len(treeColWidth) <= 3:
- for colN, colW in enumerate(treeColWidth):
- self.setColumnWidth(colN, colW)
-
- # The last column should just auto-scale
- self.resizeColumnToContents(self.C_POV)
+ # Connect signals
+ self.itemDoubleClicked.connect(self._treeDoubleClick)
+ self.itemSelectionChanged.connect(self._treeSelectionChange)
# Set custom settings
self.initSettings()
@@ -199,12 +317,15 @@ class GuiNovelTree(QTreeWidget):
self._lastBuild = 0
return
- def refreshTree(self, overRide=False):
+ def refreshTree(self, rootHandle=None, overRide=False):
"""Called whenever the Novel tab is activated.
"""
logger.verbose("Requesting refresh of the novel tree")
+ if rootHandle is None:
+ rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL)
+
treeChanged = self.mainGui.projView.changedSince(self._lastBuild)
- indexChanged = self.theProject.index.indexChangedSince(self._lastBuild)
+ indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
if not (treeChanged or indexChanged or overRide):
logger.verbose("No changes have been made to the novel index")
return
@@ -214,7 +335,8 @@ class GuiNovelTree(QTreeWidget):
if selItem:
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
- self._populateTree()
+ self._populateTree(rootHandle)
+ self.theProject.setLastNovelViewed(rootHandle)
if titleKey is not None and titleKey in self._treeMap:
self._treeMap[titleKey].setSelected(True)
@@ -268,39 +390,39 @@ class GuiNovelTree(QTreeWidget):
if tHandle is None:
return
- self.mainGui.viewDocument(tHandle)
+ self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "")
return
##
- # Slots
+ # Private Slots
##
- def _treeDoubleClick(self, tItem, tCol):
+ @pyqtSlot()
+ def _treeSelectionChange(self):
+ """Extract the handle and line number of the currently selected
+ title, and send it to the tree meta panel.
+ """
+ tHandle, _ = self.getSelectedHandle()
+ if tHandle is not None:
+ self.novelView.selectedItemChanged.emit(tHandle)
+ return
+
+ @pyqtSlot("QTreeWidgetItem*", int)
+ def _treeDoubleClick(self, tItem, colNo):
"""Extract the handle and line number of the title double-
clicked, and send it to the main gui class for opening in the
document editor.
"""
tHandle, tLine = self.getSelectedHandle()
- self.mainGui.openDocument(tHandle, tLine=tLine-1, doScroll=True)
- return
-
- def _itemSelected(self):
- """Extract the handle and line number of the currently selected
- title, and send it to the tree meta panel.
- """
- selItems = self.selectedItems()
- if selItems:
- tHandle = selItems[0].data(self.C_TITLE, Qt.UserRole)[0]
- self.mainGui.itemDetails.updateViewBox(tHandle)
-
+ self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, tLine, "")
return
##
# Internal Functions
##
- def _populateTree(self):
+ def _populateTree(self, rootHandle):
"""Build the tree based on the project index.
"""
self.clearTree()
@@ -309,7 +431,9 @@ class GuiNovelTree(QTreeWidget):
currChapter = None
currScene = None
- for tKey, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True):
+ logger.verbose("Building novel tree for root item '%s'", rootHandle)
+ novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
+ for tKey, tHandle, sTitle, novIdx in novStruct:
tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx)
self._treeMap[tKey] = tItem
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index b9bfda0d..4f320bfc 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -61,7 +61,7 @@ class GuiProjectView(QWidget):
# Signals for user interaction with the project tree
selectedItemChanged = pyqtSignal(str)
- openDocumentRequest = pyqtSignal(str, Enum)
+ openDocumentRequest = pyqtSignal(str, Enum, int, str)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
@@ -192,10 +192,10 @@ class GuiProjectToolBar(QWidget):
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue())
- # Tree Label
- self.projLabel = QLabel("%s" % self.tr("Project Content"))
- self.projLabel.setContentsMargins(0, 0, 0, 0)
- self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ # Widget Label
+ self.viewLabel = QLabel("%s" % self.tr("Project Content"))
+ self.viewLabel.setContentsMargins(0, 0, 0, 0)
+ self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Move Buttons
self.tbMoveU = QToolButton(self)
@@ -285,7 +285,7 @@ class GuiProjectToolBar(QWidget):
# Assemble
self.outerBox = QHBoxLayout()
- self.outerBox.addWidget(self.projLabel)
+ self.outerBox.addWidget(self.viewLabel)
self.outerBox.addWidget(self.tbMoveU)
self.outerBox.addWidget(self.tbMoveD)
self.outerBox.addWidget(self.tbAdd)
@@ -337,9 +337,8 @@ class GuiProjectTree(QTreeWidget):
self._lastMove = {}
self._timeChanged = 0
- ##
- # Build GUI
- ##
+ # Build GUI
+ # =========
# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
@@ -348,6 +347,7 @@ class GuiProjectTree(QTreeWidget):
# Tree Settings
iPx = self.mainTheme.baseIconSize
cMg = self.mainConf.pxInt(6)
+
self.setIconSize(QSize(iPx, iPx))
self.setFrameStyle(QFrame.NoFrame)
self.setExpandsOnDoubleClick(False)
@@ -972,7 +972,7 @@ class GuiProjectTree(QTreeWidget):
return
if tItem.itemType == nwItemType.FILE:
- self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT)
+ self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "")
else:
trItem = self._getTreeItem(tHandle)
if trItem is not None:
@@ -1016,11 +1016,11 @@ class GuiProjectTree(QTreeWidget):
if isFile:
ctxMenu.addAction(
self.tr("Open Document"),
- lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT)
+ lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "")
)
ctxMenu.addAction(
self.tr("View Document"),
- lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW)
+ lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "")
)
ctxMenu.addSeparator()
@@ -1112,7 +1112,7 @@ class GuiProjectTree(QTreeWidget):
return
if tItem.itemType == nwItemType.FILE:
- self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW)
+ self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "")
return
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 6fa52eb6..e06b9edd 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -205,6 +205,10 @@ class GuiMain(QMainWindow):
self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo)
self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox)
self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem)
+ self.projView.rootFolderChanged.connect(self.novelView.updateRootItem)
+
+ self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
+ self.novelView.openDocumentRequest.connect(self._openDocument)
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
@@ -362,6 +366,7 @@ class GuiMain(QMainWindow):
self.saveProject()
self.docEditor.setDictionaries()
self.outlineView.updateRootItem(None)
+ self.novelView.openProjectTasks()
self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
@@ -509,6 +514,7 @@ class GuiMain(QMainWindow):
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.statusBar.setRefTime(self.theProject.projOpened)
self.outlineView.updateRootItem(None)
+ self.novelView.openProjectTasks()
self._updateStatusWordCount()
# Restore previously open documents, if any
@@ -825,7 +831,7 @@ class GuiMain(QMainWindow):
"""Rebuild the project tree.
"""
self.projView.populateTree()
- self.novelView.refreshTree()
+ # self.novelView.refreshTree()
return
def requestNovelTreeRefresh(self):
@@ -1468,15 +1474,15 @@ class GuiMain(QMainWindow):
self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}")
return
- @pyqtSlot(str, Enum)
- def _openDocument(self, tHandle, tMode):
- """Handle an open document request.
+ @pyqtSlot(str, Enum, int, str)
+ def _openDocument(self, tHandle, tMode, tLine, tAnchor):
+ """Handle an open document request from one of the tree views.
"""
if tHandle is not None:
if tMode == nwDocMode.EDIT:
- self.openDocument(tHandle, changeFocus=False)
+ self.openDocument(tHandle, tLine=tLine, changeFocus=False)
elif tMode == nwDocMode.VIEW:
- self.viewDocument(tHandle=tHandle)
+ self.viewDocument(tHandle=tHandle, tAnchor=(tAnchor or None))
return
@pyqtSlot(nwView)
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index e8ee5c59..af509400 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,13 +1,13 @@
-
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 1345
- 229
- 68286
+ 1367
+ 231
+ 68784
False
@@ -17,6 +17,8 @@
True
636b6aa9b697b
636b6aa9b697b
+ 7031beac91f75
+ None
1363
954
409
@@ -70,11 +72,11 @@
Chapter One
-
-
+
Making a Scene
-
-
+
Another Scene
-
@@ -86,7 +88,7 @@
A Note on Structure
-
-
+
Chapter Two
-