From fa32ef248bffddb84c594561eb13e8f023498ebe Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 9 Nov 2023 17:49:29 +0100
Subject: [PATCH 01/22] Add a basic doc viewer panel class
---
novelwriter/gui/docviewerpanel.py | 64 +++++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
create mode 100644 novelwriter/gui/docviewerpanel.py
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
new file mode 100644
index 00000000..9316fe2f
--- /dev/null
+++ b/novelwriter/gui/docviewerpanel.py
@@ -0,0 +1,64 @@
+"""
+novelWriter – GUI Document Viewer Panel
+=======================================
+
+File History:
+Created: 2023-11-09 [2.2a1] GuiDocViewerPanel
+
+This file is a part of novelWriter
+Copyright 2018–2023, 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 .
+"""
+from __future__ import annotations
+
+import logging
+
+from PyQt5.QtWidgets import QVBoxLayout, QWidget
+
+from novelwriter.gui.docviewer import GuiDocViewer
+
+logger = logging.getLogger(__name__)
+
+
+class GuiDocViewerPanel(QWidget):
+
+ def __init__(self, docViewer: GuiDocViewer) -> None:
+ super().__init__(parent=docViewer)
+
+ logger.debug("Create: GuiDocViewerPanel")
+
+ self.panelHeader = GuiDocViewerPanelHeader(self)
+
+ # Assemble
+ self.outerBox = QVBoxLayout()
+ self.outerBox.addWidget(self.panelHeader)
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
+
+ self.setLayout(self.outerBox)
+
+ logger.debug("Ready: GuiDocViewerPanel")
+
+ return
+
+# END Class GuiDocViewerPanel
+
+
+class GuiDocViewerPanelHeader(QWidget):
+
+ def __init__(self, docViewerPanel: GuiDocViewerPanel) -> None:
+ super().__init__(parent=docViewerPanel)
+ return
+
+# END Class GuiDocViewerPanelHeader
From 4d7ef63e8370624fc55ca1087bbf0b410a492646 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 14 Nov 2023 22:41:39 +0100
Subject: [PATCH 02/22] Connect the new viewer panel and add icon for it
---
.../assets/icons/typicons_dark/icons.conf | 2 +-
.../assets/icons/typicons_dark/nw_panel.svg | 4 +
.../assets/icons/typicons_dark/typ_at.svg | 4 -
.../assets/icons/typicons_light/icons.conf | 2 +-
.../assets/icons/typicons_light/nw_panel.svg | 4 +
.../assets/icons/typicons_light/typ_at.svg | 4 -
novelwriter/config.py | 8 +-
novelwriter/constants.py | 17 +-
novelwriter/gui/docviewer.py | 284 ++++--------------
novelwriter/gui/docviewerpanel.py | 72 ++++-
novelwriter/gui/theme.py | 4 +-
novelwriter/guimain.py | 54 ++--
12 files changed, 184 insertions(+), 275 deletions(-)
create mode 100644 novelwriter/assets/icons/typicons_dark/nw_panel.svg
delete mode 100644 novelwriter/assets/icons/typicons_dark/typ_at.svg
create mode 100644 novelwriter/assets/icons/typicons_light/nw_panel.svg
delete mode 100644 novelwriter/assets/icons/typicons_light/typ_at.svg
diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf
index 94b4c633..c51b2457 100644
--- a/novelwriter/assets/icons/typicons_dark/icons.conf
+++ b/novelwriter/assets/icons/typicons_dark/icons.conf
@@ -59,6 +59,7 @@ maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg
noncheckable = mixed_input-none.svg
+panel = nw_panel.svg
proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg
proj_document = typ_document-text.svg
@@ -68,7 +69,6 @@ proj_scene = mixed_document-scene.svg
proj_section = mixed_document-section.svg
proj_stats = typ_chart-bar-grey.svg
proj_title = mixed_document-title.svg
-reference = typ_at.svg
refresh = typ_refresh.svg
remove = typ_minus.svg
revert = typ_refresh-flipped.svg
diff --git a/novelwriter/assets/icons/typicons_dark/nw_panel.svg b/novelwriter/assets/icons/typicons_dark/nw_panel.svg
new file mode 100644
index 00000000..2863918f
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/nw_panel.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_dark/typ_at.svg b/novelwriter/assets/icons/typicons_dark/typ_at.svg
deleted file mode 100644
index 9966ede0..00000000
--- a/novelwriter/assets/icons/typicons_dark/typ_at.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf
index 62314511..5f300ad2 100644
--- a/novelwriter/assets/icons/typicons_light/icons.conf
+++ b/novelwriter/assets/icons/typicons_light/icons.conf
@@ -59,6 +59,7 @@ maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg
noncheckable = mixed_input-none.svg
+panel = nw_panel.svg
proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg
proj_document = typ_document-text.svg
@@ -68,7 +69,6 @@ proj_scene = mixed_document-scene.svg
proj_section = mixed_document-section.svg
proj_stats = typ_chart-bar-grey.svg
proj_title = mixed_document-title.svg
-reference = typ_at.svg
refresh = typ_refresh.svg
remove = typ_minus.svg
revert = typ_refresh-flipped.svg
diff --git a/novelwriter/assets/icons/typicons_light/nw_panel.svg b/novelwriter/assets/icons/typicons_light/nw_panel.svg
new file mode 100644
index 00000000..c78ab50b
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/nw_panel.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/typ_at.svg b/novelwriter/assets/icons/typicons_light/typ_at.svg
deleted file mode 100644
index 0578267f..00000000
--- a/novelwriter/assets/icons/typicons_light/typ_at.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
diff --git a/novelwriter/config.py b/novelwriter/config.py
index bc8af368..08230fba 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -179,9 +179,9 @@ class Config:
self.spellLanguage = "en"
# State
- self.showRefPanel = True # The reference panel for the viewer is visible
+ self.showViewerPanel = True # The panel for the viewer is visible
self.showEditToolBar = False # The document editor toolbar visibility
- self.useShortcodes = False # Use shorcodes for basic formatting
+ self.useShortcodes = False # Use shortcodes for basic formatting
self.viewComments = True # Comments are shown in the viewer
self.viewSynopsis = True # Synopsis is shown in the viewer
@@ -602,7 +602,7 @@ class Config:
# State
sec = "State"
- self.showRefPanel = conf.rdBool(sec, "showrefpanel", self.showRefPanel)
+ self.showViewerPanel = conf.rdBool(sec, "showviewerpanel", self.showViewerPanel)
self.showEditToolBar = conf.rdBool(sec, "showedittoolbar", self.showEditToolBar)
self.useShortcodes = conf.rdBool(sec, "useshortcodes", self.useShortcodes)
self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments)
@@ -727,7 +727,7 @@ class Config:
}
conf["State"] = {
- "showrefpanel": str(self.showRefPanel),
+ "showviewerpanel": str(self.showViewerPanel),
"showedittoolbar": str(self.showEditToolBar),
"useshortcodes": str(self.useShortcodes),
"viewcomments": str(self.viewComments),
diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index 514ea9f2..d8ce3562 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -153,6 +153,21 @@ class nwKeyWords:
# END Class nwKeyWords
+class nwLists:
+
+ USER_CLASSES = [
+ nwItemClass.CHARACTER,
+ nwItemClass.PLOT,
+ nwItemClass.WORLD,
+ nwItemClass.TIMELINE,
+ nwItemClass.OBJECT,
+ nwItemClass.ENTITY,
+ nwItemClass.CUSTOM,
+ ]
+
+# END Class nwLists
+
+
class nwLabels:
CLASS_NAME = {
@@ -221,8 +236,8 @@ class nwLabels:
nwOutline.FOCUS: QT_TRANSLATE_NOOP("Constant", "Focus"),
nwOutline.CHAR: KEY_NAME[nwKeyWords.CHAR_KEY],
nwOutline.PLOT: KEY_NAME[nwKeyWords.PLOT_KEY],
- nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
nwOutline.WORLD: KEY_NAME[nwKeyWords.WORLD_KEY],
+ nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
nwOutline.OBJECT: KEY_NAME[nwKeyWords.OBJECT_KEY],
nwOutline.ENTITY: KEY_NAME[nwKeyWords.ENTITY_KEY],
nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index ff548fb6..5376a872 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -4,7 +4,6 @@ novelWriter – GUI Document Viewer
File History:
Created: 2019-05-10 [0.0.1] GuiDocViewer
-Created: 2019-10-31 [0.3.2] GuiDocViewDetails
Created: 2020-04-25 [0.4.5] GuiDocViewHeader
Created: 2020-06-09 [0.8] GuiDocViewFooter
Created: 2020-09-08 [1.0b1] GuiDocViewHistory
@@ -38,8 +37,8 @@ from PyQt5.QtGui import (
QTextOption
)
from PyQt5.QtWidgets import (
- QAction, qApp, QFrame, QHBoxLayout, QLabel, QMenu, QScrollArea,
- QTextBrowser, QToolButton, QWidget
+ QAction, qApp, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser,
+ QToolButton, QWidget
)
from novelwriter import CONFIG, SHARED
@@ -58,6 +57,7 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum)
+ togglePanelVisibility = pyqtSignal()
def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui)
@@ -242,7 +242,7 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins()
# Make sure the main GUI knows we changed the content
- self.mainGui.viewMeta.refreshReferences(tHandle)
+ # self.mainGui.viewMeta.refreshReferences(tHandle)
# Since we change the content while it may still be rendering, we mark
# the document dirty again to make sure it's re-rendered properly.
@@ -342,7 +342,7 @@ class GuiDocViewer(QTextBrowser):
@pyqtSlot(str)
def updateDocInfo(self, tHandle: str) -> None:
- """Update the header titlebar if needed."""
+ """Update the header title bar if needed."""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins()
@@ -530,20 +530,16 @@ class GuiDocViewer(QTextBrowser):
class GuiDocViewHistory:
- def __init__(self, docViewer):
-
+ def __init__(self, docViewer: GuiDocViewer) -> None:
self.docViewer = docViewer
-
self._navHistory = []
self._posHistory = []
self._currPos = -1
self._prevPos = -1
-
return
- def clear(self):
- """Clear the view history.
- """
+ def clear(self) -> None:
+ """Clear the view history."""
logger.debug("View history cleared")
self._navHistory = []
self._posHistory = []
@@ -551,7 +547,7 @@ class GuiDocViewHistory:
self._prevPos = -1
return
- def append(self, tHandle):
+ def append(self, tHandle: str) -> bool:
"""Append a document handle and its scroll bar position to the
history, but only if the document is different than the current
active entry. Any further entries are truncated.
@@ -577,74 +573,61 @@ class GuiDocViewHistory:
return True
- def forward(self):
- """Navigate to the next entry in the view history.
- """
+ def forward(self) -> None:
+ """Navigate to the next entry in the view history."""
newPos = self._currPos + 1
if newPos < len(self._navHistory):
logger.debug("Move forward in view history")
self._prevPos = self._currPos
self._updateScrollBar()
-
self.docViewer.loadText(self._navHistory[newPos], updateHistory=False)
self.docViewer.setScrollPosition(self._posHistory[newPos])
self._currPos = newPos
self._updateNavButtons()
-
self._dumpHistory()
-
return
- def backward(self):
- """Navigate to the previous entry in the view history.
- """
+ def backward(self) -> None:
+ """Navigate to the previous entry in the view history."""
newPos = self._currPos - 1
if newPos >= 0:
logger.debug("Move backward in view history")
self._prevPos = self._currPos
self._updateScrollBar()
-
self.docViewer.loadText(self._navHistory[newPos], updateHistory=False)
self.docViewer.setScrollPosition(self._posHistory[newPos])
self._currPos = newPos
self._updateNavButtons()
-
self._dumpHistory()
-
return
##
# Internal Functions
##
- def _updateScrollBar(self):
- """Update the scrollbar position of the previous entry.
- """
+ def _updateScrollBar(self) -> None:
+ """Update the scrollbar position of the previous entry."""
if self._prevPos >= 0 and self._prevPos < len(self._posHistory):
self._posHistory[self._prevPos] = self.docViewer.scrollPosition
return
- def _updateNavButtons(self):
- """Update the navigation buttons in the document header.
- """
+ def _updateNavButtons(self) -> None:
+ """Update the navigation buttons in the document header."""
self.docViewer.docHeader.updateNavButtons(0, len(self._navHistory) - 1, self._currPos)
return
- def _truncateHistory(self, atPos):
+ def _truncateHistory(self, atPos: int) -> None:
"""Truncate the navigation history to the given position. Also
enforces a maximum length of the navigation history to 20.
"""
nSkip = 1 if atPos > 19 else 0
-
self._navHistory = self._navHistory[nSkip:atPos + 1]
self._posHistory = self._posHistory[nSkip:atPos + 1]
-
self._currPos -= nSkip
self._prevPos -= nSkip
-
return
- def _dumpHistory(self):
+ def _dumpHistory(self) -> None:
"""Debug function to dump history to the logger. Since it is a
for loop, it is skipped entirely if log level isn't DEBUG.
"""
@@ -667,7 +650,7 @@ class GuiDocViewHistory:
class GuiDocViewHeader(QWidget):
- def __init__(self, docViewer):
+ def __init__(self, docViewer: GuiDocViewer) -> None:
super().__init__(parent=docViewer)
logger.debug("Create: GuiDocViewHeader")
@@ -763,9 +746,8 @@ class GuiDocViewHeader(QWidget):
# Methods
##
- def updateTheme(self):
- """Update theme elements.
- """
+ def updateTheme(self) -> None:
+ """Update theme elements."""
self.backButton.setIcon(SHARED.theme.getIcon("backward"))
self.forwardButton.setIcon(SHARED.theme.getIcon("forward"))
self.refreshButton.setIcon(SHARED.theme.getIcon("refresh"))
@@ -785,7 +767,7 @@ class GuiDocViewHeader(QWidget):
return
- def matchColours(self):
+ def matchColours(self) -> None:
"""Update the colours of the widget to match those of the syntax
theme rather than the main GUI.
"""
@@ -793,13 +775,11 @@ class GuiDocViewHeader(QWidget):
thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
-
self.setPalette(thePalette)
self.theTitle.setPalette(thePalette)
-
return
- def setTitleFromHandle(self, tHandle):
+ def setTitleFromHandle(self, tHandle: str | None) -> bool:
"""Sets the document title from the handle, or alternatively,
set the whole document path.
"""
@@ -835,28 +815,25 @@ class GuiDocViewHeader(QWidget):
return True
- def updateNavButtons(self, firstIdx, lastIdx, currIdx):
- """Enable and disable nav buttons based on index in history.
- """
+ def updateNavButtons(self, firstIdx: int, lastIdx: int, currIdx: int) -> None:
+ """Enable and disable nav buttons based on index in history."""
self.backButton.setEnabled(currIdx > firstIdx)
self.forwardButton.setEnabled(currIdx < lastIdx)
return
##
- # Slots
+ # Private Slots
##
@pyqtSlot()
- def _closeDocument(self):
- """Trigger the close editor/viewer on the main window.
- """
+ def _closeDocument(self) -> None:
+ """Trigger the close editor/viewer on the main window."""
self.mainGui.closeDocViewer()
return
@pyqtSlot()
- def _refreshDocument(self):
- """Reload the content of the document.
- """
+ def _refreshDocument(self) -> None:
+ """Reload the content of the document."""
if self.docViewer.docHandle == self.mainGui.docEditor.docHandle:
self.mainGui.saveDocument()
self.docViewer.reloadText()
@@ -866,7 +843,7 @@ class GuiDocViewHeader(QWidget):
# Events
##
- def mousePressEvent(self, theEvent):
+ def mousePressEvent(self, event: QMouseEvent) -> None:
"""Capture a click on the title and ensure that the item is
selected in the project tree.
"""
@@ -883,14 +860,13 @@ class GuiDocViewHeader(QWidget):
class GuiDocViewFooter(QWidget):
- def __init__(self, docViewer):
+ def __init__(self, docViewer: GuiDocViewer) -> None:
super().__init__(parent=docViewer)
logger.debug("Create: GuiDocViewFooter")
self.docViewer = docViewer
self.mainGui = docViewer.mainGui
- self.viewMeta = docViewer.mainGui.viewMeta
# Internal Variables
self._docHandle = None
@@ -908,19 +884,8 @@ class GuiDocViewFooter(QWidget):
self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.showHide.setIconSize(QSize(fPx, fPx))
self.showHide.setFixedSize(QSize(fPx, fPx))
- self.showHide.clicked.connect(self._doShowHide)
- self.showHide.setToolTip(self.tr("Show/hide the references panel"))
-
- # Sticky Button
- self.stickyRefs = QToolButton(self)
- self.stickyRefs.setCheckable(True)
- self.stickyRefs.setToolButtonStyle(Qt.ToolButtonIconOnly)
- self.stickyRefs.setIconSize(QSize(fPx, fPx))
- self.stickyRefs.setFixedSize(QSize(fPx, fPx))
- self.stickyRefs.toggled.connect(self._doToggleSticky)
- self.stickyRefs.setToolTip(self.tr(
- "Activate to freeze the content of the references panel when changing document"
- ))
+ self.showHide.clicked.connect(lambda: self.docViewer.togglePanelVisibility.emit())
+ self.showHide.setToolTip(self.tr("Show/Hide Viewer Panel"))
# Show Comments
self.showComments = QToolButton(self)
@@ -930,7 +895,7 @@ class GuiDocViewFooter(QWidget):
self.showComments.setIconSize(QSize(fPx, fPx))
self.showComments.setFixedSize(QSize(fPx, fPx))
self.showComments.toggled.connect(self._doToggleComments)
- self.showComments.setToolTip(self.tr("Show comments"))
+ self.showComments.setToolTip(self.tr("Show Comments"))
# Show Synopsis
self.showSynopsis = QToolButton(self)
@@ -940,27 +905,9 @@ class GuiDocViewFooter(QWidget):
self.showSynopsis.setIconSize(QSize(fPx, fPx))
self.showSynopsis.setFixedSize(QSize(fPx, fPx))
self.showSynopsis.toggled.connect(self._doToggleSynopsis)
- self.showSynopsis.setToolTip(self.tr("Show synopsis comments"))
+ self.showSynopsis.setToolTip(self.tr("Show Synopsis Comments"))
# Labels
- self.lblRefs = QLabel(self.tr("References"))
- self.lblRefs.setBuddy(self.showHide)
- self.lblRefs.setIndent(0)
- self.lblRefs.setMargin(0)
- self.lblRefs.setContentsMargins(0, 0, 0, 0)
- self.lblRefs.setAutoFillBackground(True)
- self.lblRefs.setFixedHeight(fPx)
- self.lblRefs.setAlignment(Qt.AlignLeft | Qt.AlignTop)
-
- self.lblSticky = QLabel(self.tr("Sticky"))
- self.lblSticky.setBuddy(self.stickyRefs)
- self.lblSticky.setIndent(0)
- self.lblSticky.setMargin(0)
- self.lblSticky.setContentsMargins(0, 0, 0, 0)
- self.lblSticky.setAutoFillBackground(True)
- self.lblSticky.setFixedHeight(fPx)
- self.lblSticky.setAlignment(Qt.AlignLeft | Qt.AlignTop)
-
self.lblComments = QLabel(self.tr("Comments"))
self.lblComments.setBuddy(self.showComments)
self.lblComments.setIndent(0)
@@ -981,8 +928,6 @@ class GuiDocViewFooter(QWidget):
lblFont = self.font()
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
- self.lblRefs.setFont(lblFont)
- self.lblSticky.setFont(lblFont)
self.lblComments.setFont(lblFont)
self.lblSynopsis.setFont(lblFont)
@@ -990,10 +935,6 @@ class GuiDocViewFooter(QWidget):
self.outerBox = QHBoxLayout()
self.outerBox.setSpacing(bSp)
self.outerBox.addWidget(self.showHide, 0)
- self.outerBox.addWidget(self.lblRefs, 0)
- self.outerBox.addSpacing(hSp)
- self.outerBox.addWidget(self.stickyRefs, 0)
- self.outerBox.addWidget(self.lblSticky, 0)
self.outerBox.addStretch(1)
self.outerBox.addWidget(self.showComments, 0)
self.outerBox.addWidget(self.lblComments, 0)
@@ -1024,23 +965,19 @@ class GuiDocViewFooter(QWidget):
"""Update theme elements."""
# Icons
fPx = int(0.9*SHARED.theme.fontPixelSize)
- stickyIcon = SHARED.theme.getToggleIcon("sticky", (fPx, fPx))
bulletIcon = SHARED.theme.getToggleIcon("bullet", (fPx, fPx))
- self.showHide.setIcon(SHARED.theme.getIcon("reference"))
- self.stickyRefs.setIcon(stickyIcon)
+ self.showHide.setIcon(SHARED.theme.getIcon("panel"))
self.showComments.setIcon(bulletIcon)
self.showSynopsis.setIcon(bulletIcon)
# StyleSheets
-
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*SHARED.theme.colText)
self.showHide.setStyleSheet(buttonStyle)
- self.stickyRefs.setStyleSheet(buttonStyle)
self.showComments.setStyleSheet(buttonStyle)
self.showSynopsis.setStyleSheet(buttonStyle)
@@ -1048,138 +985,35 @@ class GuiDocViewFooter(QWidget):
return
- def matchColours(self):
+ def matchColours(self) -> None:
"""Update the colours of the widget to match those of the syntax
theme rather than the main GUI.
"""
- thePalette = QPalette()
- thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
- thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
- thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
-
- self.setPalette(thePalette)
- self.lblRefs.setPalette(thePalette)
- self.lblSticky.setPalette(thePalette)
- self.lblComments.setPalette(thePalette)
- self.lblSynopsis.setPalette(thePalette)
-
- return
-
- ##
- # Slots
- ##
-
- @pyqtSlot()
- def _doShowHide(self):
- """Toggle the expand/collapse of the panel.
- """
- isVisible = self.viewMeta.isVisible()
- self.viewMeta.setVisible(not isVisible)
- return
-
- @pyqtSlot(bool)
- def _doToggleSticky(self, theState):
- """Toggle the sticky flag for the reference panel.
- """
- logger.debug("Reference sticky is %s", str(theState))
- self.docViewer.stickyRef = theState
- if not theState and self.docViewer.docHandle is not None:
- self.viewMeta.refreshReferences(self.docViewer.docHandle)
- return
-
- @pyqtSlot(bool)
- def _doToggleComments(self, theState):
- """Toggle the view comment button and reload the document.
- """
- CONFIG.viewComments = theState
- self.docViewer.reloadText()
- return
-
- @pyqtSlot(bool)
- def _doToggleSynopsis(self, theState):
- """Toggle the view synopsis button and reload the document.
- """
- CONFIG.viewSynopsis = theState
- self.docViewer.reloadText()
- return
-
-# END Class GuiDocViewFooter
-
-
-# =============================================================================================== #
-# The Document Back-Reference Panel
-# Placed in a separate QSplitter position in the main GUI window
-# =============================================================================================== #
-
-class GuiDocViewDetails(QScrollArea):
-
- def __init__(self, mainGui):
- super().__init__(parent=mainGui)
-
- logger.debug("Create: GuiDocViewDetails")
-
- self.mainGui = mainGui
-
- self.refList = QLabel("")
- self.refList.setWordWrap(True)
- self.refList.setAlignment(Qt.AlignTop)
- self.refList.setScaledContents(True)
- self.refList.linkActivated.connect(self._linkClicked)
-
- self.linkStyle = "style='color: rgb({0},{1},{2})'".format(*SHARED.theme.colLink)
-
- # Assemble
- self.outerWidget = QWidget()
- self.outerBox = QHBoxLayout()
- self.outerBox.addWidget(self.refList, 1)
-
- self.outerWidget.setLayout(self.outerBox)
- self.setWidget(self.outerWidget)
-
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
- self.setWidgetResizable(True)
- self.setMinimumHeight(CONFIG.pxInt(50))
- self.setFrameStyle(QFrame.NoFrame)
-
- logger.debug("Ready: GuiDocViewDetails")
-
- return
-
- def refreshReferences(self, tHandle):
- """Update the current list of document references from the
- project index.
- """
- if self.mainGui.docViewer.stickyRef:
- return
-
- theRefs = SHARED.project.index.getBackReferenceList(tHandle)
- theList = []
- for tHandle in theRefs:
- tItem = SHARED.project.tree[tHandle]
- if tItem is not None:
- theList.append("%s" % (
- tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName
- ))
-
- self.refList.setText(", ".join(theList))
-
+ palette = QPalette()
+ palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
+ palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
+ palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
+ self.setPalette(palette)
+ self.lblComments.setPalette(palette)
+ self.lblSynopsis.setPalette(palette)
return
##
# Private Slots
##
- @pyqtSlot(str)
- def _linkClicked(self, theLink):
- """Capture the link-click and forward it to the document viewer
- class for handling.
- """
- logger.debug("Clicked link: '%s'", theLink)
- if len(theLink) >= 13:
- tHandle = theLink[:13]
- tAnchor = theLink[13:] or None
- self.mainGui.viewDocument(tHandle, tAnchor)
+ @pyqtSlot(bool)
+ def _doToggleComments(self, state: bool) -> None:
+ """Toggle the view comment button and reload the document."""
+ CONFIG.viewComments = state
+ self.docViewer.reloadText()
return
-# END Class GuiDocViewDetails
+ @pyqtSlot(bool)
+ def _doToggleSynopsis(self, state: bool) -> None:
+ """Toggle the view synopsis button and reload the document."""
+ CONFIG.viewSynopsis = state
+ self.docViewer.reloadText()
+ return
+
+# END Class GuiDocViewFooter
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 9316fe2f..36290b8c 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -3,7 +3,7 @@ novelWriter – GUI Document Viewer Panel
=======================================
File History:
-Created: 2023-11-09 [2.2a1] GuiDocViewerPanel
+Created: 2023-11-14 [2.2rc1] GuiDocViewerPanel
This file is a part of novelWriter
Copyright 2018–2023, Veronica Berglyd Olsen
@@ -25,40 +25,90 @@ from __future__ import annotations
import logging
-from PyQt5.QtWidgets import QVBoxLayout, QWidget
+from PyQt5.QtWidgets import QTabWidget, QVBoxLayout, QWidget
-from novelwriter.gui.docviewer import GuiDocViewer
+from novelwriter import CONFIG
+from novelwriter.constants import nwLabels, nwLists, trConst
logger = logging.getLogger(__name__)
class GuiDocViewerPanel(QWidget):
- def __init__(self, docViewer: GuiDocViewer) -> None:
- super().__init__(parent=docViewer)
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
logger.debug("Create: GuiDocViewerPanel")
- self.panelHeader = GuiDocViewerPanelHeader(self)
+ self.tabBackRefs = _ViewPanelBackRefs(self)
+
+ self.mainTabs = QTabWidget(self)
+ self.mainTabs.addTab(self.tabBackRefs, self.tr("Back References"))
+
+ self.kwTabs = {}
+ for itemClass in nwLists.USER_CLASSES:
+ self.kwTabs[itemClass] = _ViewPanelKeyWords(self)
+ self.mainTabs.addTab(self.kwTabs[itemClass], trConst(nwLabels.CLASS_NAME[itemClass]))
# Assemble
self.outerBox = QVBoxLayout()
- self.outerBox.addWidget(self.panelHeader)
+ self.outerBox.addWidget(self.mainTabs)
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox)
+ self.updateTheme()
logger.debug("Ready: GuiDocViewerPanel")
return
+ def updateTheme(self) -> None:
+ """Update theme elements."""
+ vPx = CONFIG.pxInt(4)
+ lPx = CONFIG.pxInt(2)
+ rPx = CONFIG.pxInt(14)
+ hCol = self.palette().highlight().color()
+
+ styleSheet = (
+ "QTabWidget QTabBar::tab {"
+ f"border: 0; padding: {vPx}px {rPx}px {vPx}px {lPx}px;"
+ "} "
+ "QTabWidget QTabBar::tab:selected {"
+ f"color: rgb({hCol.red()}, {hCol.green()}, {hCol.blue()});"
+ "} "
+ )
+ self.mainTabs.setStyleSheet(styleSheet)
+
+ return
+
# END Class GuiDocViewerPanel
-class GuiDocViewerPanelHeader(QWidget):
+class _ViewPanelBackRefs(QWidget):
- def __init__(self, docViewerPanel: GuiDocViewerPanel) -> None:
- super().__init__(parent=docViewerPanel)
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
return
-# END Class GuiDocViewerPanelHeader
+ def refreshContent(self):
+ """"""
+ # theRefs = SHARED.project.index.getBackReferenceList(tHandle)
+ # theList = []
+ # for tHandle in theRefs:
+ # tItem = SHARED.project.tree[tHandle]
+ # if tItem is not None:
+ # theList.append("%s" % (
+ # tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName
+ # ))
+ return
+
+# END Class _ViewPanelBackRefs
+
+
+class _ViewPanelKeyWords(QWidget):
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+ return
+
+# END Class _ViewPanelRefs
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index 6c4d7e76..a169e6f8 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -464,8 +464,8 @@ class GuiIcons:
# General Button Icons
"add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit",
- "export", "forward", "maximise", "menu", "minimise", "noncheckable", "reference",
- "refresh", "remove", "revert", "search_replace", "search", "settings", "unchecked", "up",
+ "export", "forward", "maximise", "menu", "minimise", "noncheckable", "panel", "refresh",
+ "remove", "revert", "search_replace", "search", "settings", "unchecked", "up",
# Switches
"sticky-on", "sticky-off",
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index c672a1f2..b8e4b8ed 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -44,10 +44,11 @@ from novelwriter.gui.outline import GuiOutlineView
from novelwriter.gui.mainmenu import GuiMainMenu
from novelwriter.gui.projtree import GuiProjectView
from novelwriter.gui.doceditor import GuiDocEditor
-from novelwriter.gui.docviewer import GuiDocViewDetails, GuiDocViewer
+from novelwriter.gui.docviewer import GuiDocViewer
from novelwriter.gui.noveltree import GuiNovelView
from novelwriter.gui.statusbar import GuiMainStatus
from novelwriter.gui.itemdetails import GuiItemDetails
+from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
from novelwriter.dialogs.about import GuiAbout
from novelwriter.dialogs.updates import GuiUpdates
from novelwriter.dialogs.projload import GuiProjectLoad
@@ -133,16 +134,16 @@ class GuiMain(QMainWindow):
hWd = CONFIG.pxInt(4)
# Main GUI Elements
- self.mainStatus = GuiMainStatus(self)
- self.projView = GuiProjectView(self)
- self.novelView = GuiNovelView(self)
- self.docEditor = GuiDocEditor(self)
- self.viewMeta = GuiDocViewDetails(self)
- self.docViewer = GuiDocViewer(self)
- self.itemDetails = GuiItemDetails(self)
- self.outlineView = GuiOutlineView(self)
- self.mainMenu = GuiMainMenu(self)
- self.sideBar = GuiSideBar(self)
+ self.mainStatus = GuiMainStatus(self)
+ self.projView = GuiProjectView(self)
+ self.novelView = GuiNovelView(self)
+ self.docEditor = GuiDocEditor(self)
+ self.docViewer = GuiDocViewer(self)
+ self.docViewerPanel = GuiDocViewerPanel(self)
+ self.itemDetails = GuiItemDetails(self)
+ self.outlineView = GuiOutlineView(self)
+ self.mainMenu = GuiMainMenu(self)
+ self.sideBar = GuiSideBar(self)
# Project Tree Stack
self.projStack = QStackedWidget(self)
@@ -162,7 +163,7 @@ class GuiMain(QMainWindow):
# Splitter : Document Viewer / Document Meta
self.splitView = QSplitter(Qt.Vertical, self)
self.splitView.addWidget(self.docViewer)
- self.splitView.addWidget(self.viewMeta)
+ self.splitView.addWidget(self.docViewerPanel)
self.splitView.setHandleWidth(hWd)
self.splitView.setOpaqueResize(False)
self.splitView.setSizes(CONFIG.viewPanePos)
@@ -190,12 +191,12 @@ class GuiMain(QMainWindow):
self.mainStack.currentChanged.connect(self._mainStackChanged)
# Indices of Splitter Widgets
- self.idxTree = self.splitMain.indexOf(self.treePane)
- self.idxMain = self.splitMain.indexOf(self.splitDocs)
- self.idxEditor = self.splitDocs.indexOf(self.docEditor)
- self.idxViewer = self.splitDocs.indexOf(self.splitView)
- self.idxViewDoc = self.splitView.indexOf(self.docViewer)
- self.idxViewMeta = self.splitView.indexOf(self.viewMeta)
+ self.idxTree = self.splitMain.indexOf(self.treePane)
+ self.idxMain = self.splitMain.indexOf(self.splitDocs)
+ self.idxEditor = self.splitDocs.indexOf(self.docEditor)
+ self.idxViewer = self.splitDocs.indexOf(self.splitView)
+ self.idxViewDoc = self.splitView.indexOf(self.docViewer)
+ self.idxViewDocPanel = self.splitView.indexOf(self.docViewerPanel)
# Indices of Stack Widgets
self.idxEditorView = self.mainStack.indexOf(self.splitMain)
@@ -209,7 +210,7 @@ class GuiMain(QMainWindow):
self.splitDocs.setCollapsible(self.idxEditor, False)
self.splitDocs.setCollapsible(self.idxViewer, False)
self.splitView.setCollapsible(self.idxViewDoc, False)
- self.splitView.setCollapsible(self.idxViewMeta, False)
+ self.splitView.setCollapsible(self.idxViewDocPanel, False)
self.splitMain.setStretchFactor(self.idxTree, 0)
self.splitMain.setStretchFactor(self.idxMain, 1)
@@ -277,6 +278,7 @@ class GuiMain(QMainWindow):
self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode)
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
+ self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
self.outlineView.loadDocumentTagRequest.connect(self._followTag)
self.outlineView.openDocumentRequest.connect(self._openDocument)
@@ -681,7 +683,7 @@ class GuiMain(QMainWindow):
vPos[0] = int(bPos[1]/2)
vPos[1] = bPos[1] - vPos[0]
self.splitDocs.setSizes(vPos)
- self.viewMeta.setVisible(CONFIG.showRefPanel)
+ self.docViewerPanel.setVisible(CONFIG.showViewerPanel)
if sTitle:
self.docViewer.navigateTo(f"#{sTitle}")
@@ -877,6 +879,7 @@ class GuiMain(QMainWindow):
SHARED.theme.loadTheme()
self.docEditor.updateTheme()
self.docViewer.updateTheme()
+ self.docViewerPanel.updateTheme()
self.sideBar.updateTheme()
self.projView.updateTheme()
self.novelView.updateTheme()
@@ -1076,10 +1079,10 @@ class GuiMain(QMainWindow):
if not self.isFocusMode:
CONFIG.setMainPanePos(self.splitMain.sizes())
CONFIG.setOutlinePanePos(self.outlineView.splitSizes())
- if self.viewMeta.isVisible():
+ if self.docViewerPanel.isVisible():
CONFIG.setViewPanePos(self.splitView.sizes())
- CONFIG.showRefPanel = self.viewMeta.isVisible()
+ CONFIG.showViewerPanel = self.docViewerPanel.isVisible()
if self.windowState() & Qt.WindowFullScreen != Qt.WindowFullScreen:
# Ignore window size if in full screen mode
CONFIG.setMainWinSize(self.width(), self.height())
@@ -1267,6 +1270,13 @@ class GuiMain(QMainWindow):
self.docEditor.insertText(content)
return
+ @pyqtSlot()
+ def _toggleViewerPanelVisibility(self):
+ """Toggle the visibility of the document viewer panel."""
+ CONFIG.showViewerPanel = not CONFIG.showViewerPanel
+ self.docViewerPanel.setVisible(CONFIG.showViewerPanel)
+ return
+
@pyqtSlot()
def _timeTick(self) -> None:
"""Process time tick of the main timer."""
From f4be38dd654679371fe9341ad6b582a6fb69117b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 14 Nov 2023 23:56:51 +0100
Subject: [PATCH 03/22] Populate the back reference panel
---
novelwriter/core/index.py | 4 +-
novelwriter/gui/docviewer.py | 3 +
novelwriter/gui/docviewerpanel.py | 104 ++++++++++++++++++++++++++----
novelwriter/guimain.py | 1 +
4 files changed, 97 insertions(+), 15 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index e4210dd2..503cb4ea 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -596,7 +596,7 @@ class NWIndex:
return tRefs
- def getBackReferenceList(self, tHandle: str) -> dict[str, str]:
+ def getBackReferenceList(self, tHandle: str) -> dict[str, tuple[str, IndexHeading]]:
"""Build a list of files referring back to our file, specified
by tHandle.
"""
@@ -611,7 +611,7 @@ class NWIndex:
for aHandle, sTitle, hItem in self._itemIndex.iterAllHeaders():
for aTag in hItem.references:
if aTag in tTags and aHandle not in tRefs:
- tRefs[aHandle] = sTitle
+ tRefs[aHandle] = (sTitle, hItem)
return tRefs
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 5376a872..75c088d6 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -56,6 +56,7 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
+ documentLoaded = pyqtSignal(str)
loadDocumentTagRequest = pyqtSignal(str, Enum)
togglePanelVisibility = pyqtSignal()
@@ -191,6 +192,7 @@ class GuiDocViewer(QTextBrowser):
"""Load text into the viewer from an item handle."""
if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
logger.warning("Item not found")
+ self.documentLoaded.emit("")
return False
logger.debug("Generating preview for item '%s'", tHandle)
@@ -248,6 +250,7 @@ class GuiDocViewer(QTextBrowser):
# the document dirty again to make sure it's re-rendered properly.
self.redrawText()
qApp.restoreOverrideCursor()
+ self.documentLoaded.emit(tHandle)
return True
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 36290b8c..2b780060 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -25,10 +25,14 @@ from __future__ import annotations
import logging
-from PyQt5.QtWidgets import QTabWidget, QVBoxLayout, QWidget
+from PyQt5.QtCore import QSize, Qt, pyqtSlot
+from PyQt5.QtWidgets import (
+ QFrame, QHBoxLayout, QHeaderView, QTabWidget, QTreeWidget, QTreeWidgetItem,
+ QVBoxLayout, QWidget
+)
-from novelwriter import CONFIG
-from novelwriter.constants import nwLabels, nwLists, trConst
+from novelwriter import CONFIG, SHARED
+from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst
logger = logging.getLogger(__name__)
@@ -70,6 +74,7 @@ class GuiDocViewerPanel(QWidget):
hCol = self.palette().highlight().color()
styleSheet = (
+ "QTabWidget::pane {border: 0;} "
"QTabWidget QTabBar::tab {"
f"border: 0; padding: {vPx}px {rPx}px {vPx}px {lPx}px;"
"} "
@@ -81,25 +86,98 @@ class GuiDocViewerPanel(QWidget):
return
+ ##
+ # Public Slots
+ ##
+
+ @pyqtSlot(str)
+ def updateHandle(self, tHandle: str | None) -> None:
+ """Update the document handle."""
+ self.tabBackRefs.refreshContent(tHandle or None)
+ return
+
# END Class GuiDocViewerPanel
class _ViewPanelBackRefs(QWidget):
+ C_DATA = 0
+ C_TITLE = 0
+ C_NAME = 1
+ C_SYNOPSIS = 2
+
+ D_HANDLE = Qt.ItemDataRole.UserRole
+ D_TITLE = Qt.ItemDataRole.UserRole + 1
+
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
+
+ iPx = SHARED.theme.baseIconSize
+
+ # Content
+ self.listBox = QTreeWidget(self)
+ # self.listBox.setHeaderHidden(True)
+ # self.listBox.setColumnCount(3)
+ self.listBox.setHeaderLabels([
+ self.tr("Title"), self.tr("Document"), self.tr("Synopsis")
+ ])
+ self.listBox.setIndentation(0)
+ self.listBox.setIconSize(QSize(iPx, iPx))
+ self.listBox.setFrameStyle(QFrame.Shape.NoFrame)
+
+ treeHeader = self.listBox.header()
+ treeHeader.setStretchLastSection(True)
+ treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents)
+ treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.ResizeMode.ResizeToContents)
+
+ fH1 = self.font()
+ fH1.setBold(True)
+ fH1.setUnderline(True)
+
+ fH2 = self.font()
+ fH2.setBold(True)
+
+ self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
+
+ # Assemble
+ self.outerBox = QHBoxLayout()
+ self.outerBox.addWidget(self.listBox)
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
+
+ self.setLayout(self.outerBox)
+ self.setContentsMargins(0, 0, 0, 0)
+
return
- def refreshContent(self):
- """"""
- # theRefs = SHARED.project.index.getBackReferenceList(tHandle)
- # theList = []
- # for tHandle in theRefs:
- # tItem = SHARED.project.tree[tHandle]
- # if tItem is not None:
- # theList.append("%s" % (
- # tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName
- # ))
+ def refreshContent(self, dHandle: str | None) -> None:
+ """Update the content."""
+ self.listBox.clear()
+ if dHandle:
+ refs = SHARED.project.index.getBackReferenceList(dHandle)
+ for tHandle, (sTitle, hItem) in refs.items():
+ nwItem = SHARED.project.tree[tHandle]
+ if nwItem is None:
+ continue
+
+ icon = SHARED.theme.getItemIcon(
+ nwItem.itemType, nwItem.itemClass,
+ nwItem.itemLayout, nwItem.mainHeading
+ )
+ iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
+ hDec = SHARED.theme.getHeaderDecoration(iLevel)
+
+ trItem = QTreeWidgetItem()
+ trItem.setText(self.C_TITLE, hItem.title)
+ trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
+ trItem.setFont(self.C_TITLE, self._hFonts[iLevel])
+ trItem.setIcon(self.C_NAME, icon)
+ trItem.setText(self.C_NAME, nwItem.itemName)
+ trItem.setText(self.C_SYNOPSIS, hItem.synopsis)
+
+ trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
+ trItem.setData(self.C_DATA, self.D_TITLE, sTitle)
+
+ self.listBox.addTopLevelItem(trItem)
return
# END Class _ViewPanelBackRefs
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index b8e4b8ed..8c146ac8 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -277,6 +277,7 @@ class GuiMain(QMainWindow):
self.docEditor.closeDocumentRequest.connect(self.closeDocEditor)
self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode)
+ self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
From f5901a4c8cc515dfb8046b674520f91503af9f83 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 18:10:29 +0100
Subject: [PATCH 04/22] Added new icons for the viewer panel
---
.../assets/icons/typicons_dark/icons.conf | 7 ++++
.../icons/typicons_dark/nw_deco-h2-narrow.svg | 4 ++
.../icons/typicons_dark/nw_deco-h3-narrow.svg | 4 ++
.../icons/typicons_dark/nw_deco-h4-narrow.svg | 4 ++
.../icons/typicons_dark/nw_deco-note.svg | 4 ++
.../assets/icons/typicons_dark/typ_eye.svg | 4 ++
.../assets/icons/typicons_light/icons.conf | 7 ++++
.../typicons_light/nw_deco-h2-narrow.svg | 4 ++
.../typicons_light/nw_deco-h3-narrow.svg | 4 ++
.../typicons_light/nw_deco-h4-narrow.svg | 4 ++
.../icons/typicons_light/nw_deco-note.svg | 4 ++
.../assets/icons/typicons_light/typ_eye.svg | 4 ++
novelwriter/gui/docviewerpanel.py | 42 ++++++++++++-------
novelwriter/gui/theme.py | 21 +++++++++-
14 files changed, 102 insertions(+), 15 deletions(-)
create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg
create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg
create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg
create mode 100644 novelwriter/assets/icons/typicons_dark/nw_deco-note.svg
create mode 100644 novelwriter/assets/icons/typicons_dark/typ_eye.svg
create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg
create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg
create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg
create mode 100644 novelwriter/assets/icons/typicons_light/nw_deco-note.svg
create mode 100644 novelwriter/assets/icons/typicons_light/typ_eye.svg
diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf
index c51b2457..999ce000 100644
--- a/novelwriter/assets/icons/typicons_dark/icons.conf
+++ b/novelwriter/assets/icons/typicons_dark/icons.conf
@@ -91,14 +91,21 @@ sticky-off = typ_pin-outline.svg
sticky-on = typ_pin.svg
unchecked = mixed_input-unchecked.svg
up = typ_chevron-up.svg
+view = typ_eye.svg
view_build = typ_export-grey.svg
view_editor = mixed_edit.svg
view_novel = typ_book-grey.svg
view_outline = typ_puzzle-outline.svg
deco_doc_h0 = nw_deco-h0.svg
+deco_doc_h0_n = nw_deco-h0.svg
deco_doc_h1 = nw_deco-h1.svg
+deco_doc_h1_n = nw_deco-h1.svg
deco_doc_h2 = nw_deco-h2.svg
+deco_doc_h2_n = nw_deco-h2-narrow.svg
deco_doc_h3 = nw_deco-h3.svg
+deco_doc_h3_n = nw_deco-h3-narrow.svg
deco_doc_h4 = nw_deco-h4.svg
+deco_doc_h4_n = nw_deco-h4-narrow.svg
deco_doc_more = nw_deco-noveltree-more.svg
+deco_doc_nt_n = nw_deco-note.svg
diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg
new file mode 100644
index 00000000..ced9f866
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg
new file mode 100644
index 00000000..e5a9fad9
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg
new file mode 100644
index 00000000..3394646e
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-note.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-note.svg
new file mode 100644
index 00000000..85944eb6
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/nw_deco-note.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_dark/typ_eye.svg b/novelwriter/assets/icons/typicons_dark/typ_eye.svg
new file mode 100644
index 00000000..34d22862
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_dark/typ_eye.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf
index 5f300ad2..3c6f545c 100644
--- a/novelwriter/assets/icons/typicons_light/icons.conf
+++ b/novelwriter/assets/icons/typicons_light/icons.conf
@@ -91,14 +91,21 @@ sticky-off = typ_pin-outline.svg
sticky-on = typ_pin.svg
unchecked = mixed_input-unchecked.svg
up = typ_chevron-up.svg
+view = typ_eye.svg
view_build = typ_export-grey.svg
view_editor = mixed_edit.svg
view_novel = typ_book-grey.svg
view_outline = typ_puzzle-outline.svg
deco_doc_h0 = nw_deco-h0.svg
+deco_doc_h0_n = nw_deco-h0.svg
deco_doc_h1 = nw_deco-h1.svg
+deco_doc_h1_n = nw_deco-h1.svg
deco_doc_h2 = nw_deco-h2.svg
+deco_doc_h2_n = nw_deco-h2-narrow.svg
deco_doc_h3 = nw_deco-h3.svg
+deco_doc_h3_n = nw_deco-h3-narrow.svg
deco_doc_h4 = nw_deco-h4.svg
+deco_doc_h4_n = nw_deco-h4-narrow.svg
deco_doc_more = nw_deco-noveltree-more.svg
+deco_doc_nt_n = nw_deco-note.svg
diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg
new file mode 100644
index 00000000..fe43924d
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg
new file mode 100644
index 00000000..79a75ac4
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg
new file mode 100644
index 00000000..7a7279bc
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-note.svg b/novelwriter/assets/icons/typicons_light/nw_deco-note.svg
new file mode 100644
index 00000000..972d71c7
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/nw_deco-note.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/assets/icons/typicons_light/typ_eye.svg b/novelwriter/assets/icons/typicons_light/typ_eye.svg
new file mode 100644
index 00000000..9cae4229
--- /dev/null
+++ b/novelwriter/assets/icons/typicons_light/typ_eye.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 2b780060..a85c8446 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -27,7 +27,7 @@ import logging
from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtWidgets import (
- QFrame, QHBoxLayout, QHeaderView, QTabWidget, QTreeWidget, QTreeWidgetItem,
+ QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QTabWidget, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget
)
@@ -44,10 +44,12 @@ class GuiDocViewerPanel(QWidget):
logger.debug("Create: GuiDocViewerPanel")
+ self._lastHandle = None
+
self.tabBackRefs = _ViewPanelBackRefs(self)
self.mainTabs = QTabWidget(self)
- self.mainTabs.addTab(self.tabBackRefs, self.tr("Back References"))
+ self.mainTabs.addTab(self.tabBackRefs, self.tr("Backreferences"))
self.kwTabs = {}
for itemClass in nwLists.USER_CLASSES:
@@ -83,6 +85,7 @@ class GuiDocViewerPanel(QWidget):
"} "
)
self.mainTabs.setStyleSheet(styleSheet)
+ self.updateHandle(self._lastHandle)
return
@@ -93,6 +96,7 @@ class GuiDocViewerPanel(QWidget):
@pyqtSlot(str)
def updateHandle(self, tHandle: str | None) -> None:
"""Update the document handle."""
+ self._lastHandle = tHandle
self.tabBackRefs.refreshContent(tHandle or None)
return
@@ -101,10 +105,11 @@ class GuiDocViewerPanel(QWidget):
class _ViewPanelBackRefs(QWidget):
- C_DATA = 0
- C_TITLE = 0
- C_NAME = 1
- C_SYNOPSIS = 2
+ C_DATA = 0
+ C_TITLE = 0
+ C_EDIT = 1
+ C_VIEW = 2
+ C_NAME = 3
D_HANDLE = Qt.ItemDataRole.UserRole
D_TITLE = Qt.ItemDataRole.UserRole + 1
@@ -113,22 +118,28 @@ class _ViewPanelBackRefs(QWidget):
super().__init__(parent=parent)
iPx = SHARED.theme.baseIconSize
+ cMg = CONFIG.pxInt(6)
# Content
self.listBox = QTreeWidget(self)
# self.listBox.setHeaderHidden(True)
- # self.listBox.setColumnCount(3)
+ # self.listBox.setColumnCount(4)
self.listBox.setHeaderLabels([
- self.tr("Title"), self.tr("Document"), self.tr("Synopsis")
+ self.tr("Title"), "", "", self.tr("Document")
])
self.listBox.setIndentation(0)
+ self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.listBox.setIconSize(QSize(iPx, iPx))
self.listBox.setFrameStyle(QFrame.Shape.NoFrame)
treeHeader = self.listBox.header()
treeHeader.setStretchLastSection(True)
+ treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
+ treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.ResizeMode.ResizeToContents)
+ treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
+ treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
fH1 = self.font()
fH1.setBold(True)
@@ -137,7 +148,9 @@ class _ViewPanelBackRefs(QWidget):
fH2 = self.font()
fH2.setBold(True)
- self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
+ self._hFonts = [self.font(), fH1, fH2, self.font(), self.font(), self.font()]
+ self._editIcon = SHARED.theme.getIcon("edit")
+ self._viewIcon = SHARED.theme.getIcon("view")
# Assemble
self.outerBox = QHBoxLayout()
@@ -159,20 +172,21 @@ class _ViewPanelBackRefs(QWidget):
if nwItem is None:
continue
- icon = SHARED.theme.getItemIcon(
+ docIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass,
nwItem.itemLayout, nwItem.mainHeading
)
- iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
- hDec = SHARED.theme.getHeaderDecoration(iLevel)
+ iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
+ hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
trItem = QTreeWidgetItem()
trItem.setText(self.C_TITLE, hItem.title)
trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
trItem.setFont(self.C_TITLE, self._hFonts[iLevel])
- trItem.setIcon(self.C_NAME, icon)
+ trItem.setIcon(self.C_EDIT, self._editIcon)
+ trItem.setIcon(self.C_VIEW, self._viewIcon)
+ trItem.setIcon(self.C_NAME, docIcon)
trItem.setText(self.C_NAME, nwItem.itemName)
- trItem.setText(self.C_SYNOPSIS, hItem.synopsis)
trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
trItem.setData(self.C_DATA, self.D_TITLE, sTitle)
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index a169e6f8..4ac8b714 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -133,6 +133,7 @@ class GuiTheme:
self.getToggleIcon = self.iconCache.getToggleIcon
self.loadDecoration = self.iconCache.loadDecoration
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
+ self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow
# Extract Other Info
self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX()
@@ -465,7 +466,7 @@ class GuiIcons:
# General Button Icons
"add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit",
"export", "forward", "maximise", "menu", "minimise", "noncheckable", "panel", "refresh",
- "remove", "revert", "search_replace", "search", "settings", "unchecked", "up",
+ "remove", "revert", "search_replace", "search", "settings", "unchecked", "up", "view",
# Switches
"sticky-on", "sticky-off",
@@ -473,6 +474,8 @@ class GuiIcons:
# Decorations
"deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", "deco_doc_more",
+ "deco_doc_h0_n", "deco_doc_h1_n", "deco_doc_h2_n", "deco_doc_h3_n", "deco_doc_h4_n",
+ "deco_doc_nt_n",
}
TOGGLE_ICON_KEYS = {
@@ -493,6 +496,7 @@ class GuiIcons:
self._qIcons: dict[str, QIcon] = {}
self._themeMap: dict[str, Path] = {}
self._headerDec: list[QPixmap] = []
+ self._headerDecNarrow: list[QPixmap] = []
# Icon Theme Path
self._confName = "icons.conf"
@@ -580,6 +584,7 @@ class GuiIcons:
self._qIcons[iconKey] = qIcon
self._headerDec = []
+ self._headerDecNarrow = []
return True
@@ -680,6 +685,20 @@ class GuiIcons:
]
return self._headerDec[minmax(hLevel, 0, 4)]
+ def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap:
+ """Get the narrow decoration for a specific header level."""
+ if not self._headerDecNarrow:
+ iPx = self.mainTheme.baseIconSize
+ self._headerDecNarrow = [
+ self.loadDecoration("deco_doc_h0_n", h=iPx),
+ self.loadDecoration("deco_doc_h1_n", h=iPx),
+ self.loadDecoration("deco_doc_h2_n", h=iPx),
+ self.loadDecoration("deco_doc_h3_n", h=iPx),
+ self.loadDecoration("deco_doc_h4_n", h=iPx),
+ self.loadDecoration("deco_doc_nt_n", h=iPx),
+ ]
+ return self._headerDecNarrow[minmax(hLevel, 0, 5)]
+
##
# Internal Functions
##
From 3f8220f35014e6a5a4bf60d7a1d4fe2eefc0db92 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 18:11:15 +0100
Subject: [PATCH 05/22] Added tag data to viewer panels
---
novelwriter/gui/docviewerpanel.py | 125 ++++++++++++++++++++++++++----
novelwriter/gui/outline.py | 16 ++--
novelwriter/guimain.py | 1 +
3 files changed, 122 insertions(+), 20 deletions(-)
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index a85c8446..bd8ff72c 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -33,6 +33,8 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst
+from novelwriter.core.index import IndexHeading, IndexItem
+from novelwriter.enum import nwItemClass
logger = logging.getLogger(__name__)
@@ -51,10 +53,13 @@ class GuiDocViewerPanel(QWidget):
self.mainTabs = QTabWidget(self)
self.mainTabs.addTab(self.tabBackRefs, self.tr("Backreferences"))
- self.kwTabs = {}
+ self.kwTabs: dict[str, _ViewPanelKeyWords] = {}
+ self.idTabs: dict[str, int] = {}
for itemClass in nwLists.USER_CLASSES:
- self.kwTabs[itemClass] = _ViewPanelKeyWords(self)
- self.mainTabs.addTab(self.kwTabs[itemClass], trConst(nwLabels.CLASS_NAME[itemClass]))
+ cTab = _ViewPanelKeyWords(self, itemClass)
+ tabId = self.mainTabs.addTab(cTab, trConst(nwLabels.CLASS_NAME[itemClass]))
+ self.kwTabs[itemClass.name] = cTab
+ self.idTabs[itemClass.name] = tabId
# Assemble
self.outerBox = QVBoxLayout()
@@ -68,6 +73,10 @@ class GuiDocViewerPanel(QWidget):
return
+ ##
+ # Methods
+ ##
+
def updateTheme(self) -> None:
"""Update theme elements."""
vPx = CONFIG.pxInt(4)
@@ -89,6 +98,15 @@ class GuiDocViewerPanel(QWidget):
return
+ def openProjectTasks(self) -> None:
+ """Run open project tasks."""
+ for key, name, tClass, iItem, hItem in SHARED.project.index.getTagsData():
+ if tClass in self.kwTabs:
+ print(key, name, iItem, hItem)
+ self.kwTabs[tClass].addEntry(key, name, iItem, hItem)
+ self._updateTabVisibility()
+ return
+
##
# Public Slots
##
@@ -100,16 +118,26 @@ class GuiDocViewerPanel(QWidget):
self.tabBackRefs.refreshContent(tHandle or None)
return
+ ##
+ # Internal Functions
+ ##
+
+ def _updateTabVisibility(self) -> None:
+ """Hide class tabs with no content."""
+ for tClass, cTab in self.kwTabs.items():
+ self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.count() > 0)
+ return
+
# END Class GuiDocViewerPanel
class _ViewPanelBackRefs(QWidget):
- C_DATA = 0
- C_TITLE = 0
- C_EDIT = 1
- C_VIEW = 2
- C_NAME = 3
+ C_DATA = 0
+ C_TITLE = 0
+ C_EDIT = 1
+ C_VIEW = 2
+ C_DOCUMENT = 3
D_HANDLE = Qt.ItemDataRole.UserRole
D_TITLE = Qt.ItemDataRole.UserRole + 1
@@ -125,7 +153,7 @@ class _ViewPanelBackRefs(QWidget):
# self.listBox.setHeaderHidden(True)
# self.listBox.setColumnCount(4)
self.listBox.setHeaderLabels([
- self.tr("Title"), "", "", self.tr("Document")
+ self.tr("Heading"), "", "", self.tr("Document")
])
self.listBox.setIndentation(0)
self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
@@ -134,9 +162,9 @@ class _ViewPanelBackRefs(QWidget):
treeHeader = self.listBox.header()
treeHeader.setStretchLastSection(True)
+ treeHeader.setSectionResizeMode(self.C_DOCUMENT, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
- treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
@@ -185,8 +213,8 @@ class _ViewPanelBackRefs(QWidget):
trItem.setFont(self.C_TITLE, self._hFonts[iLevel])
trItem.setIcon(self.C_EDIT, self._editIcon)
trItem.setIcon(self.C_VIEW, self._viewIcon)
- trItem.setIcon(self.C_NAME, docIcon)
- trItem.setText(self.C_NAME, nwItem.itemName)
+ trItem.setIcon(self.C_DOCUMENT, docIcon)
+ trItem.setText(self.C_DOCUMENT, nwItem.itemName)
trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
trItem.setData(self.C_DATA, self.D_TITLE, sTitle)
@@ -197,10 +225,79 @@ class _ViewPanelBackRefs(QWidget):
# END Class _ViewPanelBackRefs
-class _ViewPanelKeyWords(QWidget):
+class _ViewPanelKeyWords(QTreeWidget):
- def __init__(self, parent: QWidget) -> None:
+ C_DATA = 0
+ C_NAME = 0
+ C_EDIT = 1
+ C_VIEW = 2
+ C_TITLE = 3
+ C_DOCUMENT = 4
+
+ D_TAG = Qt.ItemDataRole.UserRole
+ D_HANDLE = Qt.ItemDataRole.UserRole + 1
+
+ def __init__(self, parent: QWidget, itemClass: nwItemClass) -> None:
super().__init__(parent=parent)
+ self._itemClass = nwItemClass
+
+ iPx = SHARED.theme.baseIconSize
+ cMg = CONFIG.pxInt(6)
+
+ self.setHeaderLabels([
+ self.tr("Tag"), "", "", self.tr("Heading"), self.tr("Document")
+ ])
+ self.setIndentation(0)
+ self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
+ self.setIconSize(QSize(iPx, iPx))
+ self.setFrameStyle(QFrame.Shape.NoFrame)
+ self.setSortingEnabled(True)
+ self.sortByColumn(self.C_NAME, Qt.SortOrder.AscendingOrder)
+
+ treeHeader = self.header()
+ treeHeader.setStretchLastSection(True)
+ treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents)
+ treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
+ treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
+ treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
+ treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
+
+ self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass])
+ self._editIcon = SHARED.theme.getIcon("edit")
+ self._viewIcon = SHARED.theme.getIcon("view")
+
+ return
+
+ def count(self) -> int:
+ return self.topLevelItemCount()
+
+ def addEntry(self, tag: str, name: str, iItem: IndexItem | None,
+ hItem: IndexHeading | None) -> None:
+ """Add a tag entry to the list."""
+ if not iItem or not hItem:
+ return
+
+ nwItem = iItem.item
+ docIcon = SHARED.theme.getItemIcon(
+ nwItem.itemType, nwItem.itemClass,
+ nwItem.itemLayout, nwItem.mainHeading
+ )
+ iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
+ hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
+
+ trItem = QTreeWidgetItem()
+ trItem.setText(self.C_NAME, name)
+ trItem.setIcon(self.C_NAME, self._classIcon)
+ trItem.setIcon(self.C_EDIT, self._editIcon)
+ trItem.setIcon(self.C_VIEW, self._viewIcon)
+ trItem.setText(self.C_TITLE, hItem.title)
+ trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
+ trItem.setIcon(self.C_DOCUMENT, docIcon)
+ trItem.setText(self.C_DOCUMENT, nwItem.itemName)
+ trItem.setData(self.C_DATA, self.D_TAG, tag)
+ trItem.setData(self.C_DATA, self.D_HANDLE, iItem.handle)
+ self.addTopLevelItem(trItem)
+
return
# END Class _ViewPanelRefs
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 596a422b..95b2b612 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -377,13 +377,17 @@ class GuiOutlineTree(QTreeWidget):
fH2 = self.font()
fH2.setBold(True)
+ iType = nwItemType.FILE
+ iClass = nwItemClass.NO_CLASS
+ iLayout = nwItemLayout.DOCUMENT
+
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
self._dIcon = {
- "H0": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"),
- "H1": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"),
- "H2": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"),
- "H3": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"),
- "H4": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"),
+ "H0": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H0"),
+ "H1": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H1"),
+ "H2": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H2"),
+ "H3": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H3"),
+ "H4": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H4"),
}
# Internals
@@ -549,7 +553,7 @@ class GuiOutlineTree(QTreeWidget):
"""Load the state of the main tree header, that is, column order
and column width.
"""
- # Load whatever we saved last time, regardless of wether it
+ # Load whatever we saved last time, regardless of whether it
# contains the correct names or number of columns.
colState = SHARED.project.options.getValue("GuiOutline", "columnState", {})
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 8c146ac8..eca09789 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -513,6 +513,7 @@ class GuiMain(QMainWindow):
self.projView.openProjectTasks()
self.novelView.openProjectTasks()
self.outlineView.openProjectTasks()
+ self.docViewerPanel.openProjectTasks()
self._updateStatusWordCount()
# Restore previously open documents, if any
From 2cb6020cacd3368b4df86a415d1ab865af352ed9 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 18:11:37 +0100
Subject: [PATCH 06/22] Added tags changed signal
---
novelwriter/core/index.py | 52 ++++++++++++++++++++++------
novelwriter/enum.py | 2 +-
novelwriter/gui/doceditor.py | 8 ++---
novelwriter/shared.py | 12 ++++++-
tests/test_gui/test_gui_doceditor.py | 8 ++---
5 files changed, 61 insertions(+), 21 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 503cb4ea..dc8910aa 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -34,8 +34,9 @@ import logging
from time import time
from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator
from pathlib import Path
+from novelwriter import SHARED
-from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
+from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout, nwTrinary
from novelwriter.error import logException
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
@@ -254,7 +255,7 @@ class NWIndex:
return False
# Keep a record of existing tags, and create a new item entry
- itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), False)
+ itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), nwTrinary.NEGATIVE)
self._itemIndex.add(tHandle, tItem)
# Run word counter for the whole text
@@ -289,7 +290,8 @@ class NWIndex:
# Internal Indexer Helpers
##
- def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict) -> None:
+ def _scanActive(self, tHandle: str, nwItem: NWItem, text: str,
+ tags: dict[str, nwTrinary]) -> None:
"""Scan an active document for meta data."""
nTitle = 0 # Line Number of the previous title
cTitle = TT_NONE # Tag of the current title
@@ -345,10 +347,20 @@ class NWIndex:
self._indexWordCounts(tHandle, text, cTitle)
# Prune no longer used tags
- for tTag, isActive in tags.items():
- if not isActive:
- logger.debug("Deleting removed tag '%s'", tTag)
+ for tTag, tStatus in tags.items():
+ added = []
+ deleted = []
+ if tStatus == nwTrinary.NEGATIVE:
+ logger.debug("Removed tag '%s'", tTag)
del self._tagsIndex[tTag]
+ deleted.append(tTag)
+ elif tStatus == nwTrinary.POSITIVE:
+ logger.debug("Added new tag '%s'", tTag)
+ added.append(tTag)
+ else:
+ logger.debug("Unchanged tag '%s'", tTag)
+ if added or deleted:
+ SHARED.indexUpdatedTags(added, deleted)
return
@@ -385,7 +397,7 @@ class NWIndex:
return
def _indexKeyword(self, tHandle: str, line: str, sTitle: str,
- itemClass: nwItemClass, tags: dict) -> None:
+ itemClass: nwItemClass, tags: dict[str, nwTrinary]) -> None:
"""Validate and save the information about a reference to a tag
in another file, or the setting of a tag in the file. A record
of active tags is updated so that no longer used tags can be
@@ -402,9 +414,10 @@ class NWIndex:
if tBits[0] == nwKeyWords.TAG_KEY:
tagName = tBits[1]
+ tagKey = tagName.lower()
self._tagsIndex.add(tagName, tHandle, sTitle, itemClass)
self._itemIndex.setHeadingTag(tHandle, sTitle, tagName)
- tags[tagName.lower()] = True
+ tags[tagKey] = nwTrinary.NEUTRAL if tagKey in tags else nwTrinary.POSITIVE
else:
self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0])
@@ -615,7 +628,7 @@ class NWIndex:
return tRefs
- def getTagSource(self, tagKey: str) -> tuple[str, str]:
+ def getTagSource(self, tagKey: str) -> tuple[str | None, str]:
"""Return the source location of a given tag."""
tHandle = self._tagsIndex.tagHandle(tagKey)
sTitle = self._tagsIndex.tagHeading(tagKey)
@@ -625,6 +638,14 @@ class NWIndex:
"""Return all tags based on itemClass."""
return self._tagsIndex.filterTagNames(itemClass.name)
+ def getTagsData(self) -> Iterator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
+ """Return all known tags."""
+ for tag, data in self._tagsIndex.items():
+ iItem = self._itemIndex[data.get("handle")]
+ hItem = None if iItem is None else iItem[data.get("heading")]
+ yield tag, data.get("name", ""), data.get("class", ""), iItem, hItem
+ return
+
# END Class NWIndex
@@ -643,7 +664,7 @@ class TagsIndex:
__slots__ = ("_tags")
def __init__(self) -> None:
- self._tags: dict[str, dict] = {}
+ self._tags: dict[str, dict[str, str]] = {}
return
def __contains__(self, tagKey: str) -> bool:
@@ -665,6 +686,10 @@ class TagsIndex:
self._tags = {}
return
+ def items(self) -> ItemsView:
+ """Return a dictionary view of all tags."""
+ return self._tags.items()
+
def add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass) -> None:
"""Add a key to the index and set all values."""
self._tags[tagKey.lower()] = {
@@ -676,7 +701,7 @@ class TagsIndex:
"""Get the display name of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("name", "")
- def tagHandle(self, tagKey: str) -> str:
+ def tagHandle(self, tagKey: str) -> str | None:
"""Get the handle of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("handle", None)
@@ -937,6 +962,11 @@ class IndexItem:
# Properties
##
+ @property
+ def handle(self) -> str:
+ """Return the item handle of the index item."""
+ return self._handle
+
@property
def item(self) -> NWItem:
"""Return the project item of the index item."""
diff --git a/novelwriter/enum.py b/novelwriter/enum.py
index 3d652266..c1d2aef4 100644
--- a/novelwriter/enum.py
+++ b/novelwriter/enum.py
@@ -64,7 +64,7 @@ class nwItemLayout(Enum):
class nwTrinary(Enum):
NEGATIVE = -1
- UNKNOWN = 0
+ NEUTRAL = 0
POSITIVE = 1
# END Enum nwTrinary
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index d4743c9b..00e3a093 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -1783,13 +1783,13 @@ class GuiDocEditor(QPlainTextEdit):
block = cursor.block()
text = block.text()
if len(text) == 0:
- return nwTrinary.UNKNOWN
+ return nwTrinary.NEUTRAL
if text.startswith("@") and isinstance(self._nwItem, NWItem):
isGood, tBits, tPos = SHARED.project.index.scanThis(text)
if not isGood:
- return nwTrinary.UNKNOWN
+ return nwTrinary.NEUTRAL
tag = ""
exist = False
@@ -1806,7 +1806,7 @@ class GuiDocEditor(QPlainTextEdit):
if not tag or tag.startswith("@"):
# The keyword cannot be looked up, so we ignore that
- return nwTrinary.UNKNOWN
+ return nwTrinary.NEUTRAL
if follow and exist:
logger.debug("Attempting to follow tag '%s'", tag)
@@ -1826,7 +1826,7 @@ class GuiDocEditor(QPlainTextEdit):
return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
- return nwTrinary.UNKNOWN
+ return nwTrinary.NEUTRAL
def _openContextFromCursor(self) -> None:
"""Open the spell check context menu at the cursor."""
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 8ee3099a..deb49ce1 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -52,6 +52,7 @@ class SharedData(QObject):
projectStatusChanged = pyqtSignal(bool)
projectStatusMessage = pyqtSignal(str)
spellLanguageChanged = pyqtSignal(str, str)
+ indexChangedTags = pyqtSignal(list[str], list[str])
def __init__(self) -> None:
super().__init__()
@@ -171,7 +172,7 @@ class SharedData(QObject):
return
def updateSpellCheckLanguage(self, reload: bool = False) -> None:
- """Update the active spell check langauge from settings."""
+ """Update the active spell check language from settings."""
from novelwriter import CONFIG
language = self.project.data.spellLang or CONFIG.spellLanguage
if language != self.spelling.spellLanguage or reload:
@@ -210,6 +211,15 @@ class SharedData(QObject):
QThreadPool.globalInstance().start(runnable, priority=priority)
return
+ ##
+ # Call-Back Functions
+ ##
+
+ def indexUpdatedTags(self, added: list[str], deleted: list[str]) -> None:
+ """Emit the index changed tags signal."""
+ self.indexChangedTags.emit(added, deleted)
+ return
+
##
# Alert Boxes
##
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index 008ad053..22a67be4 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -1194,15 +1194,15 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
# Empty Block
nwGUI.docEditor.setCursorLine(2)
- assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN
+ assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL
# Not On Tag
nwGUI.docEditor.setCursorLine(1)
- assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN
+ assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL
# On Tag Keyword
nwGUI.docEditor.setCursorPosition(15)
- assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN
+ assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL
# On Known Tag, No Follow
nwGUI.docEditor.setCursorPosition(22)
@@ -1230,7 +1230,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert "0000000000012" not in SHARED.project.tree
nwGUI.docEditor.setCursorPosition(47)
- assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN
+ assert nwGUI.docEditor._processTag() is nwTrinary.NEUTRAL
# qtbot.stop()
From 8508248f1202b69834652c37c19c6f19b96e2b75 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 20:12:17 +0100
Subject: [PATCH 07/22] Add signal for processing tag changes
---
novelwriter/core/index.py | 11 ++++
novelwriter/gui/docviewerpanel.py | 85 +++++++++++++++++++------------
novelwriter/gui/projtree.py | 4 +-
novelwriter/guimain.py | 3 ++
novelwriter/shared.py | 2 +-
5 files changed, 70 insertions(+), 35 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index dc8910aa..0b7ae8ca 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -646,6 +646,17 @@ class NWIndex:
yield tag, data.get("name", ""), data.get("class", ""), iItem, hItem
return
+ def getSingleTag(self, tagKey: str) -> tuple[str, str, IndexItem | None, IndexHeading | None]:
+ """Return tag data for a specific tag."""
+ tName = self._tagsIndex.tagName(tagKey)
+ tClass = self._tagsIndex.tagClass(tagKey)
+ tHandle = self._tagsIndex.tagHandle(tagKey)
+ tHeading = self._tagsIndex.tagHeading(tagKey)
+ if tName and tClass and tHandle and tHeading:
+ iItem = self._itemIndex[tHandle]
+ return tName, tClass, iItem, None if iItem is None else iItem[tHeading]
+ return "", "", None, None
+
# END Class NWIndex
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index bd8ff72c..0326bb0c 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -27,8 +27,8 @@ import logging
from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtWidgets import (
- QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QTabWidget, QTreeWidget, QTreeWidgetItem,
- QVBoxLayout, QWidget
+ QAbstractItemView, QFrame, QHeaderView, QTabWidget, QTreeWidget,
+ QTreeWidgetItem, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
@@ -100,13 +100,25 @@ class GuiDocViewerPanel(QWidget):
def openProjectTasks(self) -> None:
"""Run open project tasks."""
+ self.clearClassTabs()
for key, name, tClass, iItem, hItem in SHARED.project.index.getTagsData():
if tClass in self.kwTabs:
- print(key, name, iItem, hItem)
self.kwTabs[tClass].addEntry(key, name, iItem, hItem)
self._updateTabVisibility()
return
+ def closeProjectTasks(self) -> None:
+ """Run closing project tasks."""
+ self.tabBackRefs.refreshContent(None)
+ self.clearClassTabs()
+ return
+
+ def clearClassTabs(self) -> None:
+ """Clear all the class tabs"""
+ for cTab in self.kwTabs.values():
+ cTab.clear()
+ return
+
##
# Public Slots
##
@@ -118,6 +130,25 @@ class GuiDocViewerPanel(QWidget):
self.tabBackRefs.refreshContent(tHandle or None)
return
+ @pyqtSlot(list, list)
+ def updateChangedTags(self, added: list[str], deleted: list[str]) -> None:
+ """Forward tags changes to the lists."""
+ for key in added:
+ name, tClass, iItem, hItem = SHARED.project.index.getSingleTag(key)
+ if tClass in self.kwTabs:
+ self.kwTabs[tClass].addEntry(key, name, iItem, hItem)
+
+ for key in deleted:
+ for cTab in self.kwTabs.values():
+ if cTab.removeEntry(key):
+ break
+ else:
+ logger.warning("Could not remove tag '%s' from view panel", key)
+
+ self._updateTabVisibility()
+
+ return
+
##
# Internal Functions
##
@@ -131,7 +162,7 @@ class GuiDocViewerPanel(QWidget):
# END Class GuiDocViewerPanel
-class _ViewPanelBackRefs(QWidget):
+class _ViewPanelBackRefs(QTreeWidget):
C_DATA = 0
C_TITLE = 0
@@ -149,18 +180,15 @@ class _ViewPanelBackRefs(QWidget):
cMg = CONFIG.pxInt(6)
# Content
- self.listBox = QTreeWidget(self)
- # self.listBox.setHeaderHidden(True)
- # self.listBox.setColumnCount(4)
- self.listBox.setHeaderLabels([
+ self.setHeaderLabels([
self.tr("Heading"), "", "", self.tr("Document")
])
- self.listBox.setIndentation(0)
- self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
- self.listBox.setIconSize(QSize(iPx, iPx))
- self.listBox.setFrameStyle(QFrame.Shape.NoFrame)
+ self.setIndentation(0)
+ self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
+ self.setIconSize(QSize(iPx, iPx))
+ self.setFrameStyle(QFrame.Shape.NoFrame)
- treeHeader = self.listBox.header()
+ treeHeader = self.header()
treeHeader.setStretchLastSection(True)
treeHeader.setSectionResizeMode(self.C_DOCUMENT, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
@@ -169,30 +197,14 @@ class _ViewPanelBackRefs(QWidget):
treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
- fH1 = self.font()
- fH1.setBold(True)
- fH1.setUnderline(True)
-
- fH2 = self.font()
- fH2.setBold(True)
-
- self._hFonts = [self.font(), fH1, fH2, self.font(), self.font(), self.font()]
self._editIcon = SHARED.theme.getIcon("edit")
self._viewIcon = SHARED.theme.getIcon("view")
- # Assemble
- self.outerBox = QHBoxLayout()
- self.outerBox.addWidget(self.listBox)
- self.outerBox.setContentsMargins(0, 0, 0, 0)
-
- self.setLayout(self.outerBox)
- self.setContentsMargins(0, 0, 0, 0)
-
return
def refreshContent(self, dHandle: str | None) -> None:
"""Update the content."""
- self.listBox.clear()
+ self.clear()
if dHandle:
refs = SHARED.project.index.getBackReferenceList(dHandle)
for tHandle, (sTitle, hItem) in refs.items():
@@ -210,7 +222,6 @@ class _ViewPanelBackRefs(QWidget):
trItem = QTreeWidgetItem()
trItem.setText(self.C_TITLE, hItem.title)
trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
- trItem.setFont(self.C_TITLE, self._hFonts[iLevel])
trItem.setIcon(self.C_EDIT, self._editIcon)
trItem.setIcon(self.C_VIEW, self._viewIcon)
trItem.setIcon(self.C_DOCUMENT, docIcon)
@@ -219,7 +230,7 @@ class _ViewPanelBackRefs(QWidget):
trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
trItem.setData(self.C_DATA, self.D_TITLE, sTitle)
- self.listBox.addTopLevelItem(trItem)
+ self.addTopLevelItem(trItem)
return
# END Class _ViewPanelBackRefs
@@ -240,6 +251,7 @@ class _ViewPanelKeyWords(QTreeWidget):
def __init__(self, parent: QWidget, itemClass: nwItemClass) -> None:
super().__init__(parent=parent)
self._itemClass = nwItemClass
+ self._tagMap: dict[str, QTreeWidgetItem] = {}
iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6)
@@ -297,7 +309,16 @@ class _ViewPanelKeyWords(QTreeWidget):
trItem.setData(self.C_DATA, self.D_TAG, tag)
trItem.setData(self.C_DATA, self.D_HANDLE, iItem.handle)
self.addTopLevelItem(trItem)
+ self._tagMap[tag] = trItem
return
+ def removeEntry(self, tag: str) -> bool:
+ """Remove a tag from the list."""
+ if tag in self._tagMap:
+ self.takeTopLevelItem(self.indexOfTopLevelItem(self._tagMap[tag]))
+ self._tagMap.pop(tag, None)
+ return True
+ return False
+
# END Class _ViewPanelRefs
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index b52680d7..7137d65a 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -511,7 +511,7 @@ class GuiProjectTree(QTreeWidget):
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setDropIndicatorShown(True)
- # Disable built-in autoscroll as it isn't working in some Qt
+ # Disable built-in auto scroll as it isn't working in some Qt
# releases (see #1561) and instead use our own implementation
self.setAutoScroll(False)
@@ -533,7 +533,7 @@ class GuiProjectTree(QTreeWidget):
self.itemDoubleClicked.connect(self._treeDoubleClick)
self.itemSelectionChanged.connect(self._treeSelectionChange)
- # Autoscroll
+ # Auto Scroll
self._scrollMargin = SHARED.theme.baseIconSize
self._scrollDirection = 0
self._scrollTimer = QTimer()
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index eca09789..bff6fb47 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -243,6 +243,7 @@ class GuiMain(QMainWindow):
SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus)
SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage)
SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
+ SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags)
self.mainMenu.requestDocAction.connect(self._passDocumentAction)
self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
@@ -440,6 +441,7 @@ class GuiMain(QMainWindow):
self.docViewer.clearNavHistory()
self.closeDocViewer(byUser=False)
+ self.docViewerPanel.closeProjectTasks()
self.outlineView.closeProjectTasks()
self.novelView.closeProjectTasks()
self.projView.clearProjectView()
@@ -809,6 +811,7 @@ class GuiMain(QMainWindow):
tStart = time()
self.projView.saveProjectTasks()
+ self.docViewerPanel.clearClassTabs()
SHARED.project.index.rebuildIndex()
self.projView.populateTree()
self.novelView.refreshTree()
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index deb49ce1..059a92d7 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -52,7 +52,7 @@ class SharedData(QObject):
projectStatusChanged = pyqtSignal(bool)
projectStatusMessage = pyqtSignal(str)
spellLanguageChanged = pyqtSignal(str, str)
- indexChangedTags = pyqtSignal(list[str], list[str])
+ indexChangedTags = pyqtSignal(list, list)
def __init__(self) -> None:
super().__init__()
From 6b9d92e75aca4fc6debb8006f448cd1c4e3b4391 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 20:13:23 +0100
Subject: [PATCH 08/22] Add handlers for document label change
---
novelwriter/core/index.py | 36 +++++++--------
novelwriter/gui/doceditor.py | 2 +-
novelwriter/gui/docviewerpanel.py | 75 ++++++++++++++++++++-----------
novelwriter/guimain.py | 3 +-
novelwriter/shared.py | 4 +-
5 files changed, 72 insertions(+), 48 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 0b7ae8ca..14012193 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -36,7 +36,7 @@ from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator
from pathlib import Path
from novelwriter import SHARED
-from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout, nwTrinary
+from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
@@ -255,7 +255,7 @@ class NWIndex:
return False
# Keep a record of existing tags, and create a new item entry
- itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), nwTrinary.NEGATIVE)
+ itemTags = dict.fromkeys(self._itemIndex.allItemTags(tHandle), False)
self._itemIndex.add(tHandle, tItem)
# Run word counter for the whole text
@@ -290,8 +290,7 @@ class NWIndex:
# Internal Indexer Helpers
##
- def _scanActive(self, tHandle: str, nwItem: NWItem, text: str,
- tags: dict[str, nwTrinary]) -> None:
+ def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict[str, bool]) -> None:
"""Scan an active document for meta data."""
nTitle = 0 # Line Number of the previous title
cTitle = TT_NONE # Tag of the current title
@@ -347,20 +346,18 @@ class NWIndex:
self._indexWordCounts(tHandle, text, cTitle)
# Prune no longer used tags
- for tTag, tStatus in tags.items():
- added = []
+ for tTag, isActive in tags.items():
+ updated = []
deleted = []
- if tStatus == nwTrinary.NEGATIVE:
+ if isActive:
+ logger.debug("Added/updated tag '%s'", tTag)
+ updated.append(tTag)
+ else:
logger.debug("Removed tag '%s'", tTag)
del self._tagsIndex[tTag]
deleted.append(tTag)
- elif tStatus == nwTrinary.POSITIVE:
- logger.debug("Added new tag '%s'", tTag)
- added.append(tTag)
- else:
- logger.debug("Unchanged tag '%s'", tTag)
- if added or deleted:
- SHARED.indexUpdatedTags(added, deleted)
+ if updated or deleted:
+ SHARED.indexUpdatedTags(updated, deleted)
return
@@ -397,7 +394,7 @@ class NWIndex:
return
def _indexKeyword(self, tHandle: str, line: str, sTitle: str,
- itemClass: nwItemClass, tags: dict[str, nwTrinary]) -> None:
+ itemClass: nwItemClass, tags: dict[str, bool]) -> None:
"""Validate and save the information about a reference to a tag
in another file, or the setting of a tag in the file. A record
of active tags is updated so that no longer used tags can be
@@ -414,10 +411,9 @@ class NWIndex:
if tBits[0] == nwKeyWords.TAG_KEY:
tagName = tBits[1]
- tagKey = tagName.lower()
self._tagsIndex.add(tagName, tHandle, sTitle, itemClass)
self._itemIndex.setHeadingTag(tHandle, sTitle, tagName)
- tags[tagKey] = nwTrinary.NEUTRAL if tagKey in tags else nwTrinary.POSITIVE
+ tags[tagName.lower()] = True
else:
self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0])
@@ -634,7 +630,11 @@ class NWIndex:
sTitle = self._tagsIndex.tagHeading(tagKey)
return tHandle, sTitle
- def getTags(self, itemClass: nwItemClass) -> list[str]:
+ def getDocumentTags(self, tHandle: str | None) -> list[str]:
+ """Return all tags used by a specific document."""
+ return self._itemIndex.allItemTags(tHandle) if tHandle else []
+
+ def getClassTags(self, itemClass: nwItemClass) -> list[str]:
"""Return all tags based on itemClass."""
return self._tagsIndex.filterTagNames(itemClass.name)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 00e3a093..a83dae05 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -2054,7 +2054,7 @@ class MetaCompleter(QMenu):
length = len(lookup)
suffix = ""
options = list(filter(
- lambda x: lookup in x.lower(), SHARED.project.index.getTags(
+ lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
nwKeyWords.KEY_CLASS.get(kw.strip(), nwItemClass.NO_CLASS)
)
))[:15]
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 0326bb0c..4673043e 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -102,8 +102,8 @@ class GuiDocViewerPanel(QWidget):
"""Run open project tasks."""
self.clearClassTabs()
for key, name, tClass, iItem, hItem in SHARED.project.index.getTagsData():
- if tClass in self.kwTabs:
- self.kwTabs[tClass].addEntry(key, name, iItem, hItem)
+ if tClass in self.kwTabs and iItem and hItem:
+ self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
self._updateTabVisibility()
return
@@ -123,6 +123,16 @@ class GuiDocViewerPanel(QWidget):
# Public Slots
##
+ @pyqtSlot(str)
+ def projectItemChanged(self, tHandle: str) -> None:
+ """Update meta data for project item."""
+ self.tabBackRefs.refreshDocument(tHandle)
+ for key in SHARED.project.index.getDocumentTags(tHandle):
+ name, tClass, iItem, hItem = SHARED.project.index.getSingleTag(key)
+ if tClass in self.kwTabs and iItem and hItem:
+ self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
+ return
+
@pyqtSlot(str)
def updateHandle(self, tHandle: str | None) -> None:
"""Update the document handle."""
@@ -131,22 +141,19 @@ class GuiDocViewerPanel(QWidget):
return
@pyqtSlot(list, list)
- def updateChangedTags(self, added: list[str], deleted: list[str]) -> None:
+ def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
"""Forward tags changes to the lists."""
- for key in added:
+ for key in updated:
name, tClass, iItem, hItem = SHARED.project.index.getSingleTag(key)
- if tClass in self.kwTabs:
- self.kwTabs[tClass].addEntry(key, name, iItem, hItem)
-
+ if tClass in self.kwTabs and iItem and hItem:
+ self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
for key in deleted:
for cTab in self.kwTabs.values():
if cTab.removeEntry(key):
break
else:
logger.warning("Could not remove tag '%s' from view panel", key)
-
self._updateTabVisibility()
-
return
##
@@ -180,9 +187,7 @@ class _ViewPanelBackRefs(QTreeWidget):
cMg = CONFIG.pxInt(6)
# Content
- self.setHeaderLabels([
- self.tr("Heading"), "", "", self.tr("Document")
- ])
+ self.setHeaderLabels([self.tr("Heading"), "", "", self.tr("Document")])
self.setIndentation(0)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setIconSize(QSize(iPx, iPx))
@@ -233,6 +238,21 @@ class _ViewPanelBackRefs(QTreeWidget):
self.addTopLevelItem(trItem)
return
+ def refreshDocument(self, tHandle: str) -> None:
+ """Refresh document meta data."""
+ nwItem = SHARED.project.tree[tHandle]
+ if nwItem:
+ docIcon = SHARED.theme.getItemIcon(
+ nwItem.itemType, nwItem.itemClass,
+ nwItem.itemLayout, nwItem.mainHeading
+ )
+ for i in range(self.topLevelItemCount()):
+ trItem = self.topLevelItem(i)
+ if trItem and trItem.data(self.C_DATA, self.D_HANDLE) == tHandle:
+ trItem.setIcon(self.C_DOCUMENT, docIcon)
+ trItem.setText(self.C_DOCUMENT, nwItem.itemName)
+ return
+
# END Class _ViewPanelBackRefs
@@ -250,15 +270,13 @@ class _ViewPanelKeyWords(QTreeWidget):
def __init__(self, parent: QWidget, itemClass: nwItemClass) -> None:
super().__init__(parent=parent)
- self._itemClass = nwItemClass
+
self._tagMap: dict[str, QTreeWidgetItem] = {}
iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6)
- self.setHeaderLabels([
- self.tr("Tag"), "", "", self.tr("Heading"), self.tr("Document")
- ])
+ self.setHeaderLabels([self.tr("Tag"), "", "", self.tr("Heading"), self.tr("Document")])
self.setIndentation(0)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setIconSize(QSize(iPx, iPx))
@@ -266,6 +284,7 @@ class _ViewPanelKeyWords(QTreeWidget):
self.setSortingEnabled(True)
self.sortByColumn(self.C_NAME, Qt.SortOrder.AscendingOrder)
+ # Set Header Sizes
treeHeader = self.header()
treeHeader.setStretchLastSection(True)
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents)
@@ -274,6 +293,7 @@ class _ViewPanelKeyWords(QTreeWidget):
treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
+ # Cache Icons Locally
self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass])
self._editIcon = SHARED.theme.getIcon("edit")
self._viewIcon = SHARED.theme.getIcon("view")
@@ -281,14 +301,11 @@ class _ViewPanelKeyWords(QTreeWidget):
return
def count(self) -> int:
+ """Return the number of items in the list."""
return self.topLevelItemCount()
- def addEntry(self, tag: str, name: str, iItem: IndexItem | None,
- hItem: IndexHeading | None) -> None:
- """Add a tag entry to the list."""
- if not iItem or not hItem:
- return
-
+ def addUpdateEntry(self, tag: str, name: str, iItem: IndexItem, hItem: IndexHeading) -> None:
+ """Add a new entry, or update an existing one."""
nwItem = iItem.item
docIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass,
@@ -297,7 +314,13 @@ class _ViewPanelKeyWords(QTreeWidget):
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
- trItem = QTreeWidgetItem()
+ # This can not use a get call to the dictionary as that creates
+ # some weird issue with Qt, so we need to do this with an if
+ if tag in self._tagMap:
+ trItem = self._tagMap[tag]
+ else:
+ trItem = QTreeWidgetItem()
+
trItem.setText(self.C_NAME, name)
trItem.setIcon(self.C_NAME, self._classIcon)
trItem.setIcon(self.C_EDIT, self._editIcon)
@@ -308,8 +331,10 @@ class _ViewPanelKeyWords(QTreeWidget):
trItem.setText(self.C_DOCUMENT, nwItem.itemName)
trItem.setData(self.C_DATA, self.D_TAG, tag)
trItem.setData(self.C_DATA, self.D_HANDLE, iItem.handle)
- self.addTopLevelItem(trItem)
- self._tagMap[tag] = trItem
+
+ if tag not in self._tagMap:
+ self.addTopLevelItem(trItem)
+ self._tagMap[tag] = trItem
return
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index bff6fb47..ad08c920 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -259,6 +259,7 @@ class GuiMain(QMainWindow):
self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo)
self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo)
self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox)
+ self.projView.treeItemChanged.connect(self.docViewerPanel.projectItemChanged)
self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem)
self.projView.rootFolderChanged.connect(self.novelView.updateRootItem)
self.projView.rootFolderChanged.connect(self.projView.updateRootItem)
@@ -788,11 +789,9 @@ class GuiMain(QMainWindow):
if not SHARED.hasProject:
logger.error("No project open")
return False
-
if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode):
tHandle = self.docEditor.docHandle
self.projView.renameTreeItem(tHandle)
-
return True
def rebuildTrees(self) -> None:
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 059a92d7..8ed333d6 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -215,9 +215,9 @@ class SharedData(QObject):
# Call-Back Functions
##
- def indexUpdatedTags(self, added: list[str], deleted: list[str]) -> None:
+ def indexUpdatedTags(self, updated: list[str], deleted: list[str]) -> None:
"""Emit the index changed tags signal."""
- self.indexChangedTags.emit(added, deleted)
+ self.indexChangedTags.emit(updated, deleted)
return
##
From e82d5277c0ba6bc359661b9f314980323a147b40 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 21:03:12 +0100
Subject: [PATCH 09/22] Add signal for re-indexed document
---
novelwriter/core/index.py | 7 +-
novelwriter/gui/docviewerpanel.py | 112 +++++++++++++++++-------------
novelwriter/guimain.py | 1 +
novelwriter/shared.py | 6 ++
4 files changed, 73 insertions(+), 53 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 14012193..dc7ac20a 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -34,8 +34,8 @@ import logging
from time import time
from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator
from pathlib import Path
-from novelwriter import SHARED
+from novelwriter import SHARED
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
@@ -283,6 +283,7 @@ class NWIndex:
nowTime = time()
self._indexChange = nowTime
self._rootChange[tItem.itemRoot] = nowTime
+ SHARED.indexDocumentScanned(tHandle)
return True
@@ -606,9 +607,7 @@ class NWIndex:
return tRefs
def getBackReferenceList(self, tHandle: str) -> dict[str, tuple[str, IndexHeading]]:
- """Build a list of files referring back to our file, specified
- by tHandle.
- """
+ """Build a dict of files referring back to our file."""
if tHandle is None or tHandle not in self._itemIndex:
return {}
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 4673043e..3bba1124 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -171,11 +171,11 @@ class GuiDocViewerPanel(QWidget):
class _ViewPanelBackRefs(QTreeWidget):
- C_DATA = 0
- C_TITLE = 0
- C_EDIT = 1
- C_VIEW = 2
- C_DOCUMENT = 3
+ C_DATA = 0
+ C_DOC = 0
+ C_EDIT = 1
+ C_VIEW = 2
+ C_TITLE = 3
D_HANDLE = Qt.ItemDataRole.UserRole
D_TITLE = Qt.ItemDataRole.UserRole + 1
@@ -183,11 +183,13 @@ class _ViewPanelBackRefs(QTreeWidget):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
+
iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6)
# Content
- self.setHeaderLabels([self.tr("Heading"), "", "", self.tr("Document")])
+ self.setHeaderLabels([self.tr("Document"), "", "", self.tr("First Heading")])
self.setIndentation(0)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setIconSize(QSize(iPx, iPx))
@@ -195,7 +197,7 @@ class _ViewPanelBackRefs(QTreeWidget):
treeHeader = self.header()
treeHeader.setStretchLastSection(True)
- treeHeader.setSectionResizeMode(self.C_DOCUMENT, QHeaderView.ResizeMode.ResizeToContents)
+ treeHeader.setSectionResizeMode(self.C_DOC, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.ResizeMode.ResizeToContents)
@@ -210,47 +212,59 @@ class _ViewPanelBackRefs(QTreeWidget):
def refreshContent(self, dHandle: str | None) -> None:
"""Update the content."""
self.clear()
+ self._treeMap = {}
if dHandle:
refs = SHARED.project.index.getBackReferenceList(dHandle)
for tHandle, (sTitle, hItem) in refs.items():
- nwItem = SHARED.project.tree[tHandle]
- if nwItem is None:
- continue
-
- docIcon = SHARED.theme.getItemIcon(
- nwItem.itemType, nwItem.itemClass,
- nwItem.itemLayout, nwItem.mainHeading
- )
- iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
- hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
-
- trItem = QTreeWidgetItem()
- trItem.setText(self.C_TITLE, hItem.title)
- trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
- trItem.setIcon(self.C_EDIT, self._editIcon)
- trItem.setIcon(self.C_VIEW, self._viewIcon)
- trItem.setIcon(self.C_DOCUMENT, docIcon)
- trItem.setText(self.C_DOCUMENT, nwItem.itemName)
-
- trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
- trItem.setData(self.C_DATA, self.D_TITLE, sTitle)
-
- self.addTopLevelItem(trItem)
+ self._setTreeItemValues(tHandle, sTitle, hItem)
return
def refreshDocument(self, tHandle: str) -> None:
"""Refresh document meta data."""
+ print("Ping")
+ iItem = SHARED.project.index.getItemData(tHandle)
+ if iItem:
+ for sTitle, hItem in iItem.items():
+ self._setTreeItemValues(tHandle, sTitle, hItem)
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _setTreeItemValues(self, tHandle: str, sTitle: str, hItem: IndexHeading) -> None:
+ """Add or update a tree item."""
nwItem = SHARED.project.tree[tHandle]
- if nwItem:
- docIcon = SHARED.theme.getItemIcon(
- nwItem.itemType, nwItem.itemClass,
- nwItem.itemLayout, nwItem.mainHeading
- )
- for i in range(self.topLevelItemCount()):
- trItem = self.topLevelItem(i)
- if trItem and trItem.data(self.C_DATA, self.D_HANDLE) == tHandle:
- trItem.setIcon(self.C_DOCUMENT, docIcon)
- trItem.setText(self.C_DOCUMENT, nwItem.itemName)
+ if nwItem is None:
+ return
+
+ docIcon = SHARED.theme.getItemIcon(
+ nwItem.itemType, nwItem.itemClass,
+ nwItem.itemLayout, nwItem.mainHeading
+ )
+ iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
+ hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
+
+ tKey = f"{tHandle}:{sTitle}"
+ if tKey in self._treeMap:
+ trItem = self._treeMap[tKey]
+ else:
+ trItem = QTreeWidgetItem()
+
+ trItem.setIcon(self.C_DOC, docIcon)
+ trItem.setText(self.C_DOC, nwItem.itemName)
+ trItem.setIcon(self.C_EDIT, self._editIcon)
+ trItem.setIcon(self.C_VIEW, self._viewIcon)
+ trItem.setText(self.C_TITLE, hItem.title)
+ trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
+
+ trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
+ trItem.setData(self.C_DATA, self.D_TITLE, sTitle)
+
+ if tKey not in self._treeMap:
+ self.addTopLevelItem(trItem)
+ self._treeMap[tKey] = trItem
+
return
# END Class _ViewPanelBackRefs
@@ -258,12 +272,12 @@ class _ViewPanelBackRefs(QTreeWidget):
class _ViewPanelKeyWords(QTreeWidget):
- C_DATA = 0
- C_NAME = 0
- C_EDIT = 1
- C_VIEW = 2
- C_TITLE = 3
- C_DOCUMENT = 4
+ C_DATA = 0
+ C_NAME = 0
+ C_EDIT = 1
+ C_VIEW = 2
+ C_DOC = 3
+ C_TITLE = 4
D_TAG = Qt.ItemDataRole.UserRole
D_HANDLE = Qt.ItemDataRole.UserRole + 1
@@ -276,7 +290,7 @@ class _ViewPanelKeyWords(QTreeWidget):
iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6)
- self.setHeaderLabels([self.tr("Tag"), "", "", self.tr("Heading"), self.tr("Document")])
+ self.setHeaderLabels([self.tr("Tag"), "", "", self.tr("Document"), self.tr("Heading")])
self.setIndentation(0)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setIconSize(QSize(iPx, iPx))
@@ -325,10 +339,10 @@ class _ViewPanelKeyWords(QTreeWidget):
trItem.setIcon(self.C_NAME, self._classIcon)
trItem.setIcon(self.C_EDIT, self._editIcon)
trItem.setIcon(self.C_VIEW, self._viewIcon)
+ trItem.setIcon(self.C_DOC, docIcon)
+ trItem.setText(self.C_DOC, nwItem.itemName)
trItem.setText(self.C_TITLE, hItem.title)
trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
- trItem.setIcon(self.C_DOCUMENT, docIcon)
- trItem.setText(self.C_DOCUMENT, nwItem.itemName)
trItem.setData(self.C_DATA, self.D_TAG, tag)
trItem.setData(self.C_DATA, self.D_HANDLE, iItem.handle)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index ad08c920..ecabc2c0 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -244,6 +244,7 @@ class GuiMain(QMainWindow):
SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage)
SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags)
+ SHARED.indexScannedDocument.connect(self.docViewerPanel.projectItemChanged)
self.mainMenu.requestDocAction.connect(self._passDocumentAction)
self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 8ed333d6..8980a520 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -52,6 +52,7 @@ class SharedData(QObject):
projectStatusChanged = pyqtSignal(bool)
projectStatusMessage = pyqtSignal(str)
spellLanguageChanged = pyqtSignal(str, str)
+ indexScannedDocument = pyqtSignal(str)
indexChangedTags = pyqtSignal(list, list)
def __init__(self) -> None:
@@ -220,6 +221,11 @@ class SharedData(QObject):
self.indexChangedTags.emit(updated, deleted)
return
+ def indexDocumentScanned(self, tHandle: str) -> None:
+ """Emit the index scanned document signal."""
+ self.indexScannedDocument.emit(tHandle)
+ return
+
##
# Alert Boxes
##
From 565350fcfb963266d5e958262a840bb98d06e07a Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 21:14:40 +0100
Subject: [PATCH 10/22] Clean up tree item updates
---
novelwriter/gui/doceditor.py | 3 ---
novelwriter/gui/docviewerpanel.py | 4 ++--
novelwriter/gui/projtree.py | 9 +++++++--
novelwriter/guimain.py | 2 ++
4 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index a83dae05..170fcc86 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -500,10 +500,7 @@ class GuiDocEditor(QPlainTextEdit):
else:
self.novelStructureChanged.emit()
- # ToDo: This should be a signal
if oldHeader != newHeader:
- self.mainGui.projView.setTreeItemValues(tHandle)
- self.mainGui.itemDetails.updateViewBox(tHandle)
self.docFooter.updateInfo()
# Update the status bar
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 3bba1124..ea5382de 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -221,11 +221,11 @@ class _ViewPanelBackRefs(QTreeWidget):
def refreshDocument(self, tHandle: str) -> None:
"""Refresh document meta data."""
- print("Ping")
iItem = SHARED.project.index.getItemData(tHandle)
if iItem:
for sTitle, hItem in iItem.items():
- self._setTreeItemValues(tHandle, sTitle, hItem)
+ if f"{tHandle}:{sTitle}" in self._treeMap:
+ self._setTreeItemValues(tHandle, sTitle, hItem)
return
##
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 7137d65a..a4f18357 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -142,7 +142,6 @@ class GuiProjectView(QWidget):
# Function Mappings
self.emptyTrash = self.projTree.emptyTrash
self.requestDeleteItem = self.projTree.requestDeleteItem
- self.setTreeItemValues = self.projTree.setTreeItemValues
self.propagateCount = self.projTree.propagateCount
self.getSelectedHandle = self.projTree.getSelectedHandle
self.setSelectedHandle = self.projTree.setSelectedHandle
@@ -210,6 +209,12 @@ class GuiProjectView(QWidget):
# Public Slots
##
+ @pyqtSlot(str)
+ def updateItemValues(self, tHandle: str) -> None:
+ """Update tree item"""
+ self.projTree.setTreeItemValues(tHandle)
+ return
+
@pyqtSlot(str, int, int, int)
def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None:
"""Slot for updating the word count of a specific item."""
@@ -431,7 +436,7 @@ class GuiProjectToolBar(QWidget):
return
##
- # Slots
+ # Private Slots
##
@pyqtSlot(str)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index ecabc2c0..9e794027 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -245,6 +245,8 @@ class GuiMain(QMainWindow):
SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags)
SHARED.indexScannedDocument.connect(self.docViewerPanel.projectItemChanged)
+ SHARED.indexScannedDocument.connect(self.projView.updateItemValues)
+ SHARED.indexScannedDocument.connect(self.itemDetails.updateViewBox)
self.mainMenu.requestDocAction.connect(self._passDocumentAction)
self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
From 2fc8e43e718ecf90157e263960573285b7d8b5dc Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 21:38:45 +0100
Subject: [PATCH 11/22] Connect the edit and view buttons
---
novelwriter/gui/docviewerpanel.py | 59 +++++++++++++++++++++++++++----
novelwriter/guimain.py | 3 ++
2 files changed, 55 insertions(+), 7 deletions(-)
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index ea5382de..e428ae53 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -25,7 +25,9 @@ from __future__ import annotations
import logging
-from PyQt5.QtCore import QSize, Qt, pyqtSlot
+from enum import Enum
+
+from PyQt5.QtCore import QModelIndex, QSize, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QFrame, QHeaderView, QTabWidget, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget
@@ -34,13 +36,16 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst
from novelwriter.core.index import IndexHeading, IndexItem
-from novelwriter.enum import nwItemClass
+from novelwriter.enum import nwDocMode, nwItemClass
logger = logging.getLogger(__name__)
class GuiDocViewerPanel(QWidget):
+ loadDocumentTagRequest = pyqtSignal(str, Enum)
+ openDocumentRequest = pyqtSignal(str, Enum, str, bool)
+
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
@@ -180,21 +185,23 @@ class _ViewPanelBackRefs(QTreeWidget):
D_HANDLE = Qt.ItemDataRole.UserRole
D_TITLE = Qt.ItemDataRole.UserRole + 1
- def __init__(self, parent: QWidget) -> None:
- super().__init__(parent=parent)
+ def __init__(self, docViewerPanel: GuiDocViewerPanel) -> None:
+ super().__init__(parent=docViewerPanel)
+
+ self.docViewerPanel = docViewerPanel
self._treeMap: dict[str, QTreeWidgetItem] = {}
iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6)
- # Content
self.setHeaderLabels([self.tr("Document"), "", "", self.tr("First Heading")])
self.setIndentation(0)
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setIconSize(QSize(iPx, iPx))
self.setFrameStyle(QFrame.Shape.NoFrame)
+ # Set Header Sizes
treeHeader = self.header()
treeHeader.setStretchLastSection(True)
treeHeader.setSectionResizeMode(self.C_DOC, QHeaderView.ResizeMode.ResizeToContents)
@@ -204,9 +211,13 @@ class _ViewPanelBackRefs(QTreeWidget):
treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
+ # Cache Icons Locally
self._editIcon = SHARED.theme.getIcon("edit")
self._viewIcon = SHARED.theme.getIcon("view")
+ # Signals
+ self.clicked.connect(self._treeItemClicked)
+
return
def refreshContent(self, dHandle: str | None) -> None:
@@ -228,6 +239,20 @@ class _ViewPanelBackRefs(QTreeWidget):
self._setTreeItemValues(tHandle, sTitle, hItem)
return
+ ##
+ # Private Slots
+ ##
+
+ @pyqtSlot("QModelIndex")
+ def _treeItemClicked(self, index: QModelIndex) -> None:
+ """The user clicked on an item in the tree."""
+ tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
+ if index.column() == self.C_EDIT:
+ self.docViewerPanel.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
+ elif index.column() == self.C_VIEW:
+ self.docViewerPanel.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
+ return
+
##
# Internal Functions
##
@@ -282,8 +307,10 @@ class _ViewPanelKeyWords(QTreeWidget):
D_TAG = Qt.ItemDataRole.UserRole
D_HANDLE = Qt.ItemDataRole.UserRole + 1
- def __init__(self, parent: QWidget, itemClass: nwItemClass) -> None:
- super().__init__(parent=parent)
+ def __init__(self, docViewerPanel: GuiDocViewerPanel, itemClass: nwItemClass) -> None:
+ super().__init__(parent=docViewerPanel)
+
+ self.docViewerPanel = docViewerPanel
self._tagMap: dict[str, QTreeWidgetItem] = {}
@@ -304,6 +331,7 @@ class _ViewPanelKeyWords(QTreeWidget):
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
+ treeHeader.setSectionResizeMode(self.C_DOC, QHeaderView.ResizeMode.ResizeToContents)
treeHeader.resizeSection(self.C_EDIT, iPx + cMg)
treeHeader.resizeSection(self.C_VIEW, iPx + cMg)
@@ -312,6 +340,9 @@ class _ViewPanelKeyWords(QTreeWidget):
self._editIcon = SHARED.theme.getIcon("edit")
self._viewIcon = SHARED.theme.getIcon("view")
+ # Signals
+ self.clicked.connect(self._treeItemClicked)
+
return
def count(self) -> int:
@@ -360,4 +391,18 @@ class _ViewPanelKeyWords(QTreeWidget):
return True
return False
+ ##
+ # Private Slots
+ ##
+
+ @pyqtSlot("QModelIndex")
+ def _treeItemClicked(self, index: QModelIndex) -> None:
+ """The user clicked on an item in the tree."""
+ tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
+ if index.column() == self.C_EDIT:
+ self.docViewerPanel.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT)
+ elif index.column() == self.C_VIEW:
+ self.docViewerPanel.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
+ return
+
# END Class _ViewPanelRefs
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 9e794027..68815c76 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -286,6 +286,9 @@ class GuiMain(QMainWindow):
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
+ self.docViewerPanel.loadDocumentTagRequest.connect(self._followTag)
+ self.docViewerPanel.openDocumentRequest.connect(self._openDocument)
+
self.outlineView.loadDocumentTagRequest.connect(self._followTag)
self.outlineView.openDocumentRequest.connect(self._openDocument)
From b94e012f8f8c8535af64f70001214f1332f952bd Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 21:46:32 +0100
Subject: [PATCH 12/22] Fix bug with clearing class tag lists
---
novelwriter/gui/docviewerpanel.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index e428ae53..3194f823 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -121,7 +121,7 @@ class GuiDocViewerPanel(QWidget):
def clearClassTabs(self) -> None:
"""Clear all the class tabs"""
for cTab in self.kwTabs.values():
- cTab.clear()
+ cTab.clearContent()
return
##
@@ -349,6 +349,12 @@ class _ViewPanelKeyWords(QTreeWidget):
"""Return the number of items in the list."""
return self.topLevelItemCount()
+ def clearContent(self) -> None:
+ """Clear the list."""
+ self._tagMap = {}
+ self.clear()
+ return
+
def addUpdateEntry(self, tag: str, name: str, iItem: IndexItem, hItem: IndexHeading) -> None:
"""Add a new entry, or update an existing one."""
nwItem = iItem.item
From f51223e7c3fba3f6a787f6cf202b33812ad55f9d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 21:58:41 +0100
Subject: [PATCH 13/22] Rename the tab count function just to be safe
---
novelwriter/gui/docviewerpanel.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 3194f823..86312a7f 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -168,7 +168,7 @@ class GuiDocViewerPanel(QWidget):
def _updateTabVisibility(self) -> None:
"""Hide class tabs with no content."""
for tClass, cTab in self.kwTabs.items():
- self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.count() > 0)
+ self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
return
# END Class GuiDocViewerPanel
@@ -345,7 +345,7 @@ class _ViewPanelKeyWords(QTreeWidget):
return
- def count(self) -> int:
+ def countEntries(self) -> int:
"""Return the number of items in the list."""
return self.topLevelItemCount()
From 3d17d8e08c7e14d9c1210145e9f82f484ad78ce4 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 15 Nov 2023 22:09:18 +0100
Subject: [PATCH 14/22] Clean up the viewer panel code a bit
---
novelwriter/gui/docviewerpanel.py | 65 ++++++++++++-------------------
1 file changed, 25 insertions(+), 40 deletions(-)
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 86312a7f..eab63a42 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -43,8 +43,8 @@ logger = logging.getLogger(__name__)
class GuiDocViewerPanel(QWidget):
- loadDocumentTagRequest = pyqtSignal(str, Enum)
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
+ loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
@@ -183,13 +183,11 @@ class _ViewPanelBackRefs(QTreeWidget):
C_TITLE = 3
D_HANDLE = Qt.ItemDataRole.UserRole
- D_TITLE = Qt.ItemDataRole.UserRole + 1
- def __init__(self, docViewerPanel: GuiDocViewerPanel) -> None:
- super().__init__(parent=docViewerPanel)
-
- self.docViewerPanel = docViewerPanel
+ def __init__(self, parent: GuiDocViewerPanel) -> None:
+ super().__init__(parent=parent)
+ self._parent = parent
self._treeMap: dict[str, QTreeWidgetItem] = {}
iPx = SHARED.theme.baseIconSize
@@ -232,8 +230,7 @@ class _ViewPanelBackRefs(QTreeWidget):
def refreshDocument(self, tHandle: str) -> None:
"""Refresh document meta data."""
- iItem = SHARED.project.index.getItemData(tHandle)
- if iItem:
+ if iItem := SHARED.project.index.getItemData(tHandle):
for sTitle, hItem in iItem.items():
if f"{tHandle}:{sTitle}" in self._treeMap:
self._setTreeItemValues(tHandle, sTitle, hItem)
@@ -245,12 +242,12 @@ class _ViewPanelBackRefs(QTreeWidget):
@pyqtSlot("QModelIndex")
def _treeItemClicked(self, index: QModelIndex) -> None:
- """The user clicked on an item in the tree."""
+ """Emit document open signal on user click."""
tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
if index.column() == self.C_EDIT:
- self.docViewerPanel.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
+ self._parent.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
elif index.column() == self.C_VIEW:
- self.docViewerPanel.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
+ self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
return
##
@@ -259,8 +256,7 @@ class _ViewPanelBackRefs(QTreeWidget):
def _setTreeItemValues(self, tHandle: str, sTitle: str, hItem: IndexHeading) -> None:
"""Add or update a tree item."""
- nwItem = SHARED.project.tree[tHandle]
- if nwItem is None:
+ if (nwItem := SHARED.project.tree[tHandle]) is None:
return
docIcon = SHARED.theme.getItemIcon(
@@ -271,10 +267,7 @@ class _ViewPanelBackRefs(QTreeWidget):
hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
tKey = f"{tHandle}:{sTitle}"
- if tKey in self._treeMap:
- trItem = self._treeMap[tKey]
- else:
- trItem = QTreeWidgetItem()
+ trItem = self._treeMap[tKey] if tKey in self._treeMap else QTreeWidgetItem()
trItem.setIcon(self.C_DOC, docIcon)
trItem.setText(self.C_DOC, nwItem.itemName)
@@ -282,9 +275,7 @@ class _ViewPanelBackRefs(QTreeWidget):
trItem.setIcon(self.C_VIEW, self._viewIcon)
trItem.setText(self.C_TITLE, hItem.title)
trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
-
trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
- trItem.setData(self.C_DATA, self.D_TITLE, sTitle)
if tKey not in self._treeMap:
self.addTopLevelItem(trItem)
@@ -304,15 +295,13 @@ class _ViewPanelKeyWords(QTreeWidget):
C_DOC = 3
C_TITLE = 4
- D_TAG = Qt.ItemDataRole.UserRole
- D_HANDLE = Qt.ItemDataRole.UserRole + 1
+ D_TAG = Qt.ItemDataRole.UserRole
- def __init__(self, docViewerPanel: GuiDocViewerPanel, itemClass: nwItemClass) -> None:
- super().__init__(parent=docViewerPanel)
+ def __init__(self, parent: GuiDocViewerPanel, itemClass: nwItemClass) -> None:
+ super().__init__(parent=parent)
- self.docViewerPanel = docViewerPanel
-
- self._tagMap: dict[str, QTreeWidgetItem] = {}
+ self._parent = parent
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6)
@@ -351,7 +340,7 @@ class _ViewPanelKeyWords(QTreeWidget):
def clearContent(self) -> None:
"""Clear the list."""
- self._tagMap = {}
+ self._treeMap = {}
self.clear()
return
@@ -367,10 +356,7 @@ class _ViewPanelKeyWords(QTreeWidget):
# This can not use a get call to the dictionary as that creates
# some weird issue with Qt, so we need to do this with an if
- if tag in self._tagMap:
- trItem = self._tagMap[tag]
- else:
- trItem = QTreeWidgetItem()
+ trItem = self._treeMap[tag] if tag in self._treeMap else QTreeWidgetItem()
trItem.setText(self.C_NAME, name)
trItem.setIcon(self.C_NAME, self._classIcon)
@@ -381,19 +367,18 @@ class _ViewPanelKeyWords(QTreeWidget):
trItem.setText(self.C_TITLE, hItem.title)
trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
trItem.setData(self.C_DATA, self.D_TAG, tag)
- trItem.setData(self.C_DATA, self.D_HANDLE, iItem.handle)
- if tag not in self._tagMap:
+ if tag not in self._treeMap:
self.addTopLevelItem(trItem)
- self._tagMap[tag] = trItem
+ self._treeMap[tag] = trItem
return
def removeEntry(self, tag: str) -> bool:
"""Remove a tag from the list."""
- if tag in self._tagMap:
- self.takeTopLevelItem(self.indexOfTopLevelItem(self._tagMap[tag]))
- self._tagMap.pop(tag, None)
+ if tag in self._treeMap:
+ self.takeTopLevelItem(self.indexOfTopLevelItem(self._treeMap[tag]))
+ self._treeMap.pop(tag, None)
return True
return False
@@ -403,12 +388,12 @@ class _ViewPanelKeyWords(QTreeWidget):
@pyqtSlot("QModelIndex")
def _treeItemClicked(self, index: QModelIndex) -> None:
- """The user clicked on an item in the tree."""
+ """Emit follow tag signal on user click."""
tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
if index.column() == self.C_EDIT:
- self.docViewerPanel.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT)
+ self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT)
elif index.column() == self.C_VIEW:
- self.docViewerPanel.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
+ self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
return
# END Class _ViewPanelRefs
From 844240b5b841377a9f3ed42e8be4a9c3c8d96263 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 16 Nov 2023 00:23:59 +0100
Subject: [PATCH 15/22] Fix broken tests
---
tests/reference/baseConfig_novelwriter.conf | 2 +-
.../reference/guiPreferences_novelwriter.conf | 2 +-
tests/test_core/test_core_index.py | 40 +++++++++----------
tests/test_gui/test_gui_docviewer.py | 28 -------------
4 files changed, 22 insertions(+), 50 deletions(-)
diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf
index 739a6378..5007dd54 100644
--- a/tests/reference/baseConfig_novelwriter.conf
+++ b/tests/reference/baseConfig_novelwriter.conf
@@ -68,7 +68,7 @@ stopwhenidle = True
useridletime = 300
[State]
-showrefpanel = True
+showviewerpanel = True
showedittoolbar = False
useshortcodes = False
viewcomments = True
diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf
index 002dc463..9275b82e 100644
--- a/tests/reference/guiPreferences_novelwriter.conf
+++ b/tests/reference/guiPreferences_novelwriter.conf
@@ -68,7 +68,7 @@ stopwhenidle = True
useridletime = 300
[State]
-showrefpanel = True
+showviewerpanel = True
showedittoolbar = False
useshortcodes = False
viewcomments = True
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 773b0e79..ba68ea18 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -562,11 +562,11 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
))
# The novel structure should contain the pointer to the novel file header
- theKeys = []
+ keys = []
for aKey, _, _, _ in index.novelStructure():
- theKeys.append(aKey)
+ keys.append(aKey)
- assert theKeys == [
+ assert keys == [
f"{C.hTitlePage}:T0001",
f"{C.hChapterDoc}:T0001",
f"{C.hSceneDoc}:T0001",
@@ -576,22 +576,22 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
# Check that excluded files can be skipped
project.tree[nHandle].setActive(False) # type: ignore
- theKeys = []
+ keys = []
for aKey, _, _, _ in index.novelStructure(skipExcl=False):
- theKeys.append(aKey)
+ keys.append(aKey)
- assert theKeys == [
+ assert keys == [
f"{C.hTitlePage}:T0001",
f"{C.hChapterDoc}:T0001",
f"{C.hSceneDoc}:T0001",
f"{nHandle}:T0001",
]
- theKeys = []
+ keys = []
for aKey, _, _, _ in index.novelStructure(skipExcl=True):
- theKeys.append(aKey)
+ keys.append(aKey)
- assert theKeys == [
+ assert keys == [
f"{C.hTitlePage}:T0001",
f"{C.hChapterDoc}:T0001",
f"{C.hSceneDoc}:T0001",
@@ -606,23 +606,23 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
# getItemData + getHandleHeaderCount
# ==================================
- theItem = index.getItemData(nHandle)
- assert isinstance(theItem, IndexItem)
- assert theItem.headings() == ["T0001"]
+ item = index.getItemData(nHandle)
+ assert isinstance(item, IndexItem)
+ assert item.headings() == ["T0001"]
assert index.getHandleHeaderCount(nHandle) == 1
# getReferences
# =============
# Look up an invalid handle
- theRefs = index.getReferences("Not a handle")
- assert theRefs["@pov"] == []
- assert theRefs["@char"] == []
+ refs = index.getReferences("Not a handle")
+ assert refs["@pov"] == []
+ assert refs["@char"] == []
# The novel file should now refer to Jane as @pov and @char
- theRefs = index.getReferences(nHandle)
- assert theRefs["@pov"] == ["Jane"]
- assert theRefs["@char"] == ["Jane"]
+ refs = index.getReferences(nHandle)
+ assert refs["@pov"] == ["Jane"]
+ assert refs["@char"] == ["Jane"]
# getBackReferenceList
# ====================
@@ -634,8 +634,8 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
assert index.getBackReferenceList(C.hTitlePage) == {}
# The character file should have a record of the reference from the novel file
- theRefs = index.getBackReferenceList(cHandle)
- assert theRefs == {nHandle: "T0001"}
+ refs = index.getBackReferenceList(cHandle)
+ assert refs[nHandle][0] == "T0001"
# getTagSource
# ============
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index cc8bc995..aea91084 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -112,7 +112,6 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
nwGUI.docViewer.setTextCursor(theCursor)
nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor)
theRect = nwGUI.docViewer.cursorRect()
- # qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100)
nwGUI.docViewer._linkClicked(QUrl("#char=Bod"))
assert nwGUI.docViewer.docHandle == "4c4f28287af27"
@@ -140,20 +139,6 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
assert nwGUI.docViewer.docHeader.theTitle.text() == "Test Title"
CONFIG.showFullPath = True
- # Document footer show/hide references
- viewState = nwGUI.viewMeta.isVisible()
- nwGUI.docViewer.docFooter._doShowHide()
- assert nwGUI.viewMeta.isVisible() is not viewState
- nwGUI.docViewer.docFooter._doShowHide()
- assert nwGUI.viewMeta.isVisible() is viewState
-
- # Document footer sticky
- viewState = nwGUI.docViewer.stickyRef
- nwGUI.docViewer.docFooter._doToggleSticky(not viewState)
- assert nwGUI.docViewer.stickyRef is not viewState
- nwGUI.docViewer.docFooter._doToggleSticky(viewState)
- assert nwGUI.docViewer.stickyRef is viewState
-
# Document footer show/hide synopsis
assert nwGUI.viewDocument("f96ec11c6a3da") is True
assert len(nwGUI.docViewer.toPlainText()) == 4315
@@ -172,19 +157,6 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
assert nwGUI.docViewer.loadText("846352075de7d") is False
assert nwGUI.docViewer.toPlainText() == "An error occurred while generating the preview."
- # Check reference panel (issue #1378)
- assert nwGUI.viewDocument("4c4f28287af27") is True
- assert nwGUI.docViewer.toPlainText().startswith("Nobody Owens")
-
- nwGUI.viewMeta._linkClicked("fb609cd8319dc")
- assert nwGUI.docViewer.toPlainText().startswith("Chapter One")
-
- nwGUI.viewMeta._linkClicked("88243afbe5ed8#T0001")
- assert nwGUI.docViewer.toPlainText().startswith("Scene One")
-
- nwGUI.viewMeta._linkClicked("88243afbe5ed8#ABCD")
- assert nwGUI.docViewer.toPlainText().startswith("Scene One")
-
# qtbot.stop()
# END Test testGuiViewer_Main
From 123d9df170b972895c172f0b6f69de57422f8311 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 16 Nov 2023 19:12:01 +0100
Subject: [PATCH 16/22] Rewrite and improve the index event signal feature
---
novelwriter/core/index.py | 40 +++++++++++++++++++---------
novelwriter/gui/docviewerpanel.py | 44 ++++++++++++++++---------------
novelwriter/guimain.py | 11 ++++----
novelwriter/shared.py | 24 ++++++++++-------
4 files changed, 70 insertions(+), 49 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index dc7ac20a..bf914e13 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -113,6 +113,7 @@ class NWIndex:
self._itemIndex.clear()
self._indexChange = 0.0
self._rootChange = {}
+ SHARED.indexCallBack({"event": "clearIndex"})
return
def rebuildIndex(self) -> None:
@@ -122,16 +123,22 @@ class NWIndex:
if nwItem.isFileType():
tHandle = nwItem.itemHandle
theDoc = self._project.storage.getDocument(tHandle)
- self.scanText(tHandle, theDoc.readDocument() or "")
+ self.scanText(tHandle, theDoc.readDocument() or "", blockSignal=True)
self._indexBroken = False
+ SHARED.indexCallBack({"event": "buildIndex"})
return
def deleteHandle(self, tHandle: str) -> None:
"""Delete all entries of a given document handle."""
logger.debug("Removing item '%s' from the index", tHandle)
- for tTag in self._itemIndex.allItemTags(tHandle):
+ delTags = self._itemIndex.allItemTags(tHandle)
+ for tTag in delTags:
del self._tagsIndex[tTag]
del self._itemIndex[tHandle]
+ SHARED.indexCallBack({
+ "event": "updateTags",
+ "deleted": delTags,
+ })
return
def reIndexHandle(self, tHandle: str | None) -> bool:
@@ -139,14 +146,12 @@ class NWIndex:
moved from the archive or trash folders back into the active
project.
"""
- if tHandle is None or not self._project.tree.checkType(tHandle, nwItemType.FILE):
- return False
-
- logger.debug("Re-indexing item '%s'", tHandle)
- theDoc = self._project.storage.getDocument(tHandle)
- self.scanText(tHandle, theDoc.readDocument() or "")
-
- return True
+ if tHandle and self._project.tree.checkType(tHandle, nwItemType.FILE):
+ logger.debug("Re-indexing item '%s'", tHandle)
+ theDoc = self._project.storage.getDocument(tHandle)
+ self.scanText(tHandle, theDoc.readDocument() or "")
+ return True
+ return False
def indexChangedSince(self, checkTime: int | float) -> bool:
"""Check if the index has changed since a given time."""
@@ -201,6 +206,7 @@ class NWIndex:
self.reIndexHandle(fHandle)
self._indexChange = time()
+ SHARED.indexCallBack({"event": "buildIndex"})
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
@@ -239,7 +245,7 @@ class NWIndex:
# Index Building
##
- def scanText(self, tHandle: str, text: str) -> bool:
+ def scanText(self, tHandle: str, text: str, blockSignal: bool = False) -> bool:
"""Scan a piece of text associated with a handle. This will
update the indices accordingly. This function takes the handle
and text as separate inputs as we want to primarily scan the
@@ -283,7 +289,11 @@ class NWIndex:
nowTime = time()
self._indexChange = nowTime
self._rootChange[tItem.itemRoot] = nowTime
- SHARED.indexDocumentScanned(tHandle)
+ if not blockSignal:
+ SHARED.indexCallBack({
+ "event": "scanText",
+ "handle": tHandle,
+ })
return True
@@ -358,7 +368,11 @@ class NWIndex:
del self._tagsIndex[tTag]
deleted.append(tTag)
if updated or deleted:
- SHARED.indexUpdatedTags(updated, deleted)
+ SHARED.indexCallBack({
+ "event": "updateTags",
+ "updated": updated,
+ "deleted": deleted,
+ })
return
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index eab63a42..9896dee8 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -103,30 +103,27 @@ class GuiDocViewerPanel(QWidget):
return
- def openProjectTasks(self) -> None:
- """Run open project tasks."""
- self.clearClassTabs()
- for key, name, tClass, iItem, hItem in SHARED.project.index.getTagsData():
- if tClass in self.kwTabs and iItem and hItem:
- self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
- self._updateTabVisibility()
- return
+ ##
+ # Public Slots
+ ##
- def closeProjectTasks(self) -> None:
- """Run closing project tasks."""
- self.tabBackRefs.refreshContent(None)
- self.clearClassTabs()
- return
-
- def clearClassTabs(self) -> None:
- """Clear all the class tabs"""
+ @pyqtSlot()
+ def indexWasCleared(self) -> None:
+ """Handle event when the index has been cleared of content."""
+ self.tabBackRefs.clearContent()
for cTab in self.kwTabs.values():
cTab.clearContent()
return
- ##
- # Public Slots
- ##
+ @pyqtSlot()
+ def indexHasAppeared(self) -> None:
+ """Handle event when the index has appeared."""
+ for key, name, tClass, iItem, hItem in SHARED.project.index.getTagsData():
+ if tClass in self.kwTabs and iItem and hItem:
+ self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem)
+ self._updateTabVisibility()
+ self.updateHandle(self._lastHandle)
+ return
@pyqtSlot(str)
def projectItemChanged(self, tHandle: str) -> None:
@@ -218,10 +215,15 @@ class _ViewPanelBackRefs(QTreeWidget):
return
- def refreshContent(self, dHandle: str | None) -> None:
- """Update the content."""
+ def clearContent(self) -> None:
+ """Clear the widget."""
self.clear()
self._treeMap = {}
+ return
+
+ def refreshContent(self, dHandle: str | None) -> None:
+ """Update the content."""
+ self.clearContent()
if dHandle:
refs = SHARED.project.index.getBackReferenceList(dHandle)
for tHandle, (sTitle, hItem) in refs.items():
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 68815c76..099cb422 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -244,9 +244,11 @@ class GuiMain(QMainWindow):
SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage)
SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags)
- SHARED.indexScannedDocument.connect(self.docViewerPanel.projectItemChanged)
- SHARED.indexScannedDocument.connect(self.projView.updateItemValues)
- SHARED.indexScannedDocument.connect(self.itemDetails.updateViewBox)
+ SHARED.indexScannedText.connect(self.docViewerPanel.projectItemChanged)
+ SHARED.indexScannedText.connect(self.projView.updateItemValues)
+ SHARED.indexScannedText.connect(self.itemDetails.updateViewBox)
+ SHARED.indexCleared.connect(self.docViewerPanel.indexWasCleared)
+ SHARED.indexAvailable.connect(self.docViewerPanel.indexHasAppeared)
self.mainMenu.requestDocAction.connect(self._passDocumentAction)
self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
@@ -448,7 +450,6 @@ class GuiMain(QMainWindow):
self.docViewer.clearNavHistory()
self.closeDocViewer(byUser=False)
- self.docViewerPanel.closeProjectTasks()
self.outlineView.closeProjectTasks()
self.novelView.closeProjectTasks()
self.projView.clearProjectView()
@@ -522,7 +523,6 @@ class GuiMain(QMainWindow):
self.projView.openProjectTasks()
self.novelView.openProjectTasks()
self.outlineView.openProjectTasks()
- self.docViewerPanel.openProjectTasks()
self._updateStatusWordCount()
# Restore previously open documents, if any
@@ -816,7 +816,6 @@ class GuiMain(QMainWindow):
tStart = time()
self.projView.saveProjectTasks()
- self.docViewerPanel.clearClassTabs()
SHARED.project.index.rebuildIndex()
self.projView.populateTree()
self.novelView.refreshTree()
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 8980a520..36b0bdaf 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -52,8 +52,10 @@ class SharedData(QObject):
projectStatusChanged = pyqtSignal(bool)
projectStatusMessage = pyqtSignal(str)
spellLanguageChanged = pyqtSignal(str, str)
- indexScannedDocument = pyqtSignal(str)
+ indexScannedText = pyqtSignal(str)
indexChangedTags = pyqtSignal(list, list)
+ indexCleared = pyqtSignal()
+ indexAvailable = pyqtSignal()
def __init__(self) -> None:
super().__init__()
@@ -216,14 +218,18 @@ class SharedData(QObject):
# Call-Back Functions
##
- def indexUpdatedTags(self, updated: list[str], deleted: list[str]) -> None:
- """Emit the index changed tags signal."""
- self.indexChangedTags.emit(updated, deleted)
- return
-
- def indexDocumentScanned(self, tHandle: str) -> None:
- """Emit the index scanned document signal."""
- self.indexScannedDocument.emit(tHandle)
+ def indexCallBack(self, data: dict) -> None:
+ """Emit signals on behalf of the index."""
+ event = data.get("event")
+ logger.debug("Received '%s' event from the index", event)
+ if event == "updateTags":
+ self.indexChangedTags.emit(data.get("updated", []), data.get("deleted", []))
+ elif event == "scanText":
+ self.indexScannedText.emit(data.get("handle", ""))
+ elif event == "clearIndex":
+ self.indexCleared.emit()
+ elif event == "buildIndex":
+ self.indexAvailable.emit()
return
##
From 9d662a0d40e959a0fb5ac28a0e68ddef7db99ff6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 16 Nov 2023 22:31:41 +0100
Subject: [PATCH 17/22] Rename index signal function
---
novelwriter/core/index.py | 12 ++++++------
novelwriter/gui/docviewer.py | 3 ---
novelwriter/shared.py | 4 ++--
3 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index bf914e13..3c833b2c 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -113,7 +113,7 @@ class NWIndex:
self._itemIndex.clear()
self._indexChange = 0.0
self._rootChange = {}
- SHARED.indexCallBack({"event": "clearIndex"})
+ SHARED.indexSignalProxy({"event": "clearIndex"})
return
def rebuildIndex(self) -> None:
@@ -125,7 +125,7 @@ class NWIndex:
theDoc = self._project.storage.getDocument(tHandle)
self.scanText(tHandle, theDoc.readDocument() or "", blockSignal=True)
self._indexBroken = False
- SHARED.indexCallBack({"event": "buildIndex"})
+ SHARED.indexSignalProxy({"event": "buildIndex"})
return
def deleteHandle(self, tHandle: str) -> None:
@@ -135,7 +135,7 @@ class NWIndex:
for tTag in delTags:
del self._tagsIndex[tTag]
del self._itemIndex[tHandle]
- SHARED.indexCallBack({
+ SHARED.indexSignalProxy({
"event": "updateTags",
"deleted": delTags,
})
@@ -206,7 +206,7 @@ class NWIndex:
self.reIndexHandle(fHandle)
self._indexChange = time()
- SHARED.indexCallBack({"event": "buildIndex"})
+ SHARED.indexSignalProxy({"event": "buildIndex"})
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
@@ -290,7 +290,7 @@ class NWIndex:
self._indexChange = nowTime
self._rootChange[tItem.itemRoot] = nowTime
if not blockSignal:
- SHARED.indexCallBack({
+ SHARED.indexSignalProxy({
"event": "scanText",
"handle": tHandle,
})
@@ -368,7 +368,7 @@ class NWIndex:
del self._tagsIndex[tTag]
deleted.append(tTag)
if updated or deleted:
- SHARED.indexCallBack({
+ SHARED.indexSignalProxy({
"event": "updateTags",
"updated": updated,
"deleted": deleted,
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 75c088d6..f7f4aab5 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -243,9 +243,6 @@ class GuiDocViewer(QTextBrowser):
self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins()
- # Make sure the main GUI knows we changed the content
- # self.mainGui.viewMeta.refreshReferences(tHandle)
-
# Since we change the content while it may still be rendering, we mark
# the document dirty again to make sure it's re-rendered properly.
self.redrawText()
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 36b0bdaf..9bad600a 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -215,10 +215,10 @@ class SharedData(QObject):
return
##
- # Call-Back Functions
+ # Signal Proxy
##
- def indexCallBack(self, data: dict) -> None:
+ def indexSignalProxy(self, data: dict) -> None:
"""Emit signals on behalf of the index."""
event = data.get("event")
logger.debug("Received '%s' event from the index", event)
From f81c9aa319d824b6f465b94b1029681b3572576a Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 17 Nov 2023 00:06:29 +0100
Subject: [PATCH 18/22] Clean up document viewer class
---
novelwriter/gui/docviewer.py | 95 +++++++++++++++++-------------------
1 file changed, 46 insertions(+), 49 deletions(-)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index f7f4aab5..9b04c8d3 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -225,14 +225,12 @@ class GuiDocViewer(QTextBrowser):
if updateHistory:
self.docHistory.append(tHandle)
- self.setHtml(aDoc.theResult.replace("\t", "!!tab!!"))
self.setDocumentTitle(tHandle)
- # Loop through the text and put back in the tabs. Tabs are removed by
- # the setHtml function, so the ToHtml class puts in a placeholder.
+ # Replace tabs before setting the HTML, and then put them back in
+ self.setHtml(aDoc.theResult.replace("\t", "!!tab!!"))
while self.find("!!tab!!"):
- theCursor = self.textCursor()
- theCursor.insertText("\t")
+ self.textCursor().insertText("\t")
if self._docHandle == tHandle:
# This is a refresh, so we set the scrollbar back to where it was
@@ -263,22 +261,22 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins()
return
- def docAction(self, theAction: nwDocAction) -> bool:
+ def docAction(self, action: nwDocAction) -> bool:
"""Process document actions on the current document."""
- logger.debug("Requesting action: '%s'", theAction.name)
+ logger.debug("Requesting action: '%s'", action.name)
if self._docHandle is None:
logger.error("No document open")
return False
- if theAction == nwDocAction.CUT:
+ if action == nwDocAction.CUT:
self.copy()
- elif theAction == nwDocAction.COPY:
+ elif action == nwDocAction.COPY:
self.copy()
- elif theAction == nwDocAction.SEL_ALL:
+ elif action == nwDocAction.SEL_ALL:
self._makeSelection(QTextCursor.Document)
- elif theAction == nwDocAction.SEL_PARA:
+ elif action == nwDocAction.SEL_PARA:
self._makeSelection(QTextCursor.BlockUnderCursor)
else:
- logger.debug("Unknown or unsupported document action '%s'", str(theAction))
+ logger.debug("Unknown or unsupported document action '%s'", str(action))
return False
return True
@@ -366,11 +364,11 @@ class GuiDocViewer(QTextBrowser):
@pyqtSlot("QUrl")
def _linkClicked(self, url: QUrl) -> None:
- """Process a clicked link internally in the document."""
- theLink = url.url()
- logger.debug("Clicked link: '%s'", theLink)
- if len(theLink) > 0:
- theBits = theLink.split("=")
+ """Process a clicked link in the document."""
+ link = url.url()
+ logger.debug("Clicked link: '%s'", link)
+ if len(link) > 0:
+ theBits = link.split("=")
if len(theBits) == 2:
self.loadDocumentTagRequest.emit(theBits[1], nwDocMode.VIEW)
return
@@ -442,29 +440,28 @@ class GuiDocViewer(QTextBrowser):
##
def _makeSelection(self, selType: QTextCursor.SelectionType) -> None:
- """Handle select of text based on a selection mode."""
- theCursor = self.textCursor()
- theCursor.clearSelection()
- theCursor.select(selType)
+ """Handle selection of text based on a selection mode."""
+ cursor = self.textCursor()
+ cursor.clearSelection()
+ cursor.select(selType)
if selType == QTextCursor.BlockUnderCursor:
# This selection mode also selects the preceding paragraph
# separator, which we want to avoid.
- posS = theCursor.selectionStart()
- posE = theCursor.selectionEnd()
- selTxt = theCursor.selectedText()
+ posS = cursor.selectionStart()
+ posE = cursor.selectionEnd()
+ selTxt = cursor.selectedText()
if selTxt.startswith(nwUnicode.U_PSEP):
- theCursor.setPosition(posS+1, QTextCursor.MoveAnchor)
- theCursor.setPosition(posE, QTextCursor.KeepAnchor)
+ cursor.setPosition(posS+1, QTextCursor.MoveAnchor)
+ cursor.setPosition(posE, QTextCursor.KeepAnchor)
- self.setTextCursor(theCursor)
+ self.setTextCursor(cursor)
return
def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None:
"""Handle text selection at a given location."""
- theCursor = self.cursorForPosition(pos)
- self.setTextCursor(theCursor)
+ self.setTextCursor(self.cursorForPosition(pos))
self._makeSelection(selType)
return
@@ -668,18 +665,18 @@ class GuiDocViewHeader(QWidget):
self.setAutoFillBackground(True)
# Title Label
- self.theTitle = QLabel()
- self.theTitle.setText("")
- self.theTitle.setIndent(0)
- self.theTitle.setMargin(0)
- self.theTitle.setContentsMargins(0, 0, 0, 0)
- self.theTitle.setAutoFillBackground(True)
- self.theTitle.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
- self.theTitle.setFixedHeight(fPx)
+ self.docTitle = QLabel()
+ self.docTitle.setText("")
+ self.docTitle.setIndent(0)
+ self.docTitle.setMargin(0)
+ self.docTitle.setContentsMargins(0, 0, 0, 0)
+ self.docTitle.setAutoFillBackground(True)
+ self.docTitle.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
+ self.docTitle.setFixedHeight(fPx)
- lblFont = self.theTitle.font()
+ lblFont = self.docTitle.font()
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
- self.theTitle.setFont(lblFont)
+ self.docTitle.setFont(lblFont)
# Buttons
self.backButton = QToolButton(self)
@@ -723,7 +720,7 @@ class GuiDocViewHeader(QWidget):
self.outerBox.setSpacing(hSp)
self.outerBox.addWidget(self.backButton, 0)
self.outerBox.addWidget(self.forwardButton, 0)
- self.outerBox.addWidget(self.theTitle, 1)
+ self.outerBox.addWidget(self.docTitle, 1)
self.outerBox.addWidget(self.refreshButton, 0)
self.outerBox.addWidget(self.closeButton, 0)
self.setLayout(self.outerBox)
@@ -771,12 +768,12 @@ class GuiDocViewHeader(QWidget):
"""Update the colours of the widget to match those of the syntax
theme rather than the main GUI.
"""
- thePalette = QPalette()
- thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
- thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
- thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
- self.setPalette(thePalette)
- self.theTitle.setPalette(thePalette)
+ palette = QPalette()
+ palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
+ palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
+ palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
+ self.setPalette(palette)
+ self.docTitle.setPalette(palette)
return
def setTitleFromHandle(self, tHandle: str | None) -> bool:
@@ -785,7 +782,7 @@ class GuiDocViewHeader(QWidget):
"""
self._docHandle = tHandle
if tHandle is None:
- self.theTitle.setText("")
+ self.docTitle.setText("")
self.backButton.setVisible(False)
self.forwardButton.setVisible(False)
self.closeButton.setVisible(False)
@@ -801,12 +798,12 @@ class GuiDocViewHeader(QWidget):
if nwItem is not None:
tTitle.append(nwItem.itemName)
sSep = " %s " % nwUnicode.U_RSAQUO
- self.theTitle.setText(sSep.join(tTitle))
+ self.docTitle.setText(sSep.join(tTitle))
else:
nwItem = pTree[tHandle]
if nwItem is None:
return False
- self.theTitle.setText(nwItem.itemName)
+ self.docTitle.setText(nwItem.itemName)
self.backButton.setVisible(True)
self.forwardButton.setVisible(True)
From 1098354fca375b14e26c72629d119df4925ac31e Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 17 Nov 2023 07:55:43 +0100
Subject: [PATCH 19/22] Fix broken test
---
tests/test_gui/test_gui_docviewer.py | 106 ++++++++++++++-------------
1 file changed, 54 insertions(+), 52 deletions(-)
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index aea91084..bda73915 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -30,6 +30,7 @@ from PyQt5.QtWidgets import qApp, QAction
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction
from novelwriter.core.tohtml import ToHtml
+from novelwriter.gui.docviewer import GuiDocViewer
@pytest.mark.gui
@@ -37,6 +38,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
"""Test the document viewer."""
# Open project
assert nwGUI.openProject(prjLipsum)
+ docViewer: GuiDocViewer = nwGUI.docViewer
# Rebuild the index
nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger)
@@ -47,40 +49,40 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
nwGUI.projView.setSelectedHandle("88243afbe5ed8")
# Middle-click the selected item
- theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8")
- theRect = nwGUI.projView.projTree.visualItemRect(theItem)
- qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center())
- assert nwGUI.docViewer.docHandle == "88243afbe5ed8"
+ item = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8")
+ rect = nwGUI.projView.projTree.visualItemRect(item)
+ qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=rect.center())
+ assert docViewer.docHandle == "88243afbe5ed8"
# Reload the text
- origText = nwGUI.docViewer.toPlainText()
- nwGUI.docViewer.setPlainText("Oops, all gone!")
- nwGUI.docViewer.docHeader._refreshDocument()
- assert nwGUI.docViewer.toPlainText() == origText
+ origText = docViewer.toPlainText()
+ docViewer.setPlainText("Oops, all gone!")
+ docViewer.docHeader._refreshDocument()
+ assert docViewer.toPlainText() == origText
# Select word
- theCursor = nwGUI.docViewer.textCursor()
- theCursor.setPosition(100)
- nwGUI.docViewer.setTextCursor(theCursor)
- nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor)
+ cursor = docViewer.textCursor()
+ cursor.setPosition(100)
+ docViewer.setTextCursor(cursor)
+ docViewer._makeSelection(QTextCursor.WordUnderCursor)
qClip = qApp.clipboard()
qClip.clear()
# Cut
- assert nwGUI.docViewer.docAction(nwDocAction.CUT) is True
+ assert docViewer.docAction(nwDocAction.CUT) is True
assert qClip.text() == "laoreet"
qClip.clear()
# Copy
- assert nwGUI.docViewer.docAction(nwDocAction.COPY) is True
+ assert docViewer.docAction(nwDocAction.COPY) is True
assert qClip.text() == "laoreet"
qClip.clear()
# Select Paragraph
- assert nwGUI.docViewer.docAction(nwDocAction.SEL_PARA) is True
- theCursor = nwGUI.docViewer.textCursor()
- assert theCursor.selectedText() == (
+ assert docViewer.docAction(nwDocAction.SEL_PARA) is True
+ cursor = docViewer.textCursor()
+ assert cursor.selectedText() == (
"Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, "
"eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et "
"mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. "
@@ -88,74 +90,74 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
)
# Select All
- assert nwGUI.docViewer.docAction(nwDocAction.SEL_ALL) is True
- theCursor = nwGUI.docViewer.textCursor()
- assert len(theCursor.selectedText()) == 3061
+ assert docViewer.docAction(nwDocAction.SEL_ALL) is True
+ cursor = docViewer.textCursor()
+ assert len(cursor.selectedText()) == 3061
# Other actions
- assert nwGUI.docViewer.docAction(nwDocAction.NO_ACTION) is False
+ assert docViewer.docAction(nwDocAction.NO_ACTION) is False
# Close document
- nwGUI.docViewer.docHeader._closeDocument()
- assert nwGUI.docViewer.docHandle is None
+ docViewer.docHeader._closeDocument()
+ assert docViewer.docHandle is None
# Action on no document
- assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False
+ assert docViewer.docAction(nwDocAction.COPY) is False
# Open again via menu
assert nwGUI.projView.setSelectedHandle("88243afbe5ed8")
nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger)
# Select "Bod" link
- theCursor = nwGUI.docViewer.textCursor()
- theCursor.setPosition(27)
- nwGUI.docViewer.setTextCursor(theCursor)
- nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor)
- theRect = nwGUI.docViewer.cursorRect()
- nwGUI.docViewer._linkClicked(QUrl("#char=Bod"))
- assert nwGUI.docViewer.docHandle == "4c4f28287af27"
+ cursor = docViewer.textCursor()
+ cursor.setPosition(27)
+ docViewer.setTextCursor(cursor)
+ docViewer._makeSelection(QTextCursor.WordUnderCursor)
+ rect = docViewer.cursorRect()
+ docViewer._linkClicked(QUrl("#char=Bod"))
+ assert docViewer.docHandle == "4c4f28287af27"
# Click mouse nav buttons
- qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.BackButton, pos=theRect.center(), delay=100)
- assert nwGUI.docViewer.docHandle == "88243afbe5ed8"
- qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.ForwardButton, pos=theRect.center(), delay=100)
- assert nwGUI.docViewer.docHandle == "4c4f28287af27"
+ qtbot.mouseClick(docViewer.viewport(), Qt.BackButton, pos=rect.center(), delay=100)
+ assert docViewer.docHandle == "88243afbe5ed8"
+ qtbot.mouseClick(docViewer.viewport(), Qt.ForwardButton, pos=rect.center(), delay=100)
+ assert docViewer.docHandle == "4c4f28287af27"
# Scroll bar default on empty document
- nwGUI.docViewer.clear()
- assert nwGUI.docViewer.scrollPosition == 0
- nwGUI.docViewer.reloadText()
+ docViewer.clear()
+ assert docViewer.scrollPosition == 0
+ docViewer.reloadText()
# Change document title
nwItem = SHARED.project.tree["4c4f28287af27"]
- nwItem.setName("Test Title")
- assert nwItem.itemName == "Test Title"
- nwGUI.docViewer.updateDocInfo("4c4f28287af27")
- assert nwGUI.docViewer.docHeader.theTitle.text() == "Characters › Test Title"
+ nwItem.setName("Test Title") # type: ignore
+ assert nwItem.itemName == "Test Title" # type: ignore
+ docViewer.updateDocInfo("4c4f28287af27")
+ assert docViewer.docHeader.docTitle.text() == "Characters \u203a Test Title"
# Title without full path
CONFIG.showFullPath = False
- nwGUI.docViewer.updateDocInfo("4c4f28287af27")
- assert nwGUI.docViewer.docHeader.theTitle.text() == "Test Title"
+ docViewer.updateDocInfo("4c4f28287af27")
+ assert docViewer.docHeader.docTitle.text() == "Test Title"
CONFIG.showFullPath = True
# Document footer show/hide synopsis
assert nwGUI.viewDocument("f96ec11c6a3da") is True
- assert len(nwGUI.docViewer.toPlainText()) == 4315
- nwGUI.docViewer.docFooter._doToggleSynopsis(False)
- assert len(nwGUI.docViewer.toPlainText()) == 4099
+ assert len(docViewer.toPlainText()) == 4315
+ docViewer.docFooter._doToggleSynopsis(False)
+ assert len(docViewer.toPlainText()) == 4099
# Document footer show/hide comments
assert nwGUI.viewDocument("846352075de7d") is True
- assert len(nwGUI.docViewer.toPlainText()) == 675
- nwGUI.docViewer.docFooter._doToggleComments(False)
- assert len(nwGUI.docViewer.toPlainText()) == 635
+ assert len(docViewer.toPlainText()) == 675
+ docViewer.docFooter._doToggleComments(False)
+ assert len(docViewer.toPlainText()) == 635
# Crash the HTML rendering
with monkeypatch.context() as mp:
mp.setattr(ToHtml, "doConvert", causeException)
- assert nwGUI.docViewer.loadText("846352075de7d") is False
- assert nwGUI.docViewer.toPlainText() == "An error occurred while generating the preview."
+ assert docViewer.loadText("846352075de7d") is False
+ assert docViewer.toPlainText() == "An error occurred while generating the preview."
# qtbot.stop()
From dd05f6532afc85a8f43af44c4f86cbbf7f832ee1 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 17 Nov 2023 16:22:59 +0100
Subject: [PATCH 20/22] Update document viewer tests
---
novelwriter/gui/docviewer.py | 28 ++++-------
tests/test_core/test_core_projectxml.py | 2 +-
tests/test_gui/test_gui_docviewer.py | 66 ++++++++++++++++++++++---
3 files changed, 69 insertions(+), 27 deletions(-)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 9b04c8d3..8992505d 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -280,14 +280,12 @@ class GuiDocViewer(QTextBrowser):
return False
return True
- def navigateTo(self, tAnchor: str) -> bool:
+ def navigateTo(self, tAnchor: str) -> None:
"""Go to a specific #link in the document."""
- if not isinstance(tAnchor, str):
- return False
- if tAnchor.startswith("#"):
+ if isinstance(tAnchor, str) and tAnchor.startswith("#"):
logger.debug("Moving to anchor '%s'", tAnchor)
self.setSource(QUrl(tAnchor))
- return True
+ return
def clearNavHistory(self) -> None:
"""Clear the navigation history."""
@@ -381,9 +379,6 @@ class GuiDocViewer(QTextBrowser):
mnuContext = QMenu(self)
- # Cut, Copy and Paste
- # ===================
-
if userSelection:
mnuCopy = QAction(self.tr("Copy"), mnuContext)
mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
@@ -391,9 +386,6 @@ class GuiDocViewer(QTextBrowser):
mnuContext.addSeparator()
- # Selections
- # ==========
-
mnuSelAll = QAction(self.tr("Select All"), mnuContext)
mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
mnuContext.addAction(mnuSelAll)
@@ -628,7 +620,7 @@ class GuiDocViewHistory:
"""Debug function to dump history to the logger. Since it is a
for loop, it is skipped entirely if log level isn't DEBUG.
"""
- if logger.getEffectiveLevel() == logging.DEBUG:
+ if logger.getEffectiveLevel() == logging.DEBUG: # pragma: no cover
for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)):
logger.debug(
"History %02d: %s %13s [x:%d]" % (
@@ -776,7 +768,7 @@ class GuiDocViewHeader(QWidget):
self.docTitle.setPalette(palette)
return
- def setTitleFromHandle(self, tHandle: str | None) -> bool:
+ def setTitleFromHandle(self, tHandle: str | None) -> None:
"""Sets the document title from the handle, or alternatively,
set the whole document path.
"""
@@ -787,7 +779,7 @@ class GuiDocViewHeader(QWidget):
self.forwardButton.setVisible(False)
self.closeButton.setVisible(False)
self.refreshButton.setVisible(False)
- return True
+ return
pTree = SHARED.project.tree
if CONFIG.showFullPath:
@@ -800,17 +792,15 @@ class GuiDocViewHeader(QWidget):
sSep = " %s " % nwUnicode.U_RSAQUO
self.docTitle.setText(sSep.join(tTitle))
else:
- nwItem = pTree[tHandle]
- if nwItem is None:
- return False
- self.docTitle.setText(nwItem.itemName)
+ if nwItem := pTree[tHandle]:
+ self.docTitle.setText(nwItem.itemName)
self.backButton.setVisible(True)
self.forwardButton.setVisible(True)
self.closeButton.setVisible(True)
self.refreshButton.setVisible(True)
- return True
+ return
def updateNavButtons(self, firstIdx: int, lastIdx: int, currIdx: int) -> None:
"""Enable and disable nav buttons based on index in history."""
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index 3448e653..a6c6e5e1 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -84,7 +84,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
assert xmlReader.read(data, content) is False
assert xmlReader.state == XMLReadState.NOT_NWX_FILE
- # Add a valid novelwriter file without a file version
+ # Add a valid novelWriter file without a file version
writeFile(xmlFile, "")
assert xmlReader.read(data, content) is False
assert xmlReader.state == XMLReadState.UNKNOWN_VERSION
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index bda73915..b05e35b1 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -23,9 +23,9 @@ import pytest
from mocked import causeException
-from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QTextCursor
-from PyQt5.QtWidgets import qApp, QAction
+from PyQt5.QtCore import Qt, QUrl
+from PyQt5.QtWidgets import QMenu, qApp, QAction
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction
@@ -40,13 +40,12 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
assert nwGUI.openProject(prjLipsum)
docViewer: GuiDocViewer = nwGUI.docViewer
- # Rebuild the index
- nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger)
- assert SHARED.project.index._tagsIndex._tags != {}
- assert SHARED.project.index._itemIndex._items != {}
-
# Select a document in the project tree
nwGUI.projView.setSelectedHandle("88243afbe5ed8")
+ nwGUI.openDocument("88243afbe5ed8")
+
+ # Can only open a document
+ assert docViewer.loadText("b3643d0f92e32") is False
# Middle-click the selected item
item = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8")
@@ -54,6 +53,14 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=rect.center())
assert docViewer.docHandle == "88243afbe5ed8"
+ # Clear selection
+ nwGUI.projView.projTree.clearSelection()
+ assert nwGUI.projView.projTree.getSelectedHandle() is None
+
+ # Re-select via header click
+ docViewer.docHeader.mousePressEvent(None) # type: ignore
+ assert nwGUI.projView.projTree.getSelectedHandle() == "88243afbe5ed8"
+
# Reload the text
origText = docViewer.toPlainText()
docViewer.setPlainText("Oops, all gone!")
@@ -88,6 +95,21 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
"mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. "
"Etiam finibus nisi vel mi molestie consectetur."
)
+ cursor.clearSelection()
+ docViewer.setTextCursor(cursor)
+
+ docViewer._makePosSelection(
+ QTextCursor.SelectionType.BlockUnderCursor, docViewer.cursorRect().center()
+ )
+ cursor = docViewer.textCursor()
+ assert cursor.selectedText() == (
+ "Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, "
+ "eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et "
+ "mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. "
+ "Etiam finibus nisi vel mi molestie consectetur."
+ )
+ cursor.clearSelection()
+ docViewer.setTextCursor(cursor)
# Select All
assert docViewer.docAction(nwDocAction.SEL_ALL) is True
@@ -108,6 +130,22 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
assert nwGUI.projView.setSelectedHandle("88243afbe5ed8")
nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger)
+ # Open context menu
+ menuOpened = False
+
+ def mockExec(*a):
+ nonlocal menuOpened
+ menuOpened = True
+
+ cursor = docViewer.textCursor()
+ cursor.setPosition(27)
+ docViewer.setTextCursor(cursor)
+ docViewer._makeSelection(QTextCursor.WordUnderCursor)
+ with monkeypatch.context() as mp:
+ mp.setattr(QMenu, "exec_", mockExec)
+ docViewer._openContextMenu(docViewer.cursorRect().center())
+ assert menuOpened
+
# Select "Bod" link
cursor = docViewer.textCursor()
cursor.setPosition(27)
@@ -122,12 +160,22 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
assert docViewer.docHandle == "88243afbe5ed8"
qtbot.mouseClick(docViewer.viewport(), Qt.ForwardButton, pos=rect.center(), delay=100)
assert docViewer.docHandle == "4c4f28287af27"
+ qtbot.mouseClick(docViewer.viewport(), Qt.LeftButton, pos=rect.center(), delay=100)
+ assert docViewer.docHandle == "4c4f28287af27"
# Scroll bar default on empty document
docViewer.clear()
assert docViewer.scrollPosition == 0
docViewer.reloadText()
+ # Flip some settings
+ CONFIG.doJustify = True
+ CONFIG.hideVScroll = True
+ CONFIG.hideHScroll = True
+ docViewer.initViewer()
+ assert docViewer.verticalScrollBar().isVisible() is False
+ assert docViewer.horizontalScrollBar().isVisible() is False
+
# Change document title
nwItem = SHARED.project.tree["4c4f28287af27"]
nwItem.setName("Test Title") # type: ignore
@@ -159,6 +207,10 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
assert docViewer.loadText("846352075de7d") is False
assert docViewer.toPlainText() == "An error occurred while generating the preview."
+ # Call the update theme function
+ # This only checks that t doesn't fail, functionality tested elsewhere
+ docViewer.updateTheme()
+
# qtbot.stop()
# END Test testGuiViewer_Main
From d4560b3be876d1cb7b852f648ceb74c91e156b1c Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 17 Nov 2023 23:01:59 +0100
Subject: [PATCH 21/22] Add test coverage of document viewer panel
---
novelwriter/gui/docviewerpanel.py | 42 +++--
tests/test_gui/test_gui_docviewerpanel.py | 193 ++++++++++++++++++++++
tests/test_gui/test_gui_noveltree.py | 3 +-
3 files changed, 214 insertions(+), 24 deletions(-)
create mode 100644 tests/test_gui/test_gui_docviewerpanel.py
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 9896dee8..f5078df9 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -258,30 +258,28 @@ class _ViewPanelBackRefs(QTreeWidget):
def _setTreeItemValues(self, tHandle: str, sTitle: str, hItem: IndexHeading) -> None:
"""Add or update a tree item."""
- if (nwItem := SHARED.project.tree[tHandle]) is None:
- return
+ if nwItem := SHARED.project.tree[tHandle]:
+ docIcon = SHARED.theme.getItemIcon(
+ nwItem.itemType, nwItem.itemClass,
+ nwItem.itemLayout, nwItem.mainHeading
+ )
+ iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
+ hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
- docIcon = SHARED.theme.getItemIcon(
- nwItem.itemType, nwItem.itemClass,
- nwItem.itemLayout, nwItem.mainHeading
- )
- iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
- hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
+ tKey = f"{tHandle}:{sTitle}"
+ trItem = self._treeMap[tKey] if tKey in self._treeMap else QTreeWidgetItem()
- tKey = f"{tHandle}:{sTitle}"
- trItem = self._treeMap[tKey] if tKey in self._treeMap else QTreeWidgetItem()
+ trItem.setIcon(self.C_DOC, docIcon)
+ trItem.setText(self.C_DOC, nwItem.itemName)
+ trItem.setIcon(self.C_EDIT, self._editIcon)
+ trItem.setIcon(self.C_VIEW, self._viewIcon)
+ trItem.setText(self.C_TITLE, hItem.title)
+ trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
+ trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
- trItem.setIcon(self.C_DOC, docIcon)
- trItem.setText(self.C_DOC, nwItem.itemName)
- trItem.setIcon(self.C_EDIT, self._editIcon)
- trItem.setIcon(self.C_VIEW, self._viewIcon)
- trItem.setText(self.C_TITLE, hItem.title)
- trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
- trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
-
- if tKey not in self._treeMap:
- self.addTopLevelItem(trItem)
- self._treeMap[tKey] = trItem
+ if tKey not in self._treeMap:
+ self.addTopLevelItem(trItem)
+ self._treeMap[tKey] = trItem
return
@@ -398,4 +396,4 @@ class _ViewPanelKeyWords(QTreeWidget):
self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
return
-# END Class _ViewPanelRefs
+# END Class _ViewPanelKeyWords
diff --git a/tests/test_gui/test_gui_docviewerpanel.py b/tests/test_gui/test_gui_docviewerpanel.py
new file mode 100644
index 00000000..e9f96ada
--- /dev/null
+++ b/tests/test_gui/test_gui_docviewerpanel.py
@@ -0,0 +1,193 @@
+"""
+novelWriter – Main GUI Viewer Panel Class Tester
+================================================
+
+This file is a part of novelWriter
+Copyright 2018–2023, 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 pytest
+
+from tools import C, buildTestProject
+
+from novelwriter import SHARED
+from novelwriter.constants import nwLists
+from novelwriter.dialogs.editlabel import GuiEditLabel
+
+
+@pytest.mark.gui
+def testGuiViewerPanel_BackRefs(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
+ """Test the document viewer backreference panel."""
+ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
+
+ buildTestProject(nwGUI, projPath)
+ projTree = nwGUI.projView.projTree
+ projTree._getTreeItem(C.hChapterDir).setExpanded(True)
+ viewPanel = nwGUI.docViewerPanel
+ tabBackRefs = viewPanel.tabBackRefs
+
+ nwGUI.openDocument(C.hSceneDoc)
+ nwGUI.viewDocument(C.hSceneDoc)
+
+ # Hide/Show
+ nwGUI._toggleViewerPanelVisibility()
+ assert viewPanel.isVisible() is False
+ nwGUI._toggleViewerPanelVisibility()
+ assert viewPanel.isVisible() is True
+
+ # Initial State
+ assert tabBackRefs.topLevelItemCount() == 0
+
+ # Add Two Tags
+ hJane = "0000000000010"
+ nwGUI.docEditor.setPlainText("### New Scene\n\n@char: Jane, John\n\n")
+ nwGUI.saveDocument()
+ cursor = nwGUI.docEditor.textCursor()
+ cursor.setPosition(22)
+ nwGUI.docEditor._processTag(cursor, create=True)
+ cursor.setPosition(28)
+ nwGUI.docEditor._processTag(cursor, create=True)
+ nwGUI.viewDocument(hJane)
+ assert tabBackRefs.topLevelItemCount() == 1
+
+ # Check Backreference
+ item = tabBackRefs.topLevelItem(0)
+ assert item.text(tabBackRefs.C_DOC) == "New Scene"
+ assert item.text(tabBackRefs.C_TITLE) == "New Scene"
+
+ # Update Title
+ nwGUI.docEditor.setPlainText("### Scene One\n\n@char: Jane, John\n\n")
+ nwGUI.saveDocument()
+ item = tabBackRefs.topLevelItem(0)
+ assert item.text(tabBackRefs.C_DOC) == "New Scene"
+ assert item.text(tabBackRefs.C_TITLE) == "Scene One"
+
+ # Update Label
+ SHARED.project.tree[C.hSceneDoc].setName("First Scene") # type: ignore
+ projTree.renameTreeItem(C.hSceneDoc)
+ item = tabBackRefs.topLevelItem(0)
+ assert item.text(tabBackRefs.C_DOC) == "First Scene"
+ assert item.text(tabBackRefs.C_TITLE) == "Scene One"
+
+ # Clear Index
+ SHARED.project.index.clearIndex()
+ assert tabBackRefs.topLevelItemCount() == 0
+
+ # Rebuild Index
+ SHARED.project.index.rebuildIndex()
+ assert tabBackRefs.topLevelItemCount() == 1
+
+ # Click the Edit Button
+ nwGUI.openDocument(C.hChapterDoc)
+ assert nwGUI.docEditor.docHandle == C.hChapterDoc
+ tabBackRefs._treeItemClicked(tabBackRefs.model().index(0, tabBackRefs.C_EDIT))
+ assert nwGUI.docEditor.docHandle == C.hSceneDoc
+
+ # Click the View Button
+ assert nwGUI.docViewer.docHandle == hJane
+ tabBackRefs._treeItemClicked(tabBackRefs.model().index(0, tabBackRefs.C_VIEW))
+ assert nwGUI.docViewer.docHandle == C.hSceneDoc
+
+ # qtbot.stop()
+
+# END Test testGuiViewerPanel_BackRefs
+
+
+@pytest.mark.gui
+def testGuiViewerPanel_Tags(qtbot, monkeypatch, caplog, nwGUI, projPath, mockRnd):
+ """Test the document viewer tags panels."""
+ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
+
+ buildTestProject(nwGUI, projPath)
+ projTree = nwGUI.projView.projTree
+ projTree._getTreeItem(C.hChapterDir).setExpanded(True)
+ viewPanel = nwGUI.docViewerPanel
+
+ nwGUI.openDocument(C.hSceneDoc)
+ nwGUI.viewDocument(C.hSceneDoc)
+
+ assert len(viewPanel.kwTabs) == len(nwLists.USER_CLASSES)
+ assert len(viewPanel.idTabs) == len(nwLists.USER_CLASSES)
+
+ # Add Two Tags
+ hJane = "0000000000010"
+ hJohn = "0000000000011"
+ nwGUI.docEditor.setPlainText("### New Scene\n\n@char: Jane, John\n\n")
+ nwGUI.saveDocument()
+ cursor = nwGUI.docEditor.textCursor()
+ cursor.setPosition(22)
+ nwGUI.docEditor._processTag(cursor, create=True)
+ cursor.setPosition(28)
+ nwGUI.docEditor._processTag(cursor, create=True)
+
+ # Check Panel Tab Visibility
+ assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["CHARACTER"]) is True
+ assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["PLOT"]) is False
+ assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["WORLD"]) is False
+ assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["TIMELINE"]) is False
+ assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["OBJECT"]) is False
+ assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["ENTITY"]) is False
+ assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["CUSTOM"]) is False
+
+ # Check Character Tab
+ charTab = viewPanel.kwTabs["CHARACTER"]
+ viewPanel.mainTabs.setCurrentIndex(viewPanel.idTabs["CHARACTER"])
+ assert charTab.topLevelItemCount() == 2
+ item = charTab.topLevelItem(0)
+ assert item.text(charTab.C_NAME) == "Jane"
+ assert item.text(charTab.C_DOC) == "Jane"
+ assert item.text(charTab.C_TITLE) == "Jane"
+ item = charTab.topLevelItem(1)
+ assert item.text(charTab.C_NAME) == "John"
+ assert item.text(charTab.C_DOC) == "John"
+ assert item.text(charTab.C_TITLE) == "John"
+
+ # Edit Jane
+ nwGUI.openDocument(hJane)
+ nwGUI.docEditor.setPlainText("# Jane Smith\n\n@tag: Janey\n\n")
+ nwGUI.saveDocument()
+ SHARED.project.tree[hJane].setName("Awesome Jane") # type: ignore
+ projTree.renameTreeItem(hJane)
+ item = charTab.topLevelItem(0)
+ assert item.text(charTab.C_NAME) == "Janey"
+ assert item.text(charTab.C_DOC) == "Awesome Jane"
+ assert item.text(charTab.C_TITLE) == "Jane Smith"
+
+ # Clear Index
+ SHARED.project.index.clearIndex()
+ assert charTab.topLevelItemCount() == 0
+
+ # Rebuild Index
+ SHARED.project.index.rebuildIndex()
+ assert charTab.topLevelItemCount() == 2
+
+ # Remove Non-Existing Tag
+ caplog.clear()
+ viewPanel.updateChangedTags(["foo"], ["bar"])
+ assert charTab.topLevelItemCount() == 2
+ assert "Could not remove tag" in caplog.text
+
+ # View/Edit John
+ assert nwGUI.docEditor.docHandle == hJane
+ charTab._treeItemClicked(charTab.model().index(1, charTab.C_EDIT))
+ assert nwGUI.docEditor.docHandle == hJohn
+ assert nwGUI.docViewer.docHandle == C.hSceneDoc
+ charTab._treeItemClicked(charTab.model().index(1, charTab.C_VIEW))
+ assert nwGUI.docViewer.docHandle == hJohn
+
+ # qtbot.stop()
+
+# END Test testGuiViewerPanel_Tags
diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py
index 4ada522b..e01c0dd9 100644
--- a/tests/test_gui/test_gui_noveltree.py
+++ b/tests/test_gui/test_gui_noveltree.py
@@ -37,8 +37,7 @@ from novelwriter.dialogs.editlabel import GuiEditLabel
@pytest.mark.gui
def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
- """Test navigating the novel tree.
- """
+ """Test navigating the novel tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
buildTestProject(nwGUI, projPath)
From 69ccb1560c0df2ad7d8c1b159a1701ca18842ca1 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 17 Nov 2023 23:20:16 +0100
Subject: [PATCH 22/22] Update index test
---
tests/test_core/test_core_index.py | 31 +++++++++++++++++++++++++++++-
1 file changed, 30 insertions(+), 1 deletion(-)
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index ba68ea18..6354b39c 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -231,6 +231,7 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
assert isinstance(cItem, NWItem)
assert index.rootChangedSince(C.hNovelRoot, 0) is False
+ assert index.rootChangedSince(None, 0) is False
assert index.indexChangedSince(0) is False
assert index.scanText(cHandle, (
@@ -610,6 +611,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
assert isinstance(item, IndexItem)
assert item.headings() == ["T0001"]
assert index.getHandleHeaderCount(nHandle) == 1
+ assert index.getHandleHeaderCount("foo") == 0
# getReferences
# =============
@@ -643,6 +645,32 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
assert index.getTagSource("Jane") == (cHandle, "T0001")
assert index.getTagSource("John") == (None, "T0000")
+ # getDocumentTags
+ # ===============
+ assert index.getDocumentTags(cHandle) == ["jane"]
+ assert index.getDocumentTags(None) == []
+
+ # getClassTags
+ # ============
+ assert index.getClassTags(nwItemClass.CHARACTER) == ["Jane"]
+
+ # getTagsData
+ # ===========
+ assert list(index.getTagsData()) == [(
+ "jane", "Jane", "CHARACTER",
+ index.getItemData(cHandle),
+ index.getItemHeader(cHandle, "T0001")
+ )]
+
+ # getSingleTag
+ # ============
+ assert index.getSingleTag("jane") == (
+ "Jane", "CHARACTER",
+ index.getItemData(cHandle),
+ index.getItemHeader(cHandle, "T0001")
+ )
+ assert index.getSingleTag("foobar") == ("", "", None, None)
+
# getCounts
# =========
# For whole text and sections
@@ -1176,9 +1204,10 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
itemIndex.unpackData({C.hInvalid: {}})
assert itemIndex._items == {}
- # Known keys can be added, even witout data
+ # Known keys can be added, even without data
itemIndex.unpackData({nHandle: {}})
assert nHandle in itemIndex
+ assert itemIndex[nHandle].handle == nHandle # type: ignore
# Title tags must be valid
with pytest.raises(ValueError):