Add a new document viewer panel for tags and references (#1606)

This commit is contained in:
Veronica Berglyd Olsen
2023-11-17 23:30:11 +01:00
committed by GitHub
36 changed files with 1147 additions and 491 deletions
@@ -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
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4" height="24" version="1.2" viewBox="0 0 4 24" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="4" height="22" rx="2" ry="2" fill="#d64848" stroke-width="0"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4" height="24" version="1.2" viewBox="0 0 4 24" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="4" height="22" rx="2" ry="2" fill="#69c" stroke-width="0"/>
</svg>

After

Width:  |  Height:  |  Size: 222 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4" height="24" version="1.2" viewBox="0 0 4 24" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="4" height="22" rx="2" ry="2" fill="#fff" stroke-width="0"/>
</svg>

After

Width:  |  Height:  |  Size: 222 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4" height="24" version="1.2" viewBox="0 0 4 24" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="4" height="22" rx="2" ry="2" fill="#ffff84" stroke-width="0"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m17.853 2.0017c-0.14248-0.00428-0.28548-1.936e-4 -0.42925 0.01171h-11.135c-2.3531-0.068148-4.4276 2.1102-4.2808 4.4518 0.00689 3.8473-0.014486 7.6959 0.00976 11.542 0.12963 2.1674 2.0751 4.0505 4.2711 3.9912 3.8794-0.0037 7.7592 0.0071 11.639-0.0059 2.3157-0.05864 4.2507-2.2581 4.0603-4.5591-0.0037-3.7862 0.0071-7.5728-0.0059-11.359-0.054966-2.1716-1.9914-4.0089-4.1286-4.0732zm-11.533 2.8514c0.10262-0.00515 0.20595 1.374e-4 0.30828 0.015613 2.2719-0.00131 4.5437 0.0026 6.8153-0.00195 1.5163 0.01818 3.0369-0.036922 4.5501 0.029275 0.83106 0.14317 1.246 1.0195 1.1395 1.7956-0.0023 1.7714-9.95e-4 3.543 0 5.3144h-14.271c-9.495e-4 -1.9468-0.0019-3.8929 0.00585-5.8394 0.049778-0.75434 0.73334-1.2775 1.4517-1.3135z" fill="#848484" stroke-width=".99912"/>
</svg>

After

Width:  |  Height:  |  Size: 912 B

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 2c-5.5138 0-10 4.4862-10 10s4.4862 10 10 10c2.02 0 3.965-0.59875 5.6238-1.73 0.57-0.39 0.7175-1.1675 0.32875-1.7375-0.38875-0.57125-1.165-0.715-1.7375-0.32875-1.2425 0.84875-2.7 1.2962-4.215 1.2962-4.1362 0-7.5-3.3638-7.5-7.5s3.3638-7.5 7.5-7.5 7.5 3.3638 7.5 7.5v0.625c0 0.69-0.56 1.25-1.25 1.25s-1.25-0.56-1.25-1.25v-3.75c0-0.69125-0.55875-1.25-1.25-1.25-0.55125 0-1.0062 0.3625-1.1725 0.86-0.725-0.53375-1.6112-0.86-2.5775-0.86-2.4125 0-4.375 1.9625-4.375 4.375s1.9625 4.375 4.375 4.375c1.3062 0 2.4688-0.5875 3.27-1.4988 0.685 0.90375 1.76 1.4988 2.98 1.4988 2.0675 0 3.75-1.6825 3.75-3.75v-0.625c0-5.5138-4.4862-10-10-10zm0 11.875c-1.0338 0-1.875-0.84125-1.875-1.875s0.84125-1.875 1.875-1.875 1.875 0.84125 1.875 1.875-0.84125 1.875-1.875 1.875z" fill="#69c" stroke-width="1.25"/>
</svg>

Before

Width:  |  Height:  |  Size: 944 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m22.803 11.373c-0.0913-0.1309-2.2682-3.2384-5.2724-5.3626-1.5576-1.1033-3.5222-1.7105-5.5309-1.7105-2.0075 0-3.9721 0.60721-5.5331 1.7105-3.0041 2.1241-5.1789 5.2317-5.2702 5.3626-0.2618 0.3773-0.2618 0.87781 0 1.2551 0.091301 0.1309 2.266 3.2384 5.2702 5.3626 1.5609 1.1022 3.5255 1.7094 5.5331 1.7094 2.0086 0 3.9732-0.60721 5.5309-1.7105 3.0041-2.1241 5.1811-5.2317 5.2724-5.3626 0.2629-0.3762 0.2629-0.87781 0-1.254zm-10.803 4.4771c-2.1274 0-3.85-1.727-3.85-3.85 0-2.1274 1.7226-3.85 3.85-3.85 2.123 0 3.85 1.7226 3.85 3.85 0 2.123-1.727 3.85-3.85 3.85zm2.2-3.85c0 1.2122-0.98781 2.2-2.2 2.2-1.2155 0-2.2-0.98781-2.2-2.2 0-1.2155 0.98451-2.2 2.2-2.2 1.2122 0 2.2 0.98451 2.2 2.2z" fill="#69c" stroke-width="1.1"/>
</svg>

After

Width:  |  Height:  |  Size: 872 B

@@ -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
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4" height="24" version="1.2" viewBox="0 0 4 24" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="4" height="22" rx="2" ry="2" fill="#c82829" stroke-width="0"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4" height="24" version="1.2" viewBox="0 0 4 24" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="4" height="22" rx="2" ry="2" fill="#4271ae" stroke-width="0"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4" height="24" version="1.2" viewBox="0 0 4 24" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="4" height="22" rx="2" ry="2" fill="#333" stroke-width="0"/>
</svg>

After

Width:  |  Height:  |  Size: 222 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4" height="24" version="1.2" viewBox="0 0 4 24" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="4" height="22" rx="2" ry="2" fill="#ad8700" stroke-width="0"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m17.853 2.0017c-0.14248-0.00428-0.28548-1.936e-4 -0.42925 0.01171h-11.135c-2.3531-0.068148-4.4276 2.1102-4.2808 4.4518 0.00689 3.8473-0.014486 7.6959 0.00976 11.542 0.12963 2.1674 2.0751 4.0505 4.2711 3.9912 3.8794-0.0037 7.7592 0.0071 11.639-0.0059 2.3157-0.05864 4.2507-2.2581 4.0603-4.5591-0.0037-3.7862 0.0071-7.5728-0.0059-11.359-0.054966-2.1716-1.9914-4.0089-4.1286-4.0732zm-11.533 2.8514c0.10262-0.00515 0.20595 1.374e-4 0.30828 0.015613 2.2719-0.00131 4.5437 0.0026 6.8153-0.00195 1.5163 0.01818 3.0369-0.036922 4.5501 0.029275 0.83106 0.14317 1.246 1.0195 1.1395 1.7956-0.0023 1.7714-9.95e-4 3.543 0 5.3144h-14.271c-9.495e-4 -1.9468-0.0019-3.8929 0.00585-5.8394 0.049778-0.75434 0.73334-1.2775 1.4517-1.3135z" fill-opacity=".78039" stroke-width=".99912"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 2c-5.5138 0-10 4.4862-10 10s4.4862 10 10 10c2.02 0 3.965-0.59875 5.6238-1.73 0.57-0.39 0.7175-1.1675 0.32875-1.7375-0.38875-0.57125-1.165-0.715-1.7375-0.32875-1.2425 0.84875-2.7 1.2962-4.215 1.2962-4.1362 0-7.5-3.3638-7.5-7.5s3.3638-7.5 7.5-7.5 7.5 3.3638 7.5 7.5v0.625c0 0.69-0.56 1.25-1.25 1.25s-1.25-0.56-1.25-1.25v-3.75c0-0.69125-0.55875-1.25-1.25-1.25-0.55125 0-1.0062 0.3625-1.1725 0.86-0.725-0.53375-1.6112-0.86-2.5775-0.86-2.4125 0-4.375 1.9625-4.375 4.375s1.9625 4.375 4.375 4.375c1.3062 0 2.4688-0.5875 3.27-1.4988 0.685 0.90375 1.76 1.4988 2.98 1.4988 2.0675 0 3.75-1.6825 3.75-3.75v-0.625c0-5.5138-4.4862-10-10-10zm0 11.875c-1.0338 0-1.875-0.84125-1.875-1.875s0.84125-1.875 1.875-1.875 1.875 0.84125 1.875 1.875-0.84125 1.875-1.875 1.875z" fill="#4271ae" stroke-width="1.25"/>
</svg>

Before

Width:  |  Height:  |  Size: 947 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m22.803 11.373c-0.0913-0.1309-2.2682-3.2384-5.2724-5.3626-1.5576-1.1033-3.5222-1.7105-5.5309-1.7105-2.0075 0-3.9721 0.60721-5.5331 1.7105-3.0041 2.1241-5.1789 5.2317-5.2702 5.3626-0.2618 0.3773-0.2618 0.87781 0 1.2551 0.091301 0.1309 2.266 3.2384 5.2702 5.3626 1.5609 1.1022 3.5255 1.7094 5.5331 1.7094 2.0086 0 3.9732-0.60721 5.5309-1.7105 3.0041-2.1241 5.1811-5.2317 5.2724-5.3626 0.2629-0.3762 0.2629-0.87781 0-1.254zm-10.803 4.4771c-2.1274 0-3.85-1.727-3.85-3.85 0-2.1274 1.7226-3.85 3.85-3.85 2.123 0 3.85 1.7226 3.85 3.85 0 2.123-1.727 3.85-3.85 3.85zm2.2-3.85c0 1.2122-0.98781 2.2-2.2 2.2-1.2155 0-2.2-0.98781-2.2-2.2 0-1.2155 0.98451-2.2 2.2-2.2 1.2122 0 2.2 0.98451 2.2 2.2z" fill="#4271ae" stroke-width="1.1"/>
</svg>

After

Width:  |  Height:  |  Size: 875 B

+3 -3
View File
@@ -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),
+16 -1
View File
@@ -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],
+78 -24
View File
@@ -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."""
+1 -1
View File
@@ -64,7 +64,7 @@ class nwItemLayout(Enum):
class nwTrinary(Enum):
NEGATIVE = -1
UNKNOWN = 0
NEUTRAL = 0
POSITIVE = 1
# END Enum nwTrinary
+5 -8
View File
@@ -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]
+114 -293
View File
@@ -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("<a href='%s#%s' %s>%s</a>" % (
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
+399
View File
@@ -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 20182023, 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 <https://www.gnu.org/licenses/>.
"""
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
+10 -6
View File
@@ -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", {})
+7 -2
View File
@@ -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)
+21 -2
View File
@@ -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
##
+43 -24
View File
@@ -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."""
+23 -1
View File
@@ -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
##
+1 -1
View File
@@ -68,7 +68,7 @@ stopwhenidle = True
useridletime = 300
[State]
showrefpanel = True
showviewerpanel = True
showedittoolbar = False
useshortcodes = False
viewcomments = True
@@ -68,7 +68,7 @@ stopwhenidle = True
useridletime = 300
[State]
showrefpanel = True
showviewerpanel = True
showedittoolbar = False
useshortcodes = False
viewcomments = True
+50 -21
View File
@@ -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):
+1 -1
View File
@@ -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, "<novelWriterXML/>")
assert xmlReader.read(data, content) is False
assert xmlReader.state == XMLReadState.UNKNOWN_VERSION
+4 -4
View File
@@ -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()
+112 -86
View File
@@ -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()
+193
View File
@@ -0,0 +1,193 @@
"""
novelWriter Main GUI Viewer Panel Class Tester
================================================
This file is a part of novelWriter
Copyright 20182023, 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 <https://www.gnu.org/licenses/>.
"""
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
+1 -2
View File
@@ -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)