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
@@ -26,29 +26,36 @@ from __future__ import annotations
26
26
  import json
27
27
  import logging
28
28
 
29
+ from datetime import datetime
29
30
  from time import time
30
31
  from typing import TYPE_CHECKING
31
- from datetime import datetime
32
32
 
33
+ from PyQt5.QtCore import QTimer, QUrl, Qt, pyqtSignal, pyqtSlot
33
34
  from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
34
- from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
35
+ from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
35
36
  from PyQt5.QtWidgets import (
36
- QAbstractItemView, QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget,
37
- QListWidgetItem, QPushButton, QSplitter, QTextBrowser, QToolButton,
38
- QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, qApp
37
+ QAbstractItemView, QApplication, QDialog, QFormLayout, QGridLayout,
38
+ QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton,
39
+ QSizePolicy, QSplitter, QStackedWidget, QTabWidget, QTextBrowser,
40
+ QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
39
41
  )
40
- from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
41
42
 
42
43
  from novelwriter import CONFIG, SHARED
43
- from novelwriter.error import logException
44
44
  from novelwriter.common import checkInt, fuzzyTime
45
- from novelwriter.core.tohtml import ToHtml
45
+ from novelwriter.core.buildsettings import BuildCollection, BuildSettings
46
46
  from novelwriter.core.docbuild import NWBuildDocument
47
+ from novelwriter.core.tohtml import ToHtml
47
48
  from novelwriter.core.tokenizer import HeadingFormatter
48
- from novelwriter.core.buildsettings import BuildCollection, BuildSettings
49
+ from novelwriter.error import logException
50
+ from novelwriter.extensions.circularprogress import NProgressCircle
51
+ from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton
52
+ from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
49
53
  from novelwriter.tools.manusbuild import GuiManuscriptBuild
50
54
  from novelwriter.tools.manussettings import GuiBuildSettings
51
- from novelwriter.extensions.circularprogress import NProgressCircle
55
+ from novelwriter.types import (
56
+ QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignRight, QtAlignTop,
57
+ QtUserRole
58
+ )
52
59
 
53
60
  if TYPE_CHECKING: # pragma: no cover
54
61
  from novelwriter.guimain import GuiMain
@@ -64,7 +71,7 @@ class GuiManuscript(QDialog):
64
71
  a document directly to disk.
65
72
  """
66
73
 
67
- D_KEY = Qt.ItemDataRole.UserRole
74
+ D_KEY = QtUserRole
68
75
 
69
76
  def __init__(self, mainGui: GuiMain) -> None:
70
77
  super().__init__(parent=mainGui)
@@ -83,7 +90,7 @@ class GuiManuscript(QDialog):
83
90
  self.setMinimumWidth(CONFIG.pxInt(600))
84
91
  self.setMinimumHeight(CONFIG.pxInt(500))
85
92
 
86
- iPx = SHARED.theme.baseIconSize
93
+ iSz = SHARED.theme.baseIconSize
87
94
  wWin = CONFIG.pxInt(900)
88
95
  hWin = CONFIG.pxInt(600)
89
96
 
@@ -97,37 +104,27 @@ class GuiManuscript(QDialog):
97
104
  # ==============
98
105
 
99
106
  qPalette = self.palette()
100
- qPalette.setBrush(QPalette.Window, qPalette.base())
107
+ qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
101
108
  self.setPalette(qPalette)
102
109
 
103
- fadeCol = qPalette.text().color()
104
- buttonStyle = (
105
- "QToolButton {{padding: {0}px; border: none; background: transparent;}} "
106
- "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
107
- ).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
110
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
108
111
 
109
- self.tbAdd = QToolButton(self)
110
- self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
111
- self.tbAdd.setIconSize(QSize(iPx, iPx))
112
+ self.tbAdd = NIconToolButton(self, iSz, "add")
112
113
  self.tbAdd.setToolTip(self.tr("Add New Build"))
113
114
  self.tbAdd.setStyleSheet(buttonStyle)
114
115
  self.tbAdd.clicked.connect(self._createNewBuild)
115
116
 
116
- self.tbDel = QToolButton(self)
117
- self.tbDel.setIcon(SHARED.theme.getIcon("remove"))
118
- self.tbDel.setIconSize(QSize(iPx, iPx))
117
+ self.tbDel = NIconToolButton(self, iSz, "remove")
119
118
  self.tbDel.setToolTip(self.tr("Delete Selected Build"))
120
119
  self.tbDel.setStyleSheet(buttonStyle)
121
120
  self.tbDel.clicked.connect(self._deleteSelectedBuild)
122
121
 
123
- self.tbEdit = QToolButton(self)
124
- self.tbEdit.setIcon(SHARED.theme.getIcon("edit"))
125
- self.tbEdit.setIconSize(QSize(iPx, iPx))
122
+ self.tbEdit = NIconToolButton(self, iSz, "edit")
126
123
  self.tbEdit.setToolTip(self.tr("Edit Selected Build"))
127
124
  self.tbEdit.setStyleSheet(buttonStyle)
128
125
  self.tbEdit.clicked.connect(self._editSelectedBuild)
129
126
 
130
- self.lblBuilds = QLabel("<b>{0}</b>".format(self.tr("Builds")))
127
+ self.lblBuilds = QLabel("<b>{0}</b>".format(self.tr("Builds")), self)
131
128
 
132
129
  self.listToolBox = QHBoxLayout()
133
130
  self.listToolBox.addWidget(self.lblBuilds)
@@ -140,21 +137,31 @@ class GuiManuscript(QDialog):
140
137
  # Builds
141
138
  # ======
142
139
 
143
- self.buildList = QListWidget()
144
- self.buildList.setIconSize(QSize(iPx, iPx))
140
+ self.buildList = QListWidget(self)
141
+ self.buildList.setIconSize(iSz)
145
142
  self.buildList.doubleClicked.connect(self._editSelectedBuild)
146
143
  self.buildList.currentItemChanged.connect(self._updateBuildDetails)
147
- self.buildList.setSelectionMode(QAbstractItemView.SingleSelection)
148
- self.buildList.setDragDropMode(QAbstractItemView.InternalMove)
144
+ self.buildList.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
145
+ self.buildList.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
146
+
147
+ # Details Tabs
148
+ # ============
149
149
 
150
150
  self.buildDetails = _DetailsWidget(self)
151
151
  self.buildDetails.setColumnWidth(
152
152
  CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsWidth", 100)),
153
153
  )
154
154
 
155
+ self.buildOutline = _OutlineWidget(self)
156
+
157
+ self.detailsTabs = QTabWidget(self)
158
+ self.detailsTabs.addTab(self.buildDetails, self.tr("Details"))
159
+ self.detailsTabs.addTab(self.buildOutline, self.tr("Outline"))
160
+ self.detailsTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
161
+
155
162
  self.buildSplit = QSplitter(Qt.Orientation.Vertical, self)
156
163
  self.buildSplit.addWidget(self.buildList)
157
- self.buildSplit.addWidget(self.buildDetails)
164
+ self.buildSplit.addWidget(self.detailsTabs)
158
165
  self.buildSplit.setSizes([
159
166
  CONFIG.pxInt(pOptions.getInt("GuiManuscript", "listHeight", 50)),
160
167
  CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsHeight", 50)),
@@ -163,16 +170,16 @@ class GuiManuscript(QDialog):
163
170
  # Process Controls
164
171
  # ================
165
172
 
166
- self.btnPreview = QPushButton(self.tr("Preview"))
173
+ self.btnPreview = QPushButton(self.tr("Preview"), self)
167
174
  self.btnPreview.clicked.connect(self._generatePreview)
168
175
 
169
- self.btnPrint = QPushButton(self.tr("Print"))
176
+ self.btnPrint = QPushButton(self.tr("Print"), self)
170
177
  self.btnPrint.clicked.connect(self._printDocument)
171
178
 
172
- self.btnBuild = QPushButton(self.tr("Build"))
179
+ self.btnBuild = QPushButton(self.tr("Build"), self)
173
180
  self.btnBuild.clicked.connect(self._buildManuscript)
174
181
 
175
- self.btnClose = QPushButton(self.tr("Close"))
182
+ self.btnClose = QPushButton(self.tr("Close"), self)
176
183
  self.btnClose.clicked.connect(self.close)
177
184
 
178
185
  self.processBox = QGridLayout()
@@ -185,6 +192,15 @@ class GuiManuscript(QDialog):
185
192
  # ============
186
193
 
187
194
  self.docPreview = _PreviewWidget(self)
195
+ self.docStats = _StatsWidget(self)
196
+
197
+ self.docBox = QVBoxLayout()
198
+ self.docBox.addWidget(self.docPreview, 1)
199
+ self.docBox.addWidget(self.docStats, 0)
200
+ self.docBox.setContentsMargins(0, 0, 0, 0)
201
+
202
+ self.docWdiget = QWidget(self)
203
+ self.docWdiget.setLayout(self.docBox)
188
204
 
189
205
  self.controlBox = QVBoxLayout()
190
206
  self.controlBox.addLayout(self.listToolBox, 0)
@@ -192,12 +208,12 @@ class GuiManuscript(QDialog):
192
208
  self.controlBox.addLayout(self.processBox, 0)
193
209
  self.controlBox.setContentsMargins(0, 0, 0, 0)
194
210
 
195
- self.optsWidget = QWidget()
211
+ self.optsWidget = QWidget(self)
196
212
  self.optsWidget.setLayout(self.controlBox)
197
213
 
198
- self.mainSplit = QSplitter()
214
+ self.mainSplit = QSplitter(self)
199
215
  self.mainSplit.addWidget(self.optsWidget)
200
- self.mainSplit.addWidget(self.docPreview)
216
+ self.mainSplit.addWidget(self.docWdiget)
201
217
  self.mainSplit.setCollapsible(0, False)
202
218
  self.mainSplit.setCollapsible(1, False)
203
219
  self.mainSplit.setStretchFactor(0, 0)
@@ -213,6 +229,9 @@ class GuiManuscript(QDialog):
213
229
  self.setLayout(self.outerBox)
214
230
  self.setSizeGripEnabled(True)
215
231
 
232
+ # Signals
233
+ self.buildOutline.outlineEntryClicked.connect(self.docPreview.navigateTo)
234
+
216
235
  logger.debug("Ready: GuiManuscript")
217
236
 
218
237
  return
@@ -245,7 +264,7 @@ class GuiManuscript(QDialog):
245
264
  if isinstance(build, BuildSettings):
246
265
  self._updatePreview(data, build)
247
266
  except Exception:
248
- logger.error("Failed to save build cache")
267
+ logger.error("Failed to load build cache")
249
268
  logException()
250
269
  return
251
270
 
@@ -284,25 +303,21 @@ class GuiManuscript(QDialog):
284
303
  @pyqtSlot()
285
304
  def _editSelectedBuild(self) -> None:
286
305
  """Edit the currently selected build settings entry."""
287
- build = self._getSelectedBuild()
288
- if build is not None:
306
+ if build := self._getSelectedBuild():
289
307
  self._openSettingsDialog(build)
290
308
  return
291
309
 
292
310
  @pyqtSlot("QListWidgetItem*", "QListWidgetItem*")
293
311
  def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None:
294
312
  """Process change of build selection to update the details."""
295
- if isinstance(current, QListWidgetItem):
296
- build = self._builds.getBuild(current.data(self.D_KEY))
297
- if build is not None:
298
- self.buildDetails.updateInfo(build)
313
+ if current and (build := self._builds.getBuild(current.data(self.D_KEY))):
314
+ self.buildDetails.updateInfo(build)
299
315
  return
300
316
 
301
317
  @pyqtSlot()
302
318
  def _deleteSelectedBuild(self) -> None:
303
319
  """Delete the currently selected build settings entry."""
304
- build = self._getSelectedBuild()
305
- if build is not None:
320
+ if build := self._getSelectedBuild():
306
321
  if SHARED.question(self.tr("Delete build '{0}'?".format(build.name))):
307
322
  self._builds.removeBuild(build.buildID)
308
323
  self._updateBuildsList()
@@ -313,8 +328,7 @@ class GuiManuscript(QDialog):
313
328
  """Process new build settings from the settings dialog."""
314
329
  self._builds.setBuild(build)
315
330
  self._updateBuildItem(build)
316
- current = self.buildList.currentItem()
317
- if isinstance(current, QListWidgetItem) and current.data(self.D_KEY) == build.buildID:
331
+ if (current := self.buildList.currentItem()) and current.data(self.D_KEY) == build.buildID:
318
332
  self._updateBuildDetails(current, current)
319
333
  return
320
334
 
@@ -323,23 +337,25 @@ class GuiManuscript(QDialog):
323
337
  """Run the document builder on the current build settings for
324
338
  the preview widget.
325
339
  """
326
- build = self._getSelectedBuild()
327
- if build is None:
340
+ if not (build := self._getSelectedBuild()):
328
341
  return
329
342
 
330
343
  docBuild = NWBuildDocument(SHARED.project, build)
344
+ docBuild.setPreviewMode(True)
331
345
  docBuild.queueAll()
332
346
 
333
347
  self.docPreview.beginNewBuild(len(docBuild))
334
348
  for step, _ in docBuild.iterBuildHTML(None):
335
349
  self.docPreview.buildStep(step + 1)
336
- qApp.processEvents()
350
+ QApplication.processEvents()
337
351
 
338
352
  buildObj = docBuild.lastBuild
339
353
  assert isinstance(buildObj, ToHtml)
340
354
  result = {
341
355
  "uuid": build.buildID,
342
356
  "time": int(time()),
357
+ "stats": buildObj.textStats,
358
+ "outline": buildObj.textOutline,
343
359
  "styles": buildObj.getStyleSheet(),
344
360
  "html": buildObj.fullHTML,
345
361
  }
@@ -361,10 +377,9 @@ class GuiManuscript(QDialog):
361
377
  @pyqtSlot()
362
378
  def _buildManuscript(self) -> None:
363
379
  """Open the build dialog and build the manuscript."""
364
- build = self._getSelectedBuild()
365
- if isinstance(build, BuildSettings):
380
+ if build := self._getSelectedBuild():
366
381
  dlgBuild = GuiManuscriptBuild(self, build)
367
- dlgBuild.exec_()
382
+ dlgBuild.exec()
368
383
 
369
384
  # After the build is done, save build settings changes
370
385
  if build.changed:
@@ -377,7 +392,7 @@ class GuiManuscript(QDialog):
377
392
  """Open the print preview dialog."""
378
393
  preview = QPrintPreviewDialog(self)
379
394
  preview.paintRequested.connect(self.docPreview.printPreview)
380
- preview.exec_()
395
+ preview.exec()
381
396
  return
382
397
 
383
398
  ##
@@ -395,6 +410,8 @@ class GuiManuscript(QDialog):
395
410
  self.docPreview.setJustify(
396
411
  build.getBool("format.justifyText")
397
412
  )
413
+ self.docStats.updateStats(data.get("stats", {}))
414
+ self.buildOutline.updateOutline(data.get("outline", {}))
398
415
  return
399
416
 
400
417
  def _getSelectedBuild(self) -> BuildSettings | None:
@@ -463,7 +480,7 @@ class GuiManuscript(QDialog):
463
480
  dlgSettings.setModal(False)
464
481
  dlgSettings.show()
465
482
  dlgSettings.raise_()
466
- qApp.processEvents()
483
+ QApplication.processEvents()
467
484
  dlgSettings.loadContent()
468
485
  dlgSettings.newSettingsReady.connect(self._processNewSettings)
469
486
 
@@ -503,8 +520,8 @@ class _DetailsWidget(QWidget):
503
520
  # Tree Widget
504
521
  self.listView = QTreeWidget(self)
505
522
  self.listView.setHeaderLabels([self.tr("Setting"), self.tr("Value")])
506
- self.listView.setIndentation(SHARED.theme.baseIconSize)
507
- self.listView.setSelectionMode(QAbstractItemView.NoSelection)
523
+ self.listView.setIndentation(SHARED.theme.baseIconHeight)
524
+ self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
508
525
 
509
526
  # Assemble
510
527
  self.outerBox = QVBoxLayout()
@@ -592,24 +609,26 @@ class _DetailsWidget(QWidget):
592
609
  hFmt.resetScene()
593
610
  hFmt.incScene()
594
611
  title = self.tr("Title")
612
+ hidden = self.tr("Hidden")
595
613
 
596
614
  item = QTreeWidgetItem()
597
615
  item.setText(0, build.getLabel("headings"))
598
616
  item.setText(1, "")
599
617
  self.listView.addTopLevelItem(item)
600
- entries = [
601
- "headings.fmtTitle", "headings.fmtChapter", "headings.fmtUnnumbered",
602
- "headings.fmtScene", "headings.fmtSection"
603
- ]
604
- for key in entries:
618
+ for hFormat, hHide in [
619
+ ("headings.fmtTitle", "headings.hideTitle"),
620
+ ("headings.fmtChapter", "headings.hideChapter"),
621
+ ("headings.fmtUnnumbered", "headings.hideUnnumbered"),
622
+ ("headings.fmtScene", "headings.hideScene"),
623
+ ("headings.fmtAltScene", "headings.hideAltScene"),
624
+ ("headings.fmtSection", "headings.hideSection"),
625
+ ]:
605
626
  sub = QTreeWidgetItem()
606
- sub.setText(0, build.getLabel(key))
607
- sub.setText(1, hFmt.apply(build.getStr(key), title, 0))
608
- item.addChild(sub)
609
- for key in ["headings.hideScene", "headings.hideSection"]:
610
- sub = QTreeWidgetItem()
611
- sub.setText(0, build.getLabel(key))
612
- sub.setIcon(1, on if build.getBool(key) else off)
627
+ sub.setText(0, build.getLabel(hFormat))
628
+ if build.getBool(hHide):
629
+ sub.setText(1, f"[{hidden}]")
630
+ else:
631
+ sub.setText(1, hFmt.apply(build.getStr(hFormat), title, 0))
613
632
  item.addChild(sub)
614
633
 
615
634
  # Text Content
@@ -617,11 +636,10 @@ class _DetailsWidget(QWidget):
617
636
  item.setText(0, build.getLabel("text.grpContent"))
618
637
  item.setText(1, "")
619
638
  self.listView.addTopLevelItem(item)
620
- entries = [
639
+ for key in [
621
640
  "text.includeSynopsis", "text.includeComments",
622
641
  "text.includeKeywords", "text.includeBodyText",
623
- ]
624
- for key in entries:
642
+ ]:
625
643
  sub = QTreeWidgetItem()
626
644
  sub.setText(0, build.getLabel(key))
627
645
  sub.setIcon(1, on if build.getBool(key) else off)
@@ -635,6 +653,87 @@ class _DetailsWidget(QWidget):
635
653
  # END Class _DetailsWidget
636
654
 
637
655
 
656
+ class _OutlineWidget(QWidget):
657
+
658
+ D_LINE = QtUserRole
659
+
660
+ outlineEntryClicked = pyqtSignal(str)
661
+
662
+ def __init__(self, parent: QWidget) -> None:
663
+ super().__init__(parent=parent)
664
+
665
+ self._outline = {}
666
+
667
+ # Tree Widget
668
+ self.listView = QTreeWidget(self)
669
+ self.listView.setHeaderHidden(True)
670
+ self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
671
+ self.listView.itemClicked.connect(self._onItemClick)
672
+
673
+ # Assemble
674
+ self.outerBox = QVBoxLayout()
675
+ self.outerBox.addWidget(self.listView)
676
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
677
+ self.setLayout(self.outerBox)
678
+
679
+ return
680
+
681
+ def updateOutline(self, data: dict[str, str]) -> None:
682
+ """Update the outline."""
683
+ if isinstance(data, dict) and data != self._outline:
684
+ self.listView.clear()
685
+
686
+ tFont = self.font()
687
+ tFont.setBold(True)
688
+ tBrush = self.palette().highlight()
689
+
690
+ hFont = self.font()
691
+ hFont.setBold(True)
692
+ hFont.setUnderline(True)
693
+
694
+ root = self.listView.invisibleRootItem()
695
+ parent = root
696
+ indent = False
697
+ for anchor, entry in data.items():
698
+ prefix, _, text = entry.partition("|")
699
+ if prefix in ("TT", "PT", "CH", "SC", "H1", "H2"):
700
+ item = QTreeWidgetItem([text])
701
+ item.setData(0, self.D_LINE, anchor)
702
+ if prefix == "TT":
703
+ item.setFont(0, tFont)
704
+ item.setForeground(0, tBrush)
705
+ root.addChild(item)
706
+ parent = root
707
+ elif prefix == "PT":
708
+ item.setFont(0, hFont)
709
+ root.addChild(item)
710
+ parent = root
711
+ elif prefix in ("CH", "H1"):
712
+ root.addChild(item)
713
+ parent = item
714
+ elif prefix in ("SC", "H2"):
715
+ parent.addChild(item)
716
+ indent = True
717
+
718
+ self.listView.setIndentation(
719
+ SHARED.theme.baseIconHeight if indent else CONFIG.pxInt(4)
720
+ )
721
+ self._outline = data
722
+
723
+ return
724
+
725
+ ##
726
+ # Private Slots
727
+ ##
728
+
729
+ def _onItemClick(self, item: QTreeWidgetItem) -> None:
730
+ """Process tree item click."""
731
+ self.outlineEntryClicked.emit(str(item.data(0, self.D_LINE)))
732
+ return
733
+
734
+ # END Class _OutlineWidget
735
+
736
+
638
737
  class _PreviewWidget(QTextBrowser):
639
738
 
640
739
  def __init__(self, parent: QWidget) -> None:
@@ -645,8 +744,8 @@ class _PreviewWidget(QTextBrowser):
645
744
 
646
745
  # Document Setup
647
746
  dPalette = self.palette()
648
- dPalette.setColor(QPalette.Base, QColor(255, 255, 255))
649
- dPalette.setColor(QPalette.Text, QColor(0, 0, 0))
747
+ dPalette.setColor(QPalette.ColorRole.Base, QColor(255, 255, 255))
748
+ dPalette.setColor(QPalette.ColorRole.Text, QColor(0, 0, 0))
650
749
  self.setPalette(dPalette)
651
750
 
652
751
  self.setMinimumWidth(40*SHARED.theme.textNWidth)
@@ -663,8 +762,8 @@ class _PreviewWidget(QTextBrowser):
663
762
 
664
763
  # Document Age
665
764
  aPalette = self.palette()
666
- aPalette.setColor(QPalette.Background, aPalette.toolTipBase().color())
667
- aPalette.setColor(QPalette.Foreground, aPalette.toolTipText().color())
765
+ aPalette.setColor(QPalette.ColorRole.Window, aPalette.toolTipBase().color())
766
+ aPalette.setColor(QPalette.ColorRole.WindowText, aPalette.toolTipText().color())
668
767
 
669
768
  aFont = self.font()
670
769
  aFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
@@ -674,7 +773,7 @@ class _PreviewWidget(QTextBrowser):
674
773
  self.ageLabel.setFont(aFont)
675
774
  self.ageLabel.setPalette(aPalette)
676
775
  self.ageLabel.setAutoFillBackground(True)
677
- self.ageLabel.setAlignment(Qt.AlignCenter)
776
+ self.ageLabel.setAlignment(QtAlignCenter)
678
777
  self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize))
679
778
 
680
779
  # Progress
@@ -692,8 +791,8 @@ class _PreviewWidget(QTextBrowser):
692
791
  self._updateBuildAge()
693
792
 
694
793
  # Age Timer
695
- self.ageTimer = QTimer()
696
- self.ageTimer.setInterval(10)
794
+ self.ageTimer = QTimer(self)
795
+ self.ageTimer.setInterval(10000)
697
796
  self.ageTimer.timeout.connect(self._updateBuildAge)
698
797
  self.ageTimer.start()
699
798
 
@@ -713,9 +812,9 @@ class _PreviewWidget(QTextBrowser):
713
812
  """Enable/disable the justify text option."""
714
813
  pOptions = self.document().defaultTextOption()
715
814
  if state:
716
- pOptions.setAlignment(Qt.AlignJustify)
815
+ pOptions.setAlignment(QtAlignJustify)
717
816
  else:
718
- pOptions.setAlignment(Qt.AlignAbsolute)
817
+ pOptions.setAlignment(QtAlignAbsolute)
719
818
  self.document().setDefaultTextOption(pOptions)
720
819
  return
721
820
 
@@ -745,31 +844,24 @@ class _PreviewWidget(QTextBrowser):
745
844
  def buildStep(self, value: int) -> None:
746
845
  """Update the progress bar value."""
747
846
  self.buildProgress.setValue(value)
748
- qApp.processEvents()
847
+ QApplication.processEvents()
749
848
  return
750
849
 
751
850
  def setContent(self, data: dict) -> None:
752
851
  """Set the content of the preview widget."""
753
852
  sPos = self.verticalScrollBar().value()
754
- qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
853
+ QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
755
854
 
756
855
  self.buildProgress.setCentreText(self.tr("Processing ..."))
757
- qApp.processEvents()
758
-
759
- styles = "\n".join(data.get("styles", [
760
- "h1, h2 {color: rgb(66, 113, 174);}",
761
- "h3, h4 {color: rgb(50, 50, 50);}",
762
- "a {color: rgb(66, 113, 174);}",
763
- ".tags {color: rgb(245, 135, 31); font-weight: bold;}",
764
- ]))
856
+ QApplication.processEvents()
857
+
858
+ styles = "\n".join(data.get("styles", []))
765
859
  self.document().setDefaultStyleSheet(styles)
766
860
 
767
861
  html = "".join(data.get("html", []))
768
862
  html = html.replace("\t", "!!tab!!")
769
- html = html.replace("<del>", "<span style='text-decoration: line-through;'>")
770
- html = html.replace("</del>", "</span>")
771
863
  self.setHtml(html)
772
- qApp.processEvents()
864
+ QApplication.processEvents()
773
865
  while self.find("!!tab!!"):
774
866
  cursor = self.textCursor()
775
867
  cursor.insertText("\t")
@@ -783,8 +875,8 @@ class _PreviewWidget(QTextBrowser):
783
875
  self.document().markContentsDirty(0, self.document().characterCount())
784
876
 
785
877
  self.buildProgress.setCentreText(self.tr("Done"))
786
- qApp.restoreOverrideCursor()
787
- qApp.processEvents()
878
+ QApplication.restoreOverrideCursor()
879
+ QApplication.processEvents()
788
880
  QTimer.singleShot(300, self._hideProgress)
789
881
 
790
882
  return
@@ -806,10 +898,17 @@ class _PreviewWidget(QTextBrowser):
806
898
  @pyqtSlot("QPrinter*")
807
899
  def printPreview(self, printer: QPrinter) -> None:
808
900
  """Connect the print preview painter to the document viewer."""
809
- qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
810
- printer.setOrientation(QPrinter.Portrait)
901
+ QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
902
+ printer.setOrientation(QPrinter.Orientation.Portrait)
811
903
  self.document().print(printer)
812
- qApp.restoreOverrideCursor()
904
+ QApplication.restoreOverrideCursor()
905
+ return
906
+
907
+ @pyqtSlot(str)
908
+ def navigateTo(self, anchor: str) -> None:
909
+ """Go to a specific #link in the document."""
910
+ logger.debug("Moving to anchor '#%s'", anchor)
911
+ self.setSource(QUrl(f"#{anchor}"))
813
912
  return
814
913
 
815
914
  ##
@@ -821,7 +920,7 @@ class _PreviewWidget(QTextBrowser):
821
920
  """Update the build time and the fuzzy age."""
822
921
  if self._docTime > 0:
823
922
  strBuildTime = "%s (%s)" % (
824
- datetime.fromtimestamp(self._docTime).strftime("%x %X"),
923
+ CONFIG.localDateTime(datetime.fromtimestamp(self._docTime)),
825
924
  fuzzyTime(int(time()) - self._docTime)
826
925
  )
827
926
  else:
@@ -859,3 +958,181 @@ class _PreviewWidget(QTextBrowser):
859
958
  return
860
959
 
861
960
  # END Class _PreviewWidget
961
+
962
+
963
+ class _StatsWidget(QWidget):
964
+
965
+ def __init__(self, parent: QWidget) -> None:
966
+ super().__init__(parent=parent)
967
+
968
+ font = self.font()
969
+ font.setPointSizeF(0.9*SHARED.theme.fontPointSize)
970
+ self.setFont(font)
971
+
972
+ self.minWidget = QWidget(self)
973
+ self.maxWidget = QWidget(self)
974
+
975
+ self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize, "unfold")
976
+ self.toggleButton.toggled.connect(self._toggleView)
977
+
978
+ self._buildMinimal()
979
+ self._buildMaximal()
980
+
981
+ self.mainStack = QStackedWidget(self)
982
+ self.mainStack.addWidget(self.minWidget)
983
+ self.mainStack.addWidget(self.maxWidget)
984
+
985
+ self.outerBox = QHBoxLayout()
986
+ self.outerBox.addWidget(self.toggleButton, 0, QtAlignTop)
987
+ self.outerBox.addWidget(self.mainStack, 1, QtAlignTop)
988
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
989
+
990
+ self.setLayout(self.outerBox)
991
+
992
+ self._toggleView(False)
993
+
994
+ return
995
+
996
+ def updateStats(self, data: dict[str, int]) -> None:
997
+ """Update the stats values from a Tokenizer stats dict."""
998
+ # Minimal
999
+ self.minWordCount.setText("{0:n}".format(data.get("allWords", 0)))
1000
+ self.minCharCount.setText("{0:n}".format(data.get("allChars", 0)))
1001
+
1002
+ # Maximal
1003
+ self.maxTotalWords.setText("{0:n}".format(data.get("allWords", 0)))
1004
+ self.maxHeadWords.setText("{0:n}".format(data.get("titleWords", 0)))
1005
+ self.maxTextWords.setText("{0:n}".format(data.get("textWords", 0)))
1006
+ self.maxTitleCount.setText("{0:n}".format(data.get("titleCount", 0)))
1007
+ self.maxParCount.setText("{0:n}".format(data.get("paragraphCount", 0)))
1008
+
1009
+ self.maxTotalChars.setText("{0:n}".format(data.get("allChars", 0)))
1010
+ self.maxHeaderChars.setText("{0:n}".format(data.get("titleChars", 0)))
1011
+ self.maxTextChars.setText("{0:n}".format(data.get("textChars", 0)))
1012
+
1013
+ self.maxTotalWordChars.setText("{0:n}".format(data.get("allWordChars", 0)))
1014
+ self.maxHeadWordChars.setText("{0:n}".format(data.get("titleWordChars", 0)))
1015
+ self.maxTextWordChars.setText("{0:n}".format(data.get("textWordChars", 0)))
1016
+
1017
+ return
1018
+
1019
+ ##
1020
+ # Private Slots
1021
+ ##
1022
+
1023
+ @pyqtSlot(bool)
1024
+ def _toggleView(self, state: bool) -> None:
1025
+ """Toggle minimal or maximal view."""
1026
+ ignored = QSizePolicy.Policy.Ignored
1027
+ expanded = QSizePolicy.Policy.Expanding
1028
+ if state:
1029
+ self.mainStack.setCurrentWidget(self.maxWidget)
1030
+ self.maxWidget.setSizePolicy(expanded, expanded)
1031
+ self.minWidget.setSizePolicy(ignored, ignored)
1032
+ else:
1033
+ self.mainStack.setCurrentWidget(self.minWidget)
1034
+ self.maxWidget.setSizePolicy(ignored, ignored)
1035
+ self.minWidget.setSizePolicy(expanded, expanded)
1036
+ self.maxWidget.adjustSize()
1037
+ self.minWidget.adjustSize()
1038
+ self.mainStack.adjustSize()
1039
+ self.adjustSize()
1040
+ return
1041
+
1042
+ ##
1043
+ # Internal Functions
1044
+ ##
1045
+
1046
+ def _buildMinimal(self) -> None:
1047
+ """Build the minimal stats page."""
1048
+ mPx = CONFIG.pxInt(8)
1049
+
1050
+ self.lblWordCount = QLabel(self.tr("Words"), self)
1051
+ self.minWordCount = QLabel(self)
1052
+
1053
+ self.lblCharCount = QLabel(self.tr("Characters"), self)
1054
+ self.minCharCount = QLabel(self)
1055
+
1056
+ # Assemble
1057
+ self.minLayout = QHBoxLayout()
1058
+ self.minLayout.addWidget(self.lblWordCount)
1059
+ self.minLayout.addWidget(self.minWordCount)
1060
+ self.minLayout.addSpacing(mPx)
1061
+ self.minLayout.addWidget(self.lblCharCount)
1062
+ self.minLayout.addWidget(self.minCharCount)
1063
+ self.minLayout.addStretch(1)
1064
+ self.minLayout.setSpacing(mPx)
1065
+ self.minLayout.setContentsMargins(0, 0, 0, 0)
1066
+
1067
+ self.minWidget.setLayout(self.minLayout)
1068
+
1069
+ return
1070
+
1071
+ def _buildMaximal(self) -> None:
1072
+ """Build the maximal stats page."""
1073
+ hPx = CONFIG.pxInt(12)
1074
+ vPx = CONFIG.pxInt(4)
1075
+
1076
+ # Left Column
1077
+ self.maxTotalWords = QLabel(self)
1078
+ self.maxHeadWords = QLabel(self)
1079
+ self.maxTextWords = QLabel(self)
1080
+ self.maxTitleCount = QLabel(self)
1081
+ self.maxParCount = QLabel(self)
1082
+
1083
+ self.maxTotalWords.setAlignment(QtAlignRight)
1084
+ self.maxHeadWords.setAlignment(QtAlignRight)
1085
+ self.maxTextWords.setAlignment(QtAlignRight)
1086
+ self.maxTitleCount.setAlignment(QtAlignRight)
1087
+ self.maxParCount.setAlignment(QtAlignRight)
1088
+
1089
+ self.leftForm = QFormLayout()
1090
+ self.leftForm.addRow(self.tr("Words"), self.maxTotalWords)
1091
+ self.leftForm.addRow(self.tr("Words in Headings"), self.maxHeadWords)
1092
+ self.leftForm.addRow(self.tr("Words in Text"), self.maxTextWords)
1093
+ self.leftForm.addRow("", QLabel(self))
1094
+ self.leftForm.addRow(self.tr("Headings"), self.maxTitleCount)
1095
+ self.leftForm.addRow(self.tr("Paragraphs"), self.maxParCount)
1096
+ self.leftForm.setHorizontalSpacing(hPx)
1097
+ self.leftForm.setVerticalSpacing(vPx)
1098
+
1099
+ # Right Column
1100
+ self.maxTotalChars = QLabel(self)
1101
+ self.maxHeaderChars = QLabel(self)
1102
+ self.maxTextChars = QLabel(self)
1103
+
1104
+ self.maxTotalWordChars = QLabel(self)
1105
+ self.maxHeadWordChars = QLabel(self)
1106
+ self.maxTextWordChars = QLabel(self)
1107
+
1108
+ self.maxTotalChars.setAlignment(QtAlignRight)
1109
+ self.maxHeaderChars.setAlignment(QtAlignRight)
1110
+ self.maxTextChars.setAlignment(QtAlignRight)
1111
+
1112
+ self.maxTotalWordChars.setAlignment(QtAlignRight)
1113
+ self.maxHeadWordChars.setAlignment(QtAlignRight)
1114
+ self.maxTextWordChars.setAlignment(QtAlignRight)
1115
+
1116
+ self.rightForm = QFormLayout()
1117
+ self.rightForm.addRow(self.tr("Characters"), self.maxTotalChars)
1118
+ self.rightForm.addRow(self.tr("Characters in Headings"), self.maxHeaderChars)
1119
+ self.rightForm.addRow(self.tr("Characters in Text"), self.maxTextChars)
1120
+ self.rightForm.addRow(self.tr("Characters, No Spaces"), self.maxTotalWordChars)
1121
+ self.rightForm.addRow(self.tr("Characters in Headings, No Spaces"), self.maxHeadWordChars)
1122
+ self.rightForm.addRow(self.tr("Characters in Text, No Spaces"), self.maxTextWordChars)
1123
+ self.rightForm.setHorizontalSpacing(hPx)
1124
+ self.rightForm.setVerticalSpacing(vPx)
1125
+
1126
+ # Assemble
1127
+ self.maxLayout = QHBoxLayout()
1128
+ self.maxLayout.addLayout(self.leftForm)
1129
+ self.maxLayout.addLayout(self.rightForm)
1130
+ self.maxLayout.addStretch(1)
1131
+ self.maxLayout.setSpacing(CONFIG.pxInt(32))
1132
+ self.maxLayout.setContentsMargins(0, 0, 0, 0)
1133
+
1134
+ self.maxWidget.setLayout(self.maxLayout)
1135
+
1136
+ return
1137
+
1138
+ # END Class _StatsWidget