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.
Files changed (83) hide show
  1. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
  2. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
  3. novelwriter/__init__.py +49 -10
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  6. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  8. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  9. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  10. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  11. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  12. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  13. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  14. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  15. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  17. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  18. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  19. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  20. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  21. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  22. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  23. novelwriter/assets/manual.pdf +0 -0
  24. novelwriter/assets/sample.zip +0 -0
  25. novelwriter/common.py +100 -2
  26. novelwriter/config.py +25 -15
  27. novelwriter/constants.py +168 -60
  28. novelwriter/core/buildsettings.py +66 -39
  29. novelwriter/core/coretools.py +145 -147
  30. novelwriter/core/docbuild.py +132 -170
  31. novelwriter/core/index.py +38 -37
  32. novelwriter/core/item.py +41 -8
  33. novelwriter/core/itemmodel.py +518 -0
  34. novelwriter/core/options.py +4 -1
  35. novelwriter/core/project.py +67 -89
  36. novelwriter/core/spellcheck.py +9 -14
  37. novelwriter/core/status.py +7 -5
  38. novelwriter/core/tree.py +268 -287
  39. novelwriter/dialogs/docmerge.py +7 -17
  40. novelwriter/dialogs/preferences.py +46 -33
  41. novelwriter/dialogs/projectsettings.py +5 -5
  42. novelwriter/enum.py +36 -23
  43. novelwriter/extensions/configlayout.py +27 -12
  44. novelwriter/extensions/modified.py +13 -1
  45. novelwriter/extensions/pagedsidebar.py +5 -5
  46. novelwriter/formats/shared.py +155 -0
  47. novelwriter/formats/todocx.py +1191 -0
  48. novelwriter/formats/tohtml.py +451 -0
  49. novelwriter/{core → formats}/tokenizer.py +487 -491
  50. novelwriter/formats/tomarkdown.py +217 -0
  51. novelwriter/{core → formats}/toodt.py +311 -432
  52. novelwriter/formats/toqdoc.py +484 -0
  53. novelwriter/formats/toraw.py +91 -0
  54. novelwriter/gui/doceditor.py +342 -284
  55. novelwriter/gui/dochighlight.py +96 -84
  56. novelwriter/gui/docviewer.py +88 -31
  57. novelwriter/gui/docviewerpanel.py +17 -25
  58. novelwriter/gui/editordocument.py +17 -2
  59. novelwriter/gui/itemdetails.py +25 -28
  60. novelwriter/gui/mainmenu.py +129 -63
  61. novelwriter/gui/noveltree.py +45 -47
  62. novelwriter/gui/outline.py +196 -249
  63. novelwriter/gui/projtree.py +594 -1241
  64. novelwriter/gui/search.py +9 -10
  65. novelwriter/gui/sidebar.py +7 -6
  66. novelwriter/gui/theme.py +10 -5
  67. novelwriter/guimain.py +100 -196
  68. novelwriter/shared.py +66 -27
  69. novelwriter/text/counting.py +2 -0
  70. novelwriter/text/patterns.py +168 -60
  71. novelwriter/tools/manusbuild.py +14 -12
  72. novelwriter/tools/manuscript.py +120 -78
  73. novelwriter/tools/manussettings.py +424 -291
  74. novelwriter/tools/welcome.py +4 -4
  75. novelwriter/tools/writingstats.py +3 -3
  76. novelwriter/types.py +23 -7
  77. novelwriter/core/tohtml.py +0 -530
  78. novelwriter/core/tomarkdown.py +0 -252
  79. novelwriter/core/toqdoc.py +0 -419
  80. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
  81. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
  82. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
  83. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.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,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 QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor
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.constants import nwHeaders, nwUnicode
42
- from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
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 QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
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
- closeDocumentRequest = pyqtSignal()
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(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
191
+ self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
181
192
  else:
182
- self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
193
+ self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
183
194
 
184
195
  if CONFIG.hideHScroll:
185
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
196
+ self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
186
197
  else:
187
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
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.setDialogueHighlight(True)
211
- qDoc.initDocument(CONFIG.textFont, self._docTheme)
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.appendFootnotes()
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, nwHeaders.H_LEVEL.get(hItem.level, 0))
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 updateDocInfo(self, tHandle: str) -> None:
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 tHandle == self._docHandle:
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
- else:
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(lambda: self.docAction(nwDocAction.COPY))
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(lambda: self.docAction(nwDocAction.SEL_ALL))
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
- lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, point)
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
- lambda: self._makePosSelection(QTextCursor.SelectionType.BlockUnderCursor, point)
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.addSpacing(iPx)
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.closeButton.setVisible(False)
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.getItemPath(tHandle, asName=True)]
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.closeButton.setVisible(True)
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
  ##