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.
- {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/METADATA +3 -3
- {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/RECORD +128 -114
- {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +10 -5
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
- novelwriter/assets/i18n/project_de_DE.json +1 -0
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/i18n/project_en_US.json +1 -0
- novelwriter/assets/i18n/project_es_419.json +11 -0
- novelwriter/assets/i18n/project_fr_FR.json +11 -0
- novelwriter/assets/i18n/project_it_IT.json +11 -0
- novelwriter/assets/i18n/project_ja_JP.json +2 -1
- novelwriter/assets/i18n/project_nb_NO.json +1 -0
- novelwriter/assets/i18n/project_zh_CN.json +11 -0
- novelwriter/assets/icons/novelwriter.ico +0 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +11 -3
- novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
- novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
- novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
- novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
- novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
- novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
- novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +11 -3
- novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
- novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
- novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
- novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
- novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
- novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
- novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
- novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/text/release_notes.htm +50 -7
- novelwriter/common.py +35 -27
- novelwriter/config.py +13 -28
- novelwriter/constants.py +21 -4
- novelwriter/core/buildsettings.py +2 -2
- novelwriter/core/coretools.py +8 -2
- novelwriter/core/docbuild.py +1 -1
- novelwriter/core/document.py +1 -1
- novelwriter/core/index.py +102 -36
- novelwriter/core/item.py +2 -2
- novelwriter/core/options.py +6 -3
- novelwriter/core/project.py +5 -5
- novelwriter/core/projectdata.py +3 -3
- novelwriter/core/projectxml.py +1 -1
- novelwriter/core/sessions.py +2 -2
- novelwriter/core/spellcheck.py +4 -3
- novelwriter/core/status.py +3 -3
- novelwriter/core/storage.py +1 -1
- novelwriter/core/tohtml.py +11 -5
- novelwriter/core/tokenizer.py +28 -21
- novelwriter/core/tomd.py +6 -2
- novelwriter/core/toodt.py +12 -5
- novelwriter/core/tree.py +2 -2
- novelwriter/dialogs/about.py +30 -31
- novelwriter/dialogs/docmerge.py +24 -15
- novelwriter/dialogs/docsplit.py +27 -16
- novelwriter/dialogs/editlabel.py +19 -7
- novelwriter/dialogs/preferences.py +116 -131
- novelwriter/dialogs/projdetails.py +29 -36
- novelwriter/dialogs/projload.py +32 -36
- novelwriter/dialogs/projsettings.py +20 -15
- novelwriter/dialogs/quotes.py +32 -25
- novelwriter/dialogs/updates.py +17 -16
- novelwriter/dialogs/wordlist.py +34 -21
- novelwriter/enum.py +19 -8
- novelwriter/error.py +1 -1
- novelwriter/extensions/circularprogress.py +1 -1
- novelwriter/extensions/configlayout.py +3 -15
- novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
- novelwriter/extensions/novelselector.py +1 -1
- novelwriter/extensions/pageddialog.py +1 -1
- novelwriter/extensions/pagedsidebar.py +2 -5
- novelwriter/extensions/simpleprogress.py +8 -9
- novelwriter/extensions/statusled.py +1 -1
- novelwriter/extensions/switch.py +4 -4
- novelwriter/extensions/switchbox.py +1 -6
- novelwriter/gui/doceditor.py +349 -236
- novelwriter/gui/dochighlight.py +10 -11
- novelwriter/gui/docviewer.py +158 -360
- novelwriter/gui/docviewerpanel.py +502 -0
- novelwriter/gui/editordocument.py +4 -4
- novelwriter/gui/itemdetails.py +2 -2
- novelwriter/gui/mainmenu.py +50 -36
- novelwriter/gui/noveltree.py +44 -53
- novelwriter/gui/outline.py +12 -7
- novelwriter/gui/projtree.py +465 -381
- novelwriter/gui/sidebar.py +9 -7
- novelwriter/gui/statusbar.py +48 -5
- novelwriter/gui/theme.py +26 -8
- novelwriter/guimain.py +212 -208
- novelwriter/shared.py +76 -30
- novelwriter/tools/dictionaries.py +268 -0
- novelwriter/tools/lipsum.py +34 -28
- novelwriter/tools/manusbuild.py +20 -10
- novelwriter/tools/manuscript.py +20 -27
- novelwriter/tools/manussettings.py +2 -4
- novelwriter/tools/projwizard.py +3 -3
- novelwriter/tools/writingstats.py +18 -5
- novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
- novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
- novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
- novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
- novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
- {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/entry_points.txt +0 -0
- {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–
|
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", "
|
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.
|
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
|
122
|
-
"""Return
|
123
|
-
return self.
|
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
|
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
|
-
|
220
|
-
|
221
|
-
|
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.
|
224
|
-
|
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
|
-
|
230
|
-
|
231
|
-
|
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.
|
234
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
276
|
+
alert = _GuiAlert(self.mainGui, self.theme)
|
277
|
+
alert.setMessage(text, info, details)
|
278
|
+
alert.setAlertType(_GuiAlert.ERROR, False)
|
243
279
|
if exc:
|
244
|
-
|
280
|
+
alert.setException(exc)
|
281
|
+
self._lastAlert = alert.logMessage
|
245
282
|
if log:
|
246
|
-
logger.error(self.
|
247
|
-
|
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
|
-
|
253
|
-
|
254
|
-
|
255
|
-
self.
|
256
|
-
|
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" \u203a <a href='{foUrl}'>{foUrl}</a>",
|
70
|
+
f" \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
|
novelwriter/tools/lipsum.py
CHANGED
@@ -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–
|
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,
|
32
|
-
|
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,
|
45
|
-
super().__init__(parent=
|
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.
|
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.
|
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.
|
101
|
-
self.
|
102
|
-
self.
|
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
|
-
|
124
|
-
|
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
|
-
|
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
|
|
novelwriter/tools/manusbuild.py
CHANGED
@@ -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–
|
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.
|
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.
|
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(
|
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
|