novelWriter 2.1.1__py3-none-any.whl → 2.2rc1__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 (109) hide show
  1. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +105 -76
  3. novelwriter/__init__.py +6 -24
  4. novelwriter/assets/i18n/project_de_DE.json +10 -0
  5. novelwriter/assets/i18n/project_en_GB.json +11 -0
  6. novelwriter/assets/i18n/project_en_US.json +10 -0
  7. novelwriter/assets/i18n/project_ja_JP.json +11 -1
  8. novelwriter/assets/i18n/project_nb_NO.json +10 -0
  9. novelwriter/assets/i18n/project_nn_NO.json +10 -0
  10. novelwriter/assets/icons/novelwriter.ico +0 -0
  11. novelwriter/assets/icons/novelwriter.svg +8 -183
  12. novelwriter/assets/icons/typicons_dark/icons.conf +17 -2
  13. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  14. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  15. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  16. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  17. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  18. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +8 -0
  21. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +8 -0
  22. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +4 -0
  23. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +5 -0
  24. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +5 -0
  25. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +5 -0
  26. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  27. novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +4 -0
  28. novelwriter/assets/icons/typicons_light/icons.conf +17 -2
  29. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  30. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  31. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  32. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  33. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  34. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +4 -0
  35. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +4 -0
  36. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +8 -0
  37. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +8 -0
  38. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +5 -0
  40. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +5 -0
  41. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +5 -0
  42. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +4 -0
  44. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  45. novelwriter/assets/icons/x-novelwriter-project.svg +7 -206
  46. novelwriter/assets/manual.pdf +0 -0
  47. novelwriter/assets/sample.zip +0 -0
  48. novelwriter/assets/syntax/default_dark.conf +1 -0
  49. novelwriter/assets/syntax/default_light.conf +1 -0
  50. novelwriter/assets/syntax/grey_dark.conf +1 -0
  51. novelwriter/assets/syntax/grey_light.conf +1 -0
  52. novelwriter/assets/syntax/light_owl.conf +1 -0
  53. novelwriter/assets/syntax/night_owl.conf +1 -0
  54. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  55. novelwriter/assets/syntax/solarized_light.conf +1 -0
  56. novelwriter/assets/syntax/tomorrow.conf +1 -0
  57. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  58. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  59. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  60. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  61. novelwriter/assets/text/credits_en.htm +7 -0
  62. novelwriter/assets/text/release_notes.htm +7 -37
  63. novelwriter/common.py +22 -1
  64. novelwriter/config.py +27 -42
  65. novelwriter/constants.py +45 -7
  66. novelwriter/core/buildsettings.py +40 -24
  67. novelwriter/core/coretools.py +8 -1
  68. novelwriter/core/docbuild.py +2 -6
  69. novelwriter/core/index.py +264 -175
  70. novelwriter/core/options.py +8 -3
  71. novelwriter/core/project.py +2 -2
  72. novelwriter/core/projectdata.py +3 -3
  73. novelwriter/core/tohtml.py +60 -59
  74. novelwriter/core/tokenizer.py +110 -70
  75. novelwriter/core/tomd.py +51 -38
  76. novelwriter/core/toodt.py +184 -147
  77. novelwriter/dialogs/preferences.py +75 -106
  78. novelwriter/dialogs/projsettings.py +101 -110
  79. novelwriter/dialogs/updates.py +25 -14
  80. novelwriter/enum.py +28 -3
  81. novelwriter/extensions/novelselector.py +1 -1
  82. novelwriter/gui/doceditor.py +1345 -1235
  83. novelwriter/gui/dochighlight.py +98 -62
  84. novelwriter/gui/docviewer.py +151 -340
  85. novelwriter/gui/docviewerpanel.py +457 -0
  86. novelwriter/gui/editordocument.py +126 -0
  87. novelwriter/gui/mainmenu.py +350 -300
  88. novelwriter/gui/noveltree.py +101 -125
  89. novelwriter/gui/outline.py +154 -171
  90. novelwriter/gui/projtree.py +480 -380
  91. novelwriter/gui/sidebar.py +106 -75
  92. novelwriter/gui/statusbar.py +1 -1
  93. novelwriter/gui/theme.py +114 -75
  94. novelwriter/guimain.py +353 -254
  95. novelwriter/shared.py +36 -3
  96. novelwriter/tools/dictionaries.py +268 -0
  97. novelwriter/tools/manusbuild.py +17 -6
  98. novelwriter/tools/manuscript.py +11 -3
  99. novelwriter/tools/manussettings.py +0 -14
  100. novelwriter/tools/projwizard.py +16 -2
  101. novelwriter/tools/writingstats.py +1 -1
  102. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  103. novelwriter/assets/icons/typicons_dark/typ_th-menu.svg +0 -4
  104. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  105. novelwriter/assets/icons/typicons_light/typ_th-menu.svg +0 -4
  106. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
  107. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
  108. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
  109. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
novelwriter/shared.py CHANGED
@@ -29,7 +29,7 @@ from time import time
29
29
  from typing import TYPE_CHECKING
30
30
  from pathlib import Path
31
31
 
32
- from PyQt5.QtCore import QObject, pyqtSignal
32
+ from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal
33
33
  from PyQt5.QtWidgets import QMessageBox, QWidget
34
34
 
35
35
  from novelwriter.core.spellcheck import NWSpellEnchant
@@ -52,17 +52,26 @@ class SharedData(QObject):
52
52
  projectStatusChanged = pyqtSignal(bool)
53
53
  projectStatusMessage = pyqtSignal(str)
54
54
  spellLanguageChanged = pyqtSignal(str, str)
55
+ indexScannedText = pyqtSignal(str)
56
+ indexChangedTags = pyqtSignal(list, list)
57
+ indexCleared = pyqtSignal()
58
+ indexAvailable = pyqtSignal()
55
59
 
56
60
  def __init__(self) -> None:
57
61
  super().__init__()
62
+
63
+ # Objects
58
64
  self._gui = None
59
65
  self._theme = None
60
66
  self._project = None
61
67
  self._spelling = None
68
+
69
+ # Settings
62
70
  self._lockedBy = None
63
71
  self._alert = None
64
72
  self._idleTime = 0.0
65
73
  self._idleRefTime = time()
74
+
66
75
  return
67
76
 
68
77
  ##
@@ -129,7 +138,8 @@ class SharedData(QObject):
129
138
  self._gui = gui
130
139
  self._theme = theme
131
140
  self._resetProject()
132
- logger.debug("SharedData instance initialised")
141
+ logger.debug("Ready: SharedData")
142
+ logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount())
133
143
  return
134
144
 
135
145
  def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
@@ -165,7 +175,7 @@ class SharedData(QObject):
165
175
  return
166
176
 
167
177
  def updateSpellCheckLanguage(self, reload: bool = False) -> None:
168
- """Update the active spell check langauge from settings."""
178
+ """Update the active spell check language from settings."""
169
179
  from novelwriter import CONFIG
170
180
  language = self.project.data.spellLang or CONFIG.spellLanguage
171
181
  if language != self.spelling.spellLanguage or reload:
@@ -199,6 +209,29 @@ class SharedData(QObject):
199
209
  self.projectStatusChanged.emit(state)
200
210
  return
201
211
 
212
+ def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None:
213
+ """Queue a runnable in the application thread pool."""
214
+ QThreadPool.globalInstance().start(runnable, priority=priority)
215
+ return
216
+
217
+ ##
218
+ # Signal Proxy
219
+ ##
220
+
221
+ def indexSignalProxy(self, data: dict) -> None:
222
+ """Emit signals on behalf of the index."""
223
+ event = data.get("event")
224
+ logger.debug("Received '%s' event from the index", event)
225
+ if event == "updateTags":
226
+ self.indexChangedTags.emit(data.get("updated", []), data.get("deleted", []))
227
+ elif event == "scanText":
228
+ self.indexScannedText.emit(data.get("handle", ""))
229
+ elif event == "clearIndex":
230
+ self.indexCleared.emit()
231
+ elif event == "buildIndex":
232
+ self.indexAvailable.emit()
233
+ return
234
+
202
235
  ##
203
236
  # Alert Boxes
204
237
  ##
@@ -0,0 +1,268 @@
1
+ """
2
+ novelWriter – GUI Dictionary Downloader
3
+ =======================================
4
+
5
+ File History:
6
+ Created: 2023-11-19 [2.2rc1]
7
+
8
+ This file is a part of novelWriter
9
+ Copyright 2018–2023, 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
@@ -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
@@ -385,4 +391,9 @@ class GuiManuscriptBuild(QDialog):
385
391
 
386
392
  return
387
393
 
394
+ def _openOutputFolder(self):
395
+ """Open the build folder in the system's file explorer."""
396
+ openExternalPath(Path(self.buildPath.text()))
397
+ return
398
+
388
399
  # END Class GuiManuscriptBuild
@@ -144,6 +144,8 @@ class GuiManuscript(QDialog):
144
144
  self.buildList.setIconSize(QSize(iPx, iPx))
145
145
  self.buildList.doubleClicked.connect(self._editSelectedBuild)
146
146
  self.buildList.currentItemChanged.connect(self._updateBuildDetails)
147
+ self.buildList.setSelectionMode(QAbstractItemView.SingleSelection)
148
+ self.buildList.setDragDropMode(QAbstractItemView.InternalMove)
147
149
 
148
150
  self.buildDetails = _DetailsWidget(self)
149
151
  self.buildDetails.setColumnWidth(
@@ -417,9 +419,15 @@ class GuiManuscript(QDialog):
417
419
  """Save the user GUI settings."""
418
420
  logger.debug("Saving GuiManuscript settings")
419
421
 
422
+ buildOrder = []
423
+ for i in range(self.buildList.count()):
424
+ if item := self.buildList.item(i):
425
+ buildOrder.append(item.data(self.D_KEY))
426
+
420
427
  current = self.buildList.currentItem()
421
- if isinstance(current, QListWidgetItem):
422
- self._builds.setLastBuild(current.data(self.D_KEY))
428
+ lastBuild = current.data(self.D_KEY) if isinstance(current, QListWidgetItem) else ""
429
+
430
+ self._builds.setBuildsState(lastBuild, buildOrder)
423
431
 
424
432
  winWidth = CONFIG.rpxInt(self.width())
425
433
  winHeight = CONFIG.rpxInt(self.height())
@@ -499,7 +507,7 @@ class _DetailsWidget(QWidget):
499
507
 
500
508
  self._initExpanded = True
501
509
 
502
- # Tree Vidget
510
+ # Tree Widget
503
511
  self.listView = QTreeWidget(self)
504
512
  self.listView.setHeaderLabels([self.tr("Setting"), self.tr("Value")])
505
513
  self.listView.setIndentation(SHARED.theme.baseIconSize)
@@ -974,15 +974,6 @@ class _FormatTab(QWidget):
974
974
  self.formFormat = NConfigLayout()
975
975
  self.formFormat.addGroupLabel(self._build.getLabel("format.grpFormat"))
976
976
 
977
- # Build Language
978
- self.buildLang = QComboBox()
979
- langauges = CONFIG.listLanguages(CONFIG.LANG_PROJ)
980
- self.buildLang.addItem("[%s]" % self.tr("Not Set"), "None")
981
- for langID, langName in langauges:
982
- self.buildLang.addItem(langName, langID)
983
-
984
- self.formFormat.addRow(self._build.getLabel("format.buildLang"), self.buildLang)
985
-
986
977
  # Font Family
987
978
  self.textFont = QLineEdit()
988
979
  self.textFont.setReadOnly(True)
@@ -1103,10 +1094,6 @@ class _FormatTab(QWidget):
1103
1094
 
1104
1095
  def loadContent(self) -> None:
1105
1096
  """Populate the widgets."""
1106
- langIdx = self.buildLang.findData(self._build.getStr("format.buildLang"))
1107
- if langIdx != -1:
1108
- self.buildLang.setCurrentIndex(langIdx)
1109
-
1110
1097
  textFont = self._build.getStr("format.textFont")
1111
1098
  if not textFont:
1112
1099
  textFont = str(CONFIG.textFont)
@@ -1146,7 +1133,6 @@ class _FormatTab(QWidget):
1146
1133
 
1147
1134
  def saveContent(self) -> None:
1148
1135
  """Save choices back into build object."""
1149
- self._build.setValue("format.buildLang", str(self.buildLang.currentData()))
1150
1136
  self._build.setValue("format.textFont", self.textFont.text())
1151
1137
  self._build.setValue("format.textSize", self.textSize.value())
1152
1138
  self._build.setValue("format.lineHeight", self.lineHeight.value())
@@ -28,8 +28,9 @@ import logging
28
28
 
29
29
  from PyQt5.QtCore import Qt
30
30
  from PyQt5.QtWidgets import (
31
- QFileDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QLineEdit,
32
- QPushButton, QRadioButton, QSpinBox, QVBoxLayout, QWizard, QWizardPage
31
+ QComboBox, QFileDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel,
32
+ QLineEdit, QPushButton, QRadioButton, QSpinBox, QVBoxLayout, QWizard,
33
+ QWizardPage
33
34
  )
34
35
 
35
36
  from novelwriter import CONFIG, SHARED
@@ -127,15 +128,28 @@ class ProjWizardIntroPage(QWizardPage):
127
128
  self.projAuthor.setFixedWidth(xW)
128
129
  self.projAuthor.setPlaceholderText(self.tr("Optional"))
129
130
 
131
+ self.projLang = QComboBox(self)
132
+ self.projLang.setMaximumWidth(xW)
133
+ for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
134
+ self.projLang.addItem(language, tag)
135
+
136
+ langIdx = self.projLang.findData(CONFIG.guiLocale)
137
+ if langIdx == -1:
138
+ langIdx = self.projLang.findData("en_GB")
139
+ if langIdx != -1:
140
+ self.projLang.setCurrentIndex(langIdx)
141
+
130
142
  self.mainForm = QFormLayout()
131
143
  self.mainForm.addRow(self.tr("Project Name"), self.projName)
132
144
  self.mainForm.addRow(self.tr("Novel Title"), self.projTitle)
133
145
  self.mainForm.addRow(self.tr("Author(s)"), self.projAuthor)
146
+ self.mainForm.addRow(self.tr("Language"), self.projLang)
134
147
  self.mainForm.setVerticalSpacing(fS)
135
148
 
136
149
  self.registerField("projName*", self.projName)
137
150
  self.registerField("projTitle", self.projTitle)
138
151
  self.registerField("projAuthor", self.projAuthor)
152
+ self.registerField("projLang", self.projLang)
139
153
 
140
154
  # Assemble
141
155
  self.outerBox = QVBoxLayout()
@@ -125,7 +125,7 @@ class GuiWritingStats(QDialog):
125
125
  pOptions.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder),
126
126
  (Qt.AscendingOrder, Qt.DescendingOrder), Qt.DescendingOrder
127
127
  )
128
- self.listBox.sortByColumn(sortCol, sortOrder)
128
+ self.listBox.sortByColumn(sortCol, sortOrder) # type: ignore
129
129
  self.listBox.setSortingEnabled(True)
130
130
 
131
131
  # Word Bar
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
3
- <path d="m12 2c-5.5138 0-10 4.4862-10 10s4.4862 10 10 10c2.02 0 3.965-0.59875 5.6238-1.73 0.57-0.39 0.7175-1.1675 0.32875-1.7375-0.38875-0.57125-1.165-0.715-1.7375-0.32875-1.2425 0.84875-2.7 1.2962-4.215 1.2962-4.1362 0-7.5-3.3638-7.5-7.5s3.3638-7.5 7.5-7.5 7.5 3.3638 7.5 7.5v0.625c0 0.69-0.56 1.25-1.25 1.25s-1.25-0.56-1.25-1.25v-3.75c0-0.69125-0.55875-1.25-1.25-1.25-0.55125 0-1.0062 0.3625-1.1725 0.86-0.725-0.53375-1.6112-0.86-2.5775-0.86-2.4125 0-4.375 1.9625-4.375 4.375s1.9625 4.375 4.375 4.375c1.3062 0 2.4688-0.5875 3.27-1.4988 0.685 0.90375 1.76 1.4988 2.98 1.4988 2.0675 0 3.75-1.6825 3.75-3.75v-0.625c0-5.5138-4.4862-10-10-10zm0 11.875c-1.0338 0-1.875-0.84125-1.875-1.875s0.84125-1.875 1.875-1.875 1.875 0.84125 1.875 1.875-0.84125 1.875-1.875 1.875z" fill="#69c" stroke-width="1.25"/>
4
- </svg>
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
3
- <path d="M19 17h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 10h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 3h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2z" fill="#69c"/>
4
- </svg>
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
3
- <path d="m12 2c-5.5138 0-10 4.4862-10 10s4.4862 10 10 10c2.02 0 3.965-0.59875 5.6238-1.73 0.57-0.39 0.7175-1.1675 0.32875-1.7375-0.38875-0.57125-1.165-0.715-1.7375-0.32875-1.2425 0.84875-2.7 1.2962-4.215 1.2962-4.1362 0-7.5-3.3638-7.5-7.5s3.3638-7.5 7.5-7.5 7.5 3.3638 7.5 7.5v0.625c0 0.69-0.56 1.25-1.25 1.25s-1.25-0.56-1.25-1.25v-3.75c0-0.69125-0.55875-1.25-1.25-1.25-0.55125 0-1.0062 0.3625-1.1725 0.86-0.725-0.53375-1.6112-0.86-2.5775-0.86-2.4125 0-4.375 1.9625-4.375 4.375s1.9625 4.375 4.375 4.375c1.3062 0 2.4688-0.5875 3.27-1.4988 0.685 0.90375 1.76 1.4988 2.98 1.4988 2.0675 0 3.75-1.6825 3.75-3.75v-0.625c0-5.5138-4.4862-10-10-10zm0 11.875c-1.0338 0-1.875-0.84125-1.875-1.875s0.84125-1.875 1.875-1.875 1.875 0.84125 1.875 1.875-0.84125 1.875-1.875 1.875z" fill="#4271ae" stroke-width="1.25"/>
4
- </svg>
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
3
- <path d="M19 17h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 10h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 3h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2z" fill="#4271ae"/>
4
- </svg>