Change how tag links are followed

This commit is contained in:
Veronica Berglyd Olsen
2022-05-26 12:11:22 +02:00
parent ecc0585c87
commit 8ac30b036a
5 changed files with 123 additions and 83 deletions
+8
View File
@@ -62,6 +62,14 @@ class nwItemLayout(Enum):
# END Enum nwItemLayout # END Enum nwItemLayout
class nwDocMode(Enum):
VIEW = 0
EDIT = 1
# END Enum nwDocMode
class nwDocAction(Enum): class nwDocAction(Enum):
NO_ACTION = 0 NO_ACTION = 0
+4 -2
View File
@@ -33,6 +33,7 @@ import bisect
import logging import logging
import novelwriter import novelwriter
from enum import Enum
from time import time from time import time
from PyQt5.QtCore import ( from PyQt5.QtCore import (
@@ -50,7 +51,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.core import NWDoc, NWSpellEnchant, countWords 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.common import transferCase
from novelwriter.constants import nwConst, nwKeyWords, nwUnicode from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
@@ -69,6 +70,7 @@ class GuiDocEditor(QTextEdit):
spellDictionaryChanged = pyqtSignal(str, str) spellDictionaryChanged = pyqtSignal(str, str)
docEditedStatusChanged = pyqtSignal(bool) docEditedStatusChanged = pyqtSignal(bool)
docCountsChanged = pyqtSignal(str, int, int, int) docCountsChanged = pyqtSignal(str, int, int, int)
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent): def __init__(self, theParent):
QTextEdit.__init__(self, theParent) QTextEdit.__init__(self, theParent)
@@ -1895,7 +1897,7 @@ class GuiDocEditor(QTextEdit):
if loadTag: if loadTag:
logger.verbose("Attempting to follow tag '%s'", theWord) logger.verbose("Attempting to follow tag '%s'", theWord)
self.theParent.docViewer.loadFromTag(theWord) self.loadDocumentTagRequest.emit(theWord, nwDocMode.VIEW)
else: else:
logger.verbose("Potential tag '%s'", theWord) logger.verbose("Potential tag '%s'", theWord)
+8 -28
View File
@@ -30,7 +30,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging import logging
import novelwriter 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 ( from PyQt5.QtGui import (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
) )
@@ -40,7 +42,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.core import ToHtml 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.error import logException
from novelwriter.constants import nwUnicode from novelwriter.constants import nwUnicode
@@ -49,6 +51,8 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser): class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent): def __init__(self, theParent):
QTextBrowser.__init__(self, theParent) QTextBrowser.__init__(self, theParent)
@@ -239,30 +243,6 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins() self.updateDocMargins()
return 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): def docAction(self, theAction):
"""Wrapper function for various document actions on the current """Wrapper function for various document actions on the current
document. document.
@@ -414,14 +394,14 @@ class GuiDocViewer(QTextBrowser):
@pyqtSlot("QUrl") @pyqtSlot("QUrl")
def _linkClicked(self, theURL): def _linkClicked(self, theURL):
"""Slot for a link in the document being clicked. """Process a clicked link internally in the document.
""" """
theLink = theURL.url() theLink = theURL.url()
logger.verbose("Clicked link: '%s'", theLink) logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) > 0: if len(theLink) > 0:
theBits = theLink.split("=") theBits = theLink.split("=")
if len(theBits) == 2: if len(theBits) == 2:
self.loadFromTag(theBits[1]) self.loadDocumentTagRequest.emit(theBits[1], nwDocMode.VIEW)
return return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
+62 -46
View File
@@ -43,7 +43,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.enum import ( from novelwriter.enum import (
nwItemClass, nwItemLayout, nwItemType, nwOutline, nwView nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
) )
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels from novelwriter.constants import trConst, nwKeyWords, nwLabels
@@ -54,14 +54,13 @@ logger = logging.getLogger(__name__)
class GuiOutline(QWidget): class GuiOutline(QWidget):
viewChangeRequested = pyqtSignal(nwView) loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent): def __init__(self, theParent):
QWidget.__init__(self, theParent) QWidget.__init__(self, theParent)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theParent = theParent self.theParent = theParent
self.theProject = theParent.theProject
self.outlineBar = GuiOutlineToolBar(self) self.outlineBar = GuiOutlineToolBar(self)
self.outlineView = GuiOutlineView(self) self.outlineView = GuiOutlineView(self)
@@ -82,7 +81,9 @@ class GuiOutline(QWidget):
# Connect Signals # Connect Signals
self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns) 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( self.outlineBar.viewRefreshRequested.connect(
lambda: self.outlineView.refreshTree(overRide=True) lambda: self.outlineView.refreshTree(overRide=True)
) )
@@ -144,13 +145,22 @@ class GuiOutline(QWidget):
self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns) self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns)
return 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 # END Class GuiOutline
class GuiOutlineToolBar(QToolBar): class GuiOutlineToolBar(QToolBar):
columnToggled = pyqtSignal(bool, Enum) novelRootChanged = pyqtSignal(str)
viewRefreshRequested = pyqtSignal() viewRefreshRequested = pyqtSignal()
viewColumnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline): def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline) QTreeWidget.__init__(self, theOutline)
@@ -158,7 +168,6 @@ class GuiOutlineToolBar(QToolBar):
logger.debug("Initialising GuiOutlineToolBar ...") logger.debug("Initialising GuiOutlineToolBar ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theOutline = theOutline
self.theParent = theOutline.theParent self.theParent = theOutline.theParent
self.theProject = theOutline.theParent.theProject self.theProject = theOutline.theParent.theProject
self.theTheme = theOutline.theParent.theTheme self.theTheme = theOutline.theParent.theTheme
@@ -180,6 +189,7 @@ class GuiOutlineToolBar(QToolBar):
self.novelValue = QComboBox(self) self.novelValue = QComboBox(self)
self.novelValue.setMinimumWidth(self.mainConf.pxInt(200)) self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
self.novelValue.currentIndexChanged.connect(self._novelValueChanged)
# Actions # Actions
self.aRefresh = QAction(self.tr("Refresh"), self) self.aRefresh = QAction(self.tr("Refresh"), self)
@@ -191,7 +201,7 @@ class GuiOutlineToolBar(QToolBar):
# Column Menu # Column Menu
self.mColumns = GuiOutlineHeaderMenu(self) self.mColumns = GuiOutlineHeaderMenu(self)
self.mColumns.columnToggled.connect( self.mColumns.columnToggled.connect(
lambda isChecked, tItem: self.columnToggled.emit(isChecked, tItem) lambda isChecked, tItem: self.viewColumnToggled.emit(isChecked, tItem)
) )
self.tbColumns = QToolButton(self) self.tbColumns = QToolButton(self)
@@ -207,10 +217,12 @@ class GuiOutlineToolBar(QToolBar):
self.addWidget(self.tbColumns) self.addWidget(self.tbColumns)
self.addWidget(stretch) self.addWidget(stretch)
self.populateNovelList()
logger.debug("GuiOutlineToolBar initialisation complete") logger.debug("GuiOutlineToolBar initialisation complete")
##
# Methods
##
def populateNovelList(self): def populateNovelList(self):
"""Fill the novel combo box. """Fill the novel combo box.
""" """
@@ -223,9 +235,23 @@ class GuiOutlineToolBar(QToolBar):
return return
def setColumnHiddenState(self, hiddenState): def setColumnHiddenState(self, hiddenState):
"""Forward the change of column hidden states to the menu.
"""
self.mColumns.setHiddenState(hiddenState) self.mColumns.setHiddenState(hiddenState)
return 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 # END Class GuiOutlineToolBar
@@ -272,6 +298,7 @@ class GuiOutlineView(QTreeWidget):
} }
hiddenStateChanged = pyqtSignal() hiddenStateChanged = pyqtSignal()
activeItemChanged = pyqtSignal(str, str)
def __init__(self, theOutline): def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline) QTreeWidget.__init__(self, theOutline)
@@ -279,7 +306,6 @@ class GuiOutlineView(QTreeWidget):
logger.debug("Initialising GuiOutlineView ...") logger.debug("Initialising GuiOutlineView ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theOutline = theOutline
self.theParent = theOutline.theParent self.theParent = theOutline.theParent
self.theProject = theOutline.theParent.theProject self.theProject = theOutline.theParent.theProject
self.theTheme = theOutline.theParent.theTheme self.theTheme = theOutline.theParent.theTheme
@@ -436,7 +462,7 @@ class GuiOutlineView(QTreeWidget):
if selItems: if selItems:
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole) sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
self.theOutline.outlineData.showItem(tHandle, sTitle) self.activeItemChanged.emit(tHandle, sTitle)
return return
@@ -729,6 +755,8 @@ class GuiOutlineDetails(QScrollArea):
"H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"), "H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
} }
itemTagClicked = pyqtSignal(str)
def __init__(self, theOutline): def __init__(self, theOutline):
QScrollArea.__init__(self, theOutline) QScrollArea.__init__(self, theOutline)
@@ -828,15 +856,18 @@ class GuiOutlineDetails(QScrollArea):
self.entKeyValue.setWordWrap(True) self.entKeyValue.setWordWrap(True)
self.cstKeyValue.setWordWrap(True) self.cstKeyValue.setWordWrap(True)
self.povKeyValue.linkActivated.connect(self._tagClicked) def tagClicked(link):
self.focKeyValue.linkActivated.connect(self._tagClicked) self.itemTagClicked.emit(link)
self.chrKeyValue.linkActivated.connect(self._tagClicked)
self.pltKeyValue.linkActivated.connect(self._tagClicked) self.povKeyValue.linkActivated.connect(tagClicked)
self.timKeyValue.linkActivated.connect(self._tagClicked) self.focKeyValue.linkActivated.connect(tagClicked)
self.wldKeyValue.linkActivated.connect(self._tagClicked) self.chrKeyValue.linkActivated.connect(tagClicked)
self.objKeyValue.linkActivated.connect(self._tagClicked) self.pltKeyValue.linkActivated.connect(tagClicked)
self.entKeyValue.linkActivated.connect(self._tagClicked) self.timKeyValue.linkActivated.connect(tagClicked)
self.cstKeyValue.linkActivated.connect(self._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.povKeyLWrap.addWidget(self.povKeyValue, 1)
self.focKeyLWrap.addWidget(self.focKeyValue, 1) self.focKeyLWrap.addWidget(self.focKeyValue, 1)
@@ -963,6 +994,11 @@ class GuiOutlineDetails(QScrollArea):
self.updateClasses() self.updateClasses()
return return
##
# Slots
##
@pyqtSlot(str, str)
def showItem(self, tHandle, sTitle): def showItem(self, tHandle, sTitle):
"""Update the content of the tree with the given handle and line """Update the content of the tree with the given handle and line
number pointing to a header. number pointing to a header.
@@ -1006,22 +1042,6 @@ class GuiOutlineDetails(QScrollArea):
return True 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() @pyqtSlot()
def updateClasses(self): def updateClasses(self):
"""Update the visibility status of class details. """Update the visibility status of class details.
@@ -1050,16 +1070,12 @@ class GuiOutlineDetails(QScrollArea):
return return
## @staticmethod
# Internal Functions def _formatTags(refs, key):
## """Convert a list of tags into a list of clickable tag links.
def _formatTags(self, refs, key):
"""Format the tags as clickable links.
""" """
mKey = key[1:]
return ", ".join( return ", ".join(
[f"<a href='#{mKey}={tag}'>{tag}</a>" for tag in refs.get(key, [])] [f"<a href='{tag}'>{tag}</a>" for tag in refs.get(key, [])]
) )
# END Class GuiOutlineDetails # END Class GuiOutlineDetails
+41 -7
View File
@@ -27,6 +27,7 @@ import os
import logging import logging
import novelwriter import novelwriter
from enum import Enum
from time import time from time import time
from datetime import datetime from datetime import datetime
@@ -52,7 +53,7 @@ from novelwriter.tools import (
) )
from novelwriter.core import NWProject, NWIndex from novelwriter.core import NWProject, NWIndex
from novelwriter.enum import ( from novelwriter.enum import (
nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
) )
from novelwriter.common import getGuiItem, hexToInt from novelwriter.common import getGuiItem, hexToInt
@@ -117,10 +118,7 @@ class GuiMain(QMainWindow):
self.viewsBar = GuiViewsBar(self) self.viewsBar = GuiViewsBar(self)
# Connect Signals Between Main Elements # Connect Signals Between Main Elements
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.viewsBar.viewChangeRequested.connect(self._changeView)
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts)
self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts)
self.treeView.itemSelectionChanged.connect(self._treeSingleClick) self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
@@ -128,8 +126,15 @@ class GuiMain(QMainWindow):
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated) self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated)
self.viewsBar.viewChangeRequested.connect(self._changeView) self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
self.projView.viewChangeRequested.connect(self._changeView) 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 # Project Tree Stack
self.projStack = QStackedWidget() self.projStack = QStackedWidget()
@@ -1444,6 +1449,23 @@ class GuiMain(QMainWindow):
return projData 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 # Events
## ##
@@ -1462,6 +1484,18 @@ class GuiMain(QMainWindow):
# Slots # 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) @pyqtSlot(nwView)
def _changeView(self, view): def _changeView(self, view):
"""Handle the requested change of view from the GuiViewBar. """Handle the requested change of view from the GuiViewBar.