From ca48e3716309f28844e885ab54ff7a8004f75c7b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 15 May 2022 17:45:51 +0200
Subject: [PATCH 01/12] Move the outline view into a wrapper widget
---
novelwriter/gui/__init__.py | 2 -
novelwriter/gui/outline.py | 407 +++++++++++++++++++++++++++++-
novelwriter/gui/outlinedetails.py | 355 --------------------------
novelwriter/guimain.py | 30 +--
4 files changed, 404 insertions(+), 390 deletions(-)
delete mode 100644 novelwriter/gui/outlinedetails.py
diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py
index 8364e342..e3560a99 100644
--- a/novelwriter/gui/__init__.py
+++ b/novelwriter/gui/__init__.py
@@ -25,7 +25,6 @@ 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.outlinedetails import GuiOutlineDetails
from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.gui.statusbar import GuiMainStatus
from novelwriter.gui.theme import GuiTheme
@@ -40,7 +39,6 @@ __all__ = [
"GuiMainStatus",
"GuiNovelTree",
"GuiOutline",
- "GuiOutlineDetails",
"GuiProjectTree",
"GuiTheme",
"GuiViewsBar",
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 1ac83ff1..3c31133a 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -4,7 +4,9 @@ novelWriter – GUI Project Outline
GUI class for the project outline view
File History:
-Created: 2019-11-16 [0.4.1]
+Created: 2019-11-16 [0.4.1] GuiOutlineView, GuiOutlineHeaderMenu
+Created: 2020-06-02 [0.7.0] GuiOutlineDetails
+Created: 2022-05-15 [1.7b1] GuiOutline
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -28,19 +30,79 @@ import novelwriter
from time import time
-from PyQt5.QtCore import Qt, QSize, pyqtSlot
+from PyQt5.QtCore import (
+ Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
+)
from PyQt5.QtWidgets import (
- QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView
+ QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
+ QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
+ QWidget
)
-from novelwriter.enum import nwItemLayout, nwItemType, nwOutline
+from novelwriter.enum import nwItemLayout, nwItemType, nwOutline, nwView
from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels
+
logger = logging.getLogger(__name__)
-class GuiOutline(QTreeWidget):
+class GuiOutline(QWidget):
+
+ viewChangeRequested = pyqtSignal(nwView)
+
+ def __init__(self, theParent):
+ QWidget.__init__(self, theParent)
+
+ self.mainConf = novelwriter.CONFIG
+ self.theParent = theParent
+ self.theProject = theParent.theProject
+
+ self.outlineView = GuiOutlineView(self)
+ self.outlineData = GuiOutlineDetails(self)
+
+ self.splitOutline = QSplitter(Qt.Vertical)
+ self.splitOutline.addWidget(self.outlineView)
+ self.splitOutline.addWidget(self.outlineData)
+ self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
+
+ # Assemble
+ self.outerBox = QVBoxLayout()
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
+ self.outerBox.addWidget(self.splitOutline)
+
+ self.setLayout(self.outerBox)
+
+ return
+
+ ##
+ # Methods
+ ##
+
+ def splitSizes(self):
+ return self.splitOutline.sizes()
+
+ def clearOutline(self):
+ self.outlineData.clearDetails()
+ return
+
+ def initOutline(self):
+ self.outlineView.initOutline()
+ self.outlineData.initDetails()
+ return
+
+ def closeOutline(self):
+ self.outlineView.closeOutline()
+ return
+
+ def refreshView(self, overRide=False, novelChanged=False):
+ self.outlineView.refreshTree(overRide=overRide, novelChanged=novelChanged)
+ return
+
+# END Class GuiOutline
+
+
+class GuiOutlineView(QTreeWidget):
DEF_WIDTH = {
nwOutline.TITLE: 200,
@@ -82,17 +144,18 @@ class GuiOutline(QTreeWidget):
nwOutline.SYNOP: False,
}
- def __init__(self, theParent):
- QTreeWidget.__init__(self, theParent)
+ def __init__(self, theOutline):
+ QTreeWidget.__init__(self, theOutline)
logger.debug("Initialising GuiOutline ...")
self.mainConf = novelwriter.CONFIG
- self.theParent = theParent
- self.theProject = theParent.theProject
- self.theTheme = theParent.theTheme
- self.theIndex = theParent.theIndex
- self.optState = theParent.theProject.optState
+ self.theOutline = theOutline
+ self.theParent = theOutline.theParent
+ self.theProject = theOutline.theParent.theProject
+ self.theTheme = theOutline.theParent.theTheme
+ self.theIndex = theOutline.theParent.theIndex
+ self.optState = theOutline.theParent.theProject.optState
self.headerMenu = GuiOutlineHeaderMenu(self)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
@@ -233,7 +296,7 @@ class GuiOutline(QTreeWidget):
if selItems:
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
- self.theParent.projMeta.showItem(tHandle, sTitle)
+ self.theOutline.outlineData.showItem(tHandle, sTitle)
self.theParent.treeView.setSelectedHandle(tHandle)
return
@@ -477,7 +540,7 @@ class GuiOutline(QTreeWidget):
return newItem
-# END Class GuiOutline
+# END Class GuiOutlineView
class GuiOutlineHeaderMenu(QMenu):
@@ -533,3 +596,319 @@ class GuiOutlineHeaderMenu(QMenu):
return
# END Class GuiOutlineHeaderMenu
+
+
+class GuiOutlineDetails(QScrollArea):
+
+ LVL_MAP = {
+ "H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
+ "H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
+ "H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
+ "H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
+ }
+
+ def __init__(self, theOutline):
+ QScrollArea.__init__(self, theOutline)
+
+ logger.debug("Initialising GuiOutlineDetails ...")
+
+ self.mainConf = novelwriter.CONFIG
+ self.theOutline = theOutline
+ self.theParent = theOutline.theParent
+ self.theProject = theOutline.theParent.theProject
+ self.theTheme = theOutline.theParent.theTheme
+ self.theIndex = theOutline.theParent.theIndex
+ self.optState = theOutline.theParent.theProject.optState
+
+ # Sizes
+ minTitle = 30*self.theTheme.textNWidth
+ maxTitle = 40*self.theTheme.textNWidth
+ wCount = self.theTheme.getTextWidth("999,999")
+ hSpace = int(self.mainConf.pxInt(10))
+ vSpace = int(self.mainConf.pxInt(4))
+
+ # Details Area
+ self.titleLabel = QLabel("%s" % self.tr("Title"))
+ self.fileLabel = QLabel("%s" % self.tr("Document"))
+ self.itemLabel = QLabel("%s" % self.tr("Status"))
+ self.titleValue = QLabel("")
+ self.fileValue = QLabel("")
+ self.itemValue = QLabel("")
+
+ self.titleValue.setMinimumWidth(minTitle)
+ self.titleValue.setMaximumWidth(maxTitle)
+ self.fileValue.setMinimumWidth(minTitle)
+ self.fileValue.setMaximumWidth(maxTitle)
+ self.itemValue.setMinimumWidth(minTitle)
+ self.itemValue.setMaximumWidth(maxTitle)
+
+ # Stats Area
+ self.cCLabel = QLabel("%s" % self.tr("Characters"))
+ self.wCLabel = QLabel("%s" % self.tr("Words"))
+ self.pCLabel = QLabel("%s" % self.tr("Paragraphs"))
+ self.cCValue = QLabel("")
+ self.wCValue = QLabel("")
+ self.pCValue = QLabel("")
+
+ self.cCValue.setMinimumWidth(wCount)
+ self.wCValue.setMinimumWidth(wCount)
+ self.pCValue.setMinimumWidth(wCount)
+ self.cCValue.setAlignment(Qt.AlignRight)
+ self.wCValue.setAlignment(Qt.AlignRight)
+ self.pCValue.setAlignment(Qt.AlignRight)
+
+ # Synopsis
+ self.synopLabel = QLabel("%s" % self.tr("Synopsis"))
+ self.synopValue = QLabel("")
+ self.synopLWrap = QHBoxLayout()
+ self.synopValue.setWordWrap(True)
+ self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
+ self.synopLWrap.addWidget(self.synopValue, 1)
+
+ # Tags
+ self.povKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
+ self.focKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
+ self.chrKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
+ self.pltKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
+ self.timKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
+ self.wldKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
+ self.objKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
+ self.entKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
+ self.cstKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
+
+ self.povKeyLWrap = QHBoxLayout()
+ self.focKeyLWrap = QHBoxLayout()
+ self.chrKeyLWrap = QHBoxLayout()
+ self.pltKeyLWrap = QHBoxLayout()
+ self.timKeyLWrap = QHBoxLayout()
+ self.wldKeyLWrap = QHBoxLayout()
+ self.objKeyLWrap = QHBoxLayout()
+ self.entKeyLWrap = QHBoxLayout()
+ self.cstKeyLWrap = QHBoxLayout()
+
+ self.povKeyValue = QLabel("")
+ self.focKeyValue = QLabel("")
+ self.chrKeyValue = QLabel("")
+ self.pltKeyValue = QLabel("")
+ self.timKeyValue = QLabel("")
+ self.wldKeyValue = QLabel("")
+ self.objKeyValue = QLabel("")
+ self.entKeyValue = QLabel("")
+ self.cstKeyValue = QLabel("")
+
+ self.povKeyValue.setWordWrap(True)
+ self.focKeyValue.setWordWrap(True)
+ self.chrKeyValue.setWordWrap(True)
+ self.pltKeyValue.setWordWrap(True)
+ self.timKeyValue.setWordWrap(True)
+ self.wldKeyValue.setWordWrap(True)
+ self.objKeyValue.setWordWrap(True)
+ self.entKeyValue.setWordWrap(True)
+ self.cstKeyValue.setWordWrap(True)
+
+ self.povKeyValue.linkActivated.connect(self._tagClicked)
+ self.focKeyValue.linkActivated.connect(self._tagClicked)
+ self.chrKeyValue.linkActivated.connect(self._tagClicked)
+ self.pltKeyValue.linkActivated.connect(self._tagClicked)
+ self.timKeyValue.linkActivated.connect(self._tagClicked)
+ self.wldKeyValue.linkActivated.connect(self._tagClicked)
+ self.objKeyValue.linkActivated.connect(self._tagClicked)
+ self.entKeyValue.linkActivated.connect(self._tagClicked)
+ self.cstKeyValue.linkActivated.connect(self._tagClicked)
+
+ self.povKeyLWrap.addWidget(self.povKeyValue, 1)
+ self.focKeyLWrap.addWidget(self.focKeyValue, 1)
+ self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
+ self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
+ self.timKeyLWrap.addWidget(self.timKeyValue, 1)
+ self.wldKeyLWrap.addWidget(self.wldKeyValue, 1)
+ self.objKeyLWrap.addWidget(self.objKeyValue, 1)
+ self.entKeyLWrap.addWidget(self.entKeyValue, 1)
+ self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
+
+ # Selected Item Details
+ self.mainGroup = QGroupBox(self.tr("Title Details"), self)
+ self.mainForm = QGridLayout()
+ self.mainGroup.setLayout(self.mainForm)
+
+ self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
+ self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
+ self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
+ self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
+ self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
+
+ self.mainForm.setColumnStretch(1, 1)
+ self.mainForm.setRowStretch(4, 1)
+ self.mainForm.setHorizontalSpacing(hSpace)
+ self.mainForm.setVerticalSpacing(vSpace)
+
+ # Selected Item Tags
+ self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
+ self.tagsForm = QGridLayout()
+ self.tagsGroup.setLayout(self.tagsForm)
+
+ self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+ self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
+
+ self.tagsForm.setColumnStretch(1, 1)
+ self.tagsForm.setRowStretch(8, 1)
+ self.tagsForm.setHorizontalSpacing(hSpace)
+ self.tagsForm.setVerticalSpacing(vSpace)
+
+ # Assemble
+ self.outerWidget = QWidget()
+ self.outerBox = QHBoxLayout()
+ self.outerBox.addWidget(self.mainGroup, 0)
+ self.outerBox.addWidget(self.tagsGroup, 1)
+
+ self.outerWidget.setLayout(self.outerBox)
+ self.setWidget(self.outerWidget)
+
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ self.setWidgetResizable(True)
+
+ self.initDetails()
+
+ logger.debug("GuiOutlineDetails initialisation complete")
+
+ return
+
+ def initDetails(self):
+ """Set or update outline settings.
+ """
+ # Scroll bars
+ if self.mainConf.hideVScroll:
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ else:
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+
+ if self.mainConf.hideHScroll:
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ else:
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+
+ return
+
+ def clearDetails(self):
+ """Clear all the data labels.
+ """
+ self.titleLabel.setText("%s" % self.tr("Title"))
+ self.titleValue.setText("")
+ self.fileValue.setText("")
+ self.itemValue.setText("")
+ self.cCValue.setText("")
+ self.wCValue.setText("")
+ self.pCValue.setText("")
+ self.synopValue.setText("")
+ self.povKeyValue.setText("")
+ self.focKeyValue.setText("")
+ self.chrKeyValue.setText("")
+ self.pltKeyValue.setText("")
+ self.timKeyValue.setText("")
+ self.wldKeyValue.setText("")
+ self.objKeyValue.setText("")
+ self.entKeyValue.setText("")
+ self.cstKeyValue.setText("")
+ return
+
+ def showItem(self, tHandle, sTitle):
+ """Update the content of the tree with the given handle and line
+ number pointing to a header.
+ """
+ nwItem = self.theProject.projTree[tHandle]
+ novIdx = self.theIndex.getNovelData(tHandle, sTitle)
+ theRefs = self.theIndex.getReferences(tHandle, sTitle)
+ if nwItem is None or novIdx is None:
+ return False
+
+ if novIdx["level"] in self.LVL_MAP:
+ self.titleLabel.setText("%s" % self.tr(self.LVL_MAP[novIdx["level"]]))
+ else:
+ self.titleLabel.setText("%s" % self.tr("Title"))
+ self.titleValue.setText(novIdx["title"])
+
+ itemStatus, _ = nwItem.getImportStatus()
+
+ self.fileValue.setText(nwItem.itemName)
+ self.itemValue.setText(itemStatus)
+
+ cC = checkInt(novIdx["cCount"], 0)
+ wC = checkInt(novIdx["wCount"], 0)
+ pC = checkInt(novIdx["pCount"], 0)
+
+ self.cCValue.setText(f"{cC:n}")
+ self.wCValue.setText(f"{wC:n}")
+ self.pCValue.setText(f"{pC:n}")
+
+ self.synopValue.setText(novIdx["synopsis"])
+
+ self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
+ self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
+ self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
+ self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
+ self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
+ self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY))
+ self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY))
+ self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY))
+ self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY))
+
+ return True
+
+ ##
+ # Slots
+ ##
+
+ def _tagClicked(self, theLink):
+ """Capture the click of a tag in the right-most column.
+ """
+ logger.verbose("Clicked link: '%s'", theLink)
+ if len(theLink) > 0:
+ theBits = theLink.split("=")
+ if len(theBits) == 2:
+ self.theOutline.viewChangeRequested.emit(nwView.PROJECT)
+ self.theParent.docViewer.loadFromTag(theBits[1])
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _formatTags(self, theRefs, theKey):
+ """Format the tags as clickable links.
+ """
+ if theKey not in theRefs:
+ return ""
+ refTags = []
+ for tTag in theRefs[theKey]:
+ refTags.append("%s" % (
+ theKey[1:], tTag, tTag
+ ))
+ return ", ".join(refTags)
+
+# END Class GuiOutlineDetails
diff --git a/novelwriter/gui/outlinedetails.py b/novelwriter/gui/outlinedetails.py
deleted file mode 100644
index 358a6531..00000000
--- a/novelwriter/gui/outlinedetails.py
+++ /dev/null
@@ -1,355 +0,0 @@
-"""
-novelWriter – GUI Project Outline Details
-=========================================
-GUI class for the project outline details panel
-
-File History:
-Created: 2020-06-02 [0.7.0]
-
-This file is a part of novelWriter
-Copyright 2018–2022, Veronica Berglyd Olsen
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but
-WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-"""
-
-import logging
-import novelwriter
-
-from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP, pyqtSignal
-from PyQt5.QtWidgets import (
- QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel
-)
-
-from novelwriter.enum import nwView
-from novelwriter.common import checkInt
-from novelwriter.constants import trConst, nwKeyWords, nwLabels
-
-logger = logging.getLogger(__name__)
-
-
-class GuiOutlineDetails(QScrollArea):
-
- LVL_MAP = {
- "H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
- "H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
- "H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
- "H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
- }
-
- viewChangeRequested = pyqtSignal(nwView)
-
- def __init__(self, theParent):
- QScrollArea.__init__(self, theParent)
-
- logger.debug("Initialising GuiOutlineDetails ...")
-
- self.mainConf = novelwriter.CONFIG
- self.theParent = theParent
- self.theProject = theParent.theProject
- self.theTheme = theParent.theTheme
- self.theIndex = theParent.theIndex
- self.optState = theParent.theProject.optState
-
- # Sizes
- minTitle = 30*self.theTheme.textNWidth
- maxTitle = 40*self.theTheme.textNWidth
- wCount = self.theTheme.getTextWidth("999,999")
- hSpace = int(self.mainConf.pxInt(10))
- vSpace = int(self.mainConf.pxInt(4))
-
- # Details Area
- self.titleLabel = QLabel("%s" % self.tr("Title"))
- self.fileLabel = QLabel("%s" % self.tr("Document"))
- self.itemLabel = QLabel("%s" % self.tr("Status"))
- self.titleValue = QLabel("")
- self.fileValue = QLabel("")
- self.itemValue = QLabel("")
-
- self.titleValue.setMinimumWidth(minTitle)
- self.titleValue.setMaximumWidth(maxTitle)
- self.fileValue.setMinimumWidth(minTitle)
- self.fileValue.setMaximumWidth(maxTitle)
- self.itemValue.setMinimumWidth(minTitle)
- self.itemValue.setMaximumWidth(maxTitle)
-
- # Stats Area
- self.cCLabel = QLabel("%s" % self.tr("Characters"))
- self.wCLabel = QLabel("%s" % self.tr("Words"))
- self.pCLabel = QLabel("%s" % self.tr("Paragraphs"))
- self.cCValue = QLabel("")
- self.wCValue = QLabel("")
- self.pCValue = QLabel("")
-
- self.cCValue.setMinimumWidth(wCount)
- self.wCValue.setMinimumWidth(wCount)
- self.pCValue.setMinimumWidth(wCount)
- self.cCValue.setAlignment(Qt.AlignRight)
- self.wCValue.setAlignment(Qt.AlignRight)
- self.pCValue.setAlignment(Qt.AlignRight)
-
- # Synopsis
- self.synopLabel = QLabel("%s" % self.tr("Synopsis"))
- self.synopValue = QLabel("")
- self.synopLWrap = QHBoxLayout()
- self.synopValue.setWordWrap(True)
- self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
- self.synopLWrap.addWidget(self.synopValue, 1)
-
- # Tags
- self.povKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
- self.focKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
- self.chrKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
- self.pltKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
- self.timKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
- self.wldKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
- self.objKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
- self.entKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
- self.cstKeyLabel = QLabel("%s" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
-
- self.povKeyLWrap = QHBoxLayout()
- self.focKeyLWrap = QHBoxLayout()
- self.chrKeyLWrap = QHBoxLayout()
- self.pltKeyLWrap = QHBoxLayout()
- self.timKeyLWrap = QHBoxLayout()
- self.wldKeyLWrap = QHBoxLayout()
- self.objKeyLWrap = QHBoxLayout()
- self.entKeyLWrap = QHBoxLayout()
- self.cstKeyLWrap = QHBoxLayout()
-
- self.povKeyValue = QLabel("")
- self.focKeyValue = QLabel("")
- self.chrKeyValue = QLabel("")
- self.pltKeyValue = QLabel("")
- self.timKeyValue = QLabel("")
- self.wldKeyValue = QLabel("")
- self.objKeyValue = QLabel("")
- self.entKeyValue = QLabel("")
- self.cstKeyValue = QLabel("")
-
- self.povKeyValue.setWordWrap(True)
- self.focKeyValue.setWordWrap(True)
- self.chrKeyValue.setWordWrap(True)
- self.pltKeyValue.setWordWrap(True)
- self.timKeyValue.setWordWrap(True)
- self.wldKeyValue.setWordWrap(True)
- self.objKeyValue.setWordWrap(True)
- self.entKeyValue.setWordWrap(True)
- self.cstKeyValue.setWordWrap(True)
-
- self.povKeyValue.linkActivated.connect(self._tagClicked)
- self.focKeyValue.linkActivated.connect(self._tagClicked)
- self.chrKeyValue.linkActivated.connect(self._tagClicked)
- self.pltKeyValue.linkActivated.connect(self._tagClicked)
- self.timKeyValue.linkActivated.connect(self._tagClicked)
- self.wldKeyValue.linkActivated.connect(self._tagClicked)
- self.objKeyValue.linkActivated.connect(self._tagClicked)
- self.entKeyValue.linkActivated.connect(self._tagClicked)
- self.cstKeyValue.linkActivated.connect(self._tagClicked)
-
- self.povKeyLWrap.addWidget(self.povKeyValue, 1)
- self.focKeyLWrap.addWidget(self.focKeyValue, 1)
- self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
- self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
- self.timKeyLWrap.addWidget(self.timKeyValue, 1)
- self.wldKeyLWrap.addWidget(self.wldKeyValue, 1)
- self.objKeyLWrap.addWidget(self.objKeyValue, 1)
- self.entKeyLWrap.addWidget(self.entKeyValue, 1)
- self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
-
- # Selected Item Details
- self.mainGroup = QGroupBox(self.tr("Title Details"), self)
- self.mainForm = QGridLayout()
- self.mainGroup.setLayout(self.mainForm)
-
- self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
- self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
- self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
- self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
- self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
-
- self.mainForm.setColumnStretch(1, 1)
- self.mainForm.setRowStretch(4, 1)
- self.mainForm.setHorizontalSpacing(hSpace)
- self.mainForm.setVerticalSpacing(vSpace)
-
- # Selected Item Tags
- self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
- self.tagsForm = QGridLayout()
- self.tagsGroup.setLayout(self.tagsForm)
-
- self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
- self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
-
- self.tagsForm.setColumnStretch(1, 1)
- self.tagsForm.setRowStretch(8, 1)
- self.tagsForm.setHorizontalSpacing(hSpace)
- self.tagsForm.setVerticalSpacing(vSpace)
-
- # Assemble
- self.outerWidget = QWidget()
- self.outerBox = QHBoxLayout()
- self.outerBox.addWidget(self.mainGroup, 0)
- self.outerBox.addWidget(self.tagsGroup, 1)
-
- self.outerWidget.setLayout(self.outerBox)
- self.setWidget(self.outerWidget)
-
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
- self.setWidgetResizable(True)
-
- self.initDetails()
-
- logger.debug("GuiOutlineDetails initialisation complete")
-
- return
-
- def initDetails(self):
- """Set or update outline settings.
- """
- # Scroll bars
- if self.mainConf.hideVScroll:
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
- else:
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
-
- if self.mainConf.hideHScroll:
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
- else:
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
-
- return
-
- def clearDetails(self):
- """Clear all the data labels.
- """
- self.titleLabel.setText("%s" % self.tr("Title"))
- self.titleValue.setText("")
- self.fileValue.setText("")
- self.itemValue.setText("")
- self.cCValue.setText("")
- self.wCValue.setText("")
- self.pCValue.setText("")
- self.synopValue.setText("")
- self.povKeyValue.setText("")
- self.focKeyValue.setText("")
- self.chrKeyValue.setText("")
- self.pltKeyValue.setText("")
- self.timKeyValue.setText("")
- self.wldKeyValue.setText("")
- self.objKeyValue.setText("")
- self.entKeyValue.setText("")
- self.cstKeyValue.setText("")
- return
-
- def showItem(self, tHandle, sTitle):
- """Update the content of the tree with the given handle and line
- number pointing to a header.
- """
- nwItem = self.theProject.projTree[tHandle]
- novIdx = self.theIndex.getNovelData(tHandle, sTitle)
- theRefs = self.theIndex.getReferences(tHandle, sTitle)
- if nwItem is None or novIdx is None:
- return False
-
- if novIdx["level"] in self.LVL_MAP:
- self.titleLabel.setText("%s" % self.tr(self.LVL_MAP[novIdx["level"]]))
- else:
- self.titleLabel.setText("%s" % self.tr("Title"))
- self.titleValue.setText(novIdx["title"])
-
- itemStatus, _ = nwItem.getImportStatus()
-
- self.fileValue.setText(nwItem.itemName)
- self.itemValue.setText(itemStatus)
-
- cC = checkInt(novIdx["cCount"], 0)
- wC = checkInt(novIdx["wCount"], 0)
- pC = checkInt(novIdx["pCount"], 0)
-
- self.cCValue.setText(f"{cC:n}")
- self.wCValue.setText(f"{wC:n}")
- self.pCValue.setText(f"{pC:n}")
-
- self.synopValue.setText(novIdx["synopsis"])
-
- self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
- self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
- self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
- self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
- self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
- self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY))
- self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY))
- self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY))
- self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY))
-
- return True
-
- ##
- # Slots
- ##
-
- def _tagClicked(self, theLink):
- """Capture the click of a tag in the right-most column.
- """
- logger.verbose("Clicked link: '%s'", theLink)
- if len(theLink) > 0:
- theBits = theLink.split("=")
- if len(theBits) == 2:
- self.viewChangeRequested.emit(nwView.PROJECT)
- self.theParent.docViewer.loadFromTag(theBits[1])
- return
-
- ##
- # Internal Functions
- ##
-
- def _formatTags(self, theRefs, theKey):
- """Format the tags as clickable links.
- """
- if theKey not in theRefs:
- return ""
- refTags = []
- for tTag in theRefs[theKey]:
- refTags.append("%s" % (
- theKey[1:], tTag, tTag
- ))
- return ", ".join(refTags)
-
-# END Class GuiOutlineDetails
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index ad06cc59..645990e9 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -39,8 +39,8 @@ from PyQt5.QtWidgets import (
from novelwriter.gui import (
GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu,
- GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails, GuiProjectTree,
- GuiTheme, GuiViewsBar
+ GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectTree, GuiTheme,
+ GuiViewsBar
)
from novelwriter.dialogs import (
GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences,
@@ -112,7 +112,6 @@ class GuiMain(QMainWindow):
self.docViewer = GuiDocViewer(self)
self.treeMeta = GuiItemDetails(self)
self.projView = GuiOutline(self)
- self.projMeta = GuiOutlineDetails(self)
self.mainMenu = GuiMainMenu(self)
self.viewsBar = GuiViewsBar(self)
@@ -128,7 +127,7 @@ class GuiMain(QMainWindow):
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
self.viewsBar.viewChangeRequested.connect(self._changeView)
- self.projMeta.viewChangeRequested.connect(self._changeView)
+ self.projView.viewChangeRequested.connect(self._changeView)
# Project Tree Stack
self.projStack = QStackedWidget()
@@ -156,12 +155,6 @@ class GuiMain(QMainWindow):
self.splitDocs.addWidget(self.docEditor)
self.splitDocs.addWidget(self.splitView)
- # Splitter : Project Outlie / Outline Details
- self.splitOutline = QSplitter(Qt.Vertical)
- self.splitOutline.addWidget(self.projView)
- self.splitOutline.addWidget(self.projMeta)
- self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
-
# Splitter : Project Tree / Main Tabs
self.splitMain = QSplitter(Qt.Horizontal)
self.splitMain.setContentsMargins(0, 0, mPx, 0)
@@ -172,7 +165,7 @@ class GuiMain(QMainWindow):
# Main Stack : Editor / Outline
self.mainStack = QStackedWidget()
self.mainStack.addWidget(self.splitMain)
- self.mainStack.addWidget(self.splitOutline)
+ self.mainStack.addWidget(self.projView)
self.mainStack.currentChanged.connect(self._mainStackChanged)
# Indices of Splitter Widgets
@@ -185,7 +178,7 @@ class GuiMain(QMainWindow):
# Indices of Tab Widgets
self.idxEditorView = self.mainStack.indexOf(self.splitMain)
- self.idxOutlineView = self.mainStack.indexOf(self.splitOutline)
+ self.idxOutlineView = self.mainStack.indexOf(self.projView)
self.idxTreeView = self.projStack.indexOf(self.treeView)
self.idxNovelView = self.projStack.indexOf(self.novelView)
@@ -293,7 +286,7 @@ class GuiMain(QMainWindow):
self.docEditor.clearEditor()
self.docEditor.setDictionaries()
self.closeDocViewer()
- self.projMeta.clearDetails()
+ self.projView.clearOutline()
# General
self.statusBar.clearStatus()
@@ -904,7 +897,7 @@ class GuiMain(QMainWindow):
logger.verbose("Forcing a rebuild of the Project Outline")
self._changeView(nwView.OUTLINE)
- self.projView.refreshTree(overRide=True)
+ self.projView.refreshView(overRide=True)
return True
@@ -955,7 +948,6 @@ class GuiMain(QMainWindow):
self.treeView.initTree()
self.novelView.initTree()
self.projView.initOutline()
- self.projMeta.initDetails()
self._updateStatusWordCount()
return
@@ -1192,7 +1184,7 @@ class GuiMain(QMainWindow):
if not self.isFocusMode:
self.mainConf.setMainPanePos(self.splitMain.sizes())
self.mainConf.setDocPanePos(self.splitDocs.sizes())
- self.mainConf.setOutlinePanePos(self.splitOutline.sizes())
+ self.mainConf.setOutlinePanePos(self.projView.splitSizes())
if self.viewMeta.isVisible():
self.mainConf.setViewPanePos(self.splitView.sizes())
@@ -1500,7 +1492,7 @@ class GuiMain(QMainWindow):
self.projStack.setCurrentWidget(self.novelView)
elif view == nwView.OUTLINE:
- self.mainStack.setCurrentWidget(self.splitOutline)
+ self.mainStack.setCurrentWidget(self.projView)
elif view == nwView.DETAILS:
self.showProjectDetailsDialog()
@@ -1590,7 +1582,7 @@ class GuiMain(QMainWindow):
logger.verbose("Novel tree changed while Outline tab active")
if self.hasProject:
self.treeView.flushTreeOrder()
- self.projView.refreshTree(novelChanged=True)
+ self.projView.refreshView(novelChanged=True)
return
@@ -1623,7 +1615,7 @@ class GuiMain(QMainWindow):
elif tabIndex == self.idxOutlineView:
logger.verbose("Project outline tab activated")
if self.hasProject:
- self.projView.refreshTree()
+ self.projView.refreshView()
return
From a5b2a06f84ad9ca714fd2f85c1387a330459c529 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 15 May 2022 17:56:36 +0200
Subject: [PATCH 02/12] Fix tests and add missing methods
---
novelwriter/gui/outline.py | 9 ++++
novelwriter/guimain.py | 4 +-
tests/test_gui/test_gui_guimain.py | 6 +--
tests/test_gui/test_gui_outline.py | 69 ++++++++++++++++--------------
4 files changed, 50 insertions(+), 38 deletions(-)
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 3c31133a..652af9d1 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -73,6 +73,9 @@ class GuiOutline(QWidget):
self.setLayout(self.outerBox)
+ # Function Mappings
+ self.getSelectedHandle = self.outlineView.getSelectedHandle
+
return
##
@@ -99,6 +102,12 @@ class GuiOutline(QWidget):
self.outlineView.refreshTree(overRide=overRide, novelChanged=novelChanged)
return
+ def treeFocus(self):
+ return self.outlineView.hasFocus()
+
+ def setTreeFocus(self):
+ return self.outlineView.setFocus()
+
# END Class GuiOutline
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 645990e9..e30654ee 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -780,7 +780,7 @@ class GuiMain(QMainWindow):
tHandle = self.treeView.getSelectedHandle()
elif self.novelView.hasFocus():
tHandle, tLine = self.novelView.getSelectedHandle()
- elif self.projView.hasFocus():
+ elif self.projView.treeFocus():
tHandle, tLine = self.projView.getSelectedHandle()
else:
logger.warning("No item selected")
@@ -1221,7 +1221,7 @@ class GuiMain(QMainWindow):
self.docViewer.setFocus()
elif paneNo == nwWidget.OUTLINE:
self._changeView(nwView.OUTLINE)
- self.projView.setFocus()
+ self.projView.setTreeFocus()
return
def closeDocEditor(self):
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index ddb4f09b..cac62ddf 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -111,12 +111,12 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
# Project Outline has focus
nwGUI.switchFocus(nwWidget.OUTLINE)
with monkeypatch.context() as mp:
- mp.setattr(GuiOutline, "hasFocus", lambda *a: True)
+ mp.setattr(GuiOutline, "treeFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
- actItem = nwGUI.projView.topLevelItem(0)
+ actItem = nwGUI.projView.outlineView.topLevelItem(0)
chpItem = actItem.child(0)
selItem = chpItem.child(0)
- nwGUI.projView.setCurrentItem(selItem)
+ nwGUI.projView.outlineView.setCurrentItem(selItem)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.closeDocument() is True
diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py
index 40c58388..02dd1041 100644
--- a/tests/test_gui/test_gui_outline.py
+++ b/tests/test_gui/test_gui_outline.py
@@ -45,69 +45,72 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.rebuildIndex()
nwGUI.mainStack.setCurrentIndex(nwGUI.idxOutlineView)
- assert nwGUI.projView.topLevelItemCount() > 0
+ outlineView = nwGUI.projView.outlineView
+ outlineData = nwGUI.projView.outlineData
+
+ assert outlineView.topLevelItemCount() > 0
# Context Menu
- nwGUI.projView._headerRightClick(QPoint(1, 1))
- nwGUI.projView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger)
- nwGUI.projView.headerMenu.close()
- qtbot.mouseClick(nwGUI.projView, Qt.LeftButton)
+ outlineView._headerRightClick(QPoint(1, 1))
+ outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger)
+ outlineView.headerMenu.close()
+ qtbot.mouseClick(outlineView, Qt.LeftButton)
- nwGUI.projView._loadHeaderState()
- assert not nwGUI.projView._colHidden[nwOutline.CCOUNT]
+ outlineView._loadHeaderState()
+ assert not outlineView._colHidden[nwOutline.CCOUNT]
# First Item
nwGUI.rebuildOutline()
- selItem = nwGUI.projView.topLevelItem(0)
+ selItem = outlineView.topLevelItem(0)
assert isinstance(selItem, QTreeWidgetItem)
- nwGUI.projView.setCurrentItem(selItem)
- assert nwGUI.projMeta.titleLabel.text() == "Title"
- assert nwGUI.projMeta.titleValue.text() == "Lorem Ipsum"
- assert nwGUI.projMeta.fileValue.text() == "Lorem Ipsum"
- assert nwGUI.projMeta.itemValue.text() == "Finished"
+ outlineView.setCurrentItem(selItem)
+ assert outlineData.titleLabel.text() == "Title"
+ assert outlineData.titleValue.text() == "Lorem Ipsum"
+ assert outlineData.fileValue.text() == "Lorem Ipsum"
+ assert outlineData.itemValue.text() == "Finished"
- assert nwGUI.projMeta.cCValue.text() == "230"
- assert nwGUI.projMeta.wCValue.text() == "40"
- assert nwGUI.projMeta.pCValue.text() == "3"
+ assert outlineData.cCValue.text() == "230"
+ assert outlineData.wCValue.text() == "40"
+ assert outlineData.pCValue.text() == "3"
# Scene One
- actItem = nwGUI.projView.topLevelItem(1)
+ actItem = outlineView.topLevelItem(1)
chpItem = actItem.child(0)
selItem = chpItem.child(0)
- nwGUI.projView.setCurrentItem(selItem)
- tHandle, tLine = nwGUI.projView.getSelectedHandle()
+ outlineView.setCurrentItem(selItem)
+ tHandle, tLine = outlineView.getSelectedHandle()
assert tHandle == "88243afbe5ed8"
assert tLine == 0
- assert nwGUI.projMeta.titleLabel.text() == "Scene"
- assert nwGUI.projMeta.titleValue.text() == "Scene One"
- assert nwGUI.projMeta.fileValue.text() == "Scene One"
- assert nwGUI.projMeta.itemValue.text() == "Finished"
+ assert outlineData.titleLabel.text() == "Scene"
+ assert outlineData.titleValue.text() == "Scene One"
+ assert outlineData.fileValue.text() == "Scene One"
+ assert outlineData.itemValue.text() == "Finished"
# Click POV Link
- assert nwGUI.projMeta.povKeyValue.text() == "Bod"
- nwGUI.projMeta._tagClicked("#pov=Bod")
+ assert outlineData.povKeyValue.text() == "Bod"
+ outlineData._tagClicked("#pov=Bod")
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
# Scene One, Section Two
- actItem = nwGUI.projView.topLevelItem(1)
+ actItem = outlineView.topLevelItem(1)
chpItem = actItem.child(0)
scnItem = chpItem.child(0)
selItem = scnItem.child(0)
- nwGUI.projView.setCurrentItem(selItem)
- tHandle, tLine = nwGUI.projView.getSelectedHandle()
+ outlineView.setCurrentItem(selItem)
+ tHandle, tLine = outlineView.getSelectedHandle()
assert tHandle == "88243afbe5ed8"
assert tLine == 12
- assert nwGUI.projMeta.titleLabel.text() == "Section"
- assert nwGUI.projMeta.titleValue.text() == "Scene One, Section Two"
- assert nwGUI.projMeta.fileValue.text() == "Scene One"
- assert nwGUI.projMeta.itemValue.text() == "Finished"
+ assert outlineData.titleLabel.text() == "Section"
+ assert outlineData.titleValue.text() == "Scene One, Section Two"
+ assert outlineData.fileValue.text() == "Scene One"
+ assert outlineData.itemValue.text() == "Finished"
- nwGUI.projView._treeDoubleClick(selItem, 0)
+ outlineView._treeDoubleClick(selItem, 0)
assert nwGUI.docEditor.docHandle() == "88243afbe5ed8"
# qtbot.stopForInteraction()
From 1f66ed3e3bd4d7260630279f23a390747943b259 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 22 May 2022 21:00:49 +0200
Subject: [PATCH 03/12] Reduce the list of tag classes in outline view when
they are not used
---
novelwriter/core/tree.py | 8 +++++++
novelwriter/gui/outline.py | 39 +++++++++++++++++++++++++++++--
novelwriter/gui/projtree.py | 7 ++++--
novelwriter/guimain.py | 3 +++
tests/test_core/test_core_tree.py | 5 ++++
5 files changed, 58 insertions(+), 4 deletions(-)
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index ee57e0db..11842a5a 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -265,6 +265,14 @@ class NWTree():
# Tree Root Methods
##
+ def rootClasses(self):
+ """Return a set of all root classes in use by the project.
+ """
+ rootClasses = set()
+ for nwItem in self._treeRoots.values():
+ rootClasses.add(nwItem.itemClass)
+ return rootClasses
+
def isRoot(self, tHandle):
"""Check if a handle is a root item.
"""
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index cdb4c7c2..c29b9ff9 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -39,7 +39,9 @@ from PyQt5.QtWidgets import (
QWidget, QFrame
)
-from novelwriter.enum import nwItemLayout, nwItemType, nwOutline, nwView
+from novelwriter.enum import (
+ nwItemClass, nwItemLayout, nwItemType, nwOutline, nwView
+)
from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels
@@ -75,6 +77,7 @@ class GuiOutline(QWidget):
# Function Mappings
self.getSelectedHandle = self.outlineView.getSelectedHandle
+ self.updateClasses = self.outlineData.updateClasses
return
@@ -96,6 +99,7 @@ class GuiOutline(QWidget):
def closeOutline(self):
self.outlineView.closeOutline()
+ self.outlineData.updateClasses()
return
def refreshView(self, overRide=False, novelChanged=False):
@@ -307,7 +311,6 @@ class GuiOutlineView(QTreeWidget):
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
self.theOutline.outlineData.showItem(tHandle, sTitle)
- self.theParent.treeView.setSelectedHandle(tHandle)
return
@@ -824,6 +827,8 @@ class GuiOutlineDetails(QScrollArea):
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ self.updateClasses()
+
return
def clearDetails(self):
@@ -846,6 +851,7 @@ class GuiOutlineDetails(QScrollArea):
self.objKeyValue.setText("")
self.entKeyValue.setText("")
self.cstKeyValue.setText("")
+ self.updateClasses()
return
def showItem(self, tHandle, sTitle):
@@ -895,6 +901,7 @@ class GuiOutlineDetails(QScrollArea):
# Slots
##
+ @pyqtSlot(str)
def _tagClicked(self, theLink):
"""Capture the click of a tag in the right-most column.
"""
@@ -906,6 +913,34 @@ class GuiOutlineDetails(QScrollArea):
self.theParent.docViewer.loadFromTag(theBits[1])
return
+ @pyqtSlot()
+ def updateClasses(self):
+ """Update the visibility status of class details.
+ """
+ usedClasses = self.theProject.projTree.rootClasses()
+
+ pltVisible = nwItemClass.PLOT in usedClasses
+ timVisible = nwItemClass.TIMELINE in usedClasses
+ wldVisible = nwItemClass.WORLD in usedClasses
+ objVisible = nwItemClass.OBJECT in usedClasses
+ entVisible = nwItemClass.ENTITY in usedClasses
+ cstVisible = nwItemClass.CUSTOM in usedClasses
+
+ self.pltKeyLabel.setVisible(pltVisible)
+ self.pltKeyValue.setVisible(pltVisible)
+ self.timKeyLabel.setVisible(timVisible)
+ self.timKeyValue.setVisible(timVisible)
+ self.wldKeyLabel.setVisible(wldVisible)
+ self.wldKeyValue.setVisible(wldVisible)
+ self.objKeyLabel.setVisible(objVisible)
+ self.objKeyValue.setVisible(objVisible)
+ self.entKeyLabel.setVisible(entVisible)
+ self.entKeyValue.setVisible(entVisible)
+ self.cstKeyLabel.setVisible(cstVisible)
+ self.cstKeyValue.setVisible(cstVisible)
+
+ return
+
##
# Internal Functions
##
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index b134b901..9cbd5d18 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -51,6 +51,7 @@ class GuiProjectTree(QTreeWidget):
novelItemChanged = pyqtSignal()
noteItemChanged = pyqtSignal()
wordCountsChanged = pyqtSignal()
+ rootFoldersChanged = pyqtSignal()
def __init__(self, theParent):
QTreeWidget.__init__(self, theParent)
@@ -177,6 +178,7 @@ class GuiProjectTree(QTreeWidget):
if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
tHandle = self.theProject.newRoot(itemClass)
+ self.rootFoldersChanged.emit()
elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
@@ -450,6 +452,7 @@ class GuiProjectTree(QTreeWidget):
self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
+ self.rootFoldersChanged.emit()
else:
self.theParent.makeAlert(self.tr(
"Cannot delete root folder. It is not empty. "
@@ -541,7 +544,7 @@ class GuiProjectTree(QTreeWidget):
else:
expIcon = self.theTheme.getIcon("cross")
- itempStatus, statusIcon = nwItem.getImportStatus()
+ itemStatus, statusIcon = nwItem.getImportStatus()
hLevel = self.theIndex.getHandleHeaderLevel(tHandle)
itemIcon = self.theTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
@@ -551,7 +554,7 @@ class GuiProjectTree(QTreeWidget):
trItem.setText(self.C_NAME, nwItem.itemName)
trItem.setIcon(self.C_EXPORT, expIcon)
trItem.setIcon(self.C_STATUS, statusIcon)
- trItem.setToolTip(self.C_STATUS, itempStatus)
+ trItem.setToolTip(self.C_STATUS, itemStatus)
if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT:
trFont = trItem.font(self.C_NAME)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index ea92aacd..8f7814e0 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -126,6 +126,7 @@ class GuiMain(QMainWindow):
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
+ self.treeView.rootFoldersChanged.connect(self.projView.updateClasses)
self.viewsBar.viewChangeRequested.connect(self._changeView)
self.projView.viewChangeRequested.connect(self._changeView)
@@ -352,6 +353,7 @@ class GuiMain(QMainWindow):
self.rebuildTrees()
self.saveProject()
self.docEditor.setDictionaries()
+ self.projView.updateClasses()
self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
@@ -499,6 +501,7 @@ class GuiMain(QMainWindow):
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
self.statusBar.setRefTime(self.theProject.projOpened)
+ self.projView.updateClasses()
self._updateStatusWordCount()
# Restore previously open documents, if any
diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py
index 5c05f679..23eb7f73 100644
--- a/tests/test_core/test_core_tree.py
+++ b/tests/test_core/test_core_tree.py
@@ -149,6 +149,11 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree.isTrash("a000000000003") is True
assert theTree.isRoot("a000000000002") is True
+ # Check that we have the root classes
+ assert theTree.rootClasses() == {
+ nwItemClass.NOVEL, nwItemClass.CHARACTER, nwItemClass.ARCHIVE, nwItemClass.TRASH
+ }
+
# Check the isTrash function
assert theTree.isTrash("0000000000000") is True # Doesn't exist
assert theTree.isTrash("a000000000003") is True # This the trash folder
From 007f2fbff4bfdcfb735884d56474354111b8779a Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 22 May 2022 22:46:42 +0200
Subject: [PATCH 04/12] Add a menu icon
---
.../assets/icons/typicons_dark/icons.conf | 1 +
.../assets/icons/typicons_dark/typ_th-menu.svg | 16 ++++++++++++++++
.../assets/icons/typicons_light/icons.conf | 1 +
.../assets/icons/typicons_light/typ_th-menu.svg | 16 ++++++++++++++++
novelwriter/gui/theme.py | 4 ++--
5 files changed, 36 insertions(+), 2 deletions(-)
create mode 100644 novelwriter/assets/icons/typicons_dark/typ_th-menu.svg
create mode 100644 novelwriter/assets/icons/typicons_light/typ_th-menu.svg
diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf
index cde02134..ae157e2c 100644
--- a/novelwriter/assets/icons/typicons_dark/icons.conf
+++ b/novelwriter/assets/icons/typicons_dark/icons.conf
@@ -47,6 +47,7 @@ edit = typ_pencil.svg
forward = typ_chevron-right.svg
hash = typ_hash.svg
maximise = typ_arrow-maximise.svg
+menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg
proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg
diff --git a/novelwriter/assets/icons/typicons_dark/typ_th-menu.svg b/novelwriter/assets/icons/typicons_dark/typ_th-menu.svg
new file mode 100644
index 00000000..89434cdf
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/typ_th-menu.svg
@@ -0,0 +1,16 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf
index 85941827..4683bb19 100644
--- a/novelwriter/assets/icons/typicons_light/icons.conf
+++ b/novelwriter/assets/icons/typicons_light/icons.conf
@@ -47,6 +47,7 @@ edit = typ_pencil.svg
forward = typ_chevron-right.svg
hash = typ_hash.svg
maximise = typ_arrow-maximise.svg
+menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg
proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg
diff --git a/novelwriter/assets/icons/typicons_light/typ_th-menu.svg b/novelwriter/assets/icons/typicons_light/typ_th-menu.svg
new file mode 100644
index 00000000..cfc8a1d9
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/typ_th-menu.svg
@@ -0,0 +1,16 @@
+
+
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index 7c0e3ce6..bb93e0b8 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -466,8 +466,8 @@ class GuiIcons:
# General Button Icons
"add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit",
- "forward", "hash", "maximise", "minimise", "reference", "refresh", "remove", "save",
- "search_replace", "search", "settings", "up",
+ "forward", "hash", "maximise", "menu", "minimise", "reference", "refresh", "remove",
+ "save", "search_replace", "search", "settings", "up",
# Switches
"sticky-on", "sticky-off",
From ecc0585c87e6163758cb31aeed95f2b470e7e418 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 22 May 2022 23:03:30 +0200
Subject: [PATCH 05/12] Add a toolbar to the Outline view
---
docs/source/usage_shortcuts.rst | 2 -
novelwriter/core/tree.py | 9 ++
novelwriter/gui/mainmenu.py | 25 ----
novelwriter/gui/outline.py | 195 ++++++++++++++++++++++-------
novelwriter/guimain.py | 20 +--
tests/test_gui/test_gui_guimain.py | 1 -
tests/test_gui/test_gui_outline.py | 19 ++-
7 files changed, 170 insertions(+), 101 deletions(-)
diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst
index fea65c8c..69c2e773 100644
--- a/docs/source/usage_shortcuts.rst
+++ b/docs/source/usage_shortcuts.rst
@@ -59,7 +59,6 @@ The main shorcuts are as follows:
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
- ":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor."
@@ -88,7 +87,6 @@ The main shorcuts are as follows:
":kbd:`F7`", "Re-run spell checker."
":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
":kbd:`F9`", "Re-build the project index."
- ":kbd:`F10`", "Re-build the project outline."
":kbd:`F11`", "Activate full screen mode."
":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available."
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document."
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index 11842a5a..d151f370 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -273,6 +273,15 @@ class NWTree():
rootClasses.add(nwItem.itemClass)
return rootClasses
+ def novelRoots(self):
+ """Return a doctionary of all novel-like root items.
+ """
+ novelItems = {}
+ for tHandle, nwItem in self._treeRoots.items():
+ if nwItem.isNovelLike():
+ novelItems[tHandle] = nwItem
+ return novelItems
+
def isRoot(self, tHandle):
"""Check if a handle is a root item.
"""
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index f69d5a56..bd8d1edd 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -81,12 +81,6 @@ class GuiMainMenu(QMenuBar):
self.aSpellCheck.setChecked(theMode)
return
- def setAutoOutline(self, theMode):
- """Forward auto outline check state to its action.
- """
- self.aAutoOutline.setChecked(theMode)
- return
-
def setFocusMode(self, theMode):
"""Forward focus mode check state to its action.
"""
@@ -105,12 +99,6 @@ class GuiMainMenu(QMenuBar):
self.theParent.docEditor.toggleSpellCheck(None)
return True
- def _toggleAutoOutline(self, theMode):
- """Toggle auto outline when the menu entry is checked.
- """
- self.theProject.setAutoOutline(theMode)
- return True
-
def _openWebsite(self, theUrl):
"""Open a URL in the system's default browser.
"""
@@ -889,19 +877,6 @@ class GuiMainMenu(QMenuBar):
self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex())
self.toolsMenu.addAction(self.aRebuildIndex)
- # Tools > Rebuild Outline
- self.aRebuildOutline = QAction(self.tr("Rebuild Outline"), self)
- self.aRebuildOutline.setShortcut("F10")
- self.aRebuildOutline.triggered.connect(lambda: self.theParent.rebuildOutline())
- self.toolsMenu.addAction(self.aRebuildOutline)
-
- # Tools > Toggle Auto Build Outline
- self.aAutoOutline = QAction(self.tr("Auto-Update Outline"), self)
- self.aAutoOutline.setCheckable(True)
- self.aAutoOutline.toggled.connect(self._toggleAutoOutline)
- self.aAutoOutline.setShortcut("Ctrl+F10")
- self.toolsMenu.addAction(self.aAutoOutline)
-
# Tools > Separator
self.toolsMenu.addSeparator()
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index c29b9ff9..6b50df5f 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -4,9 +4,11 @@ novelWriter – GUI Project Outline
GUI class for the project outline view
File History:
-Created: 2019-11-16 [0.4.1] GuiOutlineView, GuiOutlineHeaderMenu
-Created: 2020-06-02 [0.7.0] GuiOutlineDetails
Created: 2022-05-15 [1.7b1] GuiOutline
+Created: 2022-05-22 [1.7b1] GuiOutlineToolBar
+Created: 2019-11-16 [0.4.1] GuiOutlineView
+Created: 2019-11-16 [0.4.1] GuiOutlineHeaderMenu
+Created: 2020-06-02 [0.7.0] GuiOutlineDetails
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -29,6 +31,7 @@ import logging
import novelwriter
from time import time
+from enum import Enum
from PyQt5.QtCore import (
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
@@ -36,7 +39,7 @@ from PyQt5.QtCore import (
from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
- QWidget, QFrame
+ QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton
)
from novelwriter.enum import (
@@ -60,6 +63,7 @@ class GuiOutline(QWidget):
self.theParent = theParent
self.theProject = theParent.theProject
+ self.outlineBar = GuiOutlineToolBar(self)
self.outlineView = GuiOutlineView(self)
self.outlineData = GuiOutlineDetails(self)
@@ -71,13 +75,20 @@ class GuiOutline(QWidget):
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.setContentsMargins(0, 0, 0, 0)
+ self.outerBox.addWidget(self.outlineBar)
self.outerBox.addWidget(self.splitOutline)
self.setLayout(self.outerBox)
+ # Connect Signals
+ self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns)
+ self.outlineBar.columnToggled.connect(self.outlineView.menuColumnToggled)
+ self.outlineBar.viewRefreshRequested.connect(
+ lambda: self.outlineView.refreshTree(overRide=True)
+ )
+
# Function Mappings
self.getSelectedHandle = self.outlineView.getSelectedHandle
- self.updateClasses = self.outlineData.updateClasses
return
@@ -112,9 +123,112 @@ class GuiOutline(QWidget):
def setTreeFocus(self):
return self.outlineView.setFocus()
+ ##
+ # Slots
+ ##
+
+ @pyqtSlot()
+ def projectUpdated(self):
+ """Should be called whenever the number of root folders change.
+ """
+ self.outlineBar.populateNovelList()
+ self.outlineData.updateClasses()
+ return
+
+ @pyqtSlot()
+ def _updateMenuColumns(self):
+ """Trigger an update of the toggled state of the column menu
+ checkboxes whenever a signal is received that the hidden state
+ of columns has changed.
+ """
+ self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns)
+ return
+
# END Class GuiOutline
+class GuiOutlineToolBar(QToolBar):
+
+ columnToggled = pyqtSignal(bool, Enum)
+ viewRefreshRequested = pyqtSignal()
+
+ def __init__(self, theOutline):
+ QTreeWidget.__init__(self, theOutline)
+
+ logger.debug("Initialising GuiOutlineToolBar ...")
+
+ self.mainConf = novelwriter.CONFIG
+ self.theOutline = theOutline
+ self.theParent = theOutline.theParent
+ self.theProject = theOutline.theParent.theProject
+ self.theTheme = theOutline.theParent.theTheme
+
+ iPx = self.mainConf.pxInt(22)
+ mPx = self.mainConf.pxInt(12)
+
+ self.setMovable(False)
+ self.setIconSize(QSize(iPx, iPx))
+ self.setContentsMargins(0, 0, 0, 0)
+ self.setStyleSheet("QToolBar {border: 0px;}")
+
+ stretch = QWidget(self)
+ stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+
+ # Novel Selector
+ self.novelLabel = QLabel(self.tr("Outline of"))
+ self.novelLabel.setContentsMargins(0, 0, mPx, 0)
+
+ self.novelValue = QComboBox(self)
+ self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
+
+ # Actions
+ self.aRefresh = QAction(self.tr("Refresh"), self)
+ self.aRefresh.setIcon(self.theTheme.getIcon("refresh"))
+ self.aRefresh.triggered.connect(
+ lambda: self.viewRefreshRequested.emit()
+ )
+
+ # Column Menu
+ self.mColumns = GuiOutlineHeaderMenu(self)
+ self.mColumns.columnToggled.connect(
+ lambda isChecked, tItem: self.columnToggled.emit(isChecked, tItem)
+ )
+
+ self.tbColumns = QToolButton(self)
+ self.tbColumns.setIcon(self.theTheme.getIcon("menu"))
+ self.tbColumns.setMenu(self.mColumns)
+ self.tbColumns.setPopupMode(QToolButton.InstantPopup)
+
+ # Assemble
+ self.addWidget(self.novelLabel)
+ self.addWidget(self.novelValue)
+ self.addSeparator()
+ self.addAction(self.aRefresh)
+ self.addWidget(self.tbColumns)
+ self.addWidget(stretch)
+
+ self.populateNovelList()
+
+ logger.debug("GuiOutlineToolBar initialisation complete")
+
+ def populateNovelList(self):
+ """Fill the novel combo box.
+ """
+ self.novelValue.clear()
+ for tHandle, nwItem in self.theProject.projTree.novelRoots().items():
+ self.novelValue.addItem(
+ self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]),
+ nwItem.itemName, tHandle
+ )
+ return
+
+ def setColumnHiddenState(self, hiddenState):
+ self.mColumns.setHiddenState(hiddenState)
+ return
+
+# END Class GuiOutlineToolBar
+
+
class GuiOutlineView(QTreeWidget):
DEF_WIDTH = {
@@ -157,10 +271,12 @@ class GuiOutlineView(QTreeWidget):
nwOutline.SYNOP: False,
}
+ hiddenStateChanged = pyqtSignal()
+
def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline)
- logger.debug("Initialising GuiOutline ...")
+ logger.debug("Initialising GuiOutlineView ...")
self.mainConf = novelwriter.CONFIG
self.theOutline = theOutline
@@ -169,7 +285,6 @@ class GuiOutlineView(QTreeWidget):
self.theTheme = theOutline.theParent.theTheme
self.theIndex = theOutline.theParent.theIndex
self.optState = theOutline.theParent.theProject.optState
- self.headerMenu = GuiOutlineHeaderMenu(self)
self.setFrameStyle(QFrame.NoFrame)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
@@ -184,8 +299,6 @@ class GuiOutlineView(QTreeWidget):
self.setIndentation(iPx)
self.treeHead = self.header()
- self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu)
- self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
self.treeHead.sectionMoved.connect(self._columnMoved)
# Internals
@@ -199,12 +312,25 @@ class GuiOutlineView(QTreeWidget):
self.initOutline()
self.clearOutline()
- self.headerMenu.setHiddenState(self._colHidden)
- logger.debug("GuiOutline initialisation complete")
+ self.hiddenStateChanged.emit()
+
+ logger.debug("GuiOutlineView initialisation complete")
return
+ ##
+ # Properties
+ ##
+
+ @property
+ def hiddenColumns(self):
+ return self._colHidden
+
+ ##
+ # Methods
+ ##
+
def initOutline(self):
"""Set or update outline settings.
"""
@@ -314,13 +440,6 @@ class GuiOutlineView(QTreeWidget):
return
- @pyqtSlot("QPoint")
- def _headerRightClick(self, clickPos):
- """Show the header column menu.
- """
- self.headerMenu.exec_(self.mapToGlobal(clickPos))
- return
-
@pyqtSlot(int, int, int)
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
"""Make sure the order array is up to date with the actual order
@@ -330,9 +449,10 @@ class GuiOutlineView(QTreeWidget):
self._saveHeaderState()
return
- def _menuColumnToggled(self, isChecked, theItem):
+ @pyqtSlot(bool, Enum)
+ def menuColumnToggled(self, isChecked, theItem):
"""Receive the changes to column visibility forwarded by the
- header context menu.
+ column selection menu.
"""
logger.verbose("User toggled Outline column '%s'", theItem.name)
if theItem in self._colIdx:
@@ -389,7 +509,7 @@ class GuiOutlineView(QTreeWidget):
except Exception:
logger.warning("Ignored unknown outline column '%s'", str(hName))
- self.headerMenu.setHiddenState(self._colHidden)
+ self.hiddenStateChanged.emit()
return
@@ -558,10 +678,11 @@ class GuiOutlineView(QTreeWidget):
class GuiOutlineHeaderMenu(QMenu):
- def __init__(self, theParent):
- QMenu.__init__(self, theParent)
+ columnToggled = pyqtSignal(bool, Enum)
+
+ def __init__(self, theOutline):
+ QMenu.__init__(self, theOutline)
- self.theParent = theParent
self.acceptToggle = True
mnuHead = QAction(self.tr("Select Columns"), self)
@@ -575,7 +696,7 @@ class GuiOutlineHeaderMenu(QMenu):
self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self)
self.actionMap[hItem].setCheckable(True)
self.actionMap[hItem].toggled.connect(
- lambda isChecked, tItem=hItem: self._columnToggled(isChecked, tItem)
+ lambda isChecked, tItem=hItem: self.columnToggled.emit(isChecked, tItem)
)
self.addAction(self.actionMap[hItem])
@@ -596,18 +717,6 @@ class GuiOutlineHeaderMenu(QMenu):
return
- ##
- # Slots
- ##
-
- def _columnToggled(self, isChecked, theItem):
- """The user has toggled the visibility of a column. Forward the
- event to the parent class only if we're accepting changes.
- """
- if self.acceptToggle:
- self.theParent._menuColumnToggled(isChecked, theItem)
- return
-
# END Class GuiOutlineHeaderMenu
@@ -945,16 +1054,12 @@ class GuiOutlineDetails(QScrollArea):
# Internal Functions
##
- def _formatTags(self, theRefs, theKey):
+ def _formatTags(self, refs, key):
"""Format the tags as clickable links.
"""
- if theKey not in theRefs:
- return ""
- refTags = []
- for tTag in theRefs[theKey]:
- refTags.append("%s" % (
- theKey[1:], tTag, tTag
- ))
- return ", ".join(refTags)
+ mKey = key[1:]
+ return ", ".join(
+ [f"{tag}" for tag in refs.get(key, [])]
+ )
# END Class GuiOutlineDetails
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 8f7814e0..80fd06e2 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -126,7 +126,7 @@ class GuiMain(QMainWindow):
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
- self.treeView.rootFoldersChanged.connect(self.projView.updateClasses)
+ self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated)
self.viewsBar.viewChangeRequested.connect(self._changeView)
self.projView.viewChangeRequested.connect(self._changeView)
@@ -353,7 +353,7 @@ class GuiMain(QMainWindow):
self.rebuildTrees()
self.saveProject()
self.docEditor.setDictionaries()
- self.projView.updateClasses()
+ self.projView.projectUpdated()
self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
@@ -499,9 +499,8 @@ class GuiMain(QMainWindow):
self.rebuildTrees()
self.docEditor.setDictionaries()
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
- self.mainMenu.setAutoOutline(self.theProject.autoOutline)
self.statusBar.setRefTime(self.theProject.projOpened)
- self.projView.updateClasses()
+ self.projView.projectUpdated()
self._updateStatusWordCount()
# Restore previously open documents, if any
@@ -895,19 +894,6 @@ class GuiMain(QMainWindow):
return True
- def rebuildOutline(self):
- """Force a rebuild of the Outline view.
- """
- if not self.hasProject:
- logger.error("No project open")
- return False
-
- logger.verbose("Forcing a rebuild of the Project Outline")
- self._changeView(nwView.OUTLINE)
- self.projView.refreshView(overRide=True)
-
- return True
-
##
# Main Dialogs
##
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index b420e4f3..c6647dbd 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -61,7 +61,6 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
assert nwGUI.editItem() is False
assert nwGUI.requestNovelTreeRefresh() is False
assert nwGUI.rebuildIndex() is False
- assert nwGUI.rebuildOutline() is False
assert nwGUI.showProjectSettingsDialog() is False
assert nwGUI.showProjectDetailsDialog() is False
assert nwGUI.showBuildProjectDialog() is False
diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py
index 02dd1041..c1c258e6 100644
--- a/tests/test_gui/test_gui_outline.py
+++ b/tests/test_gui/test_gui_outline.py
@@ -21,10 +21,7 @@ along with this program. If not, see .
import pytest
-from PyQt5.QtCore import Qt, QPoint
-from PyQt5.QtWidgets import QAction, QTreeWidgetItem, QMessageBox
-
-from novelwriter.enum import nwOutline
+from PyQt5.QtWidgets import QTreeWidgetItem, QMessageBox
keyDelay = 2
typeDelay = 1
@@ -51,16 +48,16 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
assert outlineView.topLevelItemCount() > 0
# Context Menu
- outlineView._headerRightClick(QPoint(1, 1))
- outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger)
- outlineView.headerMenu.close()
- qtbot.mouseClick(outlineView, Qt.LeftButton)
+ # outlineView._headerRightClick(QPoint(1, 1))
+ # outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger)
+ # outlineView.headerMenu.close()
+ # qtbot.mouseClick(outlineView, Qt.LeftButton)
- outlineView._loadHeaderState()
- assert not outlineView._colHidden[nwOutline.CCOUNT]
+ # outlineView._loadHeaderState()
+ # assert not outlineView._colHidden[nwOutline.CCOUNT]
# First Item
- nwGUI.rebuildOutline()
+ outlineView.refreshTree()
selItem = outlineView.topLevelItem(0)
assert isinstance(selItem, QTreeWidgetItem)
From 8ac30b036a147d90606cdb3bff8285285c429473 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 26 May 2022 12:11:22 +0200
Subject: [PATCH 06/12] Change how tag links are followed
---
novelwriter/enum.py | 8 +++
novelwriter/gui/doceditor.py | 6 +-
novelwriter/gui/docviewer.py | 36 +++---------
novelwriter/gui/outline.py | 108 ++++++++++++++++++++---------------
novelwriter/guimain.py | 48 +++++++++++++---
5 files changed, 123 insertions(+), 83 deletions(-)
diff --git a/novelwriter/enum.py b/novelwriter/enum.py
index fc9604e0..48b894ba 100644
--- a/novelwriter/enum.py
+++ b/novelwriter/enum.py
@@ -62,6 +62,14 @@ class nwItemLayout(Enum):
# END Enum nwItemLayout
+class nwDocMode(Enum):
+
+ VIEW = 0
+ EDIT = 1
+
+# END Enum nwDocMode
+
+
class nwDocAction(Enum):
NO_ACTION = 0
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 98648ab6..b73fe943 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -33,6 +33,7 @@ import bisect
import logging
import novelwriter
+from enum import Enum
from time import time
from PyQt5.QtCore import (
@@ -50,7 +51,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.core import NWDoc, NWSpellEnchant, countWords
-from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert
+from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode
from novelwriter.common import transferCase
from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
from novelwriter.gui.dochighlight import GuiDocHighlighter
@@ -69,6 +70,7 @@ class GuiDocEditor(QTextEdit):
spellDictionaryChanged = pyqtSignal(str, str)
docEditedStatusChanged = pyqtSignal(bool)
docCountsChanged = pyqtSignal(str, int, int, int)
+ loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent):
QTextEdit.__init__(self, theParent)
@@ -1895,7 +1897,7 @@ class GuiDocEditor(QTextEdit):
if loadTag:
logger.verbose("Attempting to follow tag '%s'", theWord)
- self.theParent.docViewer.loadFromTag(theWord)
+ self.loadDocumentTagRequest.emit(theWord, nwDocMode.VIEW)
else:
logger.verbose("Potential tag '%s'", theWord)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 4c293da5..10456369 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -30,7 +30,9 @@ along with this program. If not, see .
import logging
import novelwriter
-from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot
+from enum import Enum
+
+from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtGui import (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
)
@@ -40,7 +42,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.core import ToHtml
-from novelwriter.enum import nwAlert, nwItemType, nwDocAction
+from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException
from novelwriter.constants import nwUnicode
@@ -49,6 +51,8 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
+ loadDocumentTagRequest = pyqtSignal(str, Enum)
+
def __init__(self, theParent):
QTextBrowser.__init__(self, theParent)
@@ -239,30 +243,6 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins()
return
- def loadFromTag(self, theTag):
- """Load text in the document from a reference given by a meta
- tag rather than a known handle. This function depends on the
- index being up to date.
- """
- logger.debug("Loading document from tag '%s'", theTag)
- tHandle, _, sTitle = self.theParent.theIndex.getTagSource(theTag)
- if tHandle is None:
- self.theParent.makeAlert(self.tr(
- "Could not find the reference for tag '{0}'. It either doesn't "
- "exist, or the index is out of date. The index can be updated "
- "from the Tools menu, or by pressing {1}."
- ).format(
- theTag, "F9"
- ), nwAlert.ERROR)
- return False
- else:
- # Let the parent handle the opening as it also ensures that
- # the doc view panel is visible in case this request comes
- # from outside this class.
- logger.verbose("Tag points to '%s#%s'", tHandle, sTitle)
- self.theParent.viewDocument(tHandle, "#%s" % sTitle)
- return True
-
def docAction(self, theAction):
"""Wrapper function for various document actions on the current
document.
@@ -414,14 +394,14 @@ class GuiDocViewer(QTextBrowser):
@pyqtSlot("QUrl")
def _linkClicked(self, theURL):
- """Slot for a link in the document being clicked.
+ """Process a clicked link internally in the document.
"""
theLink = theURL.url()
logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:
- self.loadFromTag(theBits[1])
+ self.loadDocumentTagRequest.emit(theBits[1], nwDocMode.VIEW)
return
@pyqtSlot("QPoint")
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 6b50df5f..21af7e4d 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -43,7 +43,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.enum import (
- nwItemClass, nwItemLayout, nwItemType, nwOutline, nwView
+ nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
)
from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels
@@ -54,14 +54,13 @@ logger = logging.getLogger(__name__)
class GuiOutline(QWidget):
- viewChangeRequested = pyqtSignal(nwView)
+ loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent):
QWidget.__init__(self, theParent)
- self.mainConf = novelwriter.CONFIG
- self.theParent = theParent
- self.theProject = theParent.theProject
+ self.mainConf = novelwriter.CONFIG
+ self.theParent = theParent
self.outlineBar = GuiOutlineToolBar(self)
self.outlineView = GuiOutlineView(self)
@@ -82,7 +81,9 @@ class GuiOutline(QWidget):
# Connect Signals
self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns)
- self.outlineBar.columnToggled.connect(self.outlineView.menuColumnToggled)
+ self.outlineView.activeItemChanged.connect(self.outlineData.showItem)
+ self.outlineData.itemTagClicked.connect(self._tagClicked)
+ self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled)
self.outlineBar.viewRefreshRequested.connect(
lambda: self.outlineView.refreshTree(overRide=True)
)
@@ -144,13 +145,22 @@ class GuiOutline(QWidget):
self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns)
return
+ @pyqtSlot(str)
+ def _tagClicked(self, link):
+ """Capture the click of a tag in the details panel.
+ """
+ if link:
+ self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW)
+ return
+
# END Class GuiOutline
class GuiOutlineToolBar(QToolBar):
- columnToggled = pyqtSignal(bool, Enum)
+ novelRootChanged = pyqtSignal(str)
viewRefreshRequested = pyqtSignal()
+ viewColumnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline)
@@ -158,7 +168,6 @@ class GuiOutlineToolBar(QToolBar):
logger.debug("Initialising GuiOutlineToolBar ...")
self.mainConf = novelwriter.CONFIG
- self.theOutline = theOutline
self.theParent = theOutline.theParent
self.theProject = theOutline.theParent.theProject
self.theTheme = theOutline.theParent.theTheme
@@ -180,6 +189,7 @@ class GuiOutlineToolBar(QToolBar):
self.novelValue = QComboBox(self)
self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
+ self.novelValue.currentIndexChanged.connect(self._novelValueChanged)
# Actions
self.aRefresh = QAction(self.tr("Refresh"), self)
@@ -191,7 +201,7 @@ class GuiOutlineToolBar(QToolBar):
# Column Menu
self.mColumns = GuiOutlineHeaderMenu(self)
self.mColumns.columnToggled.connect(
- lambda isChecked, tItem: self.columnToggled.emit(isChecked, tItem)
+ lambda isChecked, tItem: self.viewColumnToggled.emit(isChecked, tItem)
)
self.tbColumns = QToolButton(self)
@@ -207,10 +217,12 @@ class GuiOutlineToolBar(QToolBar):
self.addWidget(self.tbColumns)
self.addWidget(stretch)
- self.populateNovelList()
-
logger.debug("GuiOutlineToolBar initialisation complete")
+ ##
+ # Methods
+ ##
+
def populateNovelList(self):
"""Fill the novel combo box.
"""
@@ -223,9 +235,23 @@ class GuiOutlineToolBar(QToolBar):
return
def setColumnHiddenState(self, hiddenState):
+ """Forward the change of column hidden states to the menu.
+ """
self.mColumns.setHiddenState(hiddenState)
return
+ ##
+ # Slots
+ ##
+
+ @pyqtSlot(int)
+ def _novelValueChanged(self, index):
+ """Emit a signal containing the handle of the selected item.
+ """
+ if index >= 0:
+ self.novelRootChanged.emit(self.novelValue.currentData())
+ return
+
# END Class GuiOutlineToolBar
@@ -272,6 +298,7 @@ class GuiOutlineView(QTreeWidget):
}
hiddenStateChanged = pyqtSignal()
+ activeItemChanged = pyqtSignal(str, str)
def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline)
@@ -279,7 +306,6 @@ class GuiOutlineView(QTreeWidget):
logger.debug("Initialising GuiOutlineView ...")
self.mainConf = novelwriter.CONFIG
- self.theOutline = theOutline
self.theParent = theOutline.theParent
self.theProject = theOutline.theParent.theProject
self.theTheme = theOutline.theParent.theTheme
@@ -436,7 +462,7 @@ class GuiOutlineView(QTreeWidget):
if selItems:
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
- self.theOutline.outlineData.showItem(tHandle, sTitle)
+ self.activeItemChanged.emit(tHandle, sTitle)
return
@@ -729,6 +755,8 @@ class GuiOutlineDetails(QScrollArea):
"H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
}
+ itemTagClicked = pyqtSignal(str)
+
def __init__(self, theOutline):
QScrollArea.__init__(self, theOutline)
@@ -828,15 +856,18 @@ class GuiOutlineDetails(QScrollArea):
self.entKeyValue.setWordWrap(True)
self.cstKeyValue.setWordWrap(True)
- self.povKeyValue.linkActivated.connect(self._tagClicked)
- self.focKeyValue.linkActivated.connect(self._tagClicked)
- self.chrKeyValue.linkActivated.connect(self._tagClicked)
- self.pltKeyValue.linkActivated.connect(self._tagClicked)
- self.timKeyValue.linkActivated.connect(self._tagClicked)
- self.wldKeyValue.linkActivated.connect(self._tagClicked)
- self.objKeyValue.linkActivated.connect(self._tagClicked)
- self.entKeyValue.linkActivated.connect(self._tagClicked)
- self.cstKeyValue.linkActivated.connect(self._tagClicked)
+ def tagClicked(link):
+ self.itemTagClicked.emit(link)
+
+ self.povKeyValue.linkActivated.connect(tagClicked)
+ self.focKeyValue.linkActivated.connect(tagClicked)
+ self.chrKeyValue.linkActivated.connect(tagClicked)
+ self.pltKeyValue.linkActivated.connect(tagClicked)
+ self.timKeyValue.linkActivated.connect(tagClicked)
+ self.wldKeyValue.linkActivated.connect(tagClicked)
+ self.objKeyValue.linkActivated.connect(tagClicked)
+ self.entKeyValue.linkActivated.connect(tagClicked)
+ self.cstKeyValue.linkActivated.connect(tagClicked)
self.povKeyLWrap.addWidget(self.povKeyValue, 1)
self.focKeyLWrap.addWidget(self.focKeyValue, 1)
@@ -963,6 +994,11 @@ class GuiOutlineDetails(QScrollArea):
self.updateClasses()
return
+ ##
+ # Slots
+ ##
+
+ @pyqtSlot(str, str)
def showItem(self, tHandle, sTitle):
"""Update the content of the tree with the given handle and line
number pointing to a header.
@@ -1006,22 +1042,6 @@ class GuiOutlineDetails(QScrollArea):
return True
- ##
- # Slots
- ##
-
- @pyqtSlot(str)
- def _tagClicked(self, theLink):
- """Capture the click of a tag in the right-most column.
- """
- logger.verbose("Clicked link: '%s'", theLink)
- if len(theLink) > 0:
- theBits = theLink.split("=")
- if len(theBits) == 2:
- self.theOutline.viewChangeRequested.emit(nwView.PROJECT)
- self.theParent.docViewer.loadFromTag(theBits[1])
- return
-
@pyqtSlot()
def updateClasses(self):
"""Update the visibility status of class details.
@@ -1050,16 +1070,12 @@ class GuiOutlineDetails(QScrollArea):
return
- ##
- # Internal Functions
- ##
-
- def _formatTags(self, refs, key):
- """Format the tags as clickable links.
+ @staticmethod
+ def _formatTags(refs, key):
+ """Convert a list of tags into a list of clickable tag links.
"""
- mKey = key[1:]
return ", ".join(
- [f"{tag}" for tag in refs.get(key, [])]
+ [f"{tag}" for tag in refs.get(key, [])]
)
# END Class GuiOutlineDetails
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 80fd06e2..67bf126b 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -27,6 +27,7 @@ import os
import logging
import novelwriter
+from enum import Enum
from time import time
from datetime import datetime
@@ -52,7 +53,7 @@ from novelwriter.tools import (
)
from novelwriter.core import NWProject, NWIndex
from novelwriter.enum import (
- nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
+ nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
)
from novelwriter.common import getGuiItem, hexToInt
@@ -117,10 +118,7 @@ class GuiMain(QMainWindow):
self.viewsBar = GuiViewsBar(self)
# Connect Signals Between Main Elements
- 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.viewsBar.viewChangeRequested.connect(self._changeView)
self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
@@ -128,8 +126,15 @@ class GuiMain(QMainWindow):
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated)
- self.viewsBar.viewChangeRequested.connect(self._changeView)
- self.projView.viewChangeRequested.connect(self._changeView)
+ 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()
@@ -1444,6 +1449,23 @@ class GuiMain(QMainWindow):
return projData
+ def _getTagSource(self, tTag):
+ """A wrapper function for the index lookup of a tag that will
+ display an alert if the tag cannot be found.
+ """
+ tHandle, _, sTitle = self.theIndex.getTagSource(tTag)
+ if tHandle is None:
+ self.makeAlert(self.tr(
+ "Could not find the reference for tag '{0}'. It either doesn't "
+ "exist, or the index is out of date. The index can be updated "
+ "from the Tools menu, or by pressing {1}."
+ ).format(
+ tTag, "F9"
+ ), nwAlert.ERROR)
+ return None, None
+
+ return tHandle, sTitle
+
##
# Events
##
@@ -1462,6 +1484,18 @@ class GuiMain(QMainWindow):
# Slots
##
+ @pyqtSlot(str, Enum)
+ def _followTag(self, tTag, tMode):
+ """Follow a tag after user interaction with a link.
+ """
+ tHandle, sTitle = self._getTagSource(tTag)
+ if tHandle is not None:
+ if tMode == nwDocMode.EDIT:
+ self.openDocument(tHandle)
+ elif tMode == nwDocMode.VIEW:
+ self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}")
+ return
+
@pyqtSlot(nwView)
def _changeView(self, view):
"""Handle the requested change of view from the GuiViewBar.
From de07546e3f1a92459646d52dd108f142dbbaa51e Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 5 Jun 2022 13:41:22 +0200
Subject: [PATCH 07/12] Fix tests
---
tests/test_core/test_core_tohtml.py | 4 ----
tests/test_core/test_core_tomd.py | 4 ----
tests/test_core/test_core_toodt.py | 8 --------
tests/test_gui/test_gui_outline.py | 2 +-
4 files changed, 1 insertion(+), 17 deletions(-)
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index 12072e09..f21d5d78 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -25,7 +25,6 @@ import pytest
from tools import readFile
from novelwriter.core import NWProject, ToHtml
-from novelwriter.core.index import NWIndex
@pytest.mark.core
@@ -33,7 +32,6 @@ def testCoreToHtml_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToHtml class.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject)
# Novel Files Headers
@@ -236,7 +234,6 @@ def testCoreToHtml_ConvertDirect(mockGUI):
"""Test the converter directly using the ToHtml class.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject)
theHtml._isNovel = True
@@ -607,7 +604,6 @@ def testCoreToHtml_Format(mockGUI):
"""Test all the formatters for the ToHtml class.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject)
# Export Mode
diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py
index c2235ff8..2e49d16b 100644
--- a/tests/test_core/test_core_tomd.py
+++ b/tests/test_core/test_core_tomd.py
@@ -25,7 +25,6 @@ import pytest
from tools import readFile
from novelwriter.core import NWProject, ToMarkdown
-from novelwriter.core.index import NWIndex
@pytest.mark.core
@@ -33,7 +32,6 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
# Headers
@@ -162,7 +160,6 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
"""Test the converter directly using the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
theMD._isNovel = True
@@ -267,7 +264,6 @@ def testCoreToMarkdown_Format(mockGUI):
"""Test all the formatters for the ToMarkdown class.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject)
assert theMD._formatKeywords("", theMD.A_NONE) == ""
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index febbc94f..c714b5f5 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -29,7 +29,6 @@ from shutil import copyfile
from tools import cmpFiles
from novelwriter.core import NWProject, ToOdt
-from novelwriter.core.index import NWIndex
from novelwriter.core.toodt import ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag
XML_NS = [
@@ -56,7 +55,6 @@ def testCoreToOdt_Init(mockGUI):
"""Test initialisation of the ODT document.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
# Flat Doc
# ========
@@ -112,7 +110,6 @@ def testCoreToOdt_TextFormatting(mockGUI):
"""Test formatting of paragraphs.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc.initDocument()
@@ -234,7 +231,6 @@ def testCoreToOdt_Convert(mockGUI):
"""Test the converter of the ToOdt class.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True
@@ -566,7 +562,6 @@ def testCoreToOdt_ConvertDirect(mockGUI):
otherwise hard to reach conditions.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True
@@ -621,7 +616,6 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
"""Test the document save functions.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True
@@ -658,7 +652,6 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
"""Test the document save functions.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=False)
theDoc._isNovel = True
@@ -738,7 +731,6 @@ def testCoreToOdt_Format(mockGUI):
"""Test the formatters for the ToOdt class.
"""
theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True)
assert theDoc._formatSynopsis("synopsis text") == (
diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py
index 1a4617ff..f089928e 100644
--- a/tests/test_gui/test_gui_outline.py
+++ b/tests/test_gui/test_gui_outline.py
@@ -88,7 +88,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
# Click POV Link
assert outlineData.povKeyValue.text() == "Bod"
- outlineData._tagClicked("#pov=Bod")
+ nwGUI.projView._tagClicked("Bod")
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
# Scene One, Section Two
From 2873336dce4cddb67541807d455160d0b56f9c8a Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 5 Jun 2022 15:04:04 +0200
Subject: [PATCH 08/12] Allow switching between root novel folders
---
novelwriter/core/index.py | 48 +++++++++++-------------------
novelwriter/gui/noveltree.py | 2 +-
novelwriter/gui/outline.py | 21 +++++++++----
sample/content/a520879ca0b45.nwd | 17 +++++++++++
sample/content/bacb7059e3083.nwd | 8 +++++
sample/nwProject.nwx | 42 ++++++++++++++++----------
tests/test_core/test_core_index.py | 6 ++--
7 files changed, 88 insertions(+), 56 deletions(-)
create mode 100644 sample/content/a520879ca0b45.nwd
create mode 100644 sample/content/bacb7059e3083.nwd
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 76a7ec55..ee9ea089 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -73,9 +73,8 @@ class NWIndex:
self._indexBroken = False
# TimeStamps
- self._timeNovel = 0
- self._timeNotes = 0
- self._timeIndex = 0
+ self._indexChange = 0
+ self._rootChange = {}
return
@@ -99,9 +98,8 @@ class NWIndex:
"""
self._tagsIndex.clear()
self._itemIndex.clear()
- self._timeNovel = 0
- self._timeNotes = 0
- self._timeIndex = 0
+ self._indexChange = 0
+ self._rootChange = {}
return
def deleteHandle(self, tHandle):
@@ -129,20 +127,16 @@ class NWIndex:
return True
- def novelChangedSince(self, checkTime):
- """Check if the novel index has changed since a given time.
- """
- return self._timeNovel > checkTime
-
- def notesChangedSince(self, checkTime):
- """Check if the notes index has changed since a given time.
- """
- return self._timeNotes > checkTime
-
def indexChangedSince(self, checkTime):
"""Check if the index has changed since a given time.
"""
- return self._timeIndex > checkTime
+ return self._indexChange > checkTime
+
+ def rootChangedSince(self, rootHandle, checkTime):
+ """Check if the index has changed since a given time for a
+ given root item.
+ """
+ return self._rootChange.get(rootHandle, self._indexChange) > checkTime
##
# Load and Save Index to/from File
@@ -184,10 +178,7 @@ class NWIndex:
logger.warning("Item '%s' is not in the index", fHandle)
self.reIndexHandle(fHandle)
- nowTime = round(time())
- self._timeNovel = nowTime
- self._timeNotes = nowTime
- self._timeIndex = nowTime
+ self._indexChange = round(time())
logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000)
@@ -307,11 +298,8 @@ class NWIndex:
# Update timestamps for index changes
nowTime = round(time())
- self._timeIndex = nowTime
- if theItem.itemLayout == nwItemLayout.NOTE:
- self._timeNotes = nowTime
- else:
- self._timeNovel = nowTime
+ self._indexChange = nowTime
+ self._rootChange[theItem.itemRoot] = nowTime
return True
@@ -466,14 +454,14 @@ class NWIndex:
# Extract Data
##
- def novelStructure(self, skipExcl=True):
+ def novelStructure(self, rootHandle=None, skipExcl=True):
"""Iterate over all titles in the novel, in the correct order as
they appear in the tree view and in the respective document
files, but skipping all note files.
"""
- for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
- tKey = f"{tHandle}:{sTitle}"
- yield tKey, tHandle, sTitle, hItem
+ novStruct = self._itemIndex.iterNovelStructure(rootHandle=rootHandle, skipExcl=skipExcl)
+ for tHandle, sTitle, hItem in novStruct:
+ yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
return
def getNovelWordCount(self, skipExcl=True):
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index a1cd11d3..e5ad816e 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -136,7 +136,7 @@ class GuiNovelTree(QTreeWidget):
"""
logger.verbose("Requesting refresh of the novel tree")
treeChanged = self.theParent.treeView.changedSince(self._lastBuild)
- indexChanged = self.theProject.index.novelChangedSince(self._lastBuild)
+ indexChanged = self.theProject.index.indexChangedSince(self._lastBuild)
if not (treeChanged or indexChanged or overRide):
logger.verbose("No changes have been made to the novel index")
return
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 58710584..ea919615 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -83,6 +83,7 @@ class GuiOutline(QWidget):
self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns)
self.outlineView.activeItemChanged.connect(self.outlineData.showItem)
self.outlineData.itemTagClicked.connect(self._tagClicked)
+ self.outlineBar.novelRootChanged.connect(self._rootItemChanged)
self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled)
self.outlineBar.viewRefreshRequested.connect(
lambda: self.outlineView.refreshTree(overRide=True)
@@ -153,6 +154,13 @@ class GuiOutline(QWidget):
self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW)
return
+ @pyqtSlot(str)
+ def _rootItemChanged(self, handle):
+ """The root novel handle has been changed.
+ """
+ self.outlineView.refreshTree(rootHandle=handle, overRide=True)
+ return
+
# END Class GuiOutline
@@ -394,7 +402,7 @@ class GuiOutlineView(QTreeWidget):
return
- def refreshTree(self, overRide=False, novelChanged=False):
+ def refreshTree(self, rootHandle=None, overRide=False, novelChanged=False):
"""Called whenever the Outline tab is activated and controls
what data to load, and if necessary, force a rebuild of the
tree.
@@ -402,17 +410,17 @@ class GuiOutlineView(QTreeWidget):
# If it's the first time, we always build
if self._firstView or self._firstView and overRide:
self._loadHeaderState()
- self._populateTree()
+ self._populateTree(rootHandle)
self._firstView = False
return
# If the novel index or novel tree has changed since the tree
# was last built, we rebuild the tree from the updated index.
- indexChanged = self.theProject.index.novelChangedSince(self._lastBuild)
+ indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
if doBuild or overRide:
logger.debug("Rebuilding Project Outline")
- self._populateTree()
+ self._populateTree(rootHandle)
return
@@ -577,7 +585,7 @@ class GuiOutlineView(QTreeWidget):
return
- def _populateTree(self):
+ def _populateTree(self, rootHandle):
"""Build the tree based on the project index, and the header
based on the defined constants, default values and user selected
width, order and hidden state. All columns are populated, even
@@ -610,7 +618,8 @@ class GuiOutlineView(QTreeWidget):
currChapter = None
currScene = None
- for _, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True):
+ novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
+ for _, tHandle, sTitle, novIdx in novStruct:
tItem = self._createTreeItem(tHandle, sTitle, novIdx)
diff --git a/sample/content/a520879ca0b45.nwd b/sample/content/a520879ca0b45.nwd
new file mode 100644
index 00000000..12d7a62a
--- /dev/null
+++ b/sample/content/a520879ca0b45.nwd
@@ -0,0 +1,17 @@
+%%~name: Chapter One
+%%~path: e5e47ebf63b1c/a520879ca0b45
+%%~kind: NOVEL/DOCUMENT
+### Chapter One
+
+@pov: Jane
+
+% Synopsis: Remember Jane and John?
+
+### Scene One
+
+@pov: Jane
+@focus: John
+
+A project can have multiple novel root folders for multiple novels. This is the first scene of a sequel to the first novel.
+
+In this way, the writer can keep the same notes for multiple novels. This can be especially useful if the writer is planning a multi-novel story in advance.
diff --git a/sample/content/bacb7059e3083.nwd b/sample/content/bacb7059e3083.nwd
new file mode 100644
index 00000000..b6be7a07
--- /dev/null
+++ b/sample/content/bacb7059e3083.nwd
@@ -0,0 +1,8 @@
+%%~name: Title Page
+%%~path: e5e47ebf63b1c/bacb7059e3083
+%%~kind: NOVEL/DOCUMENT
+#! Sequel Novel
+
+>> **By Jane Doh** <<
+
+% Synopsis: Jane and John are back in a sequel to My Novel!
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index cdd3ae50..9f3a6587 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,13 +1,13 @@
-
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 1331
- 220
- 67108
+ 1334
+ 225
+ 67746
False
@@ -15,10 +15,10 @@
True
None
True
- 636b6aa9b697b
+ a520879ca0b45
636b6aa9b697b
- 1303
- 894
+ 1363
+ 954
409
B
@@ -33,10 +33,10 @@
- New
+ New
Notes
Started
- 1st Draft
+ 1st Draft
2nd Draft
3rd Draft
Finished
@@ -48,13 +48,13 @@
Main
-
+
-
Novel
-
-
+
Title Page
-
@@ -93,7 +93,19 @@
We Found John!
- -
+
-
+
+ Sequel
+
+ -
+
+ Title Page
+
+ -
+
+ Chapter One
+
+ -
Characters
@@ -109,7 +121,7 @@
Jane Smith
- -
+
-
Locations
@@ -125,7 +137,7 @@
Mars
- -
+
-
Archive
@@ -137,7 +149,7 @@
Old File
- -
+
-
Trash
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 78361bb6..8f126e56 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -197,8 +197,7 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd):
nItem = theProject.tree[nHandle]
cItem = theProject.tree[cHandle]
- assert theIndex.novelChangedSince(0) is False
- assert theIndex.notesChangedSince(0) is False
+ assert theIndex.rootChangedSince("0000000000010", 0) is False
assert theIndex.indexChangedSince(0) is False
assert theIndex.scanText(cHandle, (
@@ -228,8 +227,7 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd):
"@time": []
}
- assert theIndex.novelChangedSince(0) is True
- assert theIndex.notesChangedSince(0) is True
+ assert theIndex.rootChangedSince("0000000000010", 0) is True
assert theIndex.indexChangedSince(0) is True
assert theIndex.getHandleHeaderLevel(cHandle) == "H1"
From 88976d6800e1de38dfc2402853dad33161a2f808 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 5 Jun 2022 17:25:39 +0200
Subject: [PATCH 09/12] Streamline how project item changes are reported
---
novelwriter/gui/doceditor.py | 27 +++---
novelwriter/gui/docviewer.py | 25 +++---
novelwriter/gui/itemdetails.py | 9 +-
novelwriter/gui/noveltree.py | 1 -
novelwriter/gui/outline.py | 12 ++-
novelwriter/gui/projtree.py | 124 +++++++++++++++-------------
novelwriter/guimain.py | 41 +++------
novelwriter/tools/build.py | 1 -
tests/test_gui/test_gui_projtree.py | 5 --
9 files changed, 122 insertions(+), 123 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 9c080c96..5ede17f1 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -569,16 +569,6 @@ class GuiDocEditor(QTextEdit):
return
- def updateDocInfo(self, tHandle):
- """Called when an item label is changed to check if the document
- title bar needs updating,
- """
- if tHandle == self._docHandle:
- self.docHeader.setTitleFromHandle(self._docHandle)
- self.docFooter.updateInfo()
- self.updateDocMargins()
- return
-
##
# Properties
##
@@ -1068,7 +1058,22 @@ class GuiDocEditor(QTextEdit):
return
##
- # Slots
+ # Public Slots
+ ##
+
+ @pyqtSlot(str)
+ def updateDocInfo(self, tHandle):
+ """Called when an item label is changed to check if the document
+ title bar needs updating,
+ """
+ if tHandle == self._docHandle:
+ self.docHeader.setTitleFromHandle(self._docHandle)
+ self.docFooter.updateInfo()
+ self.updateDocMargins()
+ return
+
+ ##
+ # Private Slots
##
@pyqtSlot(int, int, int)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 313cce0d..89ae8ea2 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -322,15 +322,6 @@ class GuiDocViewer(QTextBrowser):
return
- def updateDocInfo(self, tHandle):
- """Called when an item label is changed to check if the document
- title bar needs updating,
- """
- if tHandle == self._docHandle:
- self.docHeader.setTitleFromHandle(self._docHandle)
- self.updateDocMargins()
- return
-
##
# Properties
##
@@ -389,7 +380,21 @@ class GuiDocViewer(QTextBrowser):
return 0
##
- # Slots
+ # Public Slots
+ ##
+
+ @pyqtSlot(str)
+ def updateDocInfo(self, tHandle):
+ """Called when an item label is changed to check if the document
+ title bar needs updating,
+ """
+ if tHandle == self._docHandle:
+ self.docHeader.setTitleFromHandle(self._docHandle)
+ self.updateDocMargins()
+ return
+
+ ##
+ # Private Slots
##
@pyqtSlot("QUrl")
diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py
index 89a38b76..643479a4 100644
--- a/novelwriter/gui/itemdetails.py
+++ b/novelwriter/gui/itemdetails.py
@@ -220,6 +220,11 @@ class GuiItemDetails(QWidget):
"""
self.updateViewBox(self._itemHandle)
+ ##
+ # Public Slots
+ ##
+
+ @pyqtSlot(str)
def updateViewBox(self, tHandle):
"""Populate the details box from a given handle.
"""
@@ -290,10 +295,6 @@ class GuiItemDetails(QWidget):
return
- ##
- # Slots
- ##
-
@pyqtSlot(str, int, int, int)
def doUpdateCounts(self, tHandle, cC, wC, pC):
"""Update the counts if the handle is the same as the one we're
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index e5ad816e..9ac2ea2b 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -146,7 +146,6 @@ class GuiNovelTree(QTreeWidget):
if selItem:
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
- self.theParent.treeView.flushTreeOrder()
self._populateTree()
if titleKey is not None and titleKey in self._treeMap:
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index ea919615..20464485 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -126,17 +126,21 @@ class GuiOutline(QWidget):
return self.outlineView.setFocus()
##
- # Slots
+ # Public Slots
##
- @pyqtSlot()
- def projectUpdated(self):
- """Should be called whenever the number of root folders change.
+ @pyqtSlot(str)
+ def updateRootItem(self, tHandle):
+ """Should be called whenever a root folders changes.
"""
self.outlineBar.populateNovelList()
self.outlineData.updateClasses()
return
+ ##
+ # Private Slots
+ ##
+
@pyqtSlot()
def _updateMenuColumns(self):
"""Trigger an update of the toggled state of the column menu
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 833b8b87..b4f99421 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -32,11 +32,13 @@ 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
+ QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame,
+ QDialog
)
from novelwriter.core import NWDoc
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
+from novelwriter.dialogs.itemeditor import GuiItemEditor
logger = logging.getLogger(__name__)
@@ -48,10 +50,10 @@ class GuiProjectTree(QTreeWidget):
C_EXPORT = 2
C_STATUS = 3
- novelItemChanged = pyqtSignal()
- noteItemChanged = pyqtSignal()
+ treeItemChanged = pyqtSignal(str)
+ novelItemChanged = pyqtSignal(str)
+ rootFolderChanged = pyqtSignal(str)
wordCountsChanged = pyqtSignal()
- rootFoldersChanged = pyqtSignal()
def __init__(self, theParent):
QTreeWidget.__init__(self, theParent)
@@ -64,10 +66,9 @@ class GuiProjectTree(QTreeWidget):
self.theProject = theParent.theProject
# Internal Variables
- self._treeMap = {}
- self._treeChanged = False
+ self._treeMap = {}
+ self._lastMove = {}
self._timeChanged = 0
- self._lastMove = {}
##
# Build GUI
@@ -157,15 +158,15 @@ class GuiProjectTree(QTreeWidget):
"""
self.clear()
self._treeMap = {}
- self._treeChanged = False
+ self._lastMove = {}
self._timeChanged = 0
return
def newTreeItem(self, itemType, itemClass=None):
"""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 other
- meta data is set correctly to ensure a valid project tree.
+ 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
+ other meta data is set correctly to ensure a valid project tree.
"""
if not self.theParent.hasProject:
logger.error("No project open")
@@ -177,7 +178,6 @@ class GuiProjectTree(QTreeWidget):
if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
tHandle = self.theProject.newRoot(itemClass)
- self.rootFoldersChanged.emit()
elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
@@ -223,9 +223,9 @@ class GuiProjectTree(QTreeWidget):
# Add the new item to the tree
self.revealNewTreeItem(tHandle, nHandle)
self.theParent.editItem(tHandle)
- nwItem = self.theProject.tree[tHandle]
- # If this is a folder, return here
+ # Handle new file creation
+ nwItem = self.theProject.tree[tHandle]
if nwItem.itemType != nwItemType.FILE:
return True
@@ -268,7 +268,7 @@ class GuiProjectTree(QTreeWidget):
if pHandle is not None and pHandle in self._treeMap:
self._treeMap[pHandle].setExpanded(True)
- self._emitItemChange(tHandle)
+ self._alertTreeChange(tHandle=tHandle, flush=True)
self.clearSelection()
trItem.setSelected(True)
@@ -310,10 +310,31 @@ class GuiProjectTree(QTreeWidget):
pItem.insertChild(nIndex, cItem)
self._recordLastMove(cItem, pItem, tIndex)
+ self._alertTreeChange(tHandle=tHandle, flush=True)
self.clearSelection()
cItem.setSelected(True)
- self._setTreeChanged(True)
- self._emitItemChange(tHandle)
+
+ return True
+
+ def editTreeItem(self, tHandle=None):
+ """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
+ if tItem.itemType == nwItemType.NO_TYPE:
+ return False
+
+ logger.verbose("Requesting change to item '%s'", tHandle)
+ dlgProj = GuiItemEditor(self, tHandle)
+ dlgProj.exec_()
+ if dlgProj.result() == QDialog.Accepted:
+ self.setTreeItemValues(tHandle)
+ self._alertTreeChange(tHandle=tHandle, flush=False)
return True
@@ -330,16 +351,6 @@ class GuiProjectTree(QTreeWidget):
self.theProject.setTreeOrder(theList)
return True
- def flushTreeOrder(self):
- """Calls saveTreeOrder if there are unsaved changes, otherwise
- does nothing.
- """
- if self._treeChanged:
- logger.verbose("Flushing project tree to project class")
- self.saveTreeOrder()
- self._setTreeChanged(False)
- return
-
def getTreeFromHandle(self, tHandle):
"""Recursively return all the children items starting from a
given item handle.
@@ -411,7 +422,7 @@ class GuiProjectTree(QTreeWidget):
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
if nTrash > 0:
- self._setTreeChanged(True)
+ self._alertTreeChange(tHandle=trashHandle, flush=True)
return True
@@ -445,6 +456,7 @@ class GuiProjectTree(QTreeWidget):
return False
wCount = self._getItemWordCount(tHandle)
+ autoFlush = not bulkAction
if nwItemS.itemType == nwItemType.ROOT:
# Only an empty ROOT folder can be deleted
logger.debug("User requested a root folder '%s' deleted", tHandle)
@@ -452,8 +464,7 @@ class GuiProjectTree(QTreeWidget):
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle)
- self._setTreeChanged(True)
- self.rootFoldersChanged.emit()
+ self._alertTreeChange(tHandle=tHandle, flush=True)
else:
self.theParent.makeAlert(self.tr(
"Cannot delete root folder. It is not empty. "
@@ -469,7 +480,7 @@ class GuiProjectTree(QTreeWidget):
tIndex = trItemP.indexOfChild(trItemS)
trItemP.takeChild(tIndex)
self._deleteTreeItem(tHandle)
- self._setTreeChanged(True)
+ self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
else:
# A populated FOLDER or a FILE requires confirmtation
@@ -505,7 +516,7 @@ class GuiProjectTree(QTreeWidget):
self.theParent.closeDocument()
self._deleteTreeItem(dHandle)
- self._setTreeChanged(True)
+ self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
self.wordCountsChanged.emit()
else:
@@ -524,7 +535,7 @@ class GuiProjectTree(QTreeWidget):
trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex)
- self._setTreeChanged(True)
+ self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
return True
@@ -660,6 +671,7 @@ class GuiProjectTree(QTreeWidget):
dstItem.insertChild(dstIndex, movItem)
self._postItemMove(sHandle, wCount)
+ self._alertTreeChange(tHandle=sHandle, flush=True)
self.clearSelection()
movItem.setSelected(True)
@@ -788,6 +800,7 @@ class GuiProjectTree(QTreeWidget):
QTreeWidget.dropEvent(self, theEvent)
self._postItemMove(sHandle, wCount)
self._recordLastMove(sItem, pItem, pIndex)
+ self._alertTreeChange(tHandle=sHandle, flush=True)
sItem.setExpanded(isExpanded)
return
@@ -829,8 +842,6 @@ class GuiProjectTree(QTreeWidget):
# Trigger dependent updates
self.propagateCount(tHandle, wCount)
- self._setTreeChanged(True)
- self._emitItemChange(tHandle)
return True
@@ -931,8 +942,6 @@ class GuiProjectTree(QTreeWidget):
self.setTreeItemValues(tHandle)
newItem.setExpanded(nwItem.isExpanded)
- self._setTreeChanged(True)
-
return newItem
def _addTrashRoot(self):
@@ -945,33 +954,34 @@ class GuiProjectTree(QTreeWidget):
trItem = self._getTreeItem(trashHandle)
if trItem is None:
- trItem = self._addTreeItem(
- self.theProject.tree[trashHandle]
- )
+ trItem = self._addTreeItem(self.theProject.tree[trashHandle])
if trItem is not None:
trItem.setExpanded(True)
- self._setTreeChanged(True)
+ self._alertTreeChange(tHandle=trashHandle, flush=True)
return trItem
- def _setTreeChanged(self, theState):
- """Set the tree change flag, and propagate to the project.
+ def _alertTreeChange(self, tHandle=None, flush=True):
+ """Update information on tree change state, and emit necessary
+ signals.
"""
- self._treeChanged = theState
- if theState:
- self._timeChanged = time()
- self.theProject.setProjectChanged(True)
- return
+ self._timeChanged = time()
+ self.theProject.setProjectChanged(True)
+ if flush:
+ self.saveTreeOrder()
+
+ tItem = self.theProject.tree[tHandle]
+ if tItem is None:
+ return
+
+ itemType = tItem.itemType
+ if itemType == nwItemType.ROOT:
+ self.rootFolderChanged.emit(tHandle)
+ elif itemType == nwItemType.FILE and tItem.isNovelLike():
+ self.novelItemChanged.emit(tHandle)
+
+ self.treeItemChanged.emit(tHandle)
- def _emitItemChange(self, tHandle):
- """Emit an item change signal for a given handle.
- """
- if self.theProject.tree.checkType(tHandle, nwItemType.FILE):
- nwItem = self.theProject.tree[tHandle]
- if nwItem.isNovelLike():
- self.novelItemChanged.emit()
- else:
- self.noteItemChanged.emit()
return
def _recordLastMove(self, srcItem, parItem, parIndex):
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index ad6dd742..c2051c6e 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -44,9 +44,8 @@ from novelwriter.gui import (
GuiViewsBar
)
from novelwriter.dialogs import (
- GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences,
- GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, GuiUpdates,
- GuiWordList
+ GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
+ GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
)
from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
@@ -123,7 +122,10 @@ class GuiMain(QMainWindow):
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
- self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated)
+ 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)
@@ -357,7 +359,7 @@ class GuiMain(QMainWindow):
self.rebuildTrees()
self.saveProject()
self.docEditor.setDictionaries()
- self.projView.projectUpdated()
+ self.projView.updateRootItem(None)
self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
@@ -504,7 +506,7 @@ class GuiMain(QMainWindow):
self.docEditor.setDictionaries()
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.statusBar.setRefTime(self.theProject.projOpened)
- self.projView.projectUpdated()
+ self.projView.updateRootItem(None)
self._updateStatusWordCount()
# Restore previously open documents, if any
@@ -596,7 +598,6 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
- self.treeView.flushTreeOrder()
nHandle = None # The next handle after tHandle
fHandle = None # The first file handle we encounter
foundIt = False # We've found tHandle, pick the next we see
@@ -813,27 +814,10 @@ class GuiMain(QMainWindow):
tHandle = self.docEditor.docHandle()
else:
tHandle = self.treeView.getSelectedHandle()
+ if tHandle:
+ return self.treeView.editTreeItem(tHandle)
- if tHandle is None:
- logger.warning("No item selected")
- return False
-
- tItem = self.theProject.tree[tHandle]
- if tItem is None:
- return False
- if tItem.itemType == nwItemType.NO_TYPE:
- return False
-
- logger.verbose("Requesting change to item '%s'", tHandle)
- dlgProj = GuiItemEditor(self, tHandle)
- dlgProj.exec_()
- if dlgProj.result() == QDialog.Accepted:
- self.treeView.setTreeItemValues(tHandle)
- self.treeMeta.updateViewBox(tHandle)
- self.docEditor.updateDocInfo(tHandle)
- self.docViewer.updateDocInfo(tHandle)
-
- return True
+ return False
def rebuildTrees(self):
"""Rebuild the project tree.
@@ -966,8 +950,6 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
- self.treeView.flushTreeOrder()
-
dlgDetails = getGuiItem("GuiProjectDetails")
if dlgDetails is None:
dlgDetails = GuiProjectDetails(self)
@@ -1583,7 +1565,6 @@ class GuiMain(QMainWindow):
if self.mainStack.currentIndex() == self.idxOutlineView:
logger.verbose("Novel tree changed while Outline tab active")
if self.hasProject:
- self.treeView.flushTreeOrder()
self.projView.refreshView(novelChanged=True)
return
diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py
index 06849ee9..66f203bd 100644
--- a/novelwriter/tools/build.py
+++ b/novelwriter/tools/build.py
@@ -709,7 +709,6 @@ class GuiBuildNovel(QDialog):
bldObj.initDocument()
# Make sure the project and document is up to date
- self.theParent.treeView.flushTreeOrder()
self.theParent.saveDocument()
self.buildProgress.setMaximum(len(self.theProject.tree))
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index 39e0b4dc..96e04da4 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -246,17 +246,14 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
# Move novel folder up
assert nwTree.moveTreeItem(-1) is False
- nwTree.flushTreeOrder()
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
# Move novel folder down
assert nwTree.moveTreeItem(1) is True
- nwTree.flushTreeOrder()
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1
# Move novel folder up again
assert nwTree.moveTreeItem(-1) is True
- nwTree.flushTreeOrder()
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
# Clean up
@@ -432,10 +429,8 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR
assert nwTree.emptyTrash() is False
# Empty the trash proper
- nwTree._setTreeChanged(False)
assert nwTree.emptyTrash() is True
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
- assert nwTree._treeChanged is True
# Try to delete a file, but block the underlying deletion of the file on disk
assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))
From 92924b3288e6c9abf404e197d86b3ad34edf458b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 5 Jun 2022 17:41:29 +0200
Subject: [PATCH 10/12] Make the new item function better at guessing header
level of new files
---
novelwriter/core/index.py | 5 +++++
novelwriter/gui/projtree.py | 11 ++++++++---
tests/test_core/test_core_index.py | 3 +++
tests/test_gui/test_gui_projtree.py | 2 +-
4 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index ee9ea089..f86de8c2 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -502,6 +502,11 @@ 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 b4f99421..57981904 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -38,6 +38,7 @@ from PyQt5.QtWidgets import (
from novelwriter.core import NWDoc
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
+from novelwriter.common import minmax
from novelwriter.dialogs.itemeditor import GuiItemEditor
logger = logging.getLogger(__name__)
@@ -188,9 +189,11 @@ class GuiProjectTree(QTreeWidget):
), nwAlert.ERROR)
return False
- # If the selected item is a file, the new item will be a sibling
+ # If the selected item is a file, the new item will be a
+ # sibling if the file has no children, otherwise a child
pItem = self.theProject.tree[sHandle]
- if pItem.itemType == nwItemType.FILE:
+ qItem = self._getTreeItem(sHandle)
+ if pItem.itemType == nwItemType.FILE and qItem.childCount() == 0:
nHandle = sHandle
sHandle = pItem.itemParent
if sHandle is None:
@@ -233,7 +236,9 @@ class GuiProjectTree(QTreeWidget):
newDoc = NWDoc(self.theProject, tHandle)
if not newDoc.readDocument():
if nwItem.itemLayout == nwItemLayout.DOCUMENT:
- newText = f"### {nwItem.itemName}\n\n"
+ 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"
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 8f126e56..3d40a9df 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -232,6 +232,9 @@ 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_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index 96e04da4..258ad150 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -96,7 +96,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd)
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 Document\n\n"
# Add a new file to the characters folder
nwTree.setSelectedHandle("000000000000a")
From 85f63721e04c350d91a4341a6110f7a864e4ac06 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 5 Jun 2022 18:43:09 +0200
Subject: [PATCH 11/12] Allow showing all novel files in Outline
---
novelwriter/core/tree.py | 11 +++++------
novelwriter/gui/outline.py | 39 +++++++++++++++++++------------------
novelwriter/gui/projtree.py | 2 +-
3 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index d151f370..10a7a78f 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -273,14 +273,13 @@ class NWTree():
rootClasses.add(nwItem.itemClass)
return rootClasses
- def novelRoots(self):
- """Return a doctionary of all novel-like root items.
+ def iterRoots(self, itemClass):
+ """Iterate over all items of a given class.
"""
- novelItems = {}
for tHandle, nwItem in self._treeRoots.items():
- if nwItem.isNovelLike():
- novelItems[tHandle] = nwItem
- return novelItems
+ if nwItem.itemClass == itemClass:
+ yield tHandle, nwItem
+ return
def isRoot(self, tHandle):
"""Check if a handle is a root item.
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 20464485..61b35410 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -83,11 +83,8 @@ class GuiOutline(QWidget):
self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns)
self.outlineView.activeItemChanged.connect(self.outlineData.showItem)
self.outlineData.itemTagClicked.connect(self._tagClicked)
- self.outlineBar.novelRootChanged.connect(self._rootItemChanged)
+ self.outlineBar.loadNovelRootRequest.connect(self._rootItemChanged)
self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled)
- self.outlineBar.viewRefreshRequested.connect(
- lambda: self.outlineView.refreshTree(overRide=True)
- )
# Function Mappings
self.getSelectedHandle = self.outlineView.getSelectedHandle
@@ -160,9 +157,9 @@ class GuiOutline(QWidget):
@pyqtSlot(str)
def _rootItemChanged(self, handle):
- """The root novel handle has been changed.
+ """The root novel handle has changed or needs to be refreshed.
"""
- self.outlineView.refreshTree(rootHandle=handle, overRide=True)
+ self.outlineView.refreshTree(rootHandle=(handle or None), overRide=True)
return
# END Class GuiOutline
@@ -170,8 +167,7 @@ class GuiOutline(QWidget):
class GuiOutlineToolBar(QToolBar):
- novelRootChanged = pyqtSignal(str)
- viewRefreshRequested = pyqtSignal()
+ loadNovelRootRequest = pyqtSignal(str)
viewColumnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
@@ -206,9 +202,7 @@ class GuiOutlineToolBar(QToolBar):
# Actions
self.aRefresh = QAction(self.tr("Refresh"), self)
self.aRefresh.setIcon(self.theTheme.getIcon("refresh"))
- self.aRefresh.triggered.connect(
- lambda: self.viewRefreshRequested.emit()
- )
+ self.aRefresh.triggered.connect(self._refreshRequested)
# Column Menu
self.mColumns = GuiOutlineHeaderMenu(self)
@@ -236,14 +230,14 @@ class GuiOutlineToolBar(QToolBar):
##
def populateNovelList(self):
- """Fill the novel combo box.
+ """Fill the novel combo box with a list of all novel folders.
"""
self.novelValue.clear()
- for tHandle, nwItem in self.theProject.tree.novelRoots().items():
- self.novelValue.addItem(
- self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]),
- nwItem.itemName, tHandle
- )
+ tIcon = self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
+ for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL):
+ self.novelValue.addItem(tIcon, nwItem.itemName, tHandle)
+ self.novelValue.insertSeparator(self.novelValue.count())
+ self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "")
return
def setColumnHiddenState(self, hiddenState):
@@ -253,7 +247,7 @@ class GuiOutlineToolBar(QToolBar):
return
##
- # Slots
+ # Private Slots
##
@pyqtSlot(int)
@@ -261,7 +255,14 @@ class GuiOutlineToolBar(QToolBar):
"""Emit a signal containing the handle of the selected item.
"""
if index >= 0:
- self.novelRootChanged.emit(self.novelValue.currentData())
+ self.loadNovelRootRequest.emit(self.novelValue.currentData())
+ return
+
+ @pyqtSlot()
+ def _refreshRequested(self):
+ """Emit a signal containing the handle of the selected item.
+ """
+ self.loadNovelRootRequest.emit(self.novelValue.currentData())
return
# END Class GuiOutlineToolBar
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 57981904..e0a8ef62 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -939,7 +939,7 @@ class GuiProjectTree(QTreeWidget):
except Exception:
logger.error("Failed to get index of item with handle '%s'", nHandle)
if byIndex >= 0:
- self._treeMap[pHandle].insertChild(byIndex+1, newItem)
+ self._treeMap[pHandle].insertChild(byIndex + 1, newItem)
else:
self._treeMap[pHandle].addChild(newItem)
self.propagateCount(tHandle, nwItem.wordCount, countChildren=True)
From 4d9b94cfe68d1ac1a6e240f1d52f93cbafd5a127 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 5 Jun 2022 22:31:10 +0200
Subject: [PATCH 12/12] Improve test coverage of Outline
---
novelwriter/gui/outline.py | 2 +-
novelwriter/guimain.py | 2 +-
tests/test_gui/test_gui_outline.py | 198 ++++++++++++++++++++++++++---
3 files changed, 181 insertions(+), 21 deletions(-)
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 61b35410..34cb35da 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -461,7 +461,7 @@ class GuiOutlineView(QTreeWidget):
document editor.
"""
tHandle, tLine = self.getSelectedHandle()
- self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
+ self.theParent.openDocument(tHandle, tLine=tLine - 1, doScroll=True)
return
@pyqtSlot()
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index c2051c6e..6a056dca 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -1453,7 +1453,7 @@ class GuiMain(QMainWindow):
return
##
- # Slots
+ # Private Slots
##
@pyqtSlot(str, Enum)
diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py
index f089928e..49a86b16 100644
--- a/tests/test_gui/test_gui_outline.py
+++ b/tests/test_gui/test_gui_outline.py
@@ -19,17 +19,144 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+import os
+import time
import pytest
-from PyQt5.QtWidgets import QTreeWidgetItem, QMessageBox
+from tools import buildTestProject, writeFile
-keyDelay = 2
-typeDelay = 1
-stepDelay = 20
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import QWidget, QMessageBox, QAction
+
+from novelwriter.enum import nwItemClass, nwOutline, nwView
@pytest.mark.gui
-def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
+def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir):
+ """Test the outline view.
+ """
+ # Block message box
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
+
+ # Create a project
+ prjDir = os.path.join(fncDir, "project")
+ buildTestProject(nwGUI, prjDir)
+
+ nwGUI.rebuildIndex()
+ nwGUI._changeView(nwView.OUTLINE)
+
+ outlineMain = nwGUI.projView
+ outlineView = outlineMain.outlineView
+ outlineData = outlineMain.outlineData
+ outlineMenu = outlineMain.outlineBar.mColumns
+
+ # Toggle scrollbars
+ nwGUI.mainConf.hideVScroll = True
+ nwGUI.mainConf.hideHScroll = True
+ nwGUI.projView.initOutline()
+ assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
+ assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
+ assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
+ assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
+
+ nwGUI.mainConf.hideVScroll = False
+ nwGUI.mainConf.hideHScroll = False
+ nwGUI.projView.initOutline()
+ assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
+ assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
+ assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
+ assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
+
+ # Check focus
+ with monkeypatch.context() as mp:
+ mp.setattr(QWidget, "hasFocus", lambda *a: True)
+ assert outlineMain.treeFocus() is True
+
+ outlineMain.setTreeFocus() # Can't check. just ensures that it doesn't error
+
+ # Option State
+ # ============
+ pOptions = nwGUI.theProject.options
+ colNames = [h.name for h in nwOutline]
+ colItems = [h for h in nwOutline]
+ colWidth = {h: outlineView.DEF_WIDTH[h] for h in nwOutline}
+ colHidden = {h: outlineView.DEF_HIDDEN[h] for h in nwOutline}
+
+ assert outlineView.topLevelItemCount() > 0
+
+ # Save header state not allowed
+ outlineView._lastBuild = 0
+ outlineView._saveHeaderState()
+ assert pOptions.getValue("GuiOutline", "headerOrder", []) == []
+
+ # Allow saving header state
+ outlineView._lastBuild = time.time()
+ outlineView._saveHeaderState()
+ assert pOptions.getValue("GuiOutline", "headerOrder", []) == colNames
+ assert outlineView._treeOrder == colItems
+ assert outlineView._colWidth == colWidth
+ assert outlineView._colHidden == colHidden
+
+ # Get default values
+ optItems = pOptions.getValue("GuiOutline", "headerOrder", [])
+ optWidth = pOptions.getValue("GuiOutline", "columnWidth", {})
+ optHidden = pOptions.getValue("GuiOutline", "columnHidden", {})
+
+ # Add invalid column name
+ pOptions.setValue("GuiOutline", "headerOrder", optItems + ["blabla"])
+ outlineView._loadHeaderState()
+ assert outlineView._treeOrder == colItems
+ assert outlineView._colHidden == colHidden
+
+ # Add duplicate column name
+ pOptions.setValue("GuiOutline", "headerOrder", optItems + [optItems[-1]])
+ outlineView._loadHeaderState()
+ assert outlineView._treeOrder == colItems
+ assert outlineView._colHidden == colHidden
+
+ # Invalid column width data
+ pOptions.setValue("GuiOutline", "headerOrder", optItems)
+ pOptions.setValue("GuiOutline", "columnWidth", {"blabla": None})
+ outlineView._loadHeaderState()
+ assert outlineView._treeOrder == colItems
+ assert outlineView._colHidden == colHidden
+
+ # Invalid column width data
+ pOptions.setValue("GuiOutline", "headerOrder", optItems)
+ pOptions.setValue("GuiOutline", "columnWidth", optWidth)
+ pOptions.setValue("GuiOutline", "columnHidden", {"bloabla": None})
+ outlineView._loadHeaderState()
+ assert outlineView._treeOrder == colItems
+ assert outlineView._colHidden == colHidden
+
+ # Valid settings
+ pOptions.setValue("GuiOutline", "headerOrder", optItems)
+ pOptions.setValue("GuiOutline", "columnWidth", optWidth)
+ pOptions.setValue("GuiOutline", "columnHidden", optHidden)
+ outlineView._loadHeaderState()
+ assert outlineView._treeOrder == colItems
+ assert outlineView._colHidden == colHidden
+
+ # Header Menu
+ # ===========
+
+ # Trigger the menu entry for all hidden columns
+ for hItem in nwOutline:
+ if outlineView.DEF_HIDDEN[hItem]:
+ outlineMenu.actionMap[hItem].activate(QAction.Trigger)
+
+ # Now no columns should be hidden
+ outlineView._saveHeaderState()
+ assert not any(pOptions.getValue("GuiOutline", "columnHidden", None).values())
+
+ # qtbot.stop()
+
+# END Test testGuiOutline_Main
+
+
+@pytest.mark.gui
+def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum):
"""Test the outline view.
"""
# Block message box
@@ -40,26 +167,59 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.mainConf.lastPath = nwLipsum
nwGUI.rebuildIndex()
- nwGUI.mainStack.setCurrentIndex(nwGUI.idxOutlineView)
+ nwGUI._changeView(nwView.OUTLINE)
- outlineView = nwGUI.projView.outlineView
- outlineData = nwGUI.projView.outlineData
+ outlineMain = nwGUI.projView
+ outlineBar = outlineMain.outlineBar
+ outlineView = outlineMain.outlineView
+ outlineData = outlineMain.outlineData
- assert outlineView.topLevelItemCount() > 0
+ lipHandle = "b3643d0f92e32"
- # Context Menu
- # outlineView._headerRightClick(QPoint(1, 1))
- # outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger)
- # outlineView.headerMenu.close()
- # qtbot.mouseClick(outlineView, Qt.LeftButton)
+ # Check defaults in dropdown list
+ assert outlineBar.novelValue.itemData(0) == lipHandle
+ assert outlineBar.novelValue.itemData(1) is None # Separator
+ assert outlineBar.novelValue.itemData(2) == "" # All novels
- # outlineView._loadHeaderState()
- # assert not outlineView._colHidden[nwOutline.CCOUNT]
+ # Add a second novel folder
+ newHandle = nwGUI.theProject.newRoot(nwItemClass.NOVEL)
+ nwGUI.treeView.revealNewTreeItem(newHandle)
+
+ # Check new values in dropdown list
+ assert outlineBar.novelValue.itemData(0) == lipHandle
+ assert outlineBar.novelValue.itemData(1) == newHandle
+ assert outlineBar.novelValue.itemData(2) is None # Separator
+ assert outlineBar.novelValue.itemData(3) == "" # All novels
+
+ # Add a bunch of files in a header order that hits all tree combos
+ docList = [
+ ("Section 1", 4), ("Scene 1", 3), ("Chapter 1", 2), ("Part 1", 1),
+ ("Section 2", 4), ("Scene 2", 3), ("Chapter 2", 2),
+ ("Section 3", 4), ("Scene 3", 3),
+ ("Section 4", 4),
+ ]
+ for dTitle, hLevel in docList:
+ aHandle = nwGUI.theProject.newFile(dTitle, newHandle)
+ hHash = "#"*hLevel
+ writeFile(os.path.join(nwLipsum, "content", f"{aHandle}.nwd"), f"{hHash} {dTitle}\n\n")
+ nwGUI.treeView.revealNewTreeItem(aHandle)
+
+ nwGUI.rebuildIndex()
+
+ # Build the second novel
+ outlineBar.novelValue.setCurrentIndex(1)
+ outlineBar._refreshRequested()
+
+ # Go back to Lipsum
+ outlineBar.novelValue.setCurrentIndex(0)
+ outlineBar._refreshRequested()
+
+ # Check Details
+ # =============
# First Item
outlineView.refreshTree()
selItem = outlineView.topLevelItem(0)
- assert isinstance(selItem, QTreeWidgetItem)
outlineView.setCurrentItem(selItem)
assert outlineData.titleLabel.text() == "Title"
@@ -110,6 +270,6 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
outlineView._treeDoubleClick(selItem, 0)
assert nwGUI.docEditor.docHandle() == "88243afbe5ed8"
- # qtbot.stopForInteraction()
+ # qtbot.stop()
-# END Test testGuiOutline_Main
+# END Test testGuiOutline_Content