novelWriter 2.5.1__py3-none-any.whl → 2.6b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/METADATA +2 -1
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/RECORD +61 -56
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +3 -3
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +1 -0
- novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +1 -0
- novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/themes/default_light.conf +2 -2
- novelwriter/common.py +63 -0
- novelwriter/config.py +10 -3
- novelwriter/constants.py +153 -60
- novelwriter/core/buildsettings.py +66 -39
- novelwriter/core/coretools.py +34 -22
- novelwriter/core/docbuild.py +130 -169
- novelwriter/core/index.py +29 -18
- novelwriter/core/item.py +2 -2
- novelwriter/core/options.py +4 -1
- novelwriter/core/spellcheck.py +9 -14
- novelwriter/dialogs/preferences.py +45 -32
- novelwriter/dialogs/projectsettings.py +3 -3
- novelwriter/enum.py +29 -23
- novelwriter/extensions/configlayout.py +24 -11
- novelwriter/extensions/modified.py +13 -1
- novelwriter/extensions/pagedsidebar.py +5 -5
- novelwriter/formats/shared.py +155 -0
- novelwriter/formats/todocx.py +1195 -0
- novelwriter/formats/tohtml.py +452 -0
- novelwriter/{core → formats}/tokenizer.py +483 -485
- novelwriter/formats/tomarkdown.py +217 -0
- novelwriter/{core → formats}/toodt.py +270 -320
- novelwriter/formats/toqdoc.py +436 -0
- novelwriter/formats/toraw.py +91 -0
- novelwriter/gui/doceditor.py +240 -193
- novelwriter/gui/dochighlight.py +96 -84
- novelwriter/gui/docviewer.py +56 -30
- novelwriter/gui/docviewerpanel.py +3 -3
- novelwriter/gui/editordocument.py +17 -2
- novelwriter/gui/itemdetails.py +8 -4
- novelwriter/gui/mainmenu.py +121 -60
- novelwriter/gui/noveltree.py +35 -37
- novelwriter/gui/outline.py +186 -238
- novelwriter/gui/projtree.py +142 -131
- novelwriter/gui/sidebar.py +7 -6
- novelwriter/gui/theme.py +5 -4
- novelwriter/guimain.py +43 -155
- novelwriter/shared.py +14 -4
- novelwriter/text/counting.py +2 -0
- novelwriter/text/patterns.py +155 -59
- novelwriter/tools/manusbuild.py +1 -1
- novelwriter/tools/manuscript.py +121 -78
- novelwriter/tools/manussettings.py +403 -260
- novelwriter/tools/welcome.py +4 -4
- novelwriter/tools/writingstats.py +3 -3
- novelwriter/types.py +16 -6
- novelwriter/core/tohtml.py +0 -530
- novelwriter/core/tomarkdown.py +0 -252
- novelwriter/core/toqdoc.py +0 -419
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/top_level.txt +0 -0
novelwriter/gui/dochighlight.py
CHANGED
@@ -25,10 +25,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
25
25
|
from __future__ import annotations
|
26
26
|
|
27
27
|
import logging
|
28
|
+
import re
|
28
29
|
|
29
30
|
from time import time
|
30
31
|
|
31
|
-
from PyQt5.QtCore import
|
32
|
+
from PyQt5.QtCore import Qt
|
32
33
|
from PyQt5.QtGui import (
|
33
34
|
QBrush, QColor, QFont, QSyntaxHighlighter, QTextBlockUserData,
|
34
35
|
QTextCharFormat, QTextDocument
|
@@ -36,20 +37,17 @@ from PyQt5.QtGui import (
|
|
36
37
|
|
37
38
|
from novelwriter import CONFIG, SHARED
|
38
39
|
from novelwriter.common import checkInt
|
39
|
-
from novelwriter.constants import
|
40
|
+
from novelwriter.constants import nwStyles, nwUnicode
|
40
41
|
from novelwriter.core.index import processComment
|
41
42
|
from novelwriter.enum import nwComment
|
42
|
-
from novelwriter.text.patterns import REGEX_PATTERNS
|
43
|
-
from novelwriter.types import QRegExUnicode
|
43
|
+
from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser
|
44
44
|
|
45
45
|
logger = logging.getLogger(__name__)
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
SPELLSV = QRegularExpression(nwRegEx.FMT_SV)
|
52
|
-
SPELLSV.setPatternOptions(QRegExUnicode)
|
47
|
+
RX_URL = REGEX_PATTERNS.url
|
48
|
+
RX_WORDS = REGEX_PATTERNS.wordSplit
|
49
|
+
RX_FMT_SC = REGEX_PATTERNS.shortcodePlain
|
50
|
+
RX_FMT_SV = REGEX_PATTERNS.shortcodeValue
|
53
51
|
|
54
52
|
BLOCK_NONE = 0
|
55
53
|
BLOCK_TEXT = 1
|
@@ -61,7 +59,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
61
59
|
|
62
60
|
__slots__ = (
|
63
61
|
"_tHandle", "_isNovel", "_isInactive", "_spellCheck", "_spellErr",
|
64
|
-
"_hStyles", "_minRules", "_txtRules", "_cmnRules",
|
62
|
+
"_hStyles", "_minRules", "_txtRules", "_cmnRules", "_dialogParser",
|
65
63
|
)
|
66
64
|
|
67
65
|
def __init__(self, document: QTextDocument) -> None:
|
@@ -76,9 +74,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
76
74
|
self._spellErr = QTextCharFormat()
|
77
75
|
|
78
76
|
self._hStyles: dict[str, QTextCharFormat] = {}
|
79
|
-
self._minRules: list[tuple[
|
80
|
-
self._txtRules: list[tuple[
|
81
|
-
self._cmnRules: list[tuple[
|
77
|
+
self._minRules: list[tuple[re.Pattern, dict[int, QTextCharFormat]]] = []
|
78
|
+
self._txtRules: list[tuple[re.Pattern, dict[int, QTextCharFormat]]] = []
|
79
|
+
self._cmnRules: list[tuple[re.Pattern, dict[int, QTextCharFormat]]] = []
|
80
|
+
|
81
|
+
self._dialogParser = DialogParser()
|
82
82
|
|
83
83
|
self.initHighlighter()
|
84
84
|
|
@@ -98,14 +98,14 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
98
98
|
|
99
99
|
# Create Character Formats
|
100
100
|
self._addCharFormat("text", SHARED.theme.colText)
|
101
|
-
self._addCharFormat("header1", SHARED.theme.colHead, "b",
|
102
|
-
self._addCharFormat("header2", SHARED.theme.colHead, "b",
|
103
|
-
self._addCharFormat("header3", SHARED.theme.colHead, "b",
|
104
|
-
self._addCharFormat("header4", SHARED.theme.colHead, "b",
|
105
|
-
self._addCharFormat("head1h", SHARED.theme.colHeadH, "b",
|
106
|
-
self._addCharFormat("head2h", SHARED.theme.colHeadH, "b",
|
107
|
-
self._addCharFormat("head3h", SHARED.theme.colHeadH, "b",
|
108
|
-
self._addCharFormat("head4h", SHARED.theme.colHeadH, "b",
|
101
|
+
self._addCharFormat("header1", SHARED.theme.colHead, "b", nwStyles.H_SIZES[1])
|
102
|
+
self._addCharFormat("header2", SHARED.theme.colHead, "b", nwStyles.H_SIZES[2])
|
103
|
+
self._addCharFormat("header3", SHARED.theme.colHead, "b", nwStyles.H_SIZES[3])
|
104
|
+
self._addCharFormat("header4", SHARED.theme.colHead, "b", nwStyles.H_SIZES[4])
|
105
|
+
self._addCharFormat("head1h", SHARED.theme.colHeadH, "b", nwStyles.H_SIZES[1])
|
106
|
+
self._addCharFormat("head2h", SHARED.theme.colHeadH, "b", nwStyles.H_SIZES[2])
|
107
|
+
self._addCharFormat("head3h", SHARED.theme.colHeadH, "b", nwStyles.H_SIZES[3])
|
108
|
+
self._addCharFormat("head4h", SHARED.theme.colHeadH, "b", nwStyles.H_SIZES[4])
|
109
109
|
self._addCharFormat("bold", colEmph, "b")
|
110
110
|
self._addCharFormat("italic", colEmph, "i")
|
111
111
|
self._addCharFormat("strike", SHARED.theme.colHidden, "s")
|
@@ -116,10 +116,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
116
116
|
self._addCharFormat("replace", SHARED.theme.colRepTag)
|
117
117
|
self._addCharFormat("hidden", SHARED.theme.colHidden)
|
118
118
|
self._addCharFormat("markup", SHARED.theme.colHidden)
|
119
|
+
self._addCharFormat("link", SHARED.theme.colLink, "u")
|
119
120
|
self._addCharFormat("note", SHARED.theme.colNote)
|
120
121
|
self._addCharFormat("code", SHARED.theme.colCode)
|
121
122
|
self._addCharFormat("keyword", SHARED.theme.colKey)
|
122
|
-
self._addCharFormat("tag", SHARED.theme.colTag)
|
123
|
+
self._addCharFormat("tag", SHARED.theme.colTag, "u")
|
123
124
|
self._addCharFormat("modifier", SHARED.theme.colMod)
|
124
125
|
self._addCharFormat("value", SHARED.theme.colVal)
|
125
126
|
self._addCharFormat("optional", SHARED.theme.colOpt)
|
@@ -133,10 +134,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
133
134
|
self._txtRules.clear()
|
134
135
|
self._cmnRules.clear()
|
135
136
|
|
137
|
+
self._dialogParser.initParser()
|
138
|
+
|
136
139
|
# Multiple or Trailing Spaces
|
137
140
|
if CONFIG.showMultiSpaces:
|
138
|
-
rxRule =
|
139
|
-
rxRule.setPatternOptions(QRegExUnicode)
|
141
|
+
rxRule = re.compile(r"[ ]{2,}|[ ]*$", re.UNICODE)
|
140
142
|
hlRule = {
|
141
143
|
0: self._hStyles["mspaces"],
|
142
144
|
}
|
@@ -145,8 +147,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
145
147
|
self._cmnRules.append((rxRule, hlRule))
|
146
148
|
|
147
149
|
# Non-Breaking Spaces
|
148
|
-
rxRule =
|
149
|
-
rxRule.setPatternOptions(QRegExUnicode)
|
150
|
+
rxRule = re.compile(f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+", re.UNICODE)
|
150
151
|
hlRule = {
|
151
152
|
0: self._hStyles["nobreak"],
|
152
153
|
}
|
@@ -154,30 +155,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
154
155
|
self._txtRules.append((rxRule, hlRule))
|
155
156
|
self._cmnRules.append((rxRule, hlRule))
|
156
157
|
|
157
|
-
# Dialogue
|
158
|
-
if
|
159
|
-
rxRule = REGEX_PATTERNS.dialogStyle
|
160
|
-
hlRule = {
|
161
|
-
0: self._hStyles["dialog"],
|
162
|
-
}
|
163
|
-
self._txtRules.append((rxRule, hlRule))
|
164
|
-
|
165
|
-
if CONFIG.dialogLine:
|
166
|
-
rxRule = REGEX_PATTERNS.dialogLine
|
167
|
-
hlRule = {
|
168
|
-
0: self._hStyles["dialog"],
|
169
|
-
}
|
170
|
-
self._txtRules.append((rxRule, hlRule))
|
171
|
-
|
172
|
-
if CONFIG.narratorBreak:
|
173
|
-
rxRule = REGEX_PATTERNS.narratorBreak
|
174
|
-
hlRule = {
|
175
|
-
0: self._hStyles["text"],
|
176
|
-
}
|
177
|
-
self._txtRules.append((rxRule, hlRule))
|
178
|
-
|
179
|
-
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
180
|
-
rxRule = REGEX_PATTERNS.altDialogStyle
|
158
|
+
# Alt Dialogue
|
159
|
+
if rxRule := REGEX_PATTERNS.altDialogStyle:
|
181
160
|
hlRule = {
|
182
161
|
0: self._hStyles["altdialog"],
|
183
162
|
}
|
@@ -236,9 +215,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
236
215
|
self._txtRules.append((rxRule, hlRule))
|
237
216
|
self._cmnRules.append((rxRule, hlRule))
|
238
217
|
|
218
|
+
# URLs
|
219
|
+
rxRule = REGEX_PATTERNS.url
|
220
|
+
hlRule = {
|
221
|
+
0: self._hStyles["link"],
|
222
|
+
}
|
223
|
+
self._minRules.append((rxRule, hlRule))
|
224
|
+
self._txtRules.append((rxRule, hlRule))
|
225
|
+
self._cmnRules.append((rxRule, hlRule))
|
226
|
+
|
239
227
|
# Alignment Tags
|
240
|
-
rxRule =
|
241
|
-
rxRule.setPatternOptions(QRegExUnicode)
|
228
|
+
rxRule = re.compile(r"(^>{1,2}|<{1,2}$)", re.UNICODE)
|
242
229
|
hlRule = {
|
243
230
|
1: self._hStyles["markup"],
|
244
231
|
}
|
@@ -246,8 +233,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
246
233
|
self._txtRules.append((rxRule, hlRule))
|
247
234
|
|
248
235
|
# Auto-Replace Tags
|
249
|
-
rxRule =
|
250
|
-
rxRule.setPatternOptions(QRegExUnicode)
|
236
|
+
rxRule = re.compile(r"<(\S+?)>", re.UNICODE)
|
251
237
|
hlRule = {
|
252
238
|
0: self._hStyles["replace"],
|
253
239
|
}
|
@@ -406,15 +392,17 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
406
392
|
else: # Text Paragraph
|
407
393
|
self.setCurrentBlockState(BLOCK_TEXT)
|
408
394
|
hRules = self._txtRules if self._isNovel else self._minRules
|
395
|
+
if self._dialogParser.enabled:
|
396
|
+
for pos, end in self._dialogParser(text):
|
397
|
+
length = end - pos
|
398
|
+
self.setFormat(pos, length, self._hStyles["dialog"])
|
409
399
|
|
410
400
|
if hRules:
|
411
401
|
for rX, hRule in hRules:
|
412
|
-
|
413
|
-
while rxItt.hasNext():
|
414
|
-
rxMatch = rxItt.next()
|
402
|
+
for res in re.finditer(rX, text[xOff:]):
|
415
403
|
for xM, hFmt in hRule.items():
|
416
|
-
xPos =
|
417
|
-
xEnd =
|
404
|
+
xPos = res.start(xM) + xOff
|
405
|
+
xEnd = res.end(xM) + xOff
|
418
406
|
for x in range(xPos, xEnd):
|
419
407
|
cFmt = self.format(x)
|
420
408
|
if cFmt.fontStyleName() != "markup":
|
@@ -426,9 +414,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
426
414
|
data = TextBlockData()
|
427
415
|
self.setCurrentBlockUserData(data)
|
428
416
|
|
417
|
+
data.processText(text, xOff)
|
429
418
|
if self._spellCheck:
|
430
|
-
for xPos,
|
431
|
-
for x in range(xPos,
|
419
|
+
for xPos, xEnd in data.spellCheck():
|
420
|
+
for x in range(xPos, xEnd):
|
432
421
|
cFmt = self.format(x)
|
433
422
|
cFmt.merge(self._spellErr)
|
434
423
|
self.setFormat(x, 1, cFmt)
|
@@ -456,6 +445,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
456
445
|
charFormat.setFontWeight(QFont.Weight.Bold)
|
457
446
|
if "i" in styles:
|
458
447
|
charFormat.setFontItalic(True)
|
448
|
+
if "u" in styles:
|
449
|
+
charFormat.setFontUnderline(True)
|
459
450
|
if "s" in styles:
|
460
451
|
charFormat.setFontStrikeOut(True)
|
461
452
|
if "err" in styles:
|
@@ -474,40 +465,61 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|
474
465
|
|
475
466
|
class TextBlockData(QTextBlockUserData):
|
476
467
|
|
477
|
-
__slots__ = ("_spellErrors")
|
468
|
+
__slots__ = ("_text", "_offset", "_metaData", "_spellErrors")
|
478
469
|
|
479
470
|
def __init__(self) -> None:
|
480
471
|
super().__init__()
|
481
|
-
self.
|
472
|
+
self._text = ""
|
473
|
+
self._offset = 0
|
474
|
+
self._metaData: list[tuple[int, int, str, str]] = []
|
475
|
+
self._spellErrors: list[tuple[int, int,]] = []
|
482
476
|
return
|
483
477
|
|
478
|
+
@property
|
479
|
+
def metaData(self) -> list[tuple[int, int, str, str]]:
|
480
|
+
"""Return meta data from last check."""
|
481
|
+
return self._metaData
|
482
|
+
|
484
483
|
@property
|
485
484
|
def spellErrors(self) -> list[tuple[int, int]]:
|
486
485
|
"""Return spell error data from last check."""
|
487
486
|
return self._spellErrors
|
488
487
|
|
489
|
-
def
|
490
|
-
"""
|
491
|
-
|
492
|
-
"""
|
488
|
+
def processText(self, text: str, offset: int) -> None:
|
489
|
+
"""Extract meta data from the text."""
|
490
|
+
self._metaData = []
|
493
491
|
if "[" in text:
|
494
492
|
# Strip shortcodes
|
495
|
-
for
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
493
|
+
for regEx in [RX_FMT_SC, RX_FMT_SV]:
|
494
|
+
for res in regEx.finditer(text, offset):
|
495
|
+
if (s := res.start(0)) >= 0 and (e := res.end(0)) >= 0:
|
496
|
+
pad = " "*(e - s)
|
497
|
+
text = f"{text[:s]}{pad}{text[e:]}"
|
498
|
+
|
499
|
+
if "http" in text:
|
500
|
+
# Strip URLs
|
501
|
+
for res in RX_URL.finditer(text, offset):
|
502
|
+
if (s := res.start(0)) >= 0 and (e := res.end(0)) >= 0:
|
503
|
+
pad = " "*(e - s)
|
504
|
+
text = f"{text[:s]}{pad}{text[e:]}"
|
505
|
+
self._metaData.append((s, e, res.group(0), "url"))
|
506
|
+
|
507
|
+
self._text = text
|
508
|
+
self._offset = offset
|
509
|
+
|
510
|
+
return
|
503
511
|
|
512
|
+
def spellCheck(self) -> list[tuple[int, int]]:
|
513
|
+
"""Run the spell checker and cache the result, and return the
|
514
|
+
list of spell check errors.
|
515
|
+
"""
|
504
516
|
self._spellErrors = []
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
517
|
+
checker = SHARED.spelling
|
518
|
+
for res in RX_WORDS.finditer(self._text.replace("_", " "), self._offset):
|
519
|
+
if (
|
520
|
+
(word := res.group(0))
|
521
|
+
and not (word.isnumeric() or word.isupper() or checker.checkWord(word))
|
522
|
+
):
|
523
|
+
self._spellErrors.append((res.start(0), res.end(0)))
|
524
|
+
|
513
525
|
return self._spellErrors
|
novelwriter/gui/docviewer.py
CHANGED
@@ -31,34 +31,40 @@ import logging
|
|
31
31
|
from enum import Enum
|
32
32
|
|
33
33
|
from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot
|
34
|
-
from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor
|
34
|
+
from PyQt5.QtGui import QCursor, QDesktopServices, QMouseEvent, QPalette, QResizeEvent, QTextCursor
|
35
35
|
from PyQt5.QtWidgets import (
|
36
36
|
QAction, QApplication, QFrame, QHBoxLayout, QMenu, QTextBrowser,
|
37
37
|
QToolButton, QWidget
|
38
38
|
)
|
39
39
|
|
40
40
|
from novelwriter import CONFIG, SHARED
|
41
|
-
from novelwriter.
|
42
|
-
from novelwriter.
|
41
|
+
from novelwriter.common import qtLambda
|
42
|
+
from novelwriter.constants import nwStyles, nwUnicode
|
43
43
|
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
|
44
44
|
from novelwriter.error import logException
|
45
45
|
from novelwriter.extensions.configlayout import NColourLabel
|
46
46
|
from novelwriter.extensions.eventfilters import WheelEventFilter
|
47
47
|
from novelwriter.extensions.modified import NIconToolButton
|
48
|
+
from novelwriter.formats.shared import TextDocumentTheme
|
49
|
+
from novelwriter.formats.toqdoc import ToQTextDocument
|
48
50
|
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
|
49
|
-
from novelwriter.types import
|
51
|
+
from novelwriter.types import (
|
52
|
+
QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor,
|
53
|
+
QtScrollAlwaysOff, QtScrollAsNeeded
|
54
|
+
)
|
50
55
|
|
51
56
|
logger = logging.getLogger(__name__)
|
52
57
|
|
53
58
|
|
54
59
|
class GuiDocViewer(QTextBrowser):
|
55
60
|
|
61
|
+
closeDocumentRequest = pyqtSignal()
|
56
62
|
documentLoaded = pyqtSignal(str)
|
57
63
|
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
58
|
-
|
64
|
+
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
59
65
|
reloadDocumentRequest = pyqtSignal()
|
60
|
-
togglePanelVisibility = pyqtSignal()
|
61
66
|
requestProjectItemSelected = pyqtSignal(str, bool)
|
67
|
+
togglePanelVisibility = pyqtSignal()
|
62
68
|
|
63
69
|
def __init__(self, parent: QWidget) -> None:
|
64
70
|
super().__init__(parent=parent)
|
@@ -72,6 +78,7 @@ class GuiDocViewer(QTextBrowser):
|
|
72
78
|
# Settings
|
73
79
|
self.setMinimumWidth(CONFIG.pxInt(300))
|
74
80
|
self.setAutoFillBackground(True)
|
81
|
+
self.setOpenLinks(False)
|
75
82
|
self.setOpenExternalLinks(False)
|
76
83
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
77
84
|
self.setFrameStyle(QFrame.Shape.NoFrame)
|
@@ -162,6 +169,7 @@ class GuiDocViewer(QTextBrowser):
|
|
162
169
|
self._docTheme.text = SHARED.theme.colText
|
163
170
|
self._docTheme.highlight = SHARED.theme.colMark
|
164
171
|
self._docTheme.head = SHARED.theme.colHead
|
172
|
+
self._docTheme.link = SHARED.theme.colLink
|
165
173
|
self._docTheme.comment = SHARED.theme.colHidden
|
166
174
|
self._docTheme.note = SHARED.theme.colNote
|
167
175
|
self._docTheme.code = SHARED.theme.colCode
|
@@ -177,14 +185,14 @@ class GuiDocViewer(QTextBrowser):
|
|
177
185
|
|
178
186
|
# Scroll bars
|
179
187
|
if CONFIG.hideVScroll:
|
180
|
-
self.setVerticalScrollBarPolicy(
|
188
|
+
self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
|
181
189
|
else:
|
182
|
-
self.setVerticalScrollBarPolicy(
|
190
|
+
self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
|
183
191
|
|
184
192
|
if CONFIG.hideHScroll:
|
185
|
-
self.setHorizontalScrollBarPolicy(
|
193
|
+
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
|
186
194
|
else:
|
187
|
-
self.setHorizontalScrollBarPolicy(
|
195
|
+
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
188
196
|
|
189
197
|
# Refresh the tab stops
|
190
198
|
self.setTabStopDistance(CONFIG.getTabWidth())
|
@@ -207,8 +215,10 @@ class GuiDocViewer(QTextBrowser):
|
|
207
215
|
sPos = self.verticalScrollBar().value()
|
208
216
|
qDoc = ToQTextDocument(SHARED.project)
|
209
217
|
qDoc.setJustify(CONFIG.doJustify)
|
210
|
-
qDoc.
|
211
|
-
qDoc.
|
218
|
+
qDoc.setDialogHighlight(True)
|
219
|
+
qDoc.setFont(CONFIG.textFont)
|
220
|
+
qDoc.setTheme(self._docTheme)
|
221
|
+
qDoc.initDocument()
|
212
222
|
qDoc.setKeywords(True)
|
213
223
|
qDoc.setComments(CONFIG.viewComments)
|
214
224
|
qDoc.setSynopsis(CONFIG.viewSynopsis)
|
@@ -221,7 +231,7 @@ class GuiDocViewer(QTextBrowser):
|
|
221
231
|
qDoc.doPreProcessing()
|
222
232
|
qDoc.tokenizeText()
|
223
233
|
qDoc.doConvert()
|
224
|
-
qDoc.
|
234
|
+
qDoc.closeDocument()
|
225
235
|
except Exception:
|
226
236
|
logger.error("Failed to generate preview for document with handle '%s'", tHandle)
|
227
237
|
logException()
|
@@ -229,15 +239,13 @@ class GuiDocViewer(QTextBrowser):
|
|
229
239
|
QApplication.restoreOverrideCursor()
|
230
240
|
return False
|
231
241
|
|
232
|
-
#
|
233
|
-
self.setTabStopDistance(CONFIG.getTabWidth())
|
234
|
-
|
235
|
-
# Must be before setHtml
|
242
|
+
# Must be before setDocument
|
236
243
|
if updateHistory:
|
237
244
|
self.docHistory.append(tHandle)
|
238
245
|
|
239
246
|
self.setDocumentTitle(tHandle)
|
240
247
|
self.setDocument(qDoc.document)
|
248
|
+
self.setTabStopDistance(CONFIG.getTabWidth())
|
241
249
|
|
242
250
|
if self._docHandle == tHandle:
|
243
251
|
# This is a refresh, so we set the scrollbar back to where it was
|
@@ -247,7 +255,7 @@ class GuiDocViewer(QTextBrowser):
|
|
247
255
|
SHARED.project.data.setLastHandle(tHandle, "viewer")
|
248
256
|
self.docHeader.setHandle(tHandle)
|
249
257
|
self.docHeader.setOutline({
|
250
|
-
sTitle: (hItem.title,
|
258
|
+
sTitle: (hItem.title, nwStyles.H_LEVEL.get(hItem.level, 0))
|
251
259
|
for sTitle, hItem in SHARED.project.index.iterItemHeadings(tHandle)
|
252
260
|
})
|
253
261
|
self.updateDocMargins()
|
@@ -374,8 +382,10 @@ class GuiDocViewer(QTextBrowser):
|
|
374
382
|
logger.debug("Clicked link: '%s'", link)
|
375
383
|
if (bits := link.partition("_")) and bits[0] == "#tag" and bits[2]:
|
376
384
|
self.loadDocumentTagRequest.emit(bits[2], nwDocMode.VIEW)
|
377
|
-
|
385
|
+
elif link.startswith("#"):
|
378
386
|
self.navigateTo(link)
|
387
|
+
elif link.startswith("http"):
|
388
|
+
QDesktopServices.openUrl(QUrl(url))
|
379
389
|
return
|
380
390
|
|
381
391
|
@pyqtSlot("QPoint")
|
@@ -388,25 +398,25 @@ class GuiDocViewer(QTextBrowser):
|
|
388
398
|
|
389
399
|
if userSelection:
|
390
400
|
mnuCopy = QAction(self.tr("Copy"), ctxMenu)
|
391
|
-
mnuCopy.triggered.connect(
|
401
|
+
mnuCopy.triggered.connect(qtLambda(self.docAction, nwDocAction.COPY))
|
392
402
|
ctxMenu.addAction(mnuCopy)
|
393
403
|
|
394
404
|
ctxMenu.addSeparator()
|
395
405
|
|
396
406
|
mnuSelAll = QAction(self.tr("Select All"), ctxMenu)
|
397
|
-
mnuSelAll.triggered.connect(
|
407
|
+
mnuSelAll.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL))
|
398
408
|
ctxMenu.addAction(mnuSelAll)
|
399
409
|
|
400
410
|
mnuSelWord = QAction(self.tr("Select Word"), ctxMenu)
|
401
|
-
mnuSelWord.triggered.connect(
|
402
|
-
|
403
|
-
)
|
411
|
+
mnuSelWord.triggered.connect(qtLambda(
|
412
|
+
self._makePosSelection, QTextCursor.SelectionType.WordUnderCursor, point
|
413
|
+
))
|
404
414
|
ctxMenu.addAction(mnuSelWord)
|
405
415
|
|
406
416
|
mnuSelPara = QAction(self.tr("Select Paragraph"), ctxMenu)
|
407
|
-
mnuSelPara.triggered.connect(
|
408
|
-
|
409
|
-
)
|
417
|
+
mnuSelPara.triggered.connect(qtLambda(
|
418
|
+
self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, point
|
419
|
+
))
|
410
420
|
ctxMenu.addAction(mnuSelPara)
|
411
421
|
|
412
422
|
# Open the context menu
|
@@ -631,6 +641,11 @@ class GuiDocViewHeader(QWidget):
|
|
631
641
|
self.forwardButton.setToolTip(self.tr("Go Forward"))
|
632
642
|
self.forwardButton.clicked.connect(self.docViewer.navForward)
|
633
643
|
|
644
|
+
self.editButton = NIconToolButton(self, iSz)
|
645
|
+
self.editButton.setVisible(False)
|
646
|
+
self.editButton.setToolTip(self.tr("Open in Editor"))
|
647
|
+
self.editButton.clicked.connect(self._editDocument)
|
648
|
+
|
634
649
|
self.refreshButton = NIconToolButton(self, iSz)
|
635
650
|
self.refreshButton.setVisible(False)
|
636
651
|
self.refreshButton.setToolTip(self.tr("Reload"))
|
@@ -649,7 +664,7 @@ class GuiDocViewHeader(QWidget):
|
|
649
664
|
self.outerBox.addSpacing(mPx)
|
650
665
|
self.outerBox.addWidget(self.itemTitle, 1)
|
651
666
|
self.outerBox.addSpacing(mPx)
|
652
|
-
self.outerBox.
|
667
|
+
self.outerBox.addWidget(self.editButton, 0)
|
653
668
|
self.outerBox.addWidget(self.refreshButton, 0)
|
654
669
|
self.outerBox.addWidget(self.closeButton, 0)
|
655
670
|
self.outerBox.setSpacing(0)
|
@@ -683,8 +698,9 @@ class GuiDocViewHeader(QWidget):
|
|
683
698
|
self.outlineButton.setVisible(False)
|
684
699
|
self.backButton.setVisible(False)
|
685
700
|
self.forwardButton.setVisible(False)
|
686
|
-
self.
|
701
|
+
self.editButton.setVisible(False)
|
687
702
|
self.refreshButton.setVisible(False)
|
703
|
+
self.closeButton.setVisible(False)
|
688
704
|
return
|
689
705
|
|
690
706
|
def setOutline(self, data: dict[str, tuple[str, int]]) -> None:
|
@@ -718,6 +734,7 @@ class GuiDocViewHeader(QWidget):
|
|
718
734
|
self.outlineButton.setThemeIcon("list")
|
719
735
|
self.backButton.setThemeIcon("backward")
|
720
736
|
self.forwardButton.setThemeIcon("forward")
|
737
|
+
self.editButton.setThemeIcon("edit")
|
721
738
|
self.refreshButton.setThemeIcon("refresh")
|
722
739
|
self.closeButton.setThemeIcon("close")
|
723
740
|
|
@@ -725,6 +742,7 @@ class GuiDocViewHeader(QWidget):
|
|
725
742
|
self.outlineButton.setStyleSheet(buttonStyle)
|
726
743
|
self.backButton.setStyleSheet(buttonStyle)
|
727
744
|
self.forwardButton.setStyleSheet(buttonStyle)
|
745
|
+
self.editButton.setStyleSheet(buttonStyle)
|
728
746
|
self.refreshButton.setStyleSheet(buttonStyle)
|
729
747
|
self.closeButton.setStyleSheet(buttonStyle)
|
730
748
|
|
@@ -767,8 +785,9 @@ class GuiDocViewHeader(QWidget):
|
|
767
785
|
self.backButton.setVisible(True)
|
768
786
|
self.forwardButton.setVisible(True)
|
769
787
|
self.outlineButton.setVisible(True)
|
770
|
-
self.
|
788
|
+
self.editButton.setVisible(True)
|
771
789
|
self.refreshButton.setVisible(True)
|
790
|
+
self.closeButton.setVisible(True)
|
772
791
|
|
773
792
|
return
|
774
793
|
|
@@ -795,6 +814,13 @@ class GuiDocViewHeader(QWidget):
|
|
795
814
|
self.docViewer.reloadDocumentRequest.emit()
|
796
815
|
return
|
797
816
|
|
817
|
+
@pyqtSlot()
|
818
|
+
def _editDocument(self) -> None:
|
819
|
+
"""Open the document in the editor."""
|
820
|
+
if tHandle := self._docHandle:
|
821
|
+
self.docViewer.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
|
822
|
+
return
|
823
|
+
|
798
824
|
##
|
799
825
|
# Events
|
800
826
|
##
|
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
|
|
35
35
|
|
36
36
|
from novelwriter import CONFIG, SHARED
|
37
37
|
from novelwriter.common import checkInt
|
38
|
-
from novelwriter.constants import
|
38
|
+
from novelwriter.constants import nwLabels, nwLists, nwStyles, trConst
|
39
39
|
from novelwriter.core.index import IndexHeading, IndexItem
|
40
40
|
from novelwriter.enum import nwDocMode, nwItemClass
|
41
41
|
from novelwriter.extensions.modified import NIconToolButton
|
@@ -343,7 +343,7 @@ class _ViewPanelBackRefs(QTreeWidget):
|
|
343
343
|
nwItem.itemType, nwItem.itemClass,
|
344
344
|
nwItem.itemLayout, nwItem.mainHeading
|
345
345
|
)
|
346
|
-
iLevel =
|
346
|
+
iLevel = nwStyles.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
|
347
347
|
hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
|
348
348
|
|
349
349
|
tKey = f"{tHandle}:{sTitle}"
|
@@ -453,7 +453,7 @@ class _ViewPanelKeyWords(QTreeWidget):
|
|
453
453
|
nwItem.itemLayout, nwItem.mainHeading
|
454
454
|
)
|
455
455
|
impLabel, impIcon = nwItem.getImportStatus()
|
456
|
-
iLevel =
|
456
|
+
iLevel = nwStyles.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
|
457
457
|
hDec = SHARED.theme.getHeaderDecorationNarrow(iLevel)
|
458
458
|
|
459
459
|
# This can not use a get call to the dictionary as that would create an
|
@@ -95,6 +95,21 @@ class GuiTextDocument(QTextDocument):
|
|
95
95
|
|
96
96
|
return
|
97
97
|
|
98
|
+
def metaDataAtPos(self, pos: int) -> tuple[str, str]:
|
99
|
+
"""Check if there is meta data available at a given position in
|
100
|
+
the document, and if so, return it.
|
101
|
+
"""
|
102
|
+
cursor = QTextCursor(self)
|
103
|
+
cursor.setPosition(pos)
|
104
|
+
block = cursor.block()
|
105
|
+
data = block.userData()
|
106
|
+
if block.isValid() and isinstance(data, TextBlockData):
|
107
|
+
if (check := pos - block.position()) >= 0:
|
108
|
+
for cPos, cEnd, cData, cType in data.metaData:
|
109
|
+
if cPos <= check <= cEnd:
|
110
|
+
return cData, cType
|
111
|
+
return "", ""
|
112
|
+
|
98
113
|
def spellErrorAtPos(self, pos: int) -> tuple[str, int, int, list[str]]:
|
99
114
|
"""Check if there is a misspelled word at a given position in
|
100
115
|
the document, and if so, return it.
|
@@ -107,8 +122,8 @@ class GuiTextDocument(QTextDocument):
|
|
107
122
|
text = block.text()
|
108
123
|
check = pos - block.position()
|
109
124
|
if check >= 0:
|
110
|
-
for cPos,
|
111
|
-
|
125
|
+
for cPos, cEnd in data.spellErrors:
|
126
|
+
cLen = cEnd - cPos
|
112
127
|
if cPos <= check <= cEnd:
|
113
128
|
word = text[cPos:cEnd]
|
114
129
|
return word, cPos, cLen, SHARED.spelling.suggestWords(word)
|
novelwriter/gui/itemdetails.py
CHANGED
@@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QGridLayout, QLabel, QWidget
|
|
30
30
|
|
31
31
|
from novelwriter import CONFIG, SHARED
|
32
32
|
from novelwriter.common import elide
|
33
|
-
from novelwriter.constants import nwLabels, trConst
|
33
|
+
from novelwriter.constants import nwLabels, nwStats, trConst
|
34
34
|
from novelwriter.types import (
|
35
35
|
QtAlignLeft, QtAlignLeftBase, QtAlignRight, QtAlignRightBase,
|
36
36
|
QtAlignRightMiddle
|
@@ -62,6 +62,10 @@ class GuiItemDetails(QWidget):
|
|
62
62
|
fntValue = self.font()
|
63
63
|
fntValue.setPointSizeF(0.9*fPt)
|
64
64
|
|
65
|
+
trStats1 = trConst(nwLabels.STATS_NAME[nwStats.CHARS_ALL])
|
66
|
+
trStats2 = trConst(nwLabels.STATS_NAME[nwStats.WORDS_ALL])
|
67
|
+
trStats3 = trConst(nwLabels.STATS_NAME[nwStats.PARAGRAPHS])
|
68
|
+
|
65
69
|
# Label
|
66
70
|
self.labelName = QLabel(self.tr("Label"), self)
|
67
71
|
self.labelName.setFont(fntLabel)
|
@@ -113,7 +117,7 @@ class GuiItemDetails(QWidget):
|
|
113
117
|
self.usageData.setWordWrap(True)
|
114
118
|
|
115
119
|
# Character Count
|
116
|
-
self.cCountName = QLabel(
|
120
|
+
self.cCountName = QLabel(trStats1, self)
|
117
121
|
self.cCountName.setFont(fntLabel)
|
118
122
|
self.cCountName.setAlignment(QtAlignRight)
|
119
123
|
|
@@ -122,7 +126,7 @@ class GuiItemDetails(QWidget):
|
|
122
126
|
self.cCountData.setAlignment(QtAlignRight)
|
123
127
|
|
124
128
|
# Word Count
|
125
|
-
self.wCountName = QLabel(
|
129
|
+
self.wCountName = QLabel(trStats2, self)
|
126
130
|
self.wCountName.setFont(fntLabel)
|
127
131
|
self.wCountName.setAlignment(QtAlignRight)
|
128
132
|
|
@@ -131,7 +135,7 @@ class GuiItemDetails(QWidget):
|
|
131
135
|
self.wCountData.setAlignment(QtAlignRight)
|
132
136
|
|
133
137
|
# Paragraph Count
|
134
|
-
self.pCountName = QLabel(
|
138
|
+
self.pCountName = QLabel(trStats3, self)
|
135
139
|
self.pCountName.setFont(fntLabel)
|
136
140
|
self.pCountName.setAlignment(QtAlignRight)
|
137
141
|
|