novelWriter 2.2b1__py3-none-any.whl → 2.2.1__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 (134) hide show
  1. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/RECORD +128 -114
  3. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +10 -5
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  13. novelwriter/assets/i18n/project_de_DE.json +1 -0
  14. novelwriter/assets/i18n/project_en_GB.json +1 -0
  15. novelwriter/assets/i18n/project_en_US.json +1 -0
  16. novelwriter/assets/i18n/project_es_419.json +11 -0
  17. novelwriter/assets/i18n/project_fr_FR.json +11 -0
  18. novelwriter/assets/i18n/project_it_IT.json +11 -0
  19. novelwriter/assets/i18n/project_ja_JP.json +2 -1
  20. novelwriter/assets/i18n/project_nb_NO.json +1 -0
  21. novelwriter/assets/i18n/project_zh_CN.json +11 -0
  22. novelwriter/assets/icons/novelwriter.ico +0 -0
  23. novelwriter/assets/icons/typicons_dark/icons.conf +11 -3
  24. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  25. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  27. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  28. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  29. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
  30. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
  31. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
  32. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
  33. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
  34. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
  35. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
  36. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
  37. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
  38. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/icons.conf +11 -3
  40. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  41. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  42. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  44. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  45. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
  46. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
  47. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
  48. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
  49. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
  50. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
  51. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
  52. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
  53. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
  54. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  55. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  56. novelwriter/assets/manual.pdf +0 -0
  57. novelwriter/assets/sample.zip +0 -0
  58. novelwriter/assets/text/release_notes.htm +50 -7
  59. novelwriter/common.py +35 -27
  60. novelwriter/config.py +13 -28
  61. novelwriter/constants.py +21 -4
  62. novelwriter/core/buildsettings.py +2 -2
  63. novelwriter/core/coretools.py +8 -2
  64. novelwriter/core/docbuild.py +1 -1
  65. novelwriter/core/document.py +1 -1
  66. novelwriter/core/index.py +102 -36
  67. novelwriter/core/item.py +2 -2
  68. novelwriter/core/options.py +6 -3
  69. novelwriter/core/project.py +5 -5
  70. novelwriter/core/projectdata.py +3 -3
  71. novelwriter/core/projectxml.py +1 -1
  72. novelwriter/core/sessions.py +2 -2
  73. novelwriter/core/spellcheck.py +4 -3
  74. novelwriter/core/status.py +3 -3
  75. novelwriter/core/storage.py +1 -1
  76. novelwriter/core/tohtml.py +11 -5
  77. novelwriter/core/tokenizer.py +28 -21
  78. novelwriter/core/tomd.py +6 -2
  79. novelwriter/core/toodt.py +12 -5
  80. novelwriter/core/tree.py +2 -2
  81. novelwriter/dialogs/about.py +30 -31
  82. novelwriter/dialogs/docmerge.py +24 -15
  83. novelwriter/dialogs/docsplit.py +27 -16
  84. novelwriter/dialogs/editlabel.py +19 -7
  85. novelwriter/dialogs/preferences.py +116 -131
  86. novelwriter/dialogs/projdetails.py +29 -36
  87. novelwriter/dialogs/projload.py +32 -36
  88. novelwriter/dialogs/projsettings.py +20 -15
  89. novelwriter/dialogs/quotes.py +32 -25
  90. novelwriter/dialogs/updates.py +17 -16
  91. novelwriter/dialogs/wordlist.py +34 -21
  92. novelwriter/enum.py +19 -8
  93. novelwriter/error.py +1 -1
  94. novelwriter/extensions/circularprogress.py +1 -1
  95. novelwriter/extensions/configlayout.py +3 -15
  96. novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
  97. novelwriter/extensions/novelselector.py +1 -1
  98. novelwriter/extensions/pageddialog.py +1 -1
  99. novelwriter/extensions/pagedsidebar.py +2 -5
  100. novelwriter/extensions/simpleprogress.py +8 -9
  101. novelwriter/extensions/statusled.py +1 -1
  102. novelwriter/extensions/switch.py +4 -4
  103. novelwriter/extensions/switchbox.py +1 -6
  104. novelwriter/gui/doceditor.py +349 -236
  105. novelwriter/gui/dochighlight.py +10 -11
  106. novelwriter/gui/docviewer.py +158 -360
  107. novelwriter/gui/docviewerpanel.py +502 -0
  108. novelwriter/gui/editordocument.py +4 -4
  109. novelwriter/gui/itemdetails.py +2 -2
  110. novelwriter/gui/mainmenu.py +50 -36
  111. novelwriter/gui/noveltree.py +44 -53
  112. novelwriter/gui/outline.py +12 -7
  113. novelwriter/gui/projtree.py +465 -381
  114. novelwriter/gui/sidebar.py +9 -7
  115. novelwriter/gui/statusbar.py +48 -5
  116. novelwriter/gui/theme.py +26 -8
  117. novelwriter/guimain.py +212 -208
  118. novelwriter/shared.py +76 -30
  119. novelwriter/tools/dictionaries.py +268 -0
  120. novelwriter/tools/lipsum.py +34 -28
  121. novelwriter/tools/manusbuild.py +20 -10
  122. novelwriter/tools/manuscript.py +20 -27
  123. novelwriter/tools/manussettings.py +2 -4
  124. novelwriter/tools/projwizard.py +3 -3
  125. novelwriter/tools/writingstats.py +18 -5
  126. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
  127. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
  128. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  129. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
  130. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
  131. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  132. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/LICENSE.md +0 -0
  133. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/entry_points.txt +0 -0
  134. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/top_level.txt +0 -0
novelwriter/shared.py CHANGED
@@ -3,10 +3,11 @@ novelWriter – Shared Data Class
3
3
  ===============================
4
4
 
5
5
  File History:
6
- Created: 2023-08-10 [2.1rc1]
6
+ Created: 2023-08-10 [2.1rc1] SharedData
7
+ Created: 2023-08-14 [2.1rc1] _GuiAlert
7
8
 
8
9
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
10
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
11
 
11
12
  This program is free software: you can redistribute it and/or modify
12
13
  it under the terms of the GNU General Public License as published by
@@ -26,7 +27,7 @@ from __future__ import annotations
26
27
  import logging
27
28
 
28
29
  from time import time
29
- from typing import TYPE_CHECKING
30
+ from typing import TYPE_CHECKING, TypeVar
30
31
  from pathlib import Path
31
32
 
32
33
  from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal
@@ -41,17 +42,23 @@ if TYPE_CHECKING: # pragma: no cover
41
42
 
42
43
  logger = logging.getLogger(__name__)
43
44
 
45
+ NWWidget = TypeVar("NWWidget", bound=QWidget)
46
+
44
47
 
45
48
  class SharedData(QObject):
46
49
 
47
50
  __slots__ = (
48
- "_gui", "_theme", "_project", "_spelling", "_lockedBy", "_alert",
51
+ "_gui", "_theme", "_project", "_spelling", "_lockedBy", "_lastAlert",
49
52
  "_idleTime", "_idleRefTime",
50
53
  )
51
54
 
52
55
  projectStatusChanged = pyqtSignal(bool)
53
56
  projectStatusMessage = pyqtSignal(str)
54
57
  spellLanguageChanged = pyqtSignal(str, str)
58
+ indexScannedText = pyqtSignal(str)
59
+ indexChangedTags = pyqtSignal(list, list)
60
+ indexCleared = pyqtSignal()
61
+ indexAvailable = pyqtSignal()
55
62
 
56
63
  def __init__(self) -> None:
57
64
  super().__init__()
@@ -64,7 +71,7 @@ class SharedData(QObject):
64
71
 
65
72
  # Settings
66
73
  self._lockedBy = None
67
- self._alert = None
74
+ self._lastAlert = ""
68
75
  self._idleTime = 0.0
69
76
  self._idleRefTime = time()
70
77
 
@@ -118,9 +125,9 @@ class SharedData(QObject):
118
125
  return self._idleTime
119
126
 
120
127
  @property
121
- def alert(self) -> _GuiAlert | None:
122
- """Return a pointer to the last alert box."""
123
- return self._alert
128
+ def lastAlert(self) -> str:
129
+ """Return the last alert message."""
130
+ return self._lastAlert
124
131
 
125
132
  ##
126
133
  # Methods
@@ -171,7 +178,7 @@ class SharedData(QObject):
171
178
  return
172
179
 
173
180
  def updateSpellCheckLanguage(self, reload: bool = False) -> None:
174
- """Update the active spell check langauge from settings."""
181
+ """Update the active spell check language from settings."""
175
182
  from novelwriter import CONFIG
176
183
  language = self.project.data.spellLang or CONFIG.spellLanguage
177
184
  if language != self.spelling.spellLanguage or reload:
@@ -210,50 +217,84 @@ class SharedData(QObject):
210
217
  QThreadPool.globalInstance().start(runnable, priority=priority)
211
218
  return
212
219
 
220
+ def findTopLevelWidget(self, kind: type[NWWidget]) -> NWWidget | None:
221
+ """Find a top level widget."""
222
+ for widget in self.mainGui.children():
223
+ if isinstance(widget, kind):
224
+ return widget
225
+ return None
226
+
227
+ ##
228
+ # Signal Proxy
229
+ ##
230
+
231
+ def indexSignalProxy(self, data: dict) -> None:
232
+ """Emit signals on behalf of the index."""
233
+ event = data.get("event")
234
+ logger.debug("Received '%s' event from the index", event)
235
+ if event == "updateTags":
236
+ self.indexChangedTags.emit(data.get("updated", []), data.get("deleted", []))
237
+ elif event == "scanText":
238
+ self.indexScannedText.emit(data.get("handle", ""))
239
+ elif event == "clearIndex":
240
+ self.indexCleared.emit()
241
+ elif event == "buildIndex":
242
+ self.indexAvailable.emit()
243
+ return
244
+
213
245
  ##
214
246
  # Alert Boxes
215
247
  ##
216
248
 
217
249
  def info(self, text: str, info: str = "", details: str = "", log: bool = True) -> None:
218
250
  """Open an information alert box."""
219
- self._alert = _GuiAlert(self.mainGui, self.theme)
220
- self._alert.setMessage(text, info, details)
221
- self._alert.setAlertType(_GuiAlert.INFO, False)
251
+ alert = _GuiAlert(self.mainGui, self.theme)
252
+ alert.setMessage(text, info, details)
253
+ alert.setAlertType(_GuiAlert.INFO, False)
254
+ self._lastAlert = alert.logMessage
222
255
  if log:
223
- logger.info(self._alert.logMessage, stacklevel=2)
224
- self._alert.exec_()
256
+ logger.info(self._lastAlert, stacklevel=2)
257
+ alert.exec_()
258
+ alert.deleteLater()
225
259
  return
226
260
 
227
261
  def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None:
228
262
  """Open a warning alert box."""
229
- self._alert = _GuiAlert(self.mainGui, self.theme)
230
- self._alert.setMessage(text, info, details)
231
- self._alert.setAlertType(_GuiAlert.WARN, False)
263
+ alert = _GuiAlert(self.mainGui, self.theme)
264
+ alert.setMessage(text, info, details)
265
+ alert.setAlertType(_GuiAlert.WARN, False)
266
+ self._lastAlert = alert.logMessage
232
267
  if log:
233
- logger.warning(self._alert.logMessage, stacklevel=2)
234
- self._alert.exec_()
268
+ logger.warning(self._lastAlert, stacklevel=2)
269
+ alert.exec_()
270
+ alert.deleteLater()
235
271
  return
236
272
 
237
273
  def error(self, text: str, info: str = "", details: str = "", log: bool = True,
238
274
  exc: Exception | None = None) -> None:
239
275
  """Open an error alert box."""
240
- self._alert = _GuiAlert(self.mainGui, self.theme)
241
- self._alert.setMessage(text, info, details)
242
- self._alert.setAlertType(_GuiAlert.ERROR, False)
276
+ alert = _GuiAlert(self.mainGui, self.theme)
277
+ alert.setMessage(text, info, details)
278
+ alert.setAlertType(_GuiAlert.ERROR, False)
243
279
  if exc:
244
- self._alert.setException(exc)
280
+ alert.setException(exc)
281
+ self._lastAlert = alert.logMessage
245
282
  if log:
246
- logger.error(self._alert.logMessage, stacklevel=2)
247
- self._alert.exec_()
283
+ logger.error(self._lastAlert, stacklevel=2)
284
+ alert.exec_()
285
+ alert.deleteLater()
248
286
  return
249
287
 
250
288
  def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool:
251
289
  """Open a question box."""
252
- self._alert = _GuiAlert(self.mainGui, self.theme)
253
- self._alert.setMessage(text, info, details)
254
- self._alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True)
255
- self._alert.exec_()
256
- return self._alert.result() == QMessageBox.Yes
290
+ alert = _GuiAlert(self.mainGui, self.theme)
291
+ alert.setMessage(text, info, details)
292
+ alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True)
293
+ self._lastAlert = alert.logMessage
294
+ alert.exec_()
295
+ isYes = alert.result() == QMessageBox.StandardButton.Yes
296
+ alert.deleteLater()
297
+ return isYes
257
298
 
258
299
  ##
259
300
  # Internal Functions
@@ -290,6 +331,11 @@ class _GuiAlert(QMessageBox):
290
331
  super().__init__(parent=parent)
291
332
  self._theme = theme
292
333
  self._message = ""
334
+ logger.debug("Ready: _GuiAlert")
335
+ return
336
+
337
+ def __del__(self) -> None: # pragma: no cover
338
+ logger.debug("Delete: _GuiAlert")
293
339
  return
294
340
 
295
341
  @property
@@ -0,0 +1,268 @@
1
+ """
2
+ novelWriter – GUI Dictionary Downloader
3
+ =======================================
4
+
5
+ File History:
6
+ Created: 2023-11-19 [2.2rc1] GuiDictionaries
7
+
8
+ This file is a part of novelWriter
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
+
11
+ This program is free software: you can redistribute it and/or modify
12
+ it under the terms of the GNU General Public License as published by
13
+ the Free Software Foundation, either version 3 of the License, or
14
+ (at your option) any later version.
15
+
16
+ This program is distributed in the hope that it will be useful, but
17
+ WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19
+ General Public License for more details.
20
+
21
+ You should have received a copy of the GNU General Public License
22
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+
28
+ from pathlib import Path
29
+ from zipfile import ZipFile
30
+
31
+ from PyQt5.QtGui import QCloseEvent, QTextCursor
32
+ from PyQt5.QtCore import pyqtSlot
33
+ from PyQt5.QtWidgets import (
34
+ QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel,
35
+ QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget, qApp
36
+ )
37
+
38
+ from novelwriter import CONFIG, SHARED
39
+ from novelwriter.error import formatException
40
+ from novelwriter.common import openExternalPath, formatInt, getFileSize
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ class GuiDictionaries(QDialog):
46
+
47
+ def __init__(self, parent: QWidget) -> None:
48
+ super().__init__(parent=parent)
49
+
50
+ logger.debug("Create: GuiDictionaries")
51
+ self.setObjectName("GuiDictionaries")
52
+ self.setWindowTitle(self.tr("Add Dictionaries"))
53
+
54
+ self._installPath = None
55
+ self._currDicts = set()
56
+
57
+ iPx = CONFIG.pxInt(4)
58
+ mPx = CONFIG.pxInt(8)
59
+ sPx = CONFIG.pxInt(16)
60
+
61
+ self.setMinimumWidth(CONFIG.pxInt(500))
62
+ self.setMinimumHeight(CONFIG.pxInt(300))
63
+
64
+ # Hunspell Dictionaries
65
+ foUrl = "https://www.freeoffice.com/en/download/dictionaries"
66
+ loUrl = "https://extensions.libreoffice.org"
67
+ self.huInfo = QLabel("<br>".join([
68
+ self.tr("Download a dictionary from one of the links, and add it below."),
69
+ f"&nbsp;\u203a <a href='{foUrl}'>{foUrl}</a>",
70
+ f"&nbsp;\u203a <a href='{loUrl}'>{loUrl}</a>",
71
+ ]))
72
+ self.huInfo.setOpenExternalLinks(True)
73
+ self.huInfo.setWordWrap(True)
74
+ self.huInput = QLineEdit(self)
75
+ self.huBrowse = QPushButton(self)
76
+ self.huBrowse.setIcon(SHARED.theme.getIcon("browse"))
77
+ self.huBrowse.clicked.connect(self._doBrowseHunspell)
78
+ self.huImport = QPushButton(self.tr("Add Dictionary"), self)
79
+ self.huImport.setIcon(SHARED.theme.getIcon("add"))
80
+ self.huImport.clicked.connect(self._doImportHunspell)
81
+
82
+ self.huPathBox = QHBoxLayout()
83
+ self.huPathBox.addWidget(self.huInput)
84
+ self.huPathBox.addWidget(self.huBrowse)
85
+ self.huPathBox.setSpacing(iPx)
86
+ self.huAddBox = QHBoxLayout()
87
+ self.huAddBox.addStretch(1)
88
+ self.huAddBox.addWidget(self.huImport)
89
+
90
+ # Install Path
91
+ self.inInfo = QLabel(self.tr("Dictionary install location"))
92
+ self.inPath = QLineEdit(self)
93
+ self.inPath.setReadOnly(True)
94
+ self.inBrowse = QPushButton(self)
95
+ self.inBrowse.setIcon(SHARED.theme.getIcon("browse"))
96
+ self.inBrowse.clicked.connect(self._doOpenInstallLocation)
97
+
98
+ self.inBox = QHBoxLayout()
99
+ self.inBox.addWidget(self.inPath)
100
+ self.inBox.addWidget(self.inBrowse)
101
+ self.inBox.setSpacing(iPx)
102
+
103
+ # Info Box
104
+ self.infoBox = QPlainTextEdit(self)
105
+ self.infoBox.setReadOnly(True)
106
+ self.infoBox.setFixedHeight(4*SHARED.theme.fontPixelSize)
107
+ self.infoBox.setFrameStyle(QFrame.Shape.NoFrame)
108
+
109
+ # Buttons
110
+ self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
111
+ self.buttonBox.rejected.connect(self._doClose)
112
+
113
+ # Assemble
114
+ self.innerBox = QVBoxLayout()
115
+ self.innerBox.addWidget(self.huInfo)
116
+ self.innerBox.addLayout(self.huPathBox)
117
+ self.innerBox.addLayout(self.huAddBox)
118
+ self.innerBox.addSpacing(mPx)
119
+ self.innerBox.addWidget(self.inInfo)
120
+ self.innerBox.addLayout(self.inBox)
121
+ self.innerBox.addWidget(self.infoBox)
122
+ self.innerBox.setSpacing(iPx)
123
+
124
+ self.outerBox = QVBoxLayout()
125
+ self.outerBox.addLayout(self.innerBox, 0)
126
+ self.outerBox.addStretch(1)
127
+ self.outerBox.addWidget(self.buttonBox, 0)
128
+ self.outerBox.setSpacing(sPx)
129
+
130
+ self.setLayout(self.outerBox)
131
+
132
+ logger.debug("Ready: GuiDictionaries")
133
+
134
+ return
135
+
136
+ def __del__(self) -> None: # pragma: no cover
137
+ logger.debug("Delete: GuiDictionaries")
138
+ return
139
+
140
+ def initDialog(self) -> bool:
141
+ """Prepare and check that we can proceed."""
142
+ try:
143
+ import enchant
144
+ path = Path(enchant.get_user_config_dir())
145
+ except Exception:
146
+ logger.error("Could not get enchant path")
147
+ return False
148
+
149
+ self._installPath = Path(path).resolve()
150
+ if path.is_dir():
151
+ self.inPath.setText(str(path))
152
+ hunspell = path / "hunspell"
153
+ if hunspell.is_dir():
154
+ self._currDicts = set(
155
+ i.stem for i in hunspell.iterdir() if i.is_file() and i.suffix == ".aff"
156
+ )
157
+ self._appendLog(self.tr(
158
+ "Additional dictionaries found: {0}"
159
+ ).format(len(self._currDicts)))
160
+
161
+ qApp.processEvents()
162
+ self.adjustSize()
163
+
164
+ return True
165
+
166
+ ##
167
+ # Events
168
+ ##
169
+
170
+ def closeEvent(self, event: QCloseEvent) -> None:
171
+ """Capture the user closing the window."""
172
+ event.accept()
173
+ self.deleteLater()
174
+ return
175
+
176
+ ##
177
+ # Private Slots
178
+ ##
179
+
180
+ @pyqtSlot()
181
+ def _doBrowseHunspell(self):
182
+ """Browse for a Free/Libre Office dictionary."""
183
+ extFilter = [
184
+ self.tr("Free or Libre Office extension ({0})").format("*.sox *.oxt"),
185
+ self.tr("All files ({0})").format("*"),
186
+ ]
187
+ soxFile, _ = QFileDialog.getOpenFileName(
188
+ self, self.tr("Browse Files"), "", filter=";;".join(extFilter)
189
+ )
190
+ if soxFile:
191
+ path = Path(soxFile).absolute()
192
+ self.huInput.setText(str(path))
193
+ return
194
+
195
+ @pyqtSlot()
196
+ def _doImportHunspell(self):
197
+ """Import a hunspell dictionary from .sox or .oxt file."""
198
+ procErr = self.tr("Could not process dictionary file")
199
+ if self._installPath:
200
+ temp = self.huInput.text()
201
+ if temp and (path := Path(temp)).is_file():
202
+ hunspell = self._installPath / "hunspell"
203
+ hunspell.mkdir(exist_ok=True)
204
+ try:
205
+ nAff, nDic = self._extractDicts(path, hunspell)
206
+ if nAff == 0 or nDic == 0:
207
+ self._appendLog(procErr, err=True)
208
+ except Exception as exc:
209
+ self._appendLog(procErr, err=True)
210
+ self._appendLog(formatException(exc), err=True)
211
+ else:
212
+ self._appendLog(procErr, err=True)
213
+ return
214
+
215
+ @pyqtSlot()
216
+ def _doOpenInstallLocation(self) -> None:
217
+ """Open the dictionary folder."""
218
+ if not openExternalPath(Path(self.inPath.text())):
219
+ SHARED.error("Path not found.")
220
+ return
221
+
222
+ @pyqtSlot()
223
+ def _doClose(self) -> None:
224
+ """Close the dialog."""
225
+ self.close()
226
+ return
227
+
228
+ ##
229
+ # Internal Functions
230
+ ##
231
+
232
+ def _extractDicts(self, path: Path, output: Path) -> tuple[int, int]:
233
+ """Extract a zip archive and return the number of .aff and .dic
234
+ files found in it.
235
+ """
236
+ nAff = nDic = 0
237
+ with ZipFile(path, mode="r") as zipObj:
238
+ for item in zipObj.namelist():
239
+ zPath = Path(item)
240
+ if zPath.suffix not in (".aff", ".dic"):
241
+ continue
242
+ nAff += 1 if zPath.suffix == ".aff" else 0
243
+ nDic += 1 if zPath.suffix == ".dic" else 0
244
+ with zipObj.open(item) as zF:
245
+ oPath = output / zPath.name
246
+ oPath.write_bytes(zF.read())
247
+ size = getFileSize(oPath)
248
+ self._appendLog(self.tr(
249
+ "Added: {0} [{1}B]"
250
+ ).format(zPath.name, formatInt(size)))
251
+ return nAff, nDic
252
+
253
+ def _appendLog(self, text: str, err: bool = False) -> None:
254
+ """Append a line to the log output."""
255
+ cursor = self.infoBox.textCursor()
256
+ cursor.movePosition(QTextCursor.MoveOperation.End)
257
+ if cursor.position() > 0:
258
+ cursor.insertText("\n")
259
+ if err:
260
+ cursor.insertHtml(f"<font color='red'>{text}</font>")
261
+ else:
262
+ cursor.insertText(text)
263
+ cursor.movePosition(QTextCursor.MoveOperation.End)
264
+ cursor.deleteChar()
265
+ self.infoBox.setTextCursor(cursor)
266
+ return
267
+
268
+ # END Class GuiDictionaries
@@ -3,10 +3,10 @@ novelWriter – Lorem Ipsum Tool
3
3
  ==============================
4
4
 
5
5
  File History:
6
- Created: 2022-04-02 [2.0rc1]
6
+ Created: 2022-04-02 [2.0rc1] GuiLipsum
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
@@ -26,10 +26,10 @@ from __future__ import annotations
26
26
  import random
27
27
  import logging
28
28
 
29
- from PyQt5.QtCore import Qt
29
+ from PyQt5.QtCore import Qt, pyqtSlot
30
30
  from PyQt5.QtWidgets import (
31
- QDialog, QGridLayout, QHBoxLayout, QVBoxLayout, QLabel, QDialogButtonBox,
32
- QSpinBox
31
+ QDialog, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, QSpinBox,
32
+ QVBoxLayout, QWidget
33
33
  )
34
34
 
35
35
  from novelwriter import CONFIG, SHARED
@@ -41,15 +41,15 @@ logger = logging.getLogger(__name__)
41
41
 
42
42
  class GuiLipsum(QDialog):
43
43
 
44
- def __init__(self, mainGui):
45
- super().__init__(parent=mainGui)
44
+ def __init__(self, parent: QWidget) -> None:
45
+ super().__init__(parent=parent)
46
46
 
47
47
  logger.debug("Create: GuiLipsum")
48
48
  self.setObjectName("GuiLipsum")
49
49
  if CONFIG.osDarwin:
50
50
  self.setWindowFlag(Qt.WindowType.Tool)
51
51
 
52
- self.mainGui = mainGui
52
+ self._lipsumText = ""
53
53
 
54
54
  self.setWindowTitle(self.tr("Insert Placeholder Text"))
55
55
 
@@ -92,14 +92,16 @@ class GuiLipsum(QDialog):
92
92
 
93
93
  # Buttons
94
94
  self.buttonBox = QDialogButtonBox()
95
- self.buttonBox.rejected.connect(self._doClose)
95
+ self.buttonBox.rejected.connect(self.close)
96
96
 
97
97
  self.btnClose = self.buttonBox.addButton(QDialogButtonBox.Close)
98
98
  self.btnClose.setAutoDefault(False)
99
99
 
100
- self.btnSave = self.buttonBox.addButton(self.tr("Insert"), QDialogButtonBox.ActionRole)
101
- self.btnSave.clicked.connect(self._doInsert)
102
- self.btnSave.setAutoDefault(False)
100
+ self.btnInsert = self.buttonBox.addButton(self.tr("Insert"), QDialogButtonBox.ActionRole)
101
+ self.btnInsert.clicked.connect(self._doInsert)
102
+ self.btnInsert.setAutoDefault(False)
103
+
104
+ self.rejected.connect(self.close)
103
105
 
104
106
  # Assemble
105
107
  self.outerBox = QVBoxLayout()
@@ -112,33 +114,37 @@ class GuiLipsum(QDialog):
112
114
 
113
115
  return
114
116
 
115
- def __del__(self): # pragma: no cover
117
+ def __del__(self) -> None: # pragma: no cover
116
118
  logger.debug("Delete: GuiLipsum")
117
119
  return
118
120
 
121
+ @property
122
+ def lipsumText(self) -> str:
123
+ """Return the generated text."""
124
+ return self._lipsumText
125
+
126
+ @classmethod
127
+ def getLipsum(cls, parent: QWidget) -> str:
128
+ """Pop the dialog and return the lipsum text."""
129
+ cls = GuiLipsum(parent)
130
+ cls.exec_()
131
+ text = cls.lipsumText
132
+ cls.deleteLater()
133
+ return text
134
+
119
135
  ##
120
- # Slots
136
+ # Private Slots
121
137
  ##
122
138
 
123
- def _doInsert(self):
124
- """Load the text and insert it in the open document.
125
- """
139
+ @pyqtSlot()
140
+ def _doInsert(self) -> None:
141
+ """Generate the text."""
126
142
  lipsumFile = CONFIG.assetPath("text") / "lipsum.txt"
127
143
  lipsumText = readTextFile(lipsumFile).splitlines()
128
-
129
144
  if self.randSwitch.isChecked():
130
145
  random.shuffle(lipsumText)
131
-
132
146
  pCount = self.paraCount.value()
133
- inText = "\n\n".join(lipsumText[0:pCount]) + "\n\n"
134
-
135
- self.mainGui.docEditor.insertText(inText)
136
-
137
- return
138
-
139
- def _doClose(self):
140
- """Close the dialog window without doing anything.
141
- """
147
+ self._lipsumText = "\n\n".join(lipsumText[0:pCount]) + "\n\n"
142
148
  self.close()
143
149
  return
144
150
 
@@ -6,7 +6,7 @@ File History:
6
6
  Created: 2023-05-24 [2.1b1] GuiManuscriptBuild
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
@@ -27,6 +27,7 @@ import logging
27
27
 
28
28
  from pathlib import Path
29
29
 
30
+ from PyQt5.QtGui import QCloseEvent
30
31
  from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
31
32
  from PyQt5.QtWidgets import (
32
33
  QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QFileDialog,
@@ -36,7 +37,7 @@ from PyQt5.QtWidgets import (
36
37
 
37
38
  from novelwriter import CONFIG, SHARED
38
39
  from novelwriter.enum import nwBuildFmt
39
- from novelwriter.common import makeFileNameSafe
40
+ from novelwriter.common import makeFileNameSafe, openExternalPath
40
41
  from novelwriter.constants import nwLabels
41
42
  from novelwriter.core.item import NWItem
42
43
  from novelwriter.core.docbuild import NWBuildDocument
@@ -175,9 +176,11 @@ class GuiManuscriptBuild(QDialog):
175
176
  self.buildBox.setVerticalSpacing(sp4)
176
177
 
177
178
  # Dialog Buttons
179
+ self.btnOpen = QPushButton(SHARED.theme.getIcon("browse"), self.tr("Open Folder"))
178
180
  self.btnBuild = QPushButton(SHARED.theme.getIcon("export"), self.tr("&Build"))
179
- self.dlgButtons = QDialogButtonBox(QDialogButtonBox.Close)
180
- self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ActionRole)
181
+ self.dlgButtons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
182
+ self.dlgButtons.addButton(self.btnOpen, QDialogButtonBox.ButtonRole.ActionRole)
183
+ self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ButtonRole.ActionRole)
181
184
 
182
185
  # Assemble GUI
183
186
  # ============
@@ -227,7 +230,7 @@ class GuiManuscriptBuild(QDialog):
227
230
 
228
231
  return
229
232
 
230
- def __del__(self): # pragma: no cover
233
+ def __del__(self) -> None: # pragma: no cover
231
234
  logger.debug("Delete: GuiManuscriptBuild")
232
235
  return
233
236
 
@@ -235,7 +238,7 @@ class GuiManuscriptBuild(QDialog):
235
238
  # Events
236
239
  ##
237
240
 
238
- def closeEvent(self, event):
241
+ def closeEvent(self, event: QCloseEvent) -> None:
239
242
  """Capture the user closing the window so we can save GUI
240
243
  settings.
241
244
  """
@@ -253,7 +256,10 @@ class GuiManuscriptBuild(QDialog):
253
256
  """Handle button clicks from the dialog button box."""
254
257
  role = self.dlgButtons.buttonRole(button)
255
258
  if role == QDialogButtonBox.ActionRole:
256
- self._runBuild()
259
+ if button == self.btnBuild:
260
+ self._runBuild()
261
+ elif button == self.btnOpen:
262
+ self._openOutputFolder()
257
263
  elif role == QDialogButtonBox.RejectRole:
258
264
  self.close()
259
265
  return
@@ -325,7 +331,7 @@ class GuiManuscriptBuild(QDialog):
325
331
  self._build.setLastBuildName(bName)
326
332
  self._build.setLastFormat(bFormat)
327
333
 
328
- QTimer.singleShot(1000, self._resetProgress)
334
+ QTimer.singleShot(3000, self._resetProgress)
329
335
 
330
336
  return True
331
337
 
@@ -338,8 +344,6 @@ class GuiManuscriptBuild(QDialog):
338
344
 
339
345
  def _saveSettings(self):
340
346
  """Save the user GUI settings."""
341
- logger.debug("Saving GuiManuscriptBuild settings")
342
-
343
347
  winWidth = CONFIG.rpxInt(self.width())
344
348
  winHeight = CONFIG.rpxInt(self.height())
345
349
 
@@ -347,6 +351,7 @@ class GuiManuscriptBuild(QDialog):
347
351
  fmtWidth = CONFIG.rpxInt(mainSplit[0])
348
352
  sumWidth = CONFIG.rpxInt(mainSplit[1])
349
353
 
354
+ logger.debug("Saving State: GuiManuscriptBuild")
350
355
  pOptions = SHARED.project.options
351
356
  pOptions.setValue("GuiManuscriptBuild", "winWidth", winWidth)
352
357
  pOptions.setValue("GuiManuscriptBuild", "winHeight", winHeight)
@@ -385,4 +390,9 @@ class GuiManuscriptBuild(QDialog):
385
390
 
386
391
  return
387
392
 
393
+ def _openOutputFolder(self):
394
+ """Open the build folder in the system's file explorer."""
395
+ openExternalPath(Path(self.buildPath.text()))
396
+ return
397
+
388
398
  # END Class GuiManuscriptBuild