diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf
index 94b4c633..999ce000 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
@@ -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/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_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 62314511..3c6f545c 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
@@ -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/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/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/config.py b/novelwriter/config.py
index 5f7a3487..ba422efa 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -179,7 +179,7 @@ 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 shortcodes for basic formatting
self.viewComments = True # Comments are shown in the viewer
@@ -600,7 +600,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)
@@ -707,7 +707,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/core/index.py b/novelwriter/core/index.py
index e4210dd2..3c833b2c 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -35,6 +35,7 @@ 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.error import logException
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
@@ -112,6 +113,7 @@ class NWIndex:
self._itemIndex.clear()
self._indexChange = 0.0
self._rootChange = {}
+ SHARED.indexSignalProxy({"event": "clearIndex"})
return
def rebuildIndex(self) -> None:
@@ -121,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.indexSignalProxy({"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.indexSignalProxy({
+ "event": "updateTags",
+ "deleted": delTags,
+ })
return
def reIndexHandle(self, tHandle: str | None) -> bool:
@@ -138,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."""
@@ -200,6 +206,7 @@ class NWIndex:
self.reIndexHandle(fHandle)
self._indexChange = time()
+ SHARED.indexSignalProxy({"event": "buildIndex"})
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
@@ -238,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
@@ -282,6 +289,11 @@ class NWIndex:
nowTime = time()
self._indexChange = nowTime
self._rootChange[tItem.itemRoot] = nowTime
+ if not blockSignal:
+ SHARED.indexSignalProxy({
+ "event": "scanText",
+ "handle": tHandle,
+ })
return True
@@ -289,7 +301,7 @@ 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, 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
@@ -346,9 +358,21 @@ class NWIndex:
# Prune no longer used tags
for tTag, isActive in tags.items():
- if not isActive:
- logger.debug("Deleting removed tag '%s'", tTag)
+ updated = []
+ deleted = []
+ 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)
+ if updated or deleted:
+ SHARED.indexSignalProxy({
+ "event": "updateTags",
+ "updated": updated,
+ "deleted": deleted,
+ })
return
@@ -385,7 +409,7 @@ class NWIndex:
return
def _indexKeyword(self, tHandle: str, line: str, sTitle: str,
- itemClass: nwItemClass, tags: dict) -> 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
@@ -596,10 +620,8 @@ class NWIndex:
return tRefs
- def getBackReferenceList(self, tHandle: str) -> dict[str, str]:
- """Build a list of files referring back to our file, specified
- by tHandle.
- """
+ def getBackReferenceList(self, tHandle: str) -> dict[str, tuple[str, IndexHeading]]:
+ """Build a dict of files referring back to our file."""
if tHandle is None or tHandle not in self._itemIndex:
return {}
@@ -611,20 +633,43 @@ 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
- 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)
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)
+ 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
+
+ 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
@@ -643,7 +688,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 +710,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 +725,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 +986,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..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
@@ -1783,13 +1780,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 +1803,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 +1823,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."""
@@ -2054,7 +2051,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/docviewer.py b/novelwriter/gui/docviewer.py
index ff548fb6..8992505d 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
@@ -57,7 +56,9 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
+ documentLoaded = pyqtSignal(str)
loadDocumentTagRequest = pyqtSignal(str, Enum)
+ togglePanelVisibility = pyqtSignal()
def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui)
@@ -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)
@@ -223,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
@@ -241,13 +241,11 @@ 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()
qApp.restoreOverrideCursor()
+ self.documentLoaded.emit(tHandle)
return True
@@ -263,33 +261,31 @@ 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
- 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."""
@@ -342,7 +338,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()
@@ -366,11 +362,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
@@ -383,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))
@@ -393,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)
@@ -442,29 +432,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
@@ -530,20 +519,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 +536,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,78 +562,65 @@ 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.
"""
- 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]" % (
@@ -667,7 +639,7 @@ class GuiDocViewHistory:
class GuiDocViewHeader(QWidget):
- def __init__(self, docViewer):
+ def __init__(self, docViewer: GuiDocViewer) -> None:
super().__init__(parent=docViewer)
logger.debug("Create: GuiDocViewHeader")
@@ -685,18 +657,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)
@@ -740,7 +712,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)
@@ -763,9 +735,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,32 +756,30 @@ 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.
"""
- 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):
+ def setTitleFromHandle(self, tHandle: str | None) -> None:
"""Sets the document title from the handle, or alternatively,
set the whole document path.
"""
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)
self.refreshButton.setVisible(False)
- return True
+ return
pTree = SHARED.project.tree
if CONFIG.showFullPath:
@@ -821,42 +790,37 @@ 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)
+ 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, 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 +830,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 +847,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 +871,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 +882,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 +892,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 +915,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 +922,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 +952,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 +972,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
new file mode 100644
index 00000000..f5078df9
--- /dev/null
+++ b/novelwriter/gui/docviewerpanel.py
@@ -0,0 +1,399 @@
+"""
+novelWriter – GUI Document Viewer Panel
+=======================================
+
+File History:
+Created: 2023-11-14 [2.2rc1] 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 enum import Enum
+
+from PyQt5.QtCore import QModelIndex, QSize, Qt, pyqtSignal, pyqtSlot
+from PyQt5.QtWidgets import (
+ QAbstractItemView, QFrame, QHeaderView, QTabWidget, QTreeWidget,
+ QTreeWidgetItem, QVBoxLayout, QWidget
+)
+
+from novelwriter import CONFIG, SHARED
+from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst
+from novelwriter.core.index import IndexHeading, IndexItem
+from novelwriter.enum import nwDocMode, nwItemClass
+
+logger = logging.getLogger(__name__)
+
+
+class GuiDocViewerPanel(QWidget):
+
+ openDocumentRequest = pyqtSignal(str, Enum, str, bool)
+ loadDocumentTagRequest = pyqtSignal(str, Enum)
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+
+ logger.debug("Create: GuiDocViewerPanel")
+
+ self._lastHandle = None
+
+ self.tabBackRefs = _ViewPanelBackRefs(self)
+
+ self.mainTabs = QTabWidget(self)
+ self.mainTabs.addTab(self.tabBackRefs, self.tr("Backreferences"))
+
+ self.kwTabs: dict[str, _ViewPanelKeyWords] = {}
+ self.idTabs: dict[str, int] = {}
+ for itemClass in nwLists.USER_CLASSES:
+ 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()
+ self.outerBox.addWidget(self.mainTabs)
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
+
+ self.setLayout(self.outerBox)
+ self.updateTheme()
+
+ logger.debug("Ready: GuiDocViewerPanel")
+
+ return
+
+ ##
+ # Methods
+ ##
+
+ 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::pane {border: 0;} "
+ "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)
+ self.updateHandle(self._lastHandle)
+
+ return
+
+ ##
+ # Public Slots
+ ##
+
+ @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
+
+ @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:
+ """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."""
+ self._lastHandle = tHandle
+ self.tabBackRefs.refreshContent(tHandle or None)
+ return
+
+ @pyqtSlot(list, list)
+ def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
+ """Forward tags changes to the lists."""
+ for key in updated:
+ 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)
+ 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
+ ##
+
+ def _updateTabVisibility(self) -> None:
+ """Hide class tabs with no content."""
+ for tClass, cTab in self.kwTabs.items():
+ self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
+ return
+
+# END Class GuiDocViewerPanel
+
+
+class _ViewPanelBackRefs(QTreeWidget):
+
+ C_DATA = 0
+ C_DOC = 0
+ C_EDIT = 1
+ C_VIEW = 2
+ C_TITLE = 3
+
+ D_HANDLE = Qt.ItemDataRole.UserRole
+
+ def __init__(self, parent: GuiDocViewerPanel) -> None:
+ super().__init__(parent=parent)
+
+ self._parent = parent
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
+
+ iPx = SHARED.theme.baseIconSize
+ cMg = CONFIG.pxInt(6)
+
+ 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)
+ treeHeader.setSectionResizeMode(self.C_EDIT, QHeaderView.ResizeMode.Fixed)
+ treeHeader.setSectionResizeMode(self.C_VIEW, QHeaderView.ResizeMode.Fixed)
+ treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.ResizeMode.ResizeToContents)
+ 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 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():
+ self._setTreeItemValues(tHandle, sTitle, hItem)
+ return
+
+ def refreshDocument(self, tHandle: str) -> None:
+ """Refresh document meta data."""
+ 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)
+ return
+
+ ##
+ # Private Slots
+ ##
+
+ @pyqtSlot("QModelIndex")
+ def _treeItemClicked(self, index: QModelIndex) -> None:
+ """Emit document open signal on user click."""
+ tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
+ if index.column() == self.C_EDIT:
+ self._parent.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
+ elif index.column() == self.C_VIEW:
+ self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True)
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _setTreeItemValues(self, tHandle: str, sTitle: str, hItem: IndexHeading) -> None:
+ """Add or update a tree item."""
+ 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)
+
+ 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)
+
+ if tKey not in self._treeMap:
+ self.addTopLevelItem(trItem)
+ self._treeMap[tKey] = trItem
+
+ return
+
+# END Class _ViewPanelBackRefs
+
+
+class _ViewPanelKeyWords(QTreeWidget):
+
+ C_DATA = 0
+ C_NAME = 0
+ C_EDIT = 1
+ C_VIEW = 2
+ C_DOC = 3
+ C_TITLE = 4
+
+ D_TAG = Qt.ItemDataRole.UserRole
+
+ def __init__(self, parent: GuiDocViewerPanel, itemClass: nwItemClass) -> None:
+ super().__init__(parent=parent)
+
+ self._parent = parent
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
+
+ iPx = SHARED.theme.baseIconSize
+ cMg = CONFIG.pxInt(6)
+
+ self.setHeaderLabels([self.tr("Tag"), "", "", self.tr("Document"), self.tr("Heading")])
+ 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)
+
+ # Set Header Sizes
+ 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.setSectionResizeMode(self.C_DOC, QHeaderView.ResizeMode.ResizeToContents)
+ 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")
+
+ # Signals
+ self.clicked.connect(self._treeItemClicked)
+
+ return
+
+ def countEntries(self) -> int:
+ """Return the number of items in the list."""
+ return self.topLevelItemCount()
+
+ def clearContent(self) -> None:
+ """Clear the list."""
+ self._treeMap = {}
+ 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
+ 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)
+
+ # 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
+ trItem = self._treeMap[tag] if tag in self._treeMap else 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.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.setData(self.C_DATA, self.D_TAG, tag)
+
+ if tag not in self._treeMap:
+ self.addTopLevelItem(trItem)
+ self._treeMap[tag] = trItem
+
+ return
+
+ def removeEntry(self, tag: str) -> bool:
+ """Remove a tag from the list."""
+ if tag in self._treeMap:
+ self.takeTopLevelItem(self.indexOfTopLevelItem(self._treeMap[tag]))
+ self._treeMap.pop(tag, None)
+ return True
+ return False
+
+ ##
+ # Private Slots
+ ##
+
+ @pyqtSlot("QModelIndex")
+ def _treeItemClicked(self, index: QModelIndex) -> None:
+ """Emit follow tag signal on user click."""
+ tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG)
+ if index.column() == self.C_EDIT:
+ self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT)
+ elif index.column() == self.C_VIEW:
+ self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
+ return
+
+# END Class _ViewPanelKeyWords
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/gui/projtree.py b/novelwriter/gui/projtree.py
index 5b39d78d..9e60c69a 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/gui/theme.py b/novelwriter/gui/theme.py
index 6c4d7e76..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()
@@ -464,8 +465,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", "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
##
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index c672a1f2..099cb422 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)
@@ -242,6 +243,12 @@ 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)
+ 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)
@@ -257,6 +264,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)
@@ -276,7 +284,12 @@ 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)
+
+ self.docViewerPanel.loadDocumentTagRequest.connect(self._followTag)
+ self.docViewerPanel.openDocumentRequest.connect(self._openDocument)
self.outlineView.loadDocumentTagRequest.connect(self._followTag)
self.outlineView.openDocumentRequest.connect(self._openDocument)
@@ -681,7 +694,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}")
@@ -782,11 +795,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:
@@ -877,6 +888,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 +1088,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 +1279,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."""
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 8ee3099a..9bad600a 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -52,6 +52,10 @@ class SharedData(QObject):
projectStatusChanged = pyqtSignal(bool)
projectStatusMessage = pyqtSignal(str)
spellLanguageChanged = pyqtSignal(str, str)
+ indexScannedText = pyqtSignal(str)
+ indexChangedTags = pyqtSignal(list, list)
+ indexCleared = pyqtSignal()
+ indexAvailable = pyqtSignal()
def __init__(self) -> None:
super().__init__()
@@ -171,7 +175,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 +214,24 @@ class SharedData(QObject):
QThreadPool.globalInstance().start(runnable, priority=priority)
return
+ ##
+ # Signal Proxy
+ ##
+
+ 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)
+ 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
+
##
# Alert Boxes
##
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..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, (
@@ -562,11 +563,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 +577,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 +607,24 @@ 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
+ assert index.getHandleHeaderCount("foo") == 0
# 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 +636,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
# ============
@@ -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):
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_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()
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index cc8bc995..b05e35b1 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -23,13 +23,14 @@ 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
from novelwriter.core.tohtml import ToHtml
+from novelwriter.gui.docviewer import GuiDocViewer
@pytest.mark.gui
@@ -37,153 +38,178 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
"""Test the document viewer."""
# Open project
assert nwGUI.openProject(prjLipsum)
-
- # Rebuild the index
- nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger)
- assert SHARED.project.index._tagsIndex._tags != {}
- assert SHARED.project.index._itemIndex._items != {}
+ docViewer: GuiDocViewer = nwGUI.docViewer
# 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
- 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"
+
+ # 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 = 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. "
"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 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)
+ # 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
- theCursor = nwGUI.docViewer.textCursor()
- theCursor.setPosition(27)
- 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"
+ 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"
+ qtbot.mouseClick(docViewer.viewport(), Qt.LeftButton, 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()
+
+ # 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")
- 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 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
- 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."
- # 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")
+ # Call the update theme function
+ # This only checks that t doesn't fail, functionality tested elsewhere
+ docViewer.updateTheme()
# qtbot.stop()
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)