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
novelwriter/shared.py CHANGED
@@ -26,20 +26,23 @@ from __future__ import annotations
26
26
 
27
27
  import logging
28
28
 
29
+ from enum import Enum
29
30
  from pathlib import Path
30
31
  from time import time
31
32
  from typing import TYPE_CHECKING, TypeVar
32
33
 
33
- from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal
34
- from PyQt5.QtGui import QFont
34
+ from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot
35
+ from PyQt5.QtGui import QDesktopServices, QFont
35
36
  from PyQt5.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget
36
37
 
37
38
  from novelwriter.common import formatFileFilter
38
39
  from novelwriter.constants import nwFiles
39
40
  from novelwriter.core.spellcheck import NWSpellEnchant
41
+ from novelwriter.enum import nwChange, nwItemClass
40
42
 
41
43
  if TYPE_CHECKING: # pragma: no cover
42
44
  from novelwriter.core.project import NWProject
45
+ from novelwriter.core.status import T_StatusKind
43
46
  from novelwriter.gui.theme import GuiTheme
44
47
  from novelwriter.guimain import GuiMain
45
48
 
@@ -55,15 +58,16 @@ class SharedData(QObject):
55
58
  "_idleTime", "_idleRefTime",
56
59
  )
57
60
 
58
- projectStatusChanged = pyqtSignal(bool)
59
- projectStatusMessage = pyqtSignal(str)
60
- spellLanguageChanged = pyqtSignal(str, str)
61
61
  focusModeChanged = pyqtSignal(bool)
62
- indexScannedText = pyqtSignal(str)
62
+ indexAvailable = pyqtSignal()
63
63
  indexChangedTags = pyqtSignal(list, list)
64
64
  indexCleared = pyqtSignal()
65
- indexAvailable = pyqtSignal()
66
65
  mainClockTick = pyqtSignal()
66
+ projectItemChanged = pyqtSignal(str, Enum)
67
+ rootFolderChanged = pyqtSignal(str, Enum)
68
+ projectStatusChanged = pyqtSignal(bool)
69
+ projectStatusMessage = pyqtSignal(str)
70
+ spellLanguageChanged = pyqtSignal(str, str)
67
71
  statusLabelsChanged = pyqtSignal(str)
68
72
 
69
73
  def __init__(self) -> None:
@@ -173,10 +177,12 @@ class SharedData(QObject):
173
177
  logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount())
174
178
  return
175
179
 
176
- def closeEditor(self, tHandle: str | None = None) -> None:
180
+ def closeDocument(self, tHandle: str | None = None) -> None:
177
181
  """Close the document editor, optionally a specific document."""
178
182
  if tHandle is None or tHandle == self.mainGui.docEditor.docHandle:
179
183
  self.mainGui.closeDocument()
184
+ if tHandle is None or tHandle == self.mainGui.docViewer.docHandle:
185
+ self.mainGui.closeViewerPanel()
180
186
  return
181
187
 
182
188
  def saveEditor(self, tHandle: str | None = None) -> None:
@@ -293,29 +299,61 @@ class SharedData(QObject):
293
299
  return None
294
300
 
295
301
  ##
296
- # Signal Proxy
302
+ # Public Slots
297
303
  ##
298
304
 
299
- def indexSignalProxy(self, data: dict) -> None:
300
- """Emit signals on behalf of the index."""
301
- event = data.get("event")
302
- logger.debug("Received '%s' event from the index", event)
303
- if event == "updateTags":
304
- self.indexChangedTags.emit(data.get("updated", []), data.get("deleted", []))
305
- elif event == "scanText":
306
- self.indexScannedText.emit(data.get("handle", ""))
307
- elif event == "clearIndex":
305
+ @pyqtSlot(str)
306
+ def openWebsite(self, url: str) -> None:
307
+ """Open a URL in the system's default browser."""
308
+ QDesktopServices.openUrl(QUrl(url))
309
+ return
310
+
311
+ @pyqtSlot(str, nwItemClass)
312
+ def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
313
+ """Process new note request."""
314
+ self.project.createNewNote(tag, itemClass)
315
+ return
316
+
317
+ ##
318
+ # Signal Proxies
319
+ ##
320
+
321
+ def emitIndexChangedTags(
322
+ self, project: NWProject, updated: list[str], deleted: list[str]
323
+ ) -> None:
324
+ """Emit the indexChangedTags signal."""
325
+ if self._project and self._project.data.uuid == project.data.uuid:
326
+ self.indexChangedTags.emit(updated, deleted)
327
+ return
328
+
329
+ def emitIndexCleared(self, project: NWProject) -> None:
330
+ """Emit the indexCleared signal."""
331
+ if self._project and self._project.data.uuid == project.data.uuid:
308
332
  self.indexCleared.emit()
309
- elif event == "buildIndex":
333
+ return
334
+
335
+ def emitIndexAvailable(self, project: NWProject) -> None:
336
+ """Emit the indexAvailable signal."""
337
+ if self._project and self._project.data.uuid == project.data.uuid:
310
338
  self.indexAvailable.emit()
311
339
  return
312
340
 
313
- def projectSingalProxy(self, data: dict) -> None:
314
- """Emit signals on project data change."""
315
- event = data.get("event")
316
- logger.debug("Received '%s' event from project data", event)
317
- if event == "statusLabels":
318
- self.statusLabelsChanged.emit(data.get("kind", ""))
341
+ def emitStatusLabelsChanged(self, project: NWProject, kind: T_StatusKind) -> None:
342
+ """Emit the statusLabelsChanged signal."""
343
+ if self._project and self._project.data.uuid == project.data.uuid:
344
+ self.statusLabelsChanged.emit(kind)
345
+ return
346
+
347
+ def emitProjectItemChanged(self, project: NWProject, handle: str, change: nwChange) -> None:
348
+ """Emit the projectItemChanged signal."""
349
+ if self._project and self._project.data.uuid == project.data.uuid:
350
+ self.projectItemChanged.emit(handle, change)
351
+ return
352
+
353
+ def emitRootFolderChanged(self, project: NWProject, handle: str, change: nwChange) -> None:
354
+ """Emit the rootFolderChanged signal."""
355
+ if self._project and self._project.data.uuid == project.data.uuid:
356
+ self.rootFolderChanged.emit(handle, change)
319
357
  return
320
358
 
321
359
  ##
@@ -376,6 +414,7 @@ class SharedData(QObject):
376
414
  """Create a new project and spell checking instance."""
377
415
  from novelwriter.core.project import NWProject
378
416
  if isinstance(self._project, NWProject):
417
+ self._project.clear()
379
418
  del self._project
380
419
  del self._spelling
381
420
  self._project = NWProject()
@@ -441,9 +480,9 @@ class _GuiAlert(QMessageBox):
441
480
  Yes/No buttons or just an Ok button.
442
481
  """
443
482
  if isYesNo:
444
- self.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
483
+ self.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
445
484
  else:
446
- self.setStandardButtons(QMessageBox.Ok)
485
+ self.setStandardButtons(QMessageBox.StandardButton.Ok)
447
486
  pSz = 2*self._theme.baseIconHeight
448
487
  if level == self.INFO:
449
488
  self.setIconPixmap(self._theme.getPixmap("alert_info", (pSz, pSz)))
@@ -30,6 +30,7 @@ import re
30
30
  from novelwriter.constants import nwRegEx, nwUnicode
31
31
 
32
32
  RX_SC = re.compile(nwRegEx.FMT_SC)
33
+ RX_SV = re.compile(nwRegEx.FMT_SV)
33
34
  RX_LO = re.compile(r"(?i)(?<!\\)(\[(?:vspace|newpage|new page)(:\d+)?)(?<!\\)(\])")
34
35
 
35
36
 
@@ -64,6 +65,7 @@ def preProcessText(text: str, keepHeaders: bool = True) -> list[str]:
64
65
  # Strip shortcodes and special formatting
65
66
  # RegEx is slow, so we do this only when necessary
66
67
  line = RX_SC.sub("", line)
68
+ line = RX_SV.sub("", line)
67
69
  line = RX_LO.sub("", line)
68
70
 
69
71
  result.append(line)
@@ -3,7 +3,8 @@ novelWriter – Text Pattern Functions
3
3
  ====================================
4
4
 
5
5
  File History:
6
- Created: 2024-06-01 [2.5ec1]
6
+ Created: 2024-06-01 [2.5rc1] RegExPatterns
7
+ Created: 2024-11-04 [2.6b1] DialogParser
7
8
 
8
9
  This file is a part of novelWriter
9
10
  Copyright 2018–2024, Veronica Berglyd Olsen
@@ -23,91 +24,198 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
24
  """
24
25
  from __future__ import annotations
25
26
 
26
- from PyQt5.QtCore import QRegularExpression
27
+ import re
27
28
 
28
29
  from novelwriter import CONFIG
29
- from novelwriter.constants import nwRegEx
30
- from novelwriter.types import QRegExUnicode
30
+ from novelwriter.common import compact, uniqueCompact
31
+ from novelwriter.constants import nwRegEx, nwUnicode
31
32
 
32
33
 
33
34
  class RegExPatterns:
34
35
 
36
+ AMBIGUOUS = (nwUnicode.U_APOS, nwUnicode.U_RSQUO)
37
+
38
+ # Static RegExes
39
+ _rxUrl = re.compile(nwRegEx.URL, re.ASCII)
40
+ _rxWords = re.compile(nwRegEx.WORDS, re.UNICODE)
41
+ _rxBreak = re.compile(nwRegEx.BREAK, re.UNICODE)
42
+ _rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE)
43
+ _rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE)
44
+ _rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE)
45
+ _rxSCPlain = re.compile(nwRegEx.FMT_SC, re.UNICODE)
46
+ _rxSCValue = re.compile(nwRegEx.FMT_SV, re.UNICODE)
47
+
35
48
  @property
36
- def markdownItalic(self) -> QRegularExpression:
49
+ def url(self) -> re.Pattern:
50
+ """Find URLs."""
51
+ return self._rxUrl
52
+
53
+ @property
54
+ def wordSplit(self) -> re.Pattern:
55
+ """Split text into words."""
56
+ return self._rxWords
57
+
58
+ @property
59
+ def lineBreak(self) -> re.Pattern:
60
+ """Find forced line break."""
61
+ return self._rxBreak
62
+
63
+ @property
64
+ def markdownItalic(self) -> re.Pattern:
37
65
  """Markdown italic style."""
38
- rxRule = QRegularExpression(nwRegEx.FMT_EI)
39
- rxRule.setPatternOptions(QRegExUnicode)
40
- return rxRule
66
+ return self._rxItalic
41
67
 
42
68
  @property
43
- def markdownBold(self) -> QRegularExpression:
69
+ def markdownBold(self) -> re.Pattern:
44
70
  """Markdown bold style."""
45
- rxRule = QRegularExpression(nwRegEx.FMT_EB)
46
- rxRule.setPatternOptions(QRegExUnicode)
47
- return rxRule
71
+ return self._rxBold
48
72
 
49
73
  @property
50
- def markdownStrike(self) -> QRegularExpression:
74
+ def markdownStrike(self) -> re.Pattern:
51
75
  """Markdown strikethrough style."""
52
- rxRule = QRegularExpression(nwRegEx.FMT_ST)
53
- rxRule.setPatternOptions(QRegExUnicode)
54
- return rxRule
76
+ return self._rxStrike
55
77
 
56
78
  @property
57
- def shortcodePlain(self) -> QRegularExpression:
79
+ def shortcodePlain(self) -> re.Pattern:
58
80
  """Plain shortcode style."""
59
- rxRule = QRegularExpression(nwRegEx.FMT_SC)
60
- rxRule.setPatternOptions(QRegExUnicode)
61
- return rxRule
81
+ return self._rxSCPlain
62
82
 
63
83
  @property
64
- def shortcodeValue(self) -> QRegularExpression:
84
+ def shortcodeValue(self) -> re.Pattern:
65
85
  """Plain shortcode style."""
66
- rxRule = QRegularExpression(nwRegEx.FMT_SV)
67
- rxRule.setPatternOptions(QRegExUnicode)
68
- return rxRule
86
+ return self._rxSCValue
69
87
 
70
88
  @property
71
- def dialogStyle(self) -> QRegularExpression:
89
+ def dialogStyle(self) -> re.Pattern | None:
72
90
  """Dialogue detection rule based on user settings."""
73
- symO = ""
74
- symC = ""
75
- if CONFIG.dialogStyle in (1, 3):
76
- symO += CONFIG.fmtSQuoteOpen
77
- symC += CONFIG.fmtSQuoteClose
78
- if CONFIG.dialogStyle in (2, 3):
79
- symO += CONFIG.fmtDQuoteOpen
80
- symC += CONFIG.fmtDQuoteClose
81
-
82
- rxEnd = "|$" if CONFIG.allowOpenDial else ""
83
- rxRule = QRegularExpression(f"\\B[{symO}].*?(?:[{symC}]\\B{rxEnd})")
84
- rxRule.setPatternOptions(QRegExUnicode)
85
- return rxRule
86
-
87
- @property
88
- def dialogLine(self) -> QRegularExpression:
89
- """Dialogue line rule based on user settings."""
90
- sym = QRegularExpression.escape(CONFIG.dialogLine)
91
- rxRule = QRegularExpression(f"^{sym}.*?$")
92
- rxRule.setPatternOptions(QRegExUnicode)
93
- return rxRule
94
-
95
- @property
96
- def narratorBreak(self) -> QRegularExpression:
97
- """Dialogue narrator break rule based on user settings."""
98
- sym = QRegularExpression.escape(CONFIG.narratorBreak)
99
- rxRule = QRegularExpression(f"\\B{sym}\\S.*?\\S{sym}\\B")
100
- rxRule.setPatternOptions(QRegExUnicode)
101
- return rxRule
91
+ if CONFIG.dialogStyle > 0:
92
+ rx = []
93
+ if CONFIG.dialogStyle in (1, 3):
94
+ qO = CONFIG.fmtSQuoteOpen.strip()[:1]
95
+ qC = CONFIG.fmtSQuoteClose.strip()[:1]
96
+ if qO == qC:
97
+ rx.append(f"(?:\\B{qO}.+?{qC}\\B)")
98
+ else:
99
+ rx.append(f"(?:{qO}[^{qO}]+{qC})")
100
+ if CONFIG.allowOpenDial:
101
+ rx.append(f"(?:{qO}.+?$)")
102
+ if CONFIG.dialogStyle in (2, 3):
103
+ qO = CONFIG.fmtDQuoteOpen.strip()[:1]
104
+ qC = CONFIG.fmtDQuoteClose.strip()[:1]
105
+ if qO == qC:
106
+ rx.append(f"(?:\\B{qO}.+?{qC}\\B)")
107
+ else:
108
+ rx.append(f"(?:{qO}[^{qO}]+{qC})")
109
+ if CONFIG.allowOpenDial:
110
+ rx.append(f"(?:{qO}.+?$)")
111
+ return re.compile("|".join(rx), re.UNICODE)
112
+ return None
102
113
 
103
114
  @property
104
- def altDialogStyle(self) -> QRegularExpression:
115
+ def altDialogStyle(self) -> re.Pattern | None:
105
116
  """Dialogue alternative rule based on user settings."""
106
- symO = QRegularExpression.escape(CONFIG.altDialogOpen)
107
- symC = QRegularExpression.escape(CONFIG.altDialogClose)
108
- rxRule = QRegularExpression(f"\\B{symO}.*?{symC}\\B")
109
- rxRule.setPatternOptions(QRegExUnicode)
110
- return rxRule
117
+ if CONFIG.altDialogOpen and CONFIG.altDialogClose:
118
+ qO = re.escape(compact(CONFIG.altDialogOpen))
119
+ qC = re.escape(compact(CONFIG.altDialogClose))
120
+ qB = r"\B" if (qO == qC or qC in self.AMBIGUOUS) else ""
121
+ return re.compile(f"{qO}.*?{qC}{qB}", re.UNICODE)
122
+ return None
111
123
 
112
124
 
113
125
  REGEX_PATTERNS = RegExPatterns()
126
+
127
+
128
+ class DialogParser:
129
+
130
+ __slots__ = (
131
+ "_quotes", "_dialog", "_alternate", "_enabled",
132
+ "_narrator", "_breakD", "_breakQ", "_mode",
133
+ )
134
+
135
+ def __init__(self) -> None:
136
+ self._quotes = None
137
+ self._dialog = ""
138
+ self._alternate = ""
139
+ self._enabled = False
140
+ self._narrator = ""
141
+ self._breakD = None
142
+ self._breakQ = None
143
+ self._mode = ""
144
+ return
145
+
146
+ @property
147
+ def enabled(self) -> bool:
148
+ """Return True if there are any settings to parse."""
149
+ return self._enabled
150
+
151
+ def initParser(self) -> None:
152
+ """Init parser settings. This method must also be called when
153
+ the config changes.
154
+ """
155
+ self._quotes = REGEX_PATTERNS.dialogStyle
156
+ self._dialog = uniqueCompact(CONFIG.dialogLine)
157
+ self._alternate = CONFIG.narratorDialog.strip()[:1]
158
+
159
+ # One of the three modes are needed for the class to have
160
+ # anything to do
161
+ self._enabled = bool(self._quotes or self._dialog or self._alternate)
162
+
163
+ # Build narrator break RegExes
164
+ if narrator := CONFIG.narratorBreak.strip()[:1]:
165
+ punct = re.escape(".,:;!?")
166
+ self._breakD = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?|$)", re.UNICODE)
167
+ self._breakQ = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?)", re.UNICODE)
168
+ self._narrator = narrator
169
+ self._mode = f" {narrator}"
170
+
171
+ return
172
+
173
+ def __call__(self, text: str) -> list[tuple[int, int]]:
174
+ """Caller wrapper for dialogue processing."""
175
+ temp: list[int] = []
176
+ result: list[tuple[int, int]] = []
177
+ if text:
178
+ plain = True
179
+ if self._dialog and text[0] in self._dialog:
180
+ # The whole line is dialogue
181
+ plain = False
182
+ temp.append(0)
183
+ temp.append(len(text))
184
+ if self._breakD:
185
+ # Process narrator breaks in the dialogue
186
+ for res in self._breakD.finditer(text, 1):
187
+ temp.append(res.start(0))
188
+ temp.append(res.end(0))
189
+ elif self._quotes:
190
+ # Quoted dialogue is enabled, so we look for them
191
+ for res in self._quotes.finditer(text):
192
+ plain = False
193
+ temp.append(res.start(0))
194
+ temp.append(res.end(0))
195
+ if self._breakQ:
196
+ for sub in self._breakQ.finditer(text, res.start(0), res.end(0)):
197
+ temp.append(sub.start(0))
198
+ temp.append(sub.end(0))
199
+
200
+ if plain and self._alternate:
201
+ # The main rules found no dialogue, so we check for
202
+ # alternating dialogue sections, if enabled
203
+ pos = 0
204
+ for num, bit in enumerate(text.split(self._alternate)):
205
+ length = len(bit) + (1 if num > 0 else 0)
206
+ if num%2:
207
+ temp.append(pos)
208
+ temp.append(pos + length)
209
+ pos += length
210
+
211
+ if temp:
212
+ # Sort unique edges in increasing order, and add them in pairs
213
+ start = None
214
+ for pos in sorted(set(temp)):
215
+ if start is None:
216
+ start = pos
217
+ else:
218
+ result.append((start, pos))
219
+ start = None
220
+
221
+ return result
@@ -179,14 +179,20 @@ class GuiManuscriptBuild(NDialog):
179
179
  self.buildBox.setVerticalSpacing(sp4)
180
180
 
181
181
  # Dialog Buttons
182
+ self.buttonBox = QDialogButtonBox(self)
183
+
182
184
  self.btnOpen = QPushButton(SHARED.theme.getIcon("browse"), self.tr("Open Folder"), self)
183
185
  self.btnOpen.setIconSize(bSz)
186
+ self.btnOpen.setAutoDefault(False)
187
+ self.buttonBox.addButton(self.btnOpen, QtRoleAction)
188
+
184
189
  self.btnBuild = QPushButton(SHARED.theme.getIcon("export"), self.tr("&Build"), self)
185
190
  self.btnBuild.setIconSize(bSz)
191
+ self.btnBuild.setAutoDefault(True)
192
+ self.buttonBox.addButton(self.btnBuild, QtRoleAction)
186
193
 
187
- self.dlgButtons = QDialogButtonBox(QtDialogClose, self)
188
- self.dlgButtons.addButton(self.btnOpen, QtRoleAction)
189
- self.dlgButtons.addButton(self.btnBuild, QtRoleAction)
194
+ self.btnClose = self.buttonBox.addButton(QtDialogClose)
195
+ self.btnClose.setAutoDefault(False)
190
196
 
191
197
  # Assemble GUI
192
198
  # ============
@@ -213,7 +219,7 @@ class GuiManuscriptBuild(NDialog):
213
219
  self.outerBox.addSpacing(sp4)
214
220
  self.outerBox.addLayout(self.buildBox, 0)
215
221
  self.outerBox.addSpacing(sp16)
216
- self.outerBox.addWidget(self.dlgButtons, 0)
222
+ self.outerBox.addWidget(self.buttonBox, 0)
217
223
  self.outerBox.setSpacing(0)
218
224
 
219
225
  self.setLayout(self.outerBox)
@@ -229,7 +235,7 @@ class GuiManuscriptBuild(NDialog):
229
235
  # Signals
230
236
  self.btnReset.clicked.connect(self._doResetBuildName)
231
237
  self.btnBrowse.clicked.connect(self._doSelectPath)
232
- self.dlgButtons.clicked.connect(self._dialogButtonClicked)
238
+ self.buttonBox.clicked.connect(self._dialogButtonClicked)
233
239
  self.listFormats.itemSelectionChanged.connect(self._resetProgress)
234
240
 
235
241
  logger.debug("Ready: GuiManuscriptBuild")
@@ -260,7 +266,7 @@ class GuiManuscriptBuild(NDialog):
260
266
  @pyqtSlot("QAbstractButton*")
261
267
  def _dialogButtonClicked(self, button: QAbstractButton) -> None:
262
268
  """Handle button clicks from the dialog button box."""
263
- role = self.dlgButtons.buttonRole(button)
269
+ role = self.buttonBox.buttonRole(button)
264
270
  if role == QtRoleAction:
265
271
  if button == self.btnBuild:
266
272
  self._runBuild()
@@ -333,7 +339,7 @@ class GuiManuscriptBuild(NDialog):
333
339
  docBuild.queueAll()
334
340
 
335
341
  self.buildProgress.setMaximum(len(docBuild))
336
- for i, _ in docBuild.iterBuild(buildPath, bFormat):
342
+ for i, _ in docBuild.iterBuildDocument(buildPath, bFormat):
337
343
  self.buildProgress.setValue(i+1)
338
344
 
339
345
  self._build.setLastBuildPath(bPath)
@@ -388,13 +394,9 @@ class GuiManuscriptBuild(NDialog):
388
394
  if isinstance(rItem, NWItem):
389
395
  rootMap[rHandle] = rItem.itemName
390
396
 
391
- itemIcon = SHARED.theme.getItemIcon(
392
- nwItem.itemType, nwItem.itemClass,
393
- nwItem.itemLayout, nwItem.mainHeading
394
- )
395
397
  rootName = rootMap.get(rHandle, "??????")
396
398
  item = QListWidgetItem(f"{rootName}: {nwItem.itemName}")
397
- item.setIcon(itemIcon)
399
+ item.setIcon(nwItem.getMainIcon())
398
400
  self.listContent.addItem(item)
399
401
 
400
402
  return