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] 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.