novelWriter 2.5.3__py3-none-any.whl → 2.6b2__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.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
- novelwriter/__init__.py +49 -10
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
- novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -0
- novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/common.py +100 -2
- novelwriter/config.py +25 -15
- novelwriter/constants.py +168 -60
- novelwriter/core/buildsettings.py +66 -39
- novelwriter/core/coretools.py +145 -147
- novelwriter/core/docbuild.py +132 -170
- novelwriter/core/index.py +38 -37
- novelwriter/core/item.py +41 -8
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/options.py +4 -1
- novelwriter/core/project.py +67 -89
- novelwriter/core/spellcheck.py +9 -14
- novelwriter/core/status.py +7 -5
- novelwriter/core/tree.py +268 -287
- novelwriter/dialogs/docmerge.py +7 -17
- novelwriter/dialogs/preferences.py +46 -33
- novelwriter/dialogs/projectsettings.py +5 -5
- novelwriter/enum.py +36 -23
- novelwriter/extensions/configlayout.py +27 -12
- novelwriter/extensions/modified.py +13 -1
- novelwriter/extensions/pagedsidebar.py +5 -5
- novelwriter/formats/shared.py +155 -0
- novelwriter/formats/todocx.py +1191 -0
- novelwriter/formats/tohtml.py +451 -0
- novelwriter/{core → formats}/tokenizer.py +487 -491
- novelwriter/formats/tomarkdown.py +217 -0
- novelwriter/{core → formats}/toodt.py +311 -432
- novelwriter/formats/toqdoc.py +484 -0
- novelwriter/formats/toraw.py +91 -0
- novelwriter/gui/doceditor.py +342 -284
- novelwriter/gui/dochighlight.py +96 -84
- novelwriter/gui/docviewer.py +88 -31
- novelwriter/gui/docviewerpanel.py +17 -25
- novelwriter/gui/editordocument.py +17 -2
- novelwriter/gui/itemdetails.py +25 -28
- novelwriter/gui/mainmenu.py +129 -63
- novelwriter/gui/noveltree.py +45 -47
- novelwriter/gui/outline.py +196 -249
- novelwriter/gui/projtree.py +594 -1241
- novelwriter/gui/search.py +9 -10
- novelwriter/gui/sidebar.py +7 -6
- novelwriter/gui/theme.py +10 -5
- novelwriter/guimain.py +100 -196
- novelwriter/shared.py +66 -27
- novelwriter/text/counting.py +2 -0
- novelwriter/text/patterns.py +168 -60
- novelwriter/tools/manusbuild.py +14 -12
- novelwriter/tools/manuscript.py +120 -78
- novelwriter/tools/manussettings.py +424 -291
- novelwriter/tools/welcome.py +4 -4
- novelwriter/tools/writingstats.py +3 -3
- novelwriter/types.py +23 -7
- novelwriter/core/tohtml.py +0 -530
- novelwriter/core/tomarkdown.py +0 -252
- novelwriter/core/toqdoc.py +0 -419
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.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,43 @@ 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
|
34
|
+
from PyQt5.QtGui import (
|
35
|
+
QCursor, QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent,
|
36
|
+
QMouseEvent, QPalette, QResizeEvent, QTextCursor
|
37
|
+
)
|
35
38
|
from PyQt5.QtWidgets import (
|
36
39
|
QAction, QApplication, QFrame, QHBoxLayout, QMenu, QTextBrowser,
|
37
40
|
QToolButton, QWidget
|
38
41
|
)
|
39
42
|
|
40
43
|
from novelwriter import CONFIG, SHARED
|
41
|
-
from novelwriter.
|
42
|
-
from novelwriter.
|
43
|
-
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
|
44
|
+
from novelwriter.common import decodeMimeHandles, qtLambda
|
45
|
+
from novelwriter.constants import nwConst, nwStyles, nwUnicode
|
46
|
+
from novelwriter.enum import nwChange, nwDocAction, nwDocMode, nwItemType
|
44
47
|
from novelwriter.error import logException
|
45
48
|
from novelwriter.extensions.configlayout import NColourLabel
|
46
49
|
from novelwriter.extensions.eventfilters import WheelEventFilter
|
47
50
|
from novelwriter.extensions.modified import NIconToolButton
|
51
|
+
from novelwriter.formats.shared import TextDocumentTheme
|
52
|
+
from novelwriter.formats.toqdoc import ToQTextDocument
|
48
53
|
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
|
49
|
-
from novelwriter.types import
|
54
|
+
from novelwriter.types import (
|
55
|
+
QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor,
|
56
|
+
QtScrollAlwaysOff, QtScrollAsNeeded
|
57
|
+
)
|
50
58
|
|
51
59
|
logger = logging.getLogger(__name__)
|
52
60
|
|
53
61
|
|
54
62
|
class GuiDocViewer(QTextBrowser):
|
55
63
|
|
64
|
+
closeDocumentRequest = pyqtSignal()
|
56
65
|
documentLoaded = pyqtSignal(str)
|
57
66
|
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
58
|
-
|
67
|
+
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
59
68
|
reloadDocumentRequest = pyqtSignal()
|
60
|
-
togglePanelVisibility = pyqtSignal()
|
61
69
|
requestProjectItemSelected = pyqtSignal(str, bool)
|
70
|
+
togglePanelVisibility = pyqtSignal()
|
62
71
|
|
63
72
|
def __init__(self, parent: QWidget) -> None:
|
64
73
|
super().__init__(parent=parent)
|
@@ -72,6 +81,7 @@ class GuiDocViewer(QTextBrowser):
|
|
72
81
|
# Settings
|
73
82
|
self.setMinimumWidth(CONFIG.pxInt(300))
|
74
83
|
self.setAutoFillBackground(True)
|
84
|
+
self.setOpenLinks(False)
|
75
85
|
self.setOpenExternalLinks(False)
|
76
86
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
77
87
|
self.setFrameStyle(QFrame.Shape.NoFrame)
|
@@ -162,6 +172,7 @@ class GuiDocViewer(QTextBrowser):
|
|
162
172
|
self._docTheme.text = SHARED.theme.colText
|
163
173
|
self._docTheme.highlight = SHARED.theme.colMark
|
164
174
|
self._docTheme.head = SHARED.theme.colHead
|
175
|
+
self._docTheme.link = SHARED.theme.colLink
|
165
176
|
self._docTheme.comment = SHARED.theme.colHidden
|
166
177
|
self._docTheme.note = SHARED.theme.colNote
|
167
178
|
self._docTheme.code = SHARED.theme.colCode
|
@@ -177,14 +188,14 @@ class GuiDocViewer(QTextBrowser):
|
|
177
188
|
|
178
189
|
# Scroll bars
|
179
190
|
if CONFIG.hideVScroll:
|
180
|
-
self.setVerticalScrollBarPolicy(
|
191
|
+
self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
|
181
192
|
else:
|
182
|
-
self.setVerticalScrollBarPolicy(
|
193
|
+
self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
|
183
194
|
|
184
195
|
if CONFIG.hideHScroll:
|
185
|
-
self.setHorizontalScrollBarPolicy(
|
196
|
+
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
|
186
197
|
else:
|
187
|
-
self.setHorizontalScrollBarPolicy(
|
198
|
+
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
188
199
|
|
189
200
|
# Refresh the tab stops
|
190
201
|
self.setTabStopDistance(CONFIG.getTabWidth())
|
@@ -207,8 +218,10 @@ class GuiDocViewer(QTextBrowser):
|
|
207
218
|
sPos = self.verticalScrollBar().value()
|
208
219
|
qDoc = ToQTextDocument(SHARED.project)
|
209
220
|
qDoc.setJustify(CONFIG.doJustify)
|
210
|
-
qDoc.
|
211
|
-
qDoc.
|
221
|
+
qDoc.setDialogHighlight(True)
|
222
|
+
qDoc.setTextFont(CONFIG.textFont)
|
223
|
+
qDoc.setTheme(self._docTheme)
|
224
|
+
qDoc.initDocument()
|
212
225
|
qDoc.setKeywords(True)
|
213
226
|
qDoc.setComments(CONFIG.viewComments)
|
214
227
|
qDoc.setSynopsis(CONFIG.viewSynopsis)
|
@@ -221,7 +234,7 @@ class GuiDocViewer(QTextBrowser):
|
|
221
234
|
qDoc.doPreProcessing()
|
222
235
|
qDoc.tokenizeText()
|
223
236
|
qDoc.doConvert()
|
224
|
-
qDoc.
|
237
|
+
qDoc.closeDocument()
|
225
238
|
except Exception:
|
226
239
|
logger.error("Failed to generate preview for document with handle '%s'", tHandle)
|
227
240
|
logException()
|
@@ -245,7 +258,7 @@ class GuiDocViewer(QTextBrowser):
|
|
245
258
|
SHARED.project.data.setLastHandle(tHandle, "viewer")
|
246
259
|
self.docHeader.setHandle(tHandle)
|
247
260
|
self.docHeader.setOutline({
|
248
|
-
sTitle: (hItem.title,
|
261
|
+
sTitle: (hItem.title, nwStyles.H_LEVEL.get(hItem.level, 0))
|
249
262
|
for sTitle, hItem in SHARED.project.index.iterItemHeadings(tHandle)
|
250
263
|
})
|
251
264
|
self.updateDocMargins()
|
@@ -333,10 +346,10 @@ class GuiDocViewer(QTextBrowser):
|
|
333
346
|
# Public Slots
|
334
347
|
##
|
335
348
|
|
336
|
-
@pyqtSlot(str)
|
337
|
-
def
|
349
|
+
@pyqtSlot(str, Enum)
|
350
|
+
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
|
338
351
|
"""Update the header title bar if needed."""
|
339
|
-
if tHandle and
|
352
|
+
if tHandle == self._docHandle and change == nwChange.UPDATE:
|
340
353
|
self.docHeader.setHandle(tHandle)
|
341
354
|
self.updateDocMargins()
|
342
355
|
return
|
@@ -372,8 +385,10 @@ class GuiDocViewer(QTextBrowser):
|
|
372
385
|
logger.debug("Clicked link: '%s'", link)
|
373
386
|
if (bits := link.partition("_")) and bits[0] == "#tag" and bits[2]:
|
374
387
|
self.loadDocumentTagRequest.emit(bits[2], nwDocMode.VIEW)
|
375
|
-
|
388
|
+
elif link.startswith("#"):
|
376
389
|
self.navigateTo(link)
|
390
|
+
elif link.startswith("http"):
|
391
|
+
QDesktopServices.openUrl(QUrl(url))
|
377
392
|
return
|
378
393
|
|
379
394
|
@pyqtSlot("QPoint")
|
@@ -386,25 +401,25 @@ class GuiDocViewer(QTextBrowser):
|
|
386
401
|
|
387
402
|
if userSelection:
|
388
403
|
mnuCopy = QAction(self.tr("Copy"), ctxMenu)
|
389
|
-
mnuCopy.triggered.connect(
|
404
|
+
mnuCopy.triggered.connect(qtLambda(self.docAction, nwDocAction.COPY))
|
390
405
|
ctxMenu.addAction(mnuCopy)
|
391
406
|
|
392
407
|
ctxMenu.addSeparator()
|
393
408
|
|
394
409
|
mnuSelAll = QAction(self.tr("Select All"), ctxMenu)
|
395
|
-
mnuSelAll.triggered.connect(
|
410
|
+
mnuSelAll.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL))
|
396
411
|
ctxMenu.addAction(mnuSelAll)
|
397
412
|
|
398
413
|
mnuSelWord = QAction(self.tr("Select Word"), ctxMenu)
|
399
|
-
mnuSelWord.triggered.connect(
|
400
|
-
|
401
|
-
)
|
414
|
+
mnuSelWord.triggered.connect(qtLambda(
|
415
|
+
self._makePosSelection, QTextCursor.SelectionType.WordUnderCursor, point
|
416
|
+
))
|
402
417
|
ctxMenu.addAction(mnuSelWord)
|
403
418
|
|
404
419
|
mnuSelPara = QAction(self.tr("Select Paragraph"), ctxMenu)
|
405
|
-
mnuSelPara.triggered.connect(
|
406
|
-
|
407
|
-
)
|
420
|
+
mnuSelPara.triggered.connect(qtLambda(
|
421
|
+
self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, point
|
422
|
+
))
|
408
423
|
ctxMenu.addAction(mnuSelPara)
|
409
424
|
|
410
425
|
# Open the context menu
|
@@ -433,6 +448,32 @@ class GuiDocViewer(QTextBrowser):
|
|
433
448
|
super().mouseReleaseEvent(event)
|
434
449
|
return
|
435
450
|
|
451
|
+
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
|
452
|
+
"""Overload drag enter event to handle dragged items."""
|
453
|
+
if event.mimeData().hasFormat(nwConst.MIME_HANDLE):
|
454
|
+
event.acceptProposedAction()
|
455
|
+
else:
|
456
|
+
super().dragEnterEvent(event)
|
457
|
+
return
|
458
|
+
|
459
|
+
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
|
460
|
+
"""Overload drag move event to handle dragged items."""
|
461
|
+
if event.mimeData().hasFormat(nwConst.MIME_HANDLE):
|
462
|
+
event.acceptProposedAction()
|
463
|
+
else:
|
464
|
+
super().dragMoveEvent(event)
|
465
|
+
return
|
466
|
+
|
467
|
+
def dropEvent(self, event: QDropEvent) -> None:
|
468
|
+
"""Overload drop event to handle dragged items."""
|
469
|
+
if event.mimeData().hasFormat(nwConst.MIME_HANDLE):
|
470
|
+
if handles := decodeMimeHandles(event.mimeData()):
|
471
|
+
if SHARED.project.tree.checkType(handles[0], nwItemType.FILE):
|
472
|
+
self.openDocumentRequest.emit(handles[0], nwDocMode.VIEW, "", True)
|
473
|
+
else:
|
474
|
+
super().dropEvent(event)
|
475
|
+
return
|
476
|
+
|
436
477
|
##
|
437
478
|
# Internal Functions
|
438
479
|
##
|
@@ -629,6 +670,11 @@ class GuiDocViewHeader(QWidget):
|
|
629
670
|
self.forwardButton.setToolTip(self.tr("Go Forward"))
|
630
671
|
self.forwardButton.clicked.connect(self.docViewer.navForward)
|
631
672
|
|
673
|
+
self.editButton = NIconToolButton(self, iSz)
|
674
|
+
self.editButton.setVisible(False)
|
675
|
+
self.editButton.setToolTip(self.tr("Open in Editor"))
|
676
|
+
self.editButton.clicked.connect(self._editDocument)
|
677
|
+
|
632
678
|
self.refreshButton = NIconToolButton(self, iSz)
|
633
679
|
self.refreshButton.setVisible(False)
|
634
680
|
self.refreshButton.setToolTip(self.tr("Reload"))
|
@@ -647,7 +693,7 @@ class GuiDocViewHeader(QWidget):
|
|
647
693
|
self.outerBox.addSpacing(mPx)
|
648
694
|
self.outerBox.addWidget(self.itemTitle, 1)
|
649
695
|
self.outerBox.addSpacing(mPx)
|
650
|
-
self.outerBox.
|
696
|
+
self.outerBox.addWidget(self.editButton, 0)
|
651
697
|
self.outerBox.addWidget(self.refreshButton, 0)
|
652
698
|
self.outerBox.addWidget(self.closeButton, 0)
|
653
699
|
self.outerBox.setSpacing(0)
|
@@ -681,8 +727,9 @@ class GuiDocViewHeader(QWidget):
|
|
681
727
|
self.outlineButton.setVisible(False)
|
682
728
|
self.backButton.setVisible(False)
|
683
729
|
self.forwardButton.setVisible(False)
|
684
|
-
self.
|
730
|
+
self.editButton.setVisible(False)
|
685
731
|
self.refreshButton.setVisible(False)
|
732
|
+
self.closeButton.setVisible(False)
|
686
733
|
return
|
687
734
|
|
688
735
|
def setOutline(self, data: dict[str, tuple[str, int]]) -> None:
|
@@ -716,6 +763,7 @@ class GuiDocViewHeader(QWidget):
|
|
716
763
|
self.outlineButton.setThemeIcon("list")
|
717
764
|
self.backButton.setThemeIcon("backward")
|
718
765
|
self.forwardButton.setThemeIcon("forward")
|
766
|
+
self.editButton.setThemeIcon("edit")
|
719
767
|
self.refreshButton.setThemeIcon("refresh")
|
720
768
|
self.closeButton.setThemeIcon("close")
|
721
769
|
|
@@ -723,6 +771,7 @@ class GuiDocViewHeader(QWidget):
|
|
723
771
|
self.outlineButton.setStyleSheet(buttonStyle)
|
724
772
|
self.backButton.setStyleSheet(buttonStyle)
|
725
773
|
self.forwardButton.setStyleSheet(buttonStyle)
|
774
|
+
self.editButton.setStyleSheet(buttonStyle)
|
726
775
|
self.refreshButton.setStyleSheet(buttonStyle)
|
727
776
|
self.closeButton.setStyleSheet(buttonStyle)
|
728
777
|
|
@@ -757,7 +806,7 @@ class GuiDocViewHeader(QWidget):
|
|
757
806
|
|
758
807
|
if CONFIG.showFullPath:
|
759
808
|
self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
|
760
|
-
[name for name in SHARED.project.tree.
|
809
|
+
[name for name in SHARED.project.tree.itemPath(tHandle, asName=True)]
|
761
810
|
)))
|
762
811
|
else:
|
763
812
|
self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
|
@@ -765,8 +814,9 @@ class GuiDocViewHeader(QWidget):
|
|
765
814
|
self.backButton.setVisible(True)
|
766
815
|
self.forwardButton.setVisible(True)
|
767
816
|
self.outlineButton.setVisible(True)
|
768
|
-
self.
|
817
|
+
self.editButton.setVisible(True)
|
769
818
|
self.refreshButton.setVisible(True)
|
819
|
+
self.closeButton.setVisible(True)
|
770
820
|
|
771
821
|
return
|
772
822
|
|
@@ -793,6 +843,13 @@ class GuiDocViewHeader(QWidget):
|
|
793
843
|
self.docViewer.reloadDocumentRequest.emit()
|
794
844
|
return
|
795
845
|
|
846
|
+
@pyqtSlot()
|
847
|
+
def _editDocument(self) -> None:
|
848
|
+
"""Open the document in the editor."""
|
849
|
+
if tHandle := self._docHandle:
|
850
|
+
self.docViewer.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
|
851
|
+
return
|
852
|
+
|
796
853
|
##
|
797
854
|
# Events
|
798
855
|
##
|