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