Add novel tree toolbar and connect actions

This commit is contained in:
Veronica Berglyd Olsen
2022-06-12 17:47:32 +02:00
parent b3eb276437
commit 52683d5a21
5 changed files with 224 additions and 76 deletions
+16
View File
@@ -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.
"""
+174 -50
View File
@@ -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("<b>%s</b>" % 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
+13 -13
View File
@@ -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("<b>%s</b>" % self.tr("Project Content"))
self.projLabel.setContentsMargins(0, 0, 0, 0)
self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Widget Label
self.viewLabel = QLabel("<b>%s</b>" % 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
+12 -6
View File
@@ -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)
+9 -7
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-06-11 13:36:01">
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-06-12 17:37:35">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>1345</saveCount>
<autoCount>229</autoCount>
<editTime>68286</editTime>
<saveCount>1367</saveCount>
<autoCount>231</autoCount>
<editTime>68784</editTime>
</project>
<settings>
<doBackup>False</doBackup>
@@ -17,6 +17,8 @@
<autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>636b6aa9b697b</lastViewed>
<lastNovel>7031beac91f75</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>1363</lastWordCount>
<novelWordCount>954</novelWordCount>
<notesWordCount>409</notesWordCount>
@@ -70,11 +72,11 @@
<name status="sf24ce6" import="ia857f0" exported="True">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="2687" wordCount="479" paraCount="14" cursorPos="61"/>
<meta expanded="False" charCount="2687" wordCount="479" paraCount="14" cursorPos="2183"/>
<name status="s90e6c9" import="ia857f0" exported="True">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="548" wordCount="108" paraCount="3" cursorPos="649"/>
<meta expanded="False" charCount="548" wordCount="108" paraCount="3" cursorPos="465"/>
<name status="s90e6c9" import="ia857f0" exported="True">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -86,7 +88,7 @@
<name status="sf24ce6" import="ia857f0" exported="False">A Note on Structure</name>
</item>
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="True" charCount="139" wordCount="28" paraCount="1" cursorPos="343"/>
<meta expanded="True" charCount="139" wordCount="28" paraCount="1" cursorPos="188"/>
<name status="s90e6c9" import="ia857f0" exported="True">Chapter Two</name>
</item>
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">