Add a new document viewer panel for tags and references (#1606)
@@ -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 |
@@ -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),
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -64,7 +64,7 @@ class nwItemLayout(Enum):
|
||||
class nwTrinary(Enum):
|
||||
|
||||
NEGATIVE = -1
|
||||
UNKNOWN = 0
|
||||
NEUTRAL = 0
|
||||
POSITIVE = 1
|
||||
|
||||
# END Enum nwTrinary
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
novelWriter – GUI Document Viewer Panel
|
||||
=======================================
|
||||
|
||||
File History:
|
||||
Created: 2023-11-14 [2.2rc1] GuiDocViewerPanel
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2023, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <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
|
||||
@@ -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", {})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
##
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
##
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
novelWriter – Main GUI Viewer Panel Class Tester
|
||||
================================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2023, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <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
|
||||
@@ -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)
|
||||
|
||||