novelWriter 2.7.4__py3-none-any.whl → 2.8b1__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 (196) hide show
  1. novelwriter/__init__.py +8 -7
  2. novelwriter/assets/icons/font_awesome.icons +22 -4
  3. novelwriter/assets/icons/material_filled_normal.icons +20 -2
  4. novelwriter/assets/icons/material_filled_thin.icons +20 -2
  5. novelwriter/assets/icons/material_rounded_normal.icons +20 -2
  6. novelwriter/assets/icons/material_rounded_thin.icons +20 -2
  7. novelwriter/assets/icons/material_sharp_normal.icons +20 -2
  8. novelwriter/assets/icons/material_sharp_thin.icons +20 -2
  9. novelwriter/assets/icons/remix_filled.icons +20 -2
  10. novelwriter/assets/icons/remix_outline.icons +20 -2
  11. novelwriter/assets/images/welcome.webp +0 -0
  12. novelwriter/assets/manual.pdf +0 -0
  13. novelwriter/assets/manual_fr.pdf +0 -0
  14. novelwriter/assets/sample.zip +0 -0
  15. novelwriter/assets/text/credits_en.htm +61 -11
  16. novelwriter/assets/themes/aura.conf +97 -0
  17. novelwriter/assets/themes/aura_bright.conf +95 -0
  18. novelwriter/assets/themes/aura_soft.conf +97 -0
  19. novelwriter/assets/themes/b2t_garden_dark.conf +97 -0
  20. novelwriter/assets/themes/b2t_garden_light.conf +97 -0
  21. novelwriter/assets/themes/b2t_suburb_dark.conf +97 -0
  22. novelwriter/assets/themes/b2t_suburb_light.conf +97 -0
  23. novelwriter/assets/themes/b4t_classic_o_dark.conf +97 -0
  24. novelwriter/assets/themes/b4t_classic_o_light.conf +97 -0
  25. novelwriter/assets/themes/b4t_modern_c_dark.conf +97 -0
  26. novelwriter/assets/themes/b4t_modern_c_light.conf +97 -0
  27. novelwriter/assets/themes/blue_streak_dark.conf +97 -0
  28. novelwriter/assets/themes/blue_streak_light.conf +97 -0
  29. novelwriter/assets/themes/castle_day.conf +95 -0
  30. novelwriter/assets/themes/castle_night.conf +95 -0
  31. novelwriter/assets/themes/catppuccin_latte.conf +97 -0
  32. novelwriter/assets/themes/catppuccin_mocha.conf +97 -0
  33. novelwriter/assets/themes/chalky_soil.conf +95 -0
  34. novelwriter/assets/themes/chernozem.conf +95 -0
  35. novelwriter/assets/themes/cyberpunk_night.conf +88 -40
  36. novelwriter/assets/themes/default_dark.conf +89 -41
  37. novelwriter/assets/themes/default_light.conf +89 -41
  38. novelwriter/assets/themes/dracula.conf +91 -42
  39. novelwriter/assets/themes/espresso.conf +97 -0
  40. novelwriter/assets/themes/everforest_dark.conf +97 -0
  41. novelwriter/assets/themes/everforest_light.conf +97 -0
  42. novelwriter/assets/themes/floral_daydream.conf +95 -0
  43. novelwriter/assets/themes/floral_midnight.conf +95 -0
  44. novelwriter/assets/themes/full_moon.conf +95 -0
  45. novelwriter/assets/themes/grey_dark.conf +97 -0
  46. novelwriter/assets/themes/grey_light.conf +97 -0
  47. novelwriter/assets/themes/horizon_dark.conf +97 -0
  48. novelwriter/assets/themes/horizon_light.conf +97 -0
  49. novelwriter/assets/themes/jewel_case_dark.conf +95 -0
  50. novelwriter/assets/themes/jewel_case_light.conf +95 -0
  51. novelwriter/assets/themes/lcars.conf +97 -0
  52. novelwriter/assets/themes/light_owl.conf +117 -0
  53. novelwriter/assets/themes/new_moon.conf +97 -0
  54. novelwriter/assets/themes/night_owl.conf +117 -0
  55. novelwriter/assets/themes/noctis.conf +129 -0
  56. novelwriter/assets/themes/noctis_lux.conf +129 -0
  57. novelwriter/assets/themes/nord.conf +97 -0
  58. novelwriter/assets/themes/nordlicht.conf +95 -0
  59. novelwriter/assets/themes/otium_dark.conf +95 -0
  60. novelwriter/assets/themes/otium_light.conf +95 -0
  61. novelwriter/assets/themes/paragon.conf +96 -0
  62. novelwriter/assets/themes/primer_light.conf +97 -0
  63. novelwriter/assets/themes/primer_night.conf +97 -0
  64. novelwriter/assets/themes/rose_pine.conf +97 -0
  65. novelwriter/assets/themes/rose_pine_dawn.conf +97 -0
  66. novelwriter/assets/themes/ruby_day.conf +95 -0
  67. novelwriter/assets/themes/ruby_night.conf +95 -0
  68. novelwriter/assets/themes/selenium_dark.conf +95 -0
  69. novelwriter/assets/themes/selenium_light.conf +95 -0
  70. novelwriter/assets/themes/sepia_dark.conf +95 -0
  71. novelwriter/assets/themes/sepia_light.conf +95 -0
  72. novelwriter/assets/themes/snazzy.conf +102 -40
  73. novelwriter/assets/themes/solarized_dark.conf +108 -40
  74. novelwriter/assets/themes/solarized_light.conf +108 -40
  75. novelwriter/assets/themes/sultana_light.conf +95 -0
  76. novelwriter/assets/themes/sultana_night.conf +95 -0
  77. novelwriter/assets/themes/tango_dark.conf +111 -0
  78. novelwriter/assets/themes/tango_light.conf +111 -0
  79. novelwriter/assets/themes/tomorrow.conf +117 -0
  80. novelwriter/assets/themes/tomorrow_night.conf +117 -0
  81. novelwriter/assets/themes/tomorrow_night_blue.conf +117 -0
  82. novelwriter/assets/themes/tomorrow_night_bright.conf +117 -0
  83. novelwriter/assets/themes/tomorrow_night_eighties.conf +117 -0
  84. novelwriter/assets/themes/vivid_black_green.conf +97 -0
  85. novelwriter/assets/themes/vivid_black_red.conf +97 -0
  86. novelwriter/assets/themes/vivid_white_green.conf +97 -0
  87. novelwriter/assets/themes/vivid_white_red.conf +97 -0
  88. novelwriter/assets/themes/warpgate.conf +96 -0
  89. novelwriter/assets/themes/waterlily_dark.conf +95 -0
  90. novelwriter/assets/themes/waterlily_light.conf +95 -0
  91. novelwriter/common.py +47 -17
  92. novelwriter/config.py +57 -62
  93. novelwriter/constants.py +32 -6
  94. novelwriter/core/buildsettings.py +3 -23
  95. novelwriter/core/coretools.py +21 -25
  96. novelwriter/core/docbuild.py +4 -9
  97. novelwriter/core/document.py +2 -6
  98. novelwriter/core/index.py +33 -53
  99. novelwriter/core/indexdata.py +17 -22
  100. novelwriter/core/item.py +11 -35
  101. novelwriter/core/itemmodel.py +5 -21
  102. novelwriter/core/novelmodel.py +3 -7
  103. novelwriter/core/options.py +3 -4
  104. novelwriter/core/project.py +31 -21
  105. novelwriter/core/projectdata.py +2 -21
  106. novelwriter/core/projectxml.py +13 -21
  107. novelwriter/core/sessions.py +2 -4
  108. novelwriter/core/spellcheck.py +12 -13
  109. novelwriter/core/status.py +27 -20
  110. novelwriter/core/storage.py +5 -10
  111. novelwriter/core/tree.py +6 -15
  112. novelwriter/dialogs/about.py +9 -10
  113. novelwriter/dialogs/docmerge.py +17 -14
  114. novelwriter/dialogs/docsplit.py +18 -14
  115. novelwriter/dialogs/editlabel.py +15 -9
  116. novelwriter/dialogs/preferences.py +69 -68
  117. novelwriter/dialogs/projectsettings.py +88 -67
  118. novelwriter/dialogs/quotes.py +15 -10
  119. novelwriter/dialogs/wordlist.py +18 -21
  120. novelwriter/enum.py +75 -30
  121. novelwriter/error.py +6 -11
  122. novelwriter/extensions/configlayout.py +8 -34
  123. novelwriter/extensions/eventfilters.py +3 -3
  124. novelwriter/extensions/modified.py +87 -32
  125. novelwriter/extensions/novelselector.py +13 -12
  126. novelwriter/extensions/pagedsidebar.py +10 -18
  127. novelwriter/extensions/progressbars.py +5 -11
  128. novelwriter/extensions/statusled.py +3 -6
  129. novelwriter/extensions/switch.py +8 -11
  130. novelwriter/extensions/switchbox.py +2 -11
  131. novelwriter/extensions/versioninfo.py +6 -7
  132. novelwriter/formats/shared.py +10 -2
  133. novelwriter/formats/todocx.py +15 -37
  134. novelwriter/formats/tohtml.py +52 -61
  135. novelwriter/formats/tokenizer.py +33 -64
  136. novelwriter/formats/tomarkdown.py +4 -11
  137. novelwriter/formats/toodt.py +12 -71
  138. novelwriter/formats/toqdoc.py +11 -21
  139. novelwriter/formats/toraw.py +2 -6
  140. novelwriter/gui/doceditor.py +207 -245
  141. novelwriter/gui/dochighlight.py +142 -101
  142. novelwriter/gui/docviewer.py +53 -84
  143. novelwriter/gui/docviewerpanel.py +18 -41
  144. novelwriter/gui/editordocument.py +12 -17
  145. novelwriter/gui/itemdetails.py +5 -14
  146. novelwriter/gui/mainmenu.py +24 -32
  147. novelwriter/gui/noveltree.py +13 -51
  148. novelwriter/gui/outline.py +20 -61
  149. novelwriter/gui/projtree.py +40 -96
  150. novelwriter/gui/search.py +9 -24
  151. novelwriter/gui/sidebar.py +54 -22
  152. novelwriter/gui/statusbar.py +7 -22
  153. novelwriter/gui/theme.py +482 -368
  154. novelwriter/guimain.py +87 -101
  155. novelwriter/shared.py +79 -48
  156. novelwriter/splash.py +9 -5
  157. novelwriter/text/comments.py +1 -1
  158. novelwriter/text/counting.py +9 -5
  159. novelwriter/text/patterns.py +20 -15
  160. novelwriter/tools/dictionaries.py +18 -16
  161. novelwriter/tools/lipsum.py +15 -17
  162. novelwriter/tools/manusbuild.py +25 -45
  163. novelwriter/tools/manuscript.py +94 -95
  164. novelwriter/tools/manussettings.py +149 -104
  165. novelwriter/tools/noveldetails.py +10 -24
  166. novelwriter/tools/welcome.py +24 -72
  167. novelwriter/tools/writingstats.py +17 -26
  168. novelwriter/types.py +25 -13
  169. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/METADATA +7 -7
  170. novelwriter-2.8b1.dist-info/RECORD +212 -0
  171. novelwriter/assets/images/welcome-dark.jpg +0 -0
  172. novelwriter/assets/images/welcome-light.jpg +0 -0
  173. novelwriter/assets/syntax/cyberpunk_night.conf +0 -28
  174. novelwriter/assets/syntax/default_dark.conf +0 -42
  175. novelwriter/assets/syntax/default_light.conf +0 -42
  176. novelwriter/assets/syntax/dracula.conf +0 -44
  177. novelwriter/assets/syntax/grey_dark.conf +0 -29
  178. novelwriter/assets/syntax/grey_light.conf +0 -29
  179. novelwriter/assets/syntax/light_owl.conf +0 -49
  180. novelwriter/assets/syntax/night_owl.conf +0 -49
  181. novelwriter/assets/syntax/snazzy.conf +0 -42
  182. novelwriter/assets/syntax/solarized_dark.conf +0 -29
  183. novelwriter/assets/syntax/solarized_light.conf +0 -29
  184. novelwriter/assets/syntax/tango.conf +0 -39
  185. novelwriter/assets/syntax/tomorrow.conf +0 -49
  186. novelwriter/assets/syntax/tomorrow_night.conf +0 -49
  187. novelwriter/assets/syntax/tomorrow_night_blue.conf +0 -49
  188. novelwriter/assets/syntax/tomorrow_night_bright.conf +0 -49
  189. novelwriter/assets/syntax/tomorrow_night_eighties.conf +0 -49
  190. novelwriter/assets/themes/default.conf +0 -3
  191. novelwriter-2.7.4.dist-info/RECORD +0 -163
  192. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/WHEEL +0 -0
  193. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/entry_points.txt +0 -0
  194. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/licenses/LICENSE.md +0 -0
  195. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/licenses/setup/LICENSE-Apache-2.0.txt +0 -0
  196. {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/top_level.txt +0 -0
novelwriter/shared.py CHANGED
@@ -21,10 +21,11 @@ General Public License for more details.
21
21
 
22
22
  You should have received a copy of the GNU General Public License
23
23
  along with this program. If not, see <https://www.gnu.org/licenses/>.
24
- """
24
+ """ # noqa
25
25
  from __future__ import annotations
26
26
 
27
27
  import logging
28
+ import re
28
29
 
29
30
  from enum import Enum
30
31
  from pathlib import Path
@@ -32,15 +33,17 @@ from time import time
32
33
  from typing import TYPE_CHECKING, TypeVar
33
34
 
34
35
  from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot
35
- from PyQt6.QtGui import QDesktopServices, QFont
36
- from PyQt6.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget
36
+ from PyQt6.QtGui import QDesktopServices, QFont, QScreen
37
+ from PyQt6.QtWidgets import QApplication, QFileDialog, QFontDialog, QMessageBox, QWidget
37
38
 
38
39
  from novelwriter.common import formatFileFilter
39
40
  from novelwriter.constants import nwFiles
40
41
  from novelwriter.core.spellcheck import NWSpellEnchant
41
- from novelwriter.enum import nwChange, nwItemClass
42
+ from novelwriter.enum import nwChange, nwItemClass, nwStandardButton
42
43
 
43
44
  if TYPE_CHECKING:
45
+ from collections.abc import Callable
46
+
44
47
  from novelwriter.core.project import NWProject
45
48
  from novelwriter.core.status import T_StatusKind
46
49
  from novelwriter.gui.theme import GuiTheme
@@ -50,8 +53,16 @@ logger = logging.getLogger(__name__)
50
53
 
51
54
  NWWidget = TypeVar("NWWidget", bound=QWidget)
52
55
 
56
+ RX_HTML = re.compile(r"<.*?>")
57
+
53
58
 
54
59
  class SharedData(QObject):
60
+ """Shared Data Singleton.
61
+
62
+ This is the class instantiated as the SHARED singleton. It holds
63
+ various globally needed data and pointers to important objects like
64
+ the main GUI, the current project, and the GUI theme.
65
+ """
55
66
 
56
67
  __slots__ = (
57
68
  "_gui", "_idleRefTime", "_idleTime", "_lastAlert", "_lockedBy",
@@ -91,8 +102,6 @@ class SharedData(QObject):
91
102
  self._clock.setInterval(1000)
92
103
  self._clock.timeout.connect(lambda: self.mainClockTick.emit())
93
104
 
94
- return
95
-
96
105
  ##
97
106
  # Properties
98
107
  ##
@@ -150,6 +159,11 @@ class SharedData(QObject):
150
159
  """Return the last alert message."""
151
160
  return self._lastAlert
152
161
 
162
+ @property
163
+ def mainScreen(self) -> QScreen | None:
164
+ """Return the screen of the main window."""
165
+ return QApplication.screenAt(self.mainGui.rect().center())
166
+
153
167
  ##
154
168
  # Setters
155
169
  ##
@@ -159,7 +173,6 @@ class SharedData(QObject):
159
173
  if state is not self._focusMode:
160
174
  self._focusMode = state
161
175
  self.focusModeChanged.emit(state)
162
- return
163
176
 
164
177
  ##
165
178
  # Methods
@@ -170,7 +183,7 @@ class SharedData(QObject):
170
183
  is created.
171
184
  """
172
185
  self._theme = theme
173
- return
186
+ self._theme.initThemes()
174
187
 
175
188
  def initSharedData(self, gui: GuiMain) -> None:
176
189
  """Initialise the SharedData instance. This must be called as
@@ -183,7 +196,6 @@ class SharedData(QObject):
183
196
  logger.debug("Ready: SharedData")
184
197
  if pool := QThreadPool.globalInstance():
185
198
  logger.debug("Thread Pool Max Count: %d", pool.maxThreadCount())
186
- return
187
199
 
188
200
  def closeDocument(self, tHandle: str | None = None) -> None:
189
201
  """Close the document editor, optionally a specific document."""
@@ -191,7 +203,6 @@ class SharedData(QObject):
191
203
  self.mainGui.closeDocument()
192
204
  if tHandle is None or tHandle == self.mainGui.docViewer.docHandle:
193
205
  self.mainGui.closeViewerPanel()
194
- return
195
206
 
196
207
  def saveEditor(self, tHandle: str | None = None) -> None:
197
208
  """Save the editor content, optionally a specific document."""
@@ -202,7 +213,6 @@ class SharedData(QObject):
202
213
  ):
203
214
  logger.debug("Saving editor document before action")
204
215
  docEditor.saveText()
205
- return
206
216
 
207
217
  def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
208
218
  """Open a project."""
@@ -235,7 +245,6 @@ class SharedData(QObject):
235
245
  self.project.closeProject(self._idleTime)
236
246
  self._resetProject()
237
247
  self._resetIdleTimer()
238
- return
239
248
 
240
249
  def updateSpellCheckLanguage(self, reload: bool = False) -> None:
241
250
  """Update the active spell check language from settings."""
@@ -245,7 +254,6 @@ class SharedData(QObject):
245
254
  self.spelling.setLanguage(language)
246
255
  _, provider = self.spelling.describeDict()
247
256
  self.spellLanguageChanged.emit(language, provider)
248
- return
249
257
 
250
258
  def updateIdleTime(self, currTime: float, userIdle: bool) -> None:
251
259
  """Update the idle time record. If the userIdle flag is True,
@@ -256,27 +264,40 @@ class SharedData(QObject):
256
264
  if userIdle:
257
265
  self._idleTime += currTime - self._idleRefTime
258
266
  self._idleRefTime = currTime
259
- return
267
+
268
+ def initMainProgress(self, maximum: int, inclusive: bool = False) -> None:
269
+ """Start a session for the main progress bar."""
270
+ if gui := self._gui:
271
+ gui.mainProgress.setMaximum(maximum - (1 if inclusive else 0))
272
+ gui.mainProgress.setValue(0)
273
+
274
+ def incMainProgress(self) -> None:
275
+ """Increment the value for the main progress bar."""
276
+ if gui := self._gui:
277
+ gui.mainProgress.setValue(gui.mainProgress.value() + 1)
278
+ QApplication.processEvents()
279
+
280
+ def clearMainProgress(self, delay: float = 1.0) -> None:
281
+ """Clear the main progress bar."""
282
+ if gui := self._gui:
283
+ QTimer.singleShot(int(delay*1000), gui.mainProgress.reset)
260
284
 
261
285
  def newStatusMessage(self, message: str) -> None:
262
286
  """Request a new status message. This is a callable function for
263
287
  core classes that cannot emit signals on their own.
264
288
  """
265
289
  self.projectStatusMessage.emit(message)
266
- return
267
290
 
268
291
  def setGlobalProjectState(self, state: bool) -> None:
269
292
  """Change the global project status. This is a callable function
270
293
  for core classes that cannot emit signals on their own.
271
294
  """
272
295
  self.projectStatusChanged.emit(state)
273
- return
274
296
 
275
297
  def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None:
276
298
  """Queue a runnable in the application thread pool."""
277
299
  if pool := QThreadPool.globalInstance():
278
300
  pool.start(runnable, priority=priority)
279
- return
280
301
 
281
302
  def getProjectPath(
282
303
  self, parent: QWidget,
@@ -315,13 +336,11 @@ class SharedData(QObject):
315
336
  def openWebsite(self, url: str) -> None:
316
337
  """Open a URL in the system's default browser."""
317
338
  QDesktopServices.openUrl(QUrl(url))
318
- return
319
339
 
320
340
  @pyqtSlot(str, nwItemClass)
321
341
  def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
322
342
  """Process new note request."""
323
343
  self.project.createNewNote(tag, itemClass)
324
- return
325
344
 
326
345
  ##
327
346
  # Signal Proxies
@@ -333,37 +352,31 @@ class SharedData(QObject):
333
352
  """Emit the indexChangedTags signal."""
334
353
  if self._project and self._project.data.uuid == project.data.uuid:
335
354
  self.indexChangedTags.emit(updated, deleted)
336
- return
337
355
 
338
356
  def emitIndexCleared(self, project: NWProject) -> None:
339
357
  """Emit the indexCleared signal."""
340
358
  if self._project and self._project.data.uuid == project.data.uuid:
341
359
  self.indexCleared.emit()
342
- return
343
360
 
344
361
  def emitIndexAvailable(self, project: NWProject) -> None:
345
362
  """Emit the indexAvailable signal."""
346
363
  if self._project and self._project.data.uuid == project.data.uuid:
347
364
  self.indexAvailable.emit()
348
- return
349
365
 
350
366
  def emitStatusLabelsChanged(self, project: NWProject, kind: T_StatusKind) -> None:
351
367
  """Emit the statusLabelsChanged signal."""
352
368
  if self._project and self._project.data.uuid == project.data.uuid:
353
369
  self.statusLabelsChanged.emit(kind)
354
- return
355
370
 
356
371
  def emitProjectItemChanged(self, project: NWProject, handle: str, change: nwChange) -> None:
357
372
  """Emit the projectItemChanged signal."""
358
373
  if self._project and self._project.data.uuid == project.data.uuid:
359
374
  self.projectItemChanged.emit(handle, change)
360
- return
361
375
 
362
376
  def emitRootFolderChanged(self, project: NWProject, handle: str, change: nwChange) -> None:
363
377
  """Emit the rootFolderChanged signal."""
364
378
  if self._project and self._project.data.uuid == project.data.uuid:
365
379
  self.rootFolderChanged.emit(handle, change)
366
- return
367
380
 
368
381
  ##
369
382
  # Alert Boxes
@@ -376,9 +389,8 @@ class SharedData(QObject):
376
389
  alert.setAlertType(_GuiAlert.INFO, False)
377
390
  self._lastAlert = alert.logMessage
378
391
  if log:
379
- logger.info(self._lastAlert, stacklevel=2)
392
+ self._logMessage(self._lastAlert, logger.info)
380
393
  alert.exec()
381
- return
382
394
 
383
395
  def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None:
384
396
  """Open a warning alert box."""
@@ -387,9 +399,8 @@ class SharedData(QObject):
387
399
  alert.setAlertType(_GuiAlert.WARN, False)
388
400
  self._lastAlert = alert.logMessage
389
401
  if log:
390
- logger.warning(self._lastAlert, stacklevel=2)
402
+ self._logMessage(self._lastAlert, logger.warning)
391
403
  alert.exec()
392
- return
393
404
 
394
405
  def error(self, text: str, info: str = "", details: str = "", log: bool = True,
395
406
  exc: Exception | None = None) -> None:
@@ -401,9 +412,8 @@ class SharedData(QObject):
401
412
  alert.setException(exc)
402
413
  self._lastAlert = alert.logMessage
403
414
  if log:
404
- logger.error(self._lastAlert, stacklevel=2)
415
+ self._logMessage(self._lastAlert, logger.error)
405
416
  alert.exec()
406
- return
407
417
 
408
418
  def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool:
409
419
  """Open a question box."""
@@ -412,13 +422,17 @@ class SharedData(QObject):
412
422
  alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True)
413
423
  self._lastAlert = alert.logMessage
414
424
  alert.exec()
415
- isYes = alert.result() == QMessageBox.StandardButton.Yes
416
- return isYes
425
+ return alert.finalState
417
426
 
418
427
  ##
419
428
  # Internal Functions
420
429
  ##
421
430
 
431
+ def _logMessage(self, message: str, log: Callable) -> None:
432
+ """Print message to log."""
433
+ for text in message.split("<br>"):
434
+ log(RX_HTML.sub("", text), stacklevel=3)
435
+
422
436
  def _resetProject(self) -> None:
423
437
  """Create a new project and spell checking instance."""
424
438
  from novelwriter.core.project import NWProject
@@ -430,13 +444,11 @@ class SharedData(QObject):
430
444
  self._spelling = NWSpellEnchant(self._project)
431
445
  self.updateSpellCheckLanguage()
432
446
  self._focusMode = False
433
- return
434
447
 
435
448
  def _resetIdleTimer(self) -> None:
436
449
  """Reset the timer data for the idle timer."""
437
450
  self._idleRefTime = time()
438
451
  self._idleTime = 0.0
439
- return
440
452
 
441
453
  def _closeToolDialogs(self) -> None:
442
454
  """Close all open tool dialogs."""
@@ -444,7 +456,6 @@ class SharedData(QObject):
444
456
  for widget in self.mainGui.children():
445
457
  if isinstance(widget, NToolDialog):
446
458
  widget.close()
447
- return
448
459
 
449
460
 
450
461
  class _GuiAlert(QMessageBox):
@@ -458,51 +469,71 @@ class _GuiAlert(QMessageBox):
458
469
  super().__init__(parent=parent)
459
470
  self._theme = theme
460
471
  self._message = ""
472
+ self._state = False
461
473
  logger.debug("Ready: _GuiAlert")
462
- return
463
474
 
464
475
  def __del__(self) -> None: # pragma: no cover
465
476
  logger.debug("Delete: _GuiAlert")
466
- return
467
477
 
468
478
  @property
469
479
  def logMessage(self) -> str:
470
480
  return self._message
471
481
 
482
+ @property
483
+ def finalState(self) -> bool:
484
+ return self._state
485
+
472
486
  def setMessage(self, text: str, info: str, details: str) -> None:
473
487
  """Set the alert box message."""
474
488
  self._message = " ".join(filter(None, [text, info, details]))
475
489
  self.setText(text)
476
490
  self.setInformativeText(info)
477
491
  self.setDetailedText(details)
478
- return
479
492
 
480
493
  def setException(self, exception: Exception) -> None:
481
494
  """Add exception details."""
482
495
  info = self.informativeText()
483
496
  text = f"<b>{type(exception).__name__}</b>: {exception!s}"
484
497
  self.setInformativeText(f"{info}<br>{text}" if info else text)
485
- return
486
498
 
487
499
  def setAlertType(self, level: int, isYesNo: bool) -> None:
488
500
  """Set the type of alert and whether the dialog should have
489
501
  Yes/No buttons or just an Ok button.
490
502
  """
491
503
  if isYesNo:
492
- self.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
504
+ self._btnYes = self._theme.getStandardButton(nwStandardButton.YES, self)
505
+ self._btnYes.clicked.connect(self._onAccept)
506
+ self._btnNo = self._theme.getStandardButton(nwStandardButton.NO, self)
507
+ self._btnNo.clicked.connect(self._onReject)
508
+ self.addButton(self._btnYes, QMessageBox.ButtonRole.YesRole)
509
+ self.addButton(self._btnNo, QMessageBox.ButtonRole.NoRole)
493
510
  else:
494
- self.setStandardButtons(QMessageBox.StandardButton.Ok)
495
- pSz = 2*self._theme.baseIconHeight
511
+ self._btnOk = self._theme.getStandardButton(nwStandardButton.OK, self)
512
+ self._btnOk.clicked.connect(self._onAccept)
513
+ self.addButton(self._btnOk, QMessageBox.ButtonRole.AcceptRole)
514
+
515
+ pSz = 2*self._theme.fontPixelSize
496
516
  if level == self.INFO:
497
- self.setIconPixmap(self._theme.getPixmap("alert_info", (pSz, pSz), "blue"))
517
+ self.setIconPixmap(self._theme.getPixmap("alert_info", (pSz, pSz), "info"))
498
518
  self.setWindowTitle(self.tr("Information"))
499
519
  elif level == self.WARN:
500
- self.setIconPixmap(self._theme.getPixmap("alert_warn", (pSz, pSz), "orange"))
520
+ self.setIconPixmap(self._theme.getPixmap("alert_warn", (pSz, pSz), "warning"))
501
521
  self.setWindowTitle(self.tr("Warning"))
502
522
  elif level == self.ERROR:
503
- self.setIconPixmap(self._theme.getPixmap("alert_error", (pSz, pSz), "red"))
523
+ self.setIconPixmap(self._theme.getPixmap("alert_error", (pSz, pSz), "error"))
504
524
  self.setWindowTitle(self.tr("Error"))
505
525
  elif level == self.ASK:
506
- self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz), "blue"))
526
+ self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz), "info"))
507
527
  self.setWindowTitle(self.tr("Question"))
508
- return
528
+
529
+ @pyqtSlot()
530
+ def _onAccept(self) -> None:
531
+ """Process accepted state."""
532
+ self._state = True
533
+ self.close()
534
+
535
+ @pyqtSlot()
536
+ def _onReject(self) -> None:
537
+ """Process rejected state."""
538
+ self._state = False
539
+ self.close()
novelwriter/splash.py CHANGED
@@ -20,7 +20,7 @@ General Public License for more details.
20
20
 
21
21
  You should have received a copy of the GNU General Public License
22
22
  along with this program. If not, see <https://www.gnu.org/licenses/>.
23
- """
23
+ """ # noqa
24
24
  from __future__ import annotations
25
25
 
26
26
  import logging
@@ -38,6 +38,14 @@ SPLASH_IMG = Path(__file__).parent / "assets" / "images" / "splash.png"
38
38
 
39
39
 
40
40
  class NSplashScreen(QSplashScreen):
41
+ """GUI: App Launch Splash Screen.
42
+
43
+ A small splash screen that is shown as novelWriter starts up. Its
44
+ primary purpose is to provide user feedback that the app is being
45
+ initiated when there are delays in the process while Qt waits for
46
+ responses from the OS, or has to load particularly large data sets
47
+ like when the system has a lot of fonts installed.
48
+ """
41
49
 
42
50
  __slots__ = ("_color", "_rect", "_text")
43
51
 
@@ -52,17 +60,14 @@ class NSplashScreen(QSplashScreen):
52
60
  self._color = QColor(26, 52, 78)
53
61
  self._rect = QRect(144, 110, 440, 30)
54
62
  self._text = ""
55
- return
56
63
 
57
64
  def __del__(self) -> None: # pragma: no cover
58
65
  logger.debug("Delete: NSplashScreen")
59
- return
60
66
 
61
67
  def drawContents(self, painter: QPainter) -> None:
62
68
  """Draw the text message."""
63
69
  painter.setPen(self._color)
64
70
  painter.drawText(self._rect, Qt.AlignmentFlag.AlignLeft, self._text)
65
- return
66
71
 
67
72
  def showStatus(self, message: str) -> None:
68
73
  """Update the status message."""
@@ -71,4 +76,3 @@ class NSplashScreen(QSplashScreen):
71
76
  if message:
72
77
  logger.info("[Splash] %s", message)
73
78
  sleep(0.025)
74
- return
@@ -21,7 +21,7 @@ General Public License for more details.
21
21
 
22
22
  You should have received a copy of the GNU General Public License
23
23
  along with this program. If not, see <https://www.gnu.org/licenses/>.
24
- """
24
+ """ # noqa
25
25
  from __future__ import annotations
26
26
 
27
27
  from novelwriter.enum import nwComment
@@ -22,7 +22,7 @@ General Public License for more details.
22
22
 
23
23
  You should have received a copy of the GNU General Public License
24
24
  along with this program. If not, see <https://www.gnu.org/licenses/>.
25
- """
25
+ """ # noqa
26
26
  from __future__ import annotations
27
27
 
28
28
  import re
@@ -74,9 +74,11 @@ def preProcessText(text: str, keepHeaders: bool = True) -> list[str]:
74
74
 
75
75
 
76
76
  def standardCounter(text: str) -> tuple[int, int, int]:
77
- """A counter that counts paragraphs, words and characters.
78
- This is the standard counter that includes headings in the word and
79
- character counts.
77
+ """Return a standard count.
78
+
79
+ A counter that counts paragraphs, words and characters. This is the
80
+ standard counter that includes headings in the word and character
81
+ counts.
80
82
  """
81
83
  cCount = 0
82
84
  wCount = 0
@@ -124,7 +126,9 @@ def standardCounter(text: str) -> tuple[int, int, int]:
124
126
 
125
127
 
126
128
  def bodyTextCounter(text: str) -> tuple[int, int, int]:
127
- """A counter that counts body text words, characters, and characters
129
+ """Return a body text count.
130
+
131
+ A counter that counts body text words, characters, and characters
128
132
  without white spaces.
129
133
  """
130
134
  wCount = 0
@@ -21,7 +21,7 @@ General Public License for more details.
21
21
 
22
22
  You should have received a copy of the GNU General Public License
23
23
  along with this program. If not, see <https://www.gnu.org/licenses/>.
24
- """
24
+ """ # noqa
25
25
  from __future__ import annotations
26
26
 
27
27
  import re
@@ -32,18 +32,20 @@ from novelwriter.constants import nwRegEx, nwUnicode
32
32
 
33
33
 
34
34
  class RegExPatterns:
35
+ """Compiled RegEx Patterns."""
35
36
 
36
37
  AMBIGUOUS = (nwUnicode.U_APOS, nwUnicode.U_RSQUO)
37
38
 
38
39
  # Static RegExes
39
40
  _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)
41
+ _rxWords = re.compile(nwRegEx.WORDS)
42
+ _rxBreak = re.compile(nwRegEx.BREAK)
43
+ _rxItalic = re.compile(nwRegEx.FMT_EI)
44
+ _rxBold = re.compile(nwRegEx.FMT_EB)
45
+ _rxStrike = re.compile(nwRegEx.FMT_ST)
46
+ _rxMark = re.compile(nwRegEx.FMT_HL)
47
+ _rxSCPlain = re.compile(nwRegEx.FMT_SC)
48
+ _rxSCValue = re.compile(nwRegEx.FMT_SV)
47
49
 
48
50
  @property
49
51
  def url(self) -> re.Pattern:
@@ -75,6 +77,11 @@ class RegExPatterns:
75
77
  """Markdown strikethrough style."""
76
78
  return self._rxStrike
77
79
 
80
+ @property
81
+ def markdownMark(self) -> re.Pattern:
82
+ """Markdown highlight style."""
83
+ return self._rxMark
84
+
78
85
  @property
79
86
  def shortcodePlain(self) -> re.Pattern:
80
87
  """Plain shortcode style."""
@@ -108,7 +115,7 @@ class RegExPatterns:
108
115
  rx.append(f"(?:{qO}[^{qO}]+{qC})")
109
116
  if CONFIG.allowOpenDial:
110
117
  rx.append(f"(?:{qO}.+?$)")
111
- return re.compile("|".join(rx), re.UNICODE)
118
+ return re.compile("|".join(rx))
112
119
  return None
113
120
 
114
121
  @property
@@ -118,7 +125,7 @@ class RegExPatterns:
118
125
  qO = re.escape(compact(CONFIG.altDialogOpen))
119
126
  qC = re.escape(compact(CONFIG.altDialogClose))
120
127
  qB = r"\B" if (qO == qC or qC in self.AMBIGUOUS) else ""
121
- return re.compile(f"{qO}.*?{qC}{qB}", re.UNICODE)
128
+ return re.compile(f"{qO}.*?{qC}{qB}")
122
129
  return None
123
130
 
124
131
 
@@ -126,6 +133,7 @@ REGEX_PATTERNS = RegExPatterns()
126
133
 
127
134
 
128
135
  class DialogParser:
136
+ """A callable parser for finding dialog regions in text."""
129
137
 
130
138
  __slots__ = (
131
139
  "_alternate", "_breakD", "_breakQ", "_dialog", "_enabled", "_mode",
@@ -141,7 +149,6 @@ class DialogParser:
141
149
  self._breakD = None
142
150
  self._breakQ = None
143
151
  self._mode = ""
144
- return
145
152
 
146
153
  @property
147
154
  def enabled(self) -> bool:
@@ -163,13 +170,11 @@ class DialogParser:
163
170
  # Build narrator break RegExes
164
171
  if narrator := CONFIG.narratorBreak.strip()[:1]:
165
172
  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)
173
+ self._breakD = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?|$)")
174
+ self._breakQ = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?)")
168
175
  self._narrator = narrator
169
176
  self._mode = f" {narrator}"
170
177
 
171
- return
172
-
173
178
  def __call__(self, text: str) -> list[tuple[int, int]]:
174
179
  """Caller wrapper for dialogue processing."""
175
180
  temp: list[int] = []
@@ -20,7 +20,7 @@ General Public License for more details.
20
20
 
21
21
  You should have received a copy of the GNU General Public License
22
22
  along with this program. If not, see <https://www.gnu.org/licenses/>.
23
- """
23
+ """ # noqa
24
24
  from __future__ import annotations
25
25
 
26
26
  import logging
@@ -37,14 +37,21 @@ from PyQt6.QtWidgets import (
37
37
 
38
38
  from novelwriter import CONFIG, SHARED
39
39
  from novelwriter.common import formatFileFilter, formatInt, getFileSize, openExternalPath
40
+ from novelwriter.enum import nwStandardButton
40
41
  from novelwriter.error import formatException
41
42
  from novelwriter.extensions.modified import NIconToolButton, NNonBlockingDialog
42
- from novelwriter.types import QtDialogClose, QtHexArgb
43
+ from novelwriter.types import QtHexArgb, QtRoleDestruct
43
44
 
44
45
  logger = logging.getLogger(__name__)
45
46
 
46
47
 
47
48
  class GuiDictionaries(NNonBlockingDialog):
49
+ """GUI: Spell Check Dictionary Tool.
50
+
51
+ A helper tool for downloading and extracting dictionaries to a
52
+ location where Enchant can find them. This tool is only needed on
53
+ Windows.
54
+ """
48
55
 
49
56
  def __init__(self, parent: QWidget) -> None:
50
57
  super().__init__(parent=parent)
@@ -72,10 +79,10 @@ class GuiDictionaries(NNonBlockingDialog):
72
79
  self.huInfo.setOpenExternalLinks(True)
73
80
  self.huInfo.setWordWrap(True)
74
81
  self.huInput = QLineEdit(self)
75
- self.huBrowse = NIconToolButton(self, iSz, "browse")
82
+ self.huBrowse = NIconToolButton(self, iSz, "browse", "systemio")
76
83
  self.huBrowse.clicked.connect(self._doBrowseHunspell)
77
84
  self.huImport = QPushButton(self.tr("Add Dictionary"), self)
78
- self.huImport.setIcon(SHARED.theme.getIcon("add", "green"))
85
+ self.huImport.setIcon(SHARED.theme.getIcon("add", "add"))
79
86
  self.huImport.clicked.connect(self._doImportHunspell)
80
87
 
81
88
  self.huPathBox = QHBoxLayout()
@@ -90,7 +97,7 @@ class GuiDictionaries(NNonBlockingDialog):
90
97
  self.inInfo = QLabel(self.tr("Dictionary install location"), self)
91
98
  self.inPath = QLineEdit(self)
92
99
  self.inPath.setReadOnly(True)
93
- self.inBrowse = NIconToolButton(self, iSz, "browse")
100
+ self.inBrowse = NIconToolButton(self, iSz, "browse", "systemio")
94
101
  self.inBrowse.clicked.connect(self._doOpenInstallLocation)
95
102
 
96
103
  self.inBox = QHBoxLayout()
@@ -104,8 +111,11 @@ class GuiDictionaries(NNonBlockingDialog):
104
111
  self.infoBox.setFrameStyle(QFrame.Shape.NoFrame)
105
112
 
106
113
  # Buttons
107
- self.buttonBox = QDialogButtonBox(QtDialogClose, self)
108
- self.buttonBox.rejected.connect(self.reject)
114
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
115
+ self.btnClose.clicked.connect(self.reject)
116
+
117
+ self.btnBox = QDialogButtonBox(self)
118
+ self.btnBox.addButton(self.btnClose, QtRoleDestruct)
109
119
 
110
120
  # Assemble
111
121
  self.outerBox = QVBoxLayout()
@@ -117,17 +127,14 @@ class GuiDictionaries(NNonBlockingDialog):
117
127
  self.outerBox.addLayout(self.inBox, 0)
118
128
  self.outerBox.addWidget(self.infoBox, 1)
119
129
  self.outerBox.addSpacing(8)
120
- self.outerBox.addWidget(self.buttonBox, 0)
130
+ self.outerBox.addWidget(self.btnBox, 0)
121
131
 
122
132
  self.setLayout(self.outerBox)
123
133
 
124
134
  logger.debug("Ready: GuiDictionaries")
125
135
 
126
- return
127
-
128
136
  def __del__(self) -> None: # pragma: no cover
129
137
  logger.debug("Delete: GuiDictionaries")
130
- return
131
138
 
132
139
  def initDialog(self) -> bool:
133
140
  """Prepare and check that we can proceed."""
@@ -164,7 +171,6 @@ class GuiDictionaries(NNonBlockingDialog):
164
171
  """Capture the user closing the window."""
165
172
  event.accept()
166
173
  self.softDelete()
167
- return
168
174
 
169
175
  ##
170
176
  # Private Slots
@@ -182,7 +188,6 @@ class GuiDictionaries(NNonBlockingDialog):
182
188
  if soxFile:
183
189
  path = Path(soxFile).absolute()
184
190
  self.huInput.setText(str(path))
185
- return
186
191
 
187
192
  @pyqtSlot()
188
193
  def _doImportHunspell(self) -> None:
@@ -202,14 +207,12 @@ class GuiDictionaries(NNonBlockingDialog):
202
207
  self._appendLog(formatException(exc), err=True)
203
208
  else:
204
209
  self._appendLog(procErr, err=True)
205
- return
206
210
 
207
211
  @pyqtSlot()
208
212
  def _doOpenInstallLocation(self) -> None:
209
213
  """Open the dictionary folder."""
210
214
  if not openExternalPath(Path(self.inPath.text())):
211
215
  SHARED.error("Path not found.")
212
- return
213
216
 
214
217
  ##
215
218
  # Internal Functions
@@ -247,4 +250,3 @@ class GuiDictionaries(NNonBlockingDialog):
247
250
  cursor.movePosition(QTextCursor.MoveOperation.End)
248
251
  cursor.deleteChar()
249
252
  self.infoBox.setTextCursor(cursor)
250
- return