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.
Files changed (64) hide show
  1. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/METADATA +2 -1
  2. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/RECORD +61 -56
  3. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +3 -3
  5. novelwriter/assets/i18n/project_en_GB.json +1 -0
  6. novelwriter/assets/icons/typicons_dark/icons.conf +1 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  8. novelwriter/assets/icons/typicons_light/icons.conf +1 -0
  9. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  10. novelwriter/assets/manual.pdf +0 -0
  11. novelwriter/assets/sample.zip +0 -0
  12. novelwriter/assets/themes/default_light.conf +2 -2
  13. novelwriter/common.py +63 -0
  14. novelwriter/config.py +10 -3
  15. novelwriter/constants.py +153 -60
  16. novelwriter/core/buildsettings.py +66 -39
  17. novelwriter/core/coretools.py +34 -22
  18. novelwriter/core/docbuild.py +130 -169
  19. novelwriter/core/index.py +29 -18
  20. novelwriter/core/item.py +2 -2
  21. novelwriter/core/options.py +4 -1
  22. novelwriter/core/spellcheck.py +9 -14
  23. novelwriter/dialogs/preferences.py +45 -32
  24. novelwriter/dialogs/projectsettings.py +3 -3
  25. novelwriter/enum.py +29 -23
  26. novelwriter/extensions/configlayout.py +24 -11
  27. novelwriter/extensions/modified.py +13 -1
  28. novelwriter/extensions/pagedsidebar.py +5 -5
  29. novelwriter/formats/shared.py +155 -0
  30. novelwriter/formats/todocx.py +1195 -0
  31. novelwriter/formats/tohtml.py +452 -0
  32. novelwriter/{core → formats}/tokenizer.py +483 -485
  33. novelwriter/formats/tomarkdown.py +217 -0
  34. novelwriter/{core → formats}/toodt.py +270 -320
  35. novelwriter/formats/toqdoc.py +436 -0
  36. novelwriter/formats/toraw.py +91 -0
  37. novelwriter/gui/doceditor.py +240 -193
  38. novelwriter/gui/dochighlight.py +96 -84
  39. novelwriter/gui/docviewer.py +56 -30
  40. novelwriter/gui/docviewerpanel.py +3 -3
  41. novelwriter/gui/editordocument.py +17 -2
  42. novelwriter/gui/itemdetails.py +8 -4
  43. novelwriter/gui/mainmenu.py +121 -60
  44. novelwriter/gui/noveltree.py +35 -37
  45. novelwriter/gui/outline.py +186 -238
  46. novelwriter/gui/projtree.py +142 -131
  47. novelwriter/gui/sidebar.py +7 -6
  48. novelwriter/gui/theme.py +5 -4
  49. novelwriter/guimain.py +43 -155
  50. novelwriter/shared.py +14 -4
  51. novelwriter/text/counting.py +2 -0
  52. novelwriter/text/patterns.py +155 -59
  53. novelwriter/tools/manusbuild.py +1 -1
  54. novelwriter/tools/manuscript.py +121 -78
  55. novelwriter/tools/manussettings.py +403 -260
  56. novelwriter/tools/welcome.py +4 -4
  57. novelwriter/tools/writingstats.py +3 -3
  58. novelwriter/types.py +16 -6
  59. novelwriter/core/tohtml.py +0 -530
  60. novelwriter/core/tomarkdown.py +0 -252
  61. novelwriter/core/toqdoc.py +0 -419
  62. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/LICENSE.md +0 -0
  63. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/entry_points.txt +0 -0
  64. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/top_level.txt +0 -0
@@ -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 QRegularExpression, Qt
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 nwHeaders, nwRegEx, nwUnicode
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
- SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b")
48
- SPELLRX.setPatternOptions(QRegExUnicode)
49
- SPELLSC = QRegularExpression(nwRegEx.FMT_SC)
50
- SPELLSC.setPatternOptions(QRegExUnicode)
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[QRegularExpression, dict[int, QTextCharFormat]]] = []
80
- self._txtRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
81
- self._cmnRules: list[tuple[QRegularExpression, dict[int, QTextCharFormat]]] = []
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", nwHeaders.H_SIZES[1])
102
- self._addCharFormat("header2", SHARED.theme.colHead, "b", nwHeaders.H_SIZES[2])
103
- self._addCharFormat("header3", SHARED.theme.colHead, "b", nwHeaders.H_SIZES[3])
104
- self._addCharFormat("header4", SHARED.theme.colHead, "b", nwHeaders.H_SIZES[4])
105
- self._addCharFormat("head1h", SHARED.theme.colHeadH, "b", nwHeaders.H_SIZES[1])
106
- self._addCharFormat("head2h", SHARED.theme.colHeadH, "b", nwHeaders.H_SIZES[2])
107
- self._addCharFormat("head3h", SHARED.theme.colHeadH, "b", nwHeaders.H_SIZES[3])
108
- self._addCharFormat("head4h", SHARED.theme.colHeadH, "b", nwHeaders.H_SIZES[4])
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 = QRegularExpression(r"[ ]{2,}|[ ]*$")
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 = QRegularExpression(f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+")
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 CONFIG.dialogStyle > 0:
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 = QRegularExpression(r"(^>{1,2}|<{1,2}$)")
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 = QRegularExpression(r"<(\S+?)>")
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
- rxItt = rX.globalMatch(text, xOff)
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 = rxMatch.capturedStart(xM)
417
- xEnd = rxMatch.capturedEnd(xM)
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, xLen in data.spellCheck(text, xOff):
431
- for x in range(xPos, xPos+xLen):
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._spellErrors: list[tuple[int, int]] = []
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 spellCheck(self, text: str, offset: int) -> list[tuple[int, int]]:
490
- """Run the spell checker and cache the result, and return the
491
- list of spell check errors.
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 rX in [SPELLSC, SPELLSV]:
496
- rxItt = rX.globalMatch(text, offset)
497
- while rxItt.hasNext():
498
- rxMatch = rxItt.next()
499
- xPos = rxMatch.capturedStart(0)
500
- xLen = rxMatch.capturedLength(0)
501
- xEnd = rxMatch.capturedEnd(0)
502
- text = text[:xPos] + " "*xLen + text[xEnd:]
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
- rxSpell = SPELLRX.globalMatch(text.replace("_", " "), offset)
506
- while rxSpell.hasNext():
507
- rxMatch = rxSpell.next()
508
- if not SHARED.spelling.checkWord(rxMatch.captured(0)):
509
- if not rxMatch.captured(0).isnumeric() and not rxMatch.captured(0).isupper():
510
- self._spellErrors.append(
511
- (rxMatch.capturedStart(0), rxMatch.capturedLength(0))
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
@@ -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.constants import nwHeaders, nwUnicode
42
- from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
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 QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
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
- closeDocumentRequest = pyqtSignal()
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(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
188
+ self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
181
189
  else:
182
- self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
190
+ self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
183
191
 
184
192
  if CONFIG.hideHScroll:
185
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
193
+ self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
186
194
  else:
187
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
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.setDialogueHighlight(True)
211
- qDoc.initDocument(CONFIG.textFont, self._docTheme)
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.appendFootnotes()
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
- # Refresh the tab stops
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, nwHeaders.H_LEVEL.get(hItem.level, 0))
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
- else:
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(lambda: self.docAction(nwDocAction.COPY))
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(lambda: self.docAction(nwDocAction.SEL_ALL))
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
- lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, point)
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
- lambda: self._makePosSelection(QTextCursor.SelectionType.BlockUnderCursor, point)
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.addSpacing(iPx)
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.closeButton.setVisible(False)
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.closeButton.setVisible(True)
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 nwHeaders, nwLabels, nwLists, trConst
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 = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
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 = nwHeaders.H_LEVEL.get(hItem.level, 0) if nwItem.isDocumentLayout() else 5
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, cLen in data.spellErrors:
111
- cEnd = cPos + cLen
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)
@@ -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(" "+self.tr("Characters"), self)
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(" "+self.tr("Words"), self)
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(" "+self.tr("Paragraphs"), self)
138
+ self.pCountName = QLabel(trStats3, self)
135
139
  self.pCountName.setFont(fntLabel)
136
140
  self.pCountName.setAlignment(QtAlignRight)
137
141