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/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
|
-
|
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
|
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
|
-
#
|
302
|
+
# Public Slots
|
297
303
|
##
|
298
304
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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
|
-
|
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
|
314
|
-
"""Emit
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
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)))
|
novelwriter/text/counting.py
CHANGED
@@ -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)
|
novelwriter/text/patterns.py
CHANGED
@@ -3,7 +3,8 @@ novelWriter – Text Pattern Functions
|
|
3
3
|
====================================
|
4
4
|
|
5
5
|
File History:
|
6
|
-
Created: 2024-06-01 [2.
|
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
|
-
|
27
|
+
import re
|
27
28
|
|
28
29
|
from novelwriter import CONFIG
|
29
|
-
from novelwriter.
|
30
|
-
from novelwriter.
|
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
|
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
|
-
|
39
|
-
rxRule.setPatternOptions(QRegExUnicode)
|
40
|
-
return rxRule
|
66
|
+
return self._rxItalic
|
41
67
|
|
42
68
|
@property
|
43
|
-
def markdownBold(self) ->
|
69
|
+
def markdownBold(self) -> re.Pattern:
|
44
70
|
"""Markdown bold style."""
|
45
|
-
|
46
|
-
rxRule.setPatternOptions(QRegExUnicode)
|
47
|
-
return rxRule
|
71
|
+
return self._rxBold
|
48
72
|
|
49
73
|
@property
|
50
|
-
def markdownStrike(self) ->
|
74
|
+
def markdownStrike(self) -> re.Pattern:
|
51
75
|
"""Markdown strikethrough style."""
|
52
|
-
|
53
|
-
rxRule.setPatternOptions(QRegExUnicode)
|
54
|
-
return rxRule
|
76
|
+
return self._rxStrike
|
55
77
|
|
56
78
|
@property
|
57
|
-
def shortcodePlain(self) ->
|
79
|
+
def shortcodePlain(self) -> re.Pattern:
|
58
80
|
"""Plain shortcode style."""
|
59
|
-
|
60
|
-
rxRule.setPatternOptions(QRegExUnicode)
|
61
|
-
return rxRule
|
81
|
+
return self._rxSCPlain
|
62
82
|
|
63
83
|
@property
|
64
|
-
def shortcodeValue(self) ->
|
84
|
+
def shortcodeValue(self) -> re.Pattern:
|
65
85
|
"""Plain shortcode style."""
|
66
|
-
|
67
|
-
rxRule.setPatternOptions(QRegExUnicode)
|
68
|
-
return rxRule
|
86
|
+
return self._rxSCValue
|
69
87
|
|
70
88
|
@property
|
71
|
-
def dialogStyle(self) ->
|
89
|
+
def dialogStyle(self) -> re.Pattern | None:
|
72
90
|
"""Dialogue detection rule based on user settings."""
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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) ->
|
115
|
+
def altDialogStyle(self) -> re.Pattern | None:
|
105
116
|
"""Dialogue alternative rule based on user settings."""
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
novelwriter/tools/manusbuild.py
CHANGED
@@ -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.
|
188
|
-
self.
|
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.
|
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.
|
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.
|
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.
|
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(
|
399
|
+
item.setIcon(nwItem.getMainIcon())
|
398
400
|
self.listContent.addItem(item)
|
399
401
|
|
400
402
|
return
|