novelWriter 2.3.1__py3-none-any.whl → 2.4__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 (119) hide show
  1. {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/METADATA +5 -6
  2. {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/RECORD +114 -107
  3. novelwriter/__init__.py +17 -10
  4. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  5. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  6. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  7. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  8. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  9. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  10. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  11. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  12. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  13. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  14. novelwriter/assets/icons/none.svg +4 -0
  15. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  16. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  17. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  18. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  19. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  21. novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
  22. novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
  23. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  24. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  25. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  26. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  27. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  28. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  29. novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
  30. novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
  31. novelwriter/assets/manual.pdf +0 -0
  32. novelwriter/assets/sample.zip +0 -0
  33. novelwriter/assets/syntax/default_dark.conf +1 -0
  34. novelwriter/assets/syntax/default_light.conf +1 -0
  35. novelwriter/assets/syntax/grey_dark.conf +1 -0
  36. novelwriter/assets/syntax/grey_light.conf +1 -0
  37. novelwriter/assets/syntax/light_owl.conf +1 -0
  38. novelwriter/assets/syntax/night_owl.conf +1 -0
  39. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  40. novelwriter/assets/syntax/solarized_light.conf +1 -0
  41. novelwriter/assets/syntax/tomorrow.conf +1 -0
  42. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  43. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  44. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  45. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  46. novelwriter/assets/text/credits_en.htm +25 -23
  47. novelwriter/common.py +12 -4
  48. novelwriter/config.py +47 -16
  49. novelwriter/constants.py +5 -6
  50. novelwriter/core/buildsettings.py +64 -44
  51. novelwriter/core/coretools.py +97 -13
  52. novelwriter/core/docbuild.py +74 -7
  53. novelwriter/core/document.py +24 -3
  54. novelwriter/core/index.py +31 -112
  55. novelwriter/core/project.py +10 -15
  56. novelwriter/core/projectxml.py +1 -1
  57. novelwriter/core/sessions.py +2 -2
  58. novelwriter/core/spellcheck.py +3 -3
  59. novelwriter/core/status.py +6 -5
  60. novelwriter/core/storage.py +8 -2
  61. novelwriter/core/tohtml.py +22 -25
  62. novelwriter/core/tokenizer.py +417 -233
  63. novelwriter/core/tomd.py +17 -8
  64. novelwriter/core/toodt.py +386 -351
  65. novelwriter/core/tree.py +8 -8
  66. novelwriter/dialogs/about.py +9 -11
  67. novelwriter/dialogs/docmerge.py +17 -14
  68. novelwriter/dialogs/docsplit.py +20 -19
  69. novelwriter/dialogs/editlabel.py +5 -4
  70. novelwriter/dialogs/preferences.py +32 -40
  71. novelwriter/dialogs/projectsettings.py +31 -28
  72. novelwriter/dialogs/quotes.py +10 -9
  73. novelwriter/dialogs/wordlist.py +17 -14
  74. novelwriter/enum.py +17 -14
  75. novelwriter/error.py +14 -12
  76. novelwriter/extensions/circularprogress.py +12 -8
  77. novelwriter/extensions/configlayout.py +1 -3
  78. novelwriter/extensions/modified.py +51 -2
  79. novelwriter/extensions/pagedsidebar.py +16 -14
  80. novelwriter/extensions/simpleprogress.py +3 -1
  81. novelwriter/extensions/statusled.py +3 -1
  82. novelwriter/extensions/switch.py +10 -9
  83. novelwriter/extensions/switchbox.py +14 -13
  84. novelwriter/extensions/versioninfo.py +1 -1
  85. novelwriter/gui/doceditor.py +433 -496
  86. novelwriter/gui/dochighlight.py +54 -33
  87. novelwriter/gui/docviewer.py +162 -175
  88. novelwriter/gui/docviewerpanel.py +20 -37
  89. novelwriter/gui/editordocument.py +15 -4
  90. novelwriter/gui/itemdetails.py +51 -54
  91. novelwriter/gui/mainmenu.py +37 -16
  92. novelwriter/gui/noveltree.py +31 -37
  93. novelwriter/gui/outline.py +120 -98
  94. novelwriter/gui/projtree.py +61 -67
  95. novelwriter/gui/search.py +362 -0
  96. novelwriter/gui/sidebar.py +36 -45
  97. novelwriter/gui/statusbar.py +14 -14
  98. novelwriter/gui/theme.py +107 -32
  99. novelwriter/guimain.py +209 -202
  100. novelwriter/shared.py +31 -6
  101. novelwriter/text/counting.py +138 -0
  102. novelwriter/tools/dictionaries.py +15 -14
  103. novelwriter/tools/lipsum.py +20 -17
  104. novelwriter/tools/manusbuild.py +43 -35
  105. novelwriter/tools/manuscript.py +381 -104
  106. novelwriter/tools/manussettings.py +262 -125
  107. novelwriter/tools/noveldetails.py +20 -18
  108. novelwriter/tools/welcome.py +52 -49
  109. novelwriter/tools/writingstats.py +61 -55
  110. novelwriter/types.py +90 -0
  111. novelwriter/core/__init__.py +0 -3
  112. novelwriter/dialogs/__init__.py +0 -3
  113. novelwriter/extensions/__init__.py +0 -3
  114. novelwriter/gui/__init__.py +0 -3
  115. novelwriter/tools/__init__.py +0 -3
  116. {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/LICENSE.md +0 -0
  117. {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/WHEEL +0 -0
  118. {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/entry_points.txt +0 -0
  119. {novelWriter-2.3.1.dist-info → novelWriter-2.4.dist-info}/top_level.txt +0 -0
novelwriter/shared.py CHANGED
@@ -30,7 +30,7 @@ from time import time
30
30
  from typing import TYPE_CHECKING, TypeVar
31
31
  from pathlib import Path
32
32
 
33
- from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal
33
+ from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal
34
34
  from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget
35
35
  from novelwriter.common import formatFileFilter
36
36
 
@@ -57,10 +57,12 @@ class SharedData(QObject):
57
57
  projectStatusChanged = pyqtSignal(bool)
58
58
  projectStatusMessage = pyqtSignal(str)
59
59
  spellLanguageChanged = pyqtSignal(str, str)
60
+ focusModeChanged = pyqtSignal(bool)
60
61
  indexScannedText = pyqtSignal(str)
61
62
  indexChangedTags = pyqtSignal(list, list)
62
63
  indexCleared = pyqtSignal()
63
64
  indexAvailable = pyqtSignal()
65
+ mainClockTick = pyqtSignal()
64
66
 
65
67
  def __init__(self) -> None:
66
68
  super().__init__()
@@ -76,6 +78,11 @@ class SharedData(QObject):
76
78
  self._lastAlert = ""
77
79
  self._idleTime = 0.0
78
80
  self._idleRefTime = time()
81
+ self._focusMode = False
82
+
83
+ self._clock = QTimer(self)
84
+ self._clock.setInterval(1000)
85
+ self._clock.timeout.connect(lambda: self.mainClockTick.emit())
79
86
 
80
87
  return
81
88
 
@@ -111,6 +118,11 @@ class SharedData(QObject):
111
118
  raise Exception("SharedData class not fully initialised")
112
119
  return self._spelling
113
120
 
121
+ @property
122
+ def focusMode(self) -> bool:
123
+ """Return the Focus Mode state."""
124
+ return self._focusMode
125
+
114
126
  @property
115
127
  def hasProject(self) -> bool:
116
128
  """Return True if the project instance is populated."""
@@ -131,6 +143,17 @@ class SharedData(QObject):
131
143
  """Return the last alert message."""
132
144
  return self._lastAlert
133
145
 
146
+ ##
147
+ # Setters
148
+ ##
149
+
150
+ def setFocusMode(self, state: bool) -> None:
151
+ """Set focus mode on or off."""
152
+ if state is not self._focusMode:
153
+ self._focusMode = state
154
+ self.focusModeChanged.emit(state)
155
+ return
156
+
134
157
  ##
135
158
  # Methods
136
159
  ##
@@ -140,6 +163,7 @@ class SharedData(QObject):
140
163
  soon as the Main GUI is created to ensure the SHARED singleton
141
164
  has the properties needed for operation.
142
165
  """
166
+ self._clock.start()
143
167
  self._gui = gui
144
168
  self._theme = theme
145
169
  self._resetProject()
@@ -268,7 +292,7 @@ class SharedData(QObject):
268
292
  self._lastAlert = alert.logMessage
269
293
  if log:
270
294
  logger.info(self._lastAlert, stacklevel=2)
271
- alert.exec_()
295
+ alert.exec()
272
296
  alert.deleteLater()
273
297
  return
274
298
 
@@ -280,7 +304,7 @@ class SharedData(QObject):
280
304
  self._lastAlert = alert.logMessage
281
305
  if log:
282
306
  logger.warning(self._lastAlert, stacklevel=2)
283
- alert.exec_()
307
+ alert.exec()
284
308
  alert.deleteLater()
285
309
  return
286
310
 
@@ -295,7 +319,7 @@ class SharedData(QObject):
295
319
  self._lastAlert = alert.logMessage
296
320
  if log:
297
321
  logger.error(self._lastAlert, stacklevel=2)
298
- alert.exec_()
322
+ alert.exec()
299
323
  alert.deleteLater()
300
324
  return
301
325
 
@@ -305,7 +329,7 @@ class SharedData(QObject):
305
329
  alert.setMessage(text, info, details)
306
330
  alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True)
307
331
  self._lastAlert = alert.logMessage
308
- alert.exec_()
332
+ alert.exec()
309
333
  isYes = alert.result() == QMessageBox.StandardButton.Yes
310
334
  alert.deleteLater()
311
335
  return isYes
@@ -323,6 +347,7 @@ class SharedData(QObject):
323
347
  self._project = NWProject()
324
348
  self._spelling = NWSpellEnchant(self._project)
325
349
  self.updateSpellCheckLanguage()
350
+ self._focusMode = False
326
351
  return
327
352
 
328
353
  def _resetIdleTimer(self) -> None:
@@ -379,7 +404,7 @@ class _GuiAlert(QMessageBox):
379
404
  self.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
380
405
  else:
381
406
  self.setStandardButtons(QMessageBox.Ok)
382
- pSz = 2*self._theme.baseIconSize
407
+ pSz = 2*self._theme.baseIconHeight
383
408
  if level == self.INFO:
384
409
  self.setIconPixmap(self._theme.getPixmap("alert_info", (pSz, pSz)))
385
410
  self.setWindowTitle(self.tr("Information"))
@@ -0,0 +1,138 @@
1
+ """
2
+ novelWriter – Text Counting Functions
3
+ =====================================
4
+
5
+ File History:
6
+ Created: 2019-04-22 [0.0.1] standardCounter
7
+ Rewritten: 2024-02-27 [2.4b1] preProcessText, standardCounter
8
+ Created: 2024-02-27 [2.4b1] bodyTextCounter
9
+
10
+ This file is a part of novelWriter
11
+ Copyright 2018–2024, Veronica Berglyd Olsen
12
+
13
+ This program is free software: you can redistribute it and/or modify
14
+ it under the terms of the GNU General Public License as published by
15
+ the Free Software Foundation, either version 3 of the License, or
16
+ (at your option) any later version.
17
+
18
+ This program is distributed in the hope that it will be useful, but
19
+ WITHOUT ANY WARRANTY; without even the implied warranty of
20
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21
+ General Public License for more details.
22
+
23
+ You should have received a copy of the GNU General Public License
24
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import re
29
+
30
+ from novelwriter.constants import nwRegEx, nwUnicode
31
+
32
+ RX_SC = re.compile(nwRegEx.FMT_SC)
33
+ RX_LO = re.compile(r"(?i)(?<!\\)(\[(?:vspace|newpage|new page)(:\d+)?)(?<!\\)(\])")
34
+
35
+
36
+ def preProcessText(text: str, keepHeaders: bool = True) -> list[str]:
37
+ """Strip formatting codes from the text and split into lines."""
38
+ if not isinstance(text, str):
39
+ return []
40
+
41
+ # We need to treat dashes as word separators for counting words.
42
+ # The check+replace approach is much faster than direct replace for
43
+ # large texts, and a bit slower for small texts, but in the latter
44
+ # case it doesn't really matter.
45
+ if nwUnicode.U_ENDASH in text:
46
+ text = text.replace(nwUnicode.U_ENDASH, " ")
47
+ if nwUnicode.U_EMDASH in text:
48
+ text = text.replace(nwUnicode.U_EMDASH, " ")
49
+
50
+ ignore = "%@" if keepHeaders else "%@#"
51
+
52
+ result = []
53
+ for line in text.splitlines():
54
+ line = line.rstrip()
55
+ if line:
56
+ if line[0] in ignore:
57
+ continue
58
+ if line[0] == ">":
59
+ line = line.lstrip(">").lstrip(" ")
60
+ if line: # Above block can return empty line (Issue #1816)
61
+ if line[-1] == "<":
62
+ line = line.rstrip("<").rstrip(" ")
63
+ if "[" in line:
64
+ # Strip shortcodes and special formatting
65
+ # RegEx is slow, so we do this only when necessary
66
+ line = RX_SC.sub("", line)
67
+ line = RX_LO.sub("", line)
68
+
69
+ result.append(line)
70
+
71
+ return result
72
+
73
+
74
+ def standardCounter(text: str) -> tuple[int, int, int]:
75
+ """A counter that counts paragraphs, words and characters.
76
+ This is the standard counter that includes headings in the word and
77
+ character counts.
78
+ """
79
+ cCount = 0
80
+ wCount = 0
81
+ pCount = 0
82
+ prevEmpty = True
83
+
84
+ for line in preProcessText(text):
85
+
86
+ countPara = True
87
+ if not line:
88
+ prevEmpty = True
89
+ continue
90
+
91
+ if line[0] == "#":
92
+ if line[:5] == "#### ":
93
+ line = line[5:]
94
+ countPara = False
95
+ elif line[:4] == "### ":
96
+ line = line[4:]
97
+ countPara = False
98
+ elif line[:3] == "## ":
99
+ line = line[3:]
100
+ countPara = False
101
+ elif line[:2] == "# ":
102
+ line = line[2:]
103
+ countPara = False
104
+ elif line[:3] == "#! ":
105
+ line = line[3:]
106
+ countPara = False
107
+ elif line[:4] == "##! ":
108
+ line = line[4:]
109
+ countPara = False
110
+ elif line[:5] == "###! ":
111
+ line = line[5:]
112
+ countPara = False
113
+
114
+ wCount += len(line.split())
115
+ cCount += len(line)
116
+ if countPara and prevEmpty:
117
+ pCount += 1
118
+
119
+ prevEmpty = not countPara
120
+
121
+ return cCount, wCount, pCount
122
+
123
+
124
+ def bodyTextCounter(text: str) -> tuple[int, int, int]:
125
+ """A counter that counts body text words, characters, and characters
126
+ without white spaces.
127
+ """
128
+ wCount = 0
129
+ cCount = 0
130
+ sCount = 0
131
+
132
+ for line in preProcessText(text, keepHeaders=False):
133
+ words = line.split()
134
+ wCount += len(words)
135
+ cCount += len(line)
136
+ sCount += len("".join(words))
137
+
138
+ return wCount, cCount, sCount
@@ -28,16 +28,18 @@ import logging
28
28
  from pathlib import Path
29
29
  from zipfile import ZipFile
30
30
 
31
- from PyQt5.QtGui import QCloseEvent, QTextCursor
32
31
  from PyQt5.QtCore import pyqtSlot
32
+ from PyQt5.QtGui import QCloseEvent, QTextCursor
33
33
  from PyQt5.QtWidgets import (
34
- QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel,
35
- QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget, qApp
34
+ QApplication, QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout,
35
+ QLabel, QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget
36
36
  )
37
37
 
38
38
  from novelwriter import CONFIG, SHARED
39
- from novelwriter.error import formatException
40
39
  from novelwriter.common import formatFileFilter, openExternalPath, formatInt, getFileSize
40
+ from novelwriter.error import formatException
41
+ from novelwriter.extensions.modified import NIconToolButton
42
+ from novelwriter.types import QtDialogClose
41
43
 
42
44
  logger = logging.getLogger(__name__)
43
45
 
@@ -54,6 +56,7 @@ class GuiDictionaries(QDialog):
54
56
  self._installPath = None
55
57
  self._currDicts = set()
56
58
 
59
+ iSz = SHARED.theme.baseIconSize
57
60
  iPx = CONFIG.pxInt(4)
58
61
  mPx = CONFIG.pxInt(8)
59
62
  sPx = CONFIG.pxInt(16)
@@ -68,12 +71,11 @@ class GuiDictionaries(QDialog):
68
71
  self.tr("Download a dictionary from one of the links, and add it below."),
69
72
  f"&nbsp;\u203a <a href='{foUrl}'>{foUrl}</a>",
70
73
  f"&nbsp;\u203a <a href='{loUrl}'>{loUrl}</a>",
71
- ]))
74
+ ]), self)
72
75
  self.huInfo.setOpenExternalLinks(True)
73
76
  self.huInfo.setWordWrap(True)
74
77
  self.huInput = QLineEdit(self)
75
- self.huBrowse = QPushButton(self)
76
- self.huBrowse.setIcon(SHARED.theme.getIcon("browse"))
78
+ self.huBrowse = NIconToolButton(self, iSz, "browse")
77
79
  self.huBrowse.clicked.connect(self._doBrowseHunspell)
78
80
  self.huImport = QPushButton(self.tr("Add Dictionary"), self)
79
81
  self.huImport.setIcon(SHARED.theme.getIcon("add"))
@@ -88,11 +90,10 @@ class GuiDictionaries(QDialog):
88
90
  self.huAddBox.addWidget(self.huImport)
89
91
 
90
92
  # Install Path
91
- self.inInfo = QLabel(self.tr("Dictionary install location"))
93
+ self.inInfo = QLabel(self.tr("Dictionary install location"), self)
92
94
  self.inPath = QLineEdit(self)
93
95
  self.inPath.setReadOnly(True)
94
- self.inBrowse = QPushButton(self)
95
- self.inBrowse.setIcon(SHARED.theme.getIcon("browse"))
96
+ self.inBrowse = NIconToolButton(self, iSz, "browse")
96
97
  self.inBrowse.clicked.connect(self._doOpenInstallLocation)
97
98
 
98
99
  self.inBox = QHBoxLayout()
@@ -107,7 +108,7 @@ class GuiDictionaries(QDialog):
107
108
  self.infoBox.setFrameStyle(QFrame.Shape.NoFrame)
108
109
 
109
110
  # Buttons
110
- self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
111
+ self.buttonBox = QDialogButtonBox(QtDialogClose, self)
111
112
  self.buttonBox.rejected.connect(self._doClose)
112
113
 
113
114
  # Assemble
@@ -158,7 +159,7 @@ class GuiDictionaries(QDialog):
158
159
  "Additional dictionaries found: {0}"
159
160
  ).format(len(self._currDicts)))
160
161
 
161
- qApp.processEvents()
162
+ QApplication.processEvents()
162
163
  self.adjustSize()
163
164
 
164
165
  return True
@@ -178,7 +179,7 @@ class GuiDictionaries(QDialog):
178
179
  ##
179
180
 
180
181
  @pyqtSlot()
181
- def _doBrowseHunspell(self):
182
+ def _doBrowseHunspell(self) -> None:
182
183
  """Browse for a Free/Libre Office dictionary."""
183
184
  ffilter = formatFileFilter([
184
185
  (self.tr("Free or Libre Office extension"), "*.sox *.oxt"), "*"
@@ -192,7 +193,7 @@ class GuiDictionaries(QDialog):
192
193
  return
193
194
 
194
195
  @pyqtSlot()
195
- def _doImportHunspell(self):
196
+ def _doImportHunspell(self) -> None:
196
197
  """Import a hunspell dictionary from .sox or .oxt file."""
197
198
  procErr = self.tr("Could not process dictionary file")
198
199
  if self._installPath:
@@ -35,6 +35,7 @@ from PyQt5.QtWidgets import (
35
35
  from novelwriter import CONFIG, SHARED
36
36
  from novelwriter.common import readTextFile
37
37
  from novelwriter.extensions.switch import NSwitch
38
+ from novelwriter.types import QtAlignLeft, QtAlignRight, QtRoleAction, QtDialogClose
38
39
 
39
40
  logger = logging.getLogger(__name__)
40
41
 
@@ -53,13 +54,14 @@ class GuiLipsum(QDialog):
53
54
 
54
55
  self.setWindowTitle(self.tr("Insert Placeholder Text"))
55
56
 
57
+ vSp = CONFIG.pxInt(4)
58
+ nPx = CONFIG.pxInt(64)
59
+
56
60
  self.innerBox = QHBoxLayout()
57
61
  self.innerBox.setSpacing(CONFIG.pxInt(16))
58
62
 
59
63
  # Icon
60
- nPx = CONFIG.pxInt(64)
61
- vSp = CONFIG.pxInt(4)
62
- self.docIcon = QLabel()
64
+ self.docIcon = QLabel(self)
63
65
  self.docIcon.setPixmap(SHARED.theme.getPixmap("proj_document", (nPx, nPx)))
64
66
 
65
67
  self.leftBox = QVBoxLayout()
@@ -69,35 +71,36 @@ class GuiLipsum(QDialog):
69
71
  self.innerBox.addLayout(self.leftBox)
70
72
 
71
73
  # Form
72
- self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Insert Lorem Ipsum Text")))
74
+ self.headLabel = QLabel(self.tr("Insert Lorem Ipsum Text"))
75
+ self.headLabel.setFont(SHARED.theme.guiFontB)
73
76
 
74
- self.paraLabel = QLabel(self.tr("Number of paragraphs"))
75
- self.paraCount = QSpinBox()
77
+ self.paraLabel = QLabel(self.tr("Number of paragraphs"), self)
78
+ self.paraCount = QSpinBox(self)
76
79
  self.paraCount.setMinimum(1)
77
80
  self.paraCount.setMaximum(100)
78
81
  self.paraCount.setValue(5)
79
82
 
80
- self.randLabel = QLabel(self.tr("Randomise order"))
81
- self.randSwitch = NSwitch()
83
+ self.randLabel = QLabel(self.tr("Randomise order"), self)
84
+ self.randSwitch = NSwitch(self)
82
85
 
83
86
  self.formBox = QGridLayout()
84
- self.formBox.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignLeft)
85
- self.formBox.addWidget(self.paraLabel, 1, 0, 1, 1, Qt.AlignLeft)
86
- self.formBox.addWidget(self.paraCount, 1, 1, 1, 1, Qt.AlignRight)
87
- self.formBox.addWidget(self.randLabel, 2, 0, 1, 1, Qt.AlignLeft)
88
- self.formBox.addWidget(self.randSwitch, 2, 1, 1, 1, Qt.AlignRight)
87
+ self.formBox.addWidget(self.headLabel, 0, 0, 1, 2, QtAlignLeft)
88
+ self.formBox.addWidget(self.paraLabel, 1, 0, 1, 1, QtAlignLeft)
89
+ self.formBox.addWidget(self.paraCount, 1, 1, 1, 1, QtAlignRight)
90
+ self.formBox.addWidget(self.randLabel, 2, 0, 1, 1, QtAlignLeft)
91
+ self.formBox.addWidget(self.randSwitch, 2, 1, 1, 1, QtAlignRight)
89
92
  self.formBox.setVerticalSpacing(vSp)
90
93
  self.formBox.setRowStretch(3, 1)
91
94
  self.innerBox.addLayout(self.formBox)
92
95
 
93
96
  # Buttons
94
- self.buttonBox = QDialogButtonBox()
97
+ self.buttonBox = QDialogButtonBox(self)
95
98
  self.buttonBox.rejected.connect(self.close)
96
99
 
97
- self.btnClose = self.buttonBox.addButton(QDialogButtonBox.Close)
100
+ self.btnClose = self.buttonBox.addButton(QtDialogClose)
98
101
  self.btnClose.setAutoDefault(False)
99
102
 
100
- self.btnInsert = self.buttonBox.addButton(self.tr("Insert"), QDialogButtonBox.ActionRole)
103
+ self.btnInsert = self.buttonBox.addButton(self.tr("Insert"), QtRoleAction)
101
104
  self.btnInsert.clicked.connect(self._doInsert)
102
105
  self.btnInsert.setAutoDefault(False)
103
106
 
@@ -127,7 +130,7 @@ class GuiLipsum(QDialog):
127
130
  def getLipsum(cls, parent: QWidget) -> str:
128
131
  """Pop the dialog and return the lipsum text."""
129
132
  cls = GuiLipsum(parent)
130
- cls.exec_()
133
+ cls.exec()
131
134
  text = cls.lipsumText
132
135
  cls.deleteLater()
133
136
  return text
@@ -27,8 +27,8 @@ import logging
27
27
 
28
28
  from pathlib import Path
29
29
 
30
+ from PyQt5.QtCore import QTimer, pyqtSlot
30
31
  from PyQt5.QtGui import QCloseEvent
31
- from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
32
32
  from PyQt5.QtWidgets import (
33
33
  QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QFileDialog,
34
34
  QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
@@ -36,13 +36,17 @@ from PyQt5.QtWidgets import (
36
36
  )
37
37
 
38
38
  from novelwriter import CONFIG, SHARED
39
- from novelwriter.enum import nwBuildFmt
40
39
  from novelwriter.common import makeFileNameSafe, openExternalPath
41
40
  from novelwriter.constants import nwLabels
42
- from novelwriter.core.item import NWItem
43
- from novelwriter.core.docbuild import NWBuildDocument
44
41
  from novelwriter.core.buildsettings import BuildSettings
42
+ from novelwriter.core.docbuild import NWBuildDocument
43
+ from novelwriter.core.item import NWItem
44
+ from novelwriter.enum import nwBuildFmt
45
+ from novelwriter.extensions.modified import NIconToolButton
45
46
  from novelwriter.extensions.simpleprogress import NProgressSimple
47
+ from novelwriter.types import (
48
+ QtAlignCenter, QtDialogClose, QtRoleAction, QtRoleReject, QtUserRole
49
+ )
46
50
 
47
51
  logger = logging.getLogger(__name__)
48
52
 
@@ -54,9 +58,9 @@ class GuiManuscriptBuild(QDialog):
54
58
  independently of the Manuscript Build Tool.
55
59
  """
56
60
 
57
- D_KEY = Qt.ItemDataRole.UserRole
61
+ D_KEY = QtUserRole
58
62
 
59
- def __init__(self, parent: QWidget, build: BuildSettings):
63
+ def __init__(self, parent: QWidget, build: BuildSettings) -> None:
60
64
  super().__init__(parent=parent)
61
65
 
62
66
  logger.debug("Create: GuiManuscriptBuild")
@@ -69,7 +73,8 @@ class GuiManuscriptBuild(QDialog):
69
73
  self.setMinimumWidth(CONFIG.pxInt(500))
70
74
  self.setMinimumHeight(CONFIG.pxInt(300))
71
75
 
72
- iPx = SHARED.theme.baseIconSize
76
+ iSz = SHARED.theme.baseIconSize
77
+ bSz = SHARED.theme.buttonIconSize
73
78
  sp4 = CONFIG.pxInt(4)
74
79
  sp8 = CONFIG.pxInt(8)
75
80
  sp16 = CONFIG.pxInt(16)
@@ -85,9 +90,9 @@ class GuiManuscriptBuild(QDialog):
85
90
  # Output Format
86
91
  # =============
87
92
 
88
- self.lblFormat = QLabel(self.tr("Output Format"))
89
- self.listFormats = QListWidget()
90
- self.listFormats.setIconSize(QSize(iPx, iPx))
93
+ self.lblFormat = QLabel(self.tr("Output Format"), self)
94
+ self.listFormats = QListWidget(self)
95
+ self.listFormats.setIconSize(iSz)
91
96
  current = None
92
97
  for key in nwBuildFmt:
93
98
  item = QListWidgetItem()
@@ -104,17 +109,17 @@ class GuiManuscriptBuild(QDialog):
104
109
  self.formatBox.addWidget(self.listFormats, 1)
105
110
  self.formatBox.setContentsMargins(0, 0, 0, 0)
106
111
 
107
- self.formatWidget = QWidget()
112
+ self.formatWidget = QWidget(self)
108
113
  self.formatWidget.setLayout(self.formatBox)
109
114
  self.formatWidget.setContentsMargins(0, 0, 0, 0)
110
115
 
111
116
  # Table of Contents
112
117
  # =================
113
118
 
114
- self.lblContent = QLabel(self.tr("Table of Contents"))
119
+ self.lblContent = QLabel(self.tr("Table of Contents"), self)
115
120
 
116
121
  self.listContent = QListWidget(self)
117
- self.listContent.setIconSize(QSize(iPx, iPx))
122
+ self.listContent.setIconSize(iSz)
118
123
  self.listContent.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
119
124
 
120
125
  self.contentBox = QVBoxLayout()
@@ -122,7 +127,7 @@ class GuiManuscriptBuild(QDialog):
122
127
  self.contentBox.addWidget(self.listContent, 0)
123
128
  self.contentBox.setContentsMargins(0, 0, 0, 0)
124
129
 
125
- self.contentWidget = QWidget()
130
+ self.contentWidget = QWidget(self)
126
131
  self.contentWidget.setLayout(self.contentBox)
127
132
  self.contentWidget.setContentsMargins(0, 0, 0, 0)
128
133
 
@@ -134,14 +139,14 @@ class GuiManuscriptBuild(QDialog):
134
139
  font.setUnderline(True)
135
140
  font.setPointSizeF(1.5*font.pointSizeF())
136
141
 
137
- self.lblMain = QLabel(self._build.name)
142
+ self.lblMain = QLabel(self._build.name, self)
138
143
  self.lblMain.setWordWrap(True)
139
144
  self.lblMain.setFont(font)
140
145
 
141
146
  # Build Path
142
- self.lblPath = QLabel(self.tr("Path"))
147
+ self.lblPath = QLabel(self.tr("Path"), self)
143
148
  self.buildPath = QLineEdit(self)
144
- self.btnBrowse = QPushButton(SHARED.theme.getIcon("browse"), "")
149
+ self.btnBrowse = NIconToolButton(self, iSz, "browse")
145
150
 
146
151
  self.pathBox = QHBoxLayout()
147
152
  self.pathBox.addWidget(self.buildPath)
@@ -149,9 +154,9 @@ class GuiManuscriptBuild(QDialog):
149
154
  self.pathBox.setSpacing(sp8)
150
155
 
151
156
  # Build Name
152
- self.lblName = QLabel(self.tr("File Name"))
157
+ self.lblName = QLabel(self.tr("File Name"), self)
153
158
  self.buildName = QLineEdit(self)
154
- self.btnReset = QPushButton(SHARED.theme.getIcon("revert"), "")
159
+ self.btnReset = NIconToolButton(self, iSz, "revert")
155
160
  self.btnReset.setToolTip(self.tr("Reset file name to default"))
156
161
 
157
162
  self.nameBox = QHBoxLayout()
@@ -176,16 +181,19 @@ class GuiManuscriptBuild(QDialog):
176
181
  self.buildBox.setVerticalSpacing(sp4)
177
182
 
178
183
  # Dialog Buttons
179
- self.btnOpen = QPushButton(SHARED.theme.getIcon("browse"), self.tr("Open Folder"))
180
- self.btnBuild = QPushButton(SHARED.theme.getIcon("export"), self.tr("&Build"))
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)
184
+ self.btnOpen = QPushButton(SHARED.theme.getIcon("browse"), self.tr("Open Folder"), self)
185
+ self.btnOpen.setIconSize(bSz)
186
+ self.btnBuild = QPushButton(SHARED.theme.getIcon("export"), self.tr("&Build"), self)
187
+ self.btnBuild.setIconSize(bSz)
188
+
189
+ self.dlgButtons = QDialogButtonBox(QtDialogClose, self)
190
+ self.dlgButtons.addButton(self.btnOpen, QtRoleAction)
191
+ self.dlgButtons.addButton(self.btnBuild, QtRoleAction)
184
192
 
185
193
  # Assemble GUI
186
194
  # ============
187
195
 
188
- self.mainSplit = QSplitter()
196
+ self.mainSplit = QSplitter(self)
189
197
  self.mainSplit.addWidget(self.formatWidget)
190
198
  self.mainSplit.addWidget(self.contentWidget)
191
199
  self.mainSplit.setHandleWidth(sp16)
@@ -199,7 +207,7 @@ class GuiManuscriptBuild(QDialog):
199
207
  ])
200
208
 
201
209
  self.outerBox = QVBoxLayout()
202
- self.outerBox.addWidget(self.lblMain, 0, Qt.AlignCenter)
210
+ self.outerBox.addWidget(self.lblMain, 0, QtAlignCenter)
203
211
  self.outerBox.addSpacing(sp16)
204
212
  self.outerBox.addWidget(self.mainSplit, 1)
205
213
  self.outerBox.addSpacing(sp4)
@@ -252,20 +260,20 @@ class GuiManuscriptBuild(QDialog):
252
260
  ##
253
261
 
254
262
  @pyqtSlot("QAbstractButton*")
255
- def _dialogButtonClicked(self, button: QAbstractButton):
263
+ def _dialogButtonClicked(self, button: QAbstractButton) -> None:
256
264
  """Handle button clicks from the dialog button box."""
257
265
  role = self.dlgButtons.buttonRole(button)
258
- if role == QDialogButtonBox.ActionRole:
266
+ if role == QtRoleAction:
259
267
  if button == self.btnBuild:
260
268
  self._runBuild()
261
269
  elif button == self.btnOpen:
262
270
  self._openOutputFolder()
263
- elif role == QDialogButtonBox.RejectRole:
271
+ elif role == QtRoleReject:
264
272
  self.close()
265
273
  return
266
274
 
267
275
  @pyqtSlot()
268
- def _doSelectPath(self):
276
+ def _doSelectPath(self) -> None:
269
277
  """Select a folder for output."""
270
278
  bPath = Path(self.buildPath.text())
271
279
  bPath = bPath if bPath.is_dir() else self._build.lastPath
@@ -277,7 +285,7 @@ class GuiManuscriptBuild(QDialog):
277
285
  return
278
286
 
279
287
  @pyqtSlot()
280
- def _doResetBuildName(self):
288
+ def _doResetBuildName(self) -> None:
281
289
  """Generate a default build name."""
282
290
  bName = f"{SHARED.project.data.name} - {self._build.name}"
283
291
  self.buildName.setText(bName)
@@ -285,7 +293,7 @@ class GuiManuscriptBuild(QDialog):
285
293
  return
286
294
 
287
295
  @pyqtSlot()
288
- def _resetProgress(self):
296
+ def _resetProgress(self) -> None:
289
297
  """Set the progress bar back to 0."""
290
298
  self.buildProgress.setValue(0)
291
299
  return
@@ -342,7 +350,7 @@ class GuiManuscriptBuild(QDialog):
342
350
  return items[0].data(self.D_KEY)
343
351
  return None
344
352
 
345
- def _saveSettings(self):
353
+ def _saveSettings(self) -> None:
346
354
  """Save the user GUI settings."""
347
355
  winWidth = CONFIG.rpxInt(self.width())
348
356
  winHeight = CONFIG.rpxInt(self.height())
@@ -361,7 +369,7 @@ class GuiManuscriptBuild(QDialog):
361
369
 
362
370
  return
363
371
 
364
- def _populateContentList(self):
372
+ def _populateContentList(self) -> None:
365
373
  """Build the content list."""
366
374
  rootMap = {}
367
375
  filtered = self._build.buildItemFilter(SHARED.project)
@@ -390,7 +398,7 @@ class GuiManuscriptBuild(QDialog):
390
398
 
391
399
  return
392
400
 
393
- def _openOutputFolder(self):
401
+ def _openOutputFolder(self) -> None:
394
402
  """Open the build folder in the system's file explorer."""
395
403
  openExternalPath(Path(self.buildPath.text()))
396
404
  return