novelWriter 2.3.1__py3-none-any.whl → 2.4rc1__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 (107) hide show
  1. {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/METADATA +5 -6
  2. {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/RECORD +102 -95
  3. novelwriter/__init__.py +7 -7
  4. novelwriter/assets/icons/none.svg +4 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  6. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  7. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  8. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  9. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  10. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  11. novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
  12. novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
  13. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  14. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  15. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  16. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  17. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  18. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  19. novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
  20. novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
  21. novelwriter/assets/manual.pdf +0 -0
  22. novelwriter/assets/sample.zip +0 -0
  23. novelwriter/assets/syntax/default_dark.conf +1 -0
  24. novelwriter/assets/syntax/default_light.conf +1 -0
  25. novelwriter/assets/syntax/grey_dark.conf +1 -0
  26. novelwriter/assets/syntax/grey_light.conf +1 -0
  27. novelwriter/assets/syntax/light_owl.conf +1 -0
  28. novelwriter/assets/syntax/night_owl.conf +1 -0
  29. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  30. novelwriter/assets/syntax/solarized_light.conf +1 -0
  31. novelwriter/assets/syntax/tomorrow.conf +1 -0
  32. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  33. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  34. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  35. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  36. novelwriter/assets/text/credits_en.htm +25 -23
  37. novelwriter/common.py +7 -2
  38. novelwriter/config.py +43 -16
  39. novelwriter/constants.py +5 -6
  40. novelwriter/core/buildsettings.py +60 -40
  41. novelwriter/core/coretools.py +97 -13
  42. novelwriter/core/docbuild.py +74 -7
  43. novelwriter/core/document.py +24 -3
  44. novelwriter/core/index.py +31 -112
  45. novelwriter/core/project.py +10 -15
  46. novelwriter/core/sessions.py +2 -2
  47. novelwriter/core/status.py +6 -5
  48. novelwriter/core/storage.py +8 -2
  49. novelwriter/core/tohtml.py +22 -25
  50. novelwriter/core/tokenizer.py +416 -232
  51. novelwriter/core/tomd.py +17 -8
  52. novelwriter/core/toodt.py +385 -350
  53. novelwriter/core/tree.py +8 -8
  54. novelwriter/dialogs/about.py +9 -11
  55. novelwriter/dialogs/docmerge.py +17 -14
  56. novelwriter/dialogs/docsplit.py +20 -19
  57. novelwriter/dialogs/editlabel.py +5 -4
  58. novelwriter/dialogs/preferences.py +31 -39
  59. novelwriter/dialogs/projectsettings.py +29 -26
  60. novelwriter/dialogs/quotes.py +10 -9
  61. novelwriter/dialogs/wordlist.py +15 -12
  62. novelwriter/enum.py +17 -14
  63. novelwriter/error.py +13 -11
  64. novelwriter/extensions/circularprogress.py +12 -8
  65. novelwriter/extensions/configlayout.py +1 -3
  66. novelwriter/extensions/modified.py +51 -2
  67. novelwriter/extensions/pagedsidebar.py +16 -14
  68. novelwriter/extensions/simpleprogress.py +3 -1
  69. novelwriter/extensions/statusled.py +3 -1
  70. novelwriter/extensions/switch.py +10 -9
  71. novelwriter/extensions/switchbox.py +14 -13
  72. novelwriter/extensions/versioninfo.py +1 -1
  73. novelwriter/gui/doceditor.py +413 -478
  74. novelwriter/gui/dochighlight.py +33 -29
  75. novelwriter/gui/docviewer.py +162 -175
  76. novelwriter/gui/docviewerpanel.py +20 -37
  77. novelwriter/gui/editordocument.py +15 -4
  78. novelwriter/gui/itemdetails.py +51 -54
  79. novelwriter/gui/mainmenu.py +37 -16
  80. novelwriter/gui/noveltree.py +30 -36
  81. novelwriter/gui/outline.py +114 -92
  82. novelwriter/gui/projtree.py +60 -66
  83. novelwriter/gui/search.py +362 -0
  84. novelwriter/gui/sidebar.py +36 -45
  85. novelwriter/gui/statusbar.py +14 -14
  86. novelwriter/gui/theme.py +93 -28
  87. novelwriter/guimain.py +207 -200
  88. novelwriter/shared.py +31 -6
  89. novelwriter/text/counting.py +137 -0
  90. novelwriter/tools/dictionaries.py +13 -12
  91. novelwriter/tools/lipsum.py +20 -17
  92. novelwriter/tools/manusbuild.py +35 -27
  93. novelwriter/tools/manuscript.py +374 -90
  94. novelwriter/tools/manussettings.py +261 -124
  95. novelwriter/tools/noveldetails.py +20 -18
  96. novelwriter/tools/welcome.py +48 -44
  97. novelwriter/tools/writingstats.py +61 -55
  98. novelwriter/types.py +90 -0
  99. novelwriter/core/__init__.py +0 -3
  100. novelwriter/dialogs/__init__.py +0 -3
  101. novelwriter/extensions/__init__.py +0 -3
  102. novelwriter/gui/__init__.py +0 -3
  103. novelwriter/tools/__init__.py +0 -3
  104. {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/LICENSE.md +0 -0
  105. {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/WHEEL +0 -0
  106. {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.dist-info}/entry_points.txt +0 -0
  107. {novelWriter-2.3.1.dist-info → novelWriter-2.4rc1.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("Build"))
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
 
@@ -328,18 +347,21 @@ class GuiManuscript(QDialog):
328
347
  return
329
348
 
330
349
  docBuild = NWBuildDocument(SHARED.project, build)
350
+ docBuild.setPreviewMode(True)
331
351
  docBuild.queueAll()
332
352
 
333
353
  self.docPreview.beginNewBuild(len(docBuild))
334
354
  for step, _ in docBuild.iterBuildHTML(None):
335
355
  self.docPreview.buildStep(step + 1)
336
- qApp.processEvents()
356
+ QApplication.processEvents()
337
357
 
338
358
  buildObj = docBuild.lastBuild
339
359
  assert isinstance(buildObj, ToHtml)
340
360
  result = {
341
361
  "uuid": build.buildID,
342
362
  "time": int(time()),
363
+ "stats": buildObj.textStats,
364
+ "outline": buildObj.textOutline,
343
365
  "styles": buildObj.getStyleSheet(),
344
366
  "html": buildObj.fullHTML,
345
367
  }
@@ -364,7 +386,7 @@ class GuiManuscript(QDialog):
364
386
  build = self._getSelectedBuild()
365
387
  if isinstance(build, BuildSettings):
366
388
  dlgBuild = GuiManuscriptBuild(self, build)
367
- dlgBuild.exec_()
389
+ dlgBuild.exec()
368
390
 
369
391
  # After the build is done, save build settings changes
370
392
  if build.changed:
@@ -377,7 +399,7 @@ class GuiManuscript(QDialog):
377
399
  """Open the print preview dialog."""
378
400
  preview = QPrintPreviewDialog(self)
379
401
  preview.paintRequested.connect(self.docPreview.printPreview)
380
- preview.exec_()
402
+ preview.exec()
381
403
  return
382
404
 
383
405
  ##
@@ -395,6 +417,8 @@ class GuiManuscript(QDialog):
395
417
  self.docPreview.setJustify(
396
418
  build.getBool("format.justifyText")
397
419
  )
420
+ self.docStats.updateStats(data.get("stats", {}))
421
+ self.buildOutline.updateOutline(data.get("outline", {}))
398
422
  return
399
423
 
400
424
  def _getSelectedBuild(self) -> BuildSettings | None:
@@ -463,7 +487,7 @@ class GuiManuscript(QDialog):
463
487
  dlgSettings.setModal(False)
464
488
  dlgSettings.show()
465
489
  dlgSettings.raise_()
466
- qApp.processEvents()
490
+ QApplication.processEvents()
467
491
  dlgSettings.loadContent()
468
492
  dlgSettings.newSettingsReady.connect(self._processNewSettings)
469
493
 
@@ -503,8 +527,8 @@ class _DetailsWidget(QWidget):
503
527
  # Tree Widget
504
528
  self.listView = QTreeWidget(self)
505
529
  self.listView.setHeaderLabels([self.tr("Setting"), self.tr("Value")])
506
- self.listView.setIndentation(SHARED.theme.baseIconSize)
507
- self.listView.setSelectionMode(QAbstractItemView.NoSelection)
530
+ self.listView.setIndentation(SHARED.theme.baseIconHeight)
531
+ self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
508
532
 
509
533
  # Assemble
510
534
  self.outerBox = QVBoxLayout()
@@ -592,24 +616,26 @@ class _DetailsWidget(QWidget):
592
616
  hFmt.resetScene()
593
617
  hFmt.incScene()
594
618
  title = self.tr("Title")
619
+ hidden = self.tr("Hidden")
595
620
 
596
621
  item = QTreeWidgetItem()
597
622
  item.setText(0, build.getLabel("headings"))
598
623
  item.setText(1, "")
599
624
  self.listView.addTopLevelItem(item)
600
- entries = [
601
- "headings.fmtTitle", "headings.fmtChapter", "headings.fmtUnnumbered",
602
- "headings.fmtScene", "headings.fmtSection"
603
- ]
604
- for key in entries:
625
+ for hFormat, hHide in [
626
+ ("headings.fmtTitle", "headings.hideTitle"),
627
+ ("headings.fmtChapter", "headings.hideChapter"),
628
+ ("headings.fmtUnnumbered", "headings.hideUnnumbered"),
629
+ ("headings.fmtScene", "headings.hideScene"),
630
+ ("headings.fmtHardScene", "headings.hideHardScene"),
631
+ ("headings.fmtSection", "headings.hideSection"),
632
+ ]:
605
633
  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)
634
+ sub.setText(0, build.getLabel(hFormat))
635
+ if build.getBool(hHide):
636
+ sub.setText(1, f"[{hidden}]")
637
+ else:
638
+ sub.setText(1, hFmt.apply(build.getStr(hFormat), title, 0))
613
639
  item.addChild(sub)
614
640
 
615
641
  # Text Content
@@ -617,11 +643,10 @@ class _DetailsWidget(QWidget):
617
643
  item.setText(0, build.getLabel("text.grpContent"))
618
644
  item.setText(1, "")
619
645
  self.listView.addTopLevelItem(item)
620
- entries = [
646
+ for key in [
621
647
  "text.includeSynopsis", "text.includeComments",
622
648
  "text.includeKeywords", "text.includeBodyText",
623
- ]
624
- for key in entries:
649
+ ]:
625
650
  sub = QTreeWidgetItem()
626
651
  sub.setText(0, build.getLabel(key))
627
652
  sub.setIcon(1, on if build.getBool(key) else off)
@@ -635,6 +660,87 @@ class _DetailsWidget(QWidget):
635
660
  # END Class _DetailsWidget
636
661
 
637
662
 
663
+ class _OutlineWidget(QWidget):
664
+
665
+ D_LINE = QtUserRole
666
+
667
+ outlineEntryClicked = pyqtSignal(str)
668
+
669
+ def __init__(self, parent: QWidget) -> None:
670
+ super().__init__(parent=parent)
671
+
672
+ self._outline = {}
673
+
674
+ # Tree Widget
675
+ self.listView = QTreeWidget(self)
676
+ self.listView.setHeaderHidden(True)
677
+ self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
678
+ self.listView.itemClicked.connect(self._onItemClick)
679
+
680
+ # Assemble
681
+ self.outerBox = QVBoxLayout()
682
+ self.outerBox.addWidget(self.listView)
683
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
684
+ self.setLayout(self.outerBox)
685
+
686
+ return
687
+
688
+ def updateOutline(self, data: dict[str, str]) -> None:
689
+ """Update the outline."""
690
+ if isinstance(data, dict) and data != self._outline:
691
+ self.listView.clear()
692
+
693
+ tFont = self.font()
694
+ tFont.setBold(True)
695
+ tBrush = self.palette().highlight()
696
+
697
+ hFont = self.font()
698
+ hFont.setBold(True)
699
+ hFont.setUnderline(True)
700
+
701
+ root = self.listView.invisibleRootItem()
702
+ parent = root
703
+ indent = False
704
+ for anchor, entry in data.items():
705
+ prefix, _, text = entry.partition("|")
706
+ if prefix in ("TT", "PT", "CH", "SC", "H1", "H2"):
707
+ item = QTreeWidgetItem([text])
708
+ item.setData(0, self.D_LINE, anchor)
709
+ if prefix == "TT":
710
+ item.setFont(0, tFont)
711
+ item.setForeground(0, tBrush)
712
+ root.addChild(item)
713
+ parent = root
714
+ elif prefix == "PT":
715
+ item.setFont(0, hFont)
716
+ root.addChild(item)
717
+ parent = root
718
+ elif prefix in ("CH", "H1"):
719
+ root.addChild(item)
720
+ parent = item
721
+ elif prefix in ("SC", "H2"):
722
+ parent.addChild(item)
723
+ indent = True
724
+
725
+ self.listView.setIndentation(
726
+ SHARED.theme.baseIconHeight if indent else CONFIG.pxInt(4)
727
+ )
728
+ self._outline = data
729
+
730
+ return
731
+
732
+ ##
733
+ # Private Slots
734
+ ##
735
+
736
+ def _onItemClick(self, item: QTreeWidgetItem) -> None:
737
+ """Process tree item click."""
738
+ self.outlineEntryClicked.emit(str(item.data(0, self.D_LINE)))
739
+ return
740
+
741
+ # END Class _OutlineWidget
742
+
743
+
638
744
  class _PreviewWidget(QTextBrowser):
639
745
 
640
746
  def __init__(self, parent: QWidget) -> None:
@@ -645,8 +751,8 @@ class _PreviewWidget(QTextBrowser):
645
751
 
646
752
  # Document Setup
647
753
  dPalette = self.palette()
648
- dPalette.setColor(QPalette.Base, QColor(255, 255, 255))
649
- dPalette.setColor(QPalette.Text, QColor(0, 0, 0))
754
+ dPalette.setColor(QPalette.ColorRole.Base, QColor(255, 255, 255))
755
+ dPalette.setColor(QPalette.ColorRole.Text, QColor(0, 0, 0))
650
756
  self.setPalette(dPalette)
651
757
 
652
758
  self.setMinimumWidth(40*SHARED.theme.textNWidth)
@@ -663,8 +769,8 @@ class _PreviewWidget(QTextBrowser):
663
769
 
664
770
  # Document Age
665
771
  aPalette = self.palette()
666
- aPalette.setColor(QPalette.Background, aPalette.toolTipBase().color())
667
- aPalette.setColor(QPalette.Foreground, aPalette.toolTipText().color())
772
+ aPalette.setColor(QPalette.ColorRole.Window, aPalette.toolTipBase().color())
773
+ aPalette.setColor(QPalette.ColorRole.WindowText, aPalette.toolTipText().color())
668
774
 
669
775
  aFont = self.font()
670
776
  aFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
@@ -674,7 +780,7 @@ class _PreviewWidget(QTextBrowser):
674
780
  self.ageLabel.setFont(aFont)
675
781
  self.ageLabel.setPalette(aPalette)
676
782
  self.ageLabel.setAutoFillBackground(True)
677
- self.ageLabel.setAlignment(Qt.AlignCenter)
783
+ self.ageLabel.setAlignment(QtAlignCenter)
678
784
  self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize))
679
785
 
680
786
  # Progress
@@ -692,8 +798,8 @@ class _PreviewWidget(QTextBrowser):
692
798
  self._updateBuildAge()
693
799
 
694
800
  # Age Timer
695
- self.ageTimer = QTimer()
696
- self.ageTimer.setInterval(10)
801
+ self.ageTimer = QTimer(self)
802
+ self.ageTimer.setInterval(10000)
697
803
  self.ageTimer.timeout.connect(self._updateBuildAge)
698
804
  self.ageTimer.start()
699
805
 
@@ -713,9 +819,9 @@ class _PreviewWidget(QTextBrowser):
713
819
  """Enable/disable the justify text option."""
714
820
  pOptions = self.document().defaultTextOption()
715
821
  if state:
716
- pOptions.setAlignment(Qt.AlignJustify)
822
+ pOptions.setAlignment(QtAlignJustify)
717
823
  else:
718
- pOptions.setAlignment(Qt.AlignAbsolute)
824
+ pOptions.setAlignment(QtAlignAbsolute)
719
825
  self.document().setDefaultTextOption(pOptions)
720
826
  return
721
827
 
@@ -745,31 +851,24 @@ class _PreviewWidget(QTextBrowser):
745
851
  def buildStep(self, value: int) -> None:
746
852
  """Update the progress bar value."""
747
853
  self.buildProgress.setValue(value)
748
- qApp.processEvents()
854
+ QApplication.processEvents()
749
855
  return
750
856
 
751
857
  def setContent(self, data: dict) -> None:
752
858
  """Set the content of the preview widget."""
753
859
  sPos = self.verticalScrollBar().value()
754
- qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
860
+ QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
755
861
 
756
862
  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
- ]))
863
+ QApplication.processEvents()
864
+
865
+ styles = "\n".join(data.get("styles", []))
765
866
  self.document().setDefaultStyleSheet(styles)
766
867
 
767
868
  html = "".join(data.get("html", []))
768
869
  html = html.replace("\t", "!!tab!!")
769
- html = html.replace("<del>", "<span style='text-decoration: line-through;'>")
770
- html = html.replace("</del>", "</span>")
771
870
  self.setHtml(html)
772
- qApp.processEvents()
871
+ QApplication.processEvents()
773
872
  while self.find("!!tab!!"):
774
873
  cursor = self.textCursor()
775
874
  cursor.insertText("\t")
@@ -783,8 +882,8 @@ class _PreviewWidget(QTextBrowser):
783
882
  self.document().markContentsDirty(0, self.document().characterCount())
784
883
 
785
884
  self.buildProgress.setCentreText(self.tr("Done"))
786
- qApp.restoreOverrideCursor()
787
- qApp.processEvents()
885
+ QApplication.restoreOverrideCursor()
886
+ QApplication.processEvents()
788
887
  QTimer.singleShot(300, self._hideProgress)
789
888
 
790
889
  return
@@ -806,10 +905,17 @@ class _PreviewWidget(QTextBrowser):
806
905
  @pyqtSlot("QPrinter*")
807
906
  def printPreview(self, printer: QPrinter) -> None:
808
907
  """Connect the print preview painter to the document viewer."""
809
- qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
810
- printer.setOrientation(QPrinter.Portrait)
908
+ QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
909
+ printer.setOrientation(QPrinter.Orientation.Portrait)
811
910
  self.document().print(printer)
812
- qApp.restoreOverrideCursor()
911
+ QApplication.restoreOverrideCursor()
912
+ return
913
+
914
+ @pyqtSlot(str)
915
+ def navigateTo(self, anchor: str) -> None:
916
+ """Go to a specific #link in the document."""
917
+ logger.debug("Moving to anchor '#%s'", anchor)
918
+ self.setSource(QUrl(f"#{anchor}"))
813
919
  return
814
920
 
815
921
  ##
@@ -821,7 +927,7 @@ class _PreviewWidget(QTextBrowser):
821
927
  """Update the build time and the fuzzy age."""
822
928
  if self._docTime > 0:
823
929
  strBuildTime = "%s (%s)" % (
824
- datetime.fromtimestamp(self._docTime).strftime("%x %X"),
930
+ CONFIG.localDateTime(datetime.fromtimestamp(self._docTime)),
825
931
  fuzzyTime(int(time()) - self._docTime)
826
932
  )
827
933
  else:
@@ -859,3 +965,181 @@ class _PreviewWidget(QTextBrowser):
859
965
  return
860
966
 
861
967
  # END Class _PreviewWidget
968
+
969
+
970
+ class _StatsWidget(QWidget):
971
+
972
+ def __init__(self, parent: QWidget) -> None:
973
+ super().__init__(parent=parent)
974
+
975
+ font = self.font()
976
+ font.setPointSizeF(0.9*SHARED.theme.fontPointSize)
977
+ self.setFont(font)
978
+
979
+ self.minWidget = QWidget(self)
980
+ self.maxWidget = QWidget(self)
981
+
982
+ self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize, "unfold")
983
+ self.toggleButton.toggled.connect(self._toggleView)
984
+
985
+ self._buildMinimal()
986
+ self._buildMaximal()
987
+
988
+ self.mainStack = QStackedWidget(self)
989
+ self.mainStack.addWidget(self.minWidget)
990
+ self.mainStack.addWidget(self.maxWidget)
991
+
992
+ self.outerBox = QHBoxLayout()
993
+ self.outerBox.addWidget(self.toggleButton, 0, QtAlignTop)
994
+ self.outerBox.addWidget(self.mainStack, 1, QtAlignTop)
995
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
996
+
997
+ self.setLayout(self.outerBox)
998
+
999
+ self._toggleView(False)
1000
+
1001
+ return
1002
+
1003
+ def updateStats(self, data: dict[str, int]) -> None:
1004
+ """Update the stats values from a Tokenizer stats dict."""
1005
+ # Minimal
1006
+ self.minWordCount.setText("{0:n}".format(data.get("allWords", 0)))
1007
+ self.minCharCount.setText("{0:n}".format(data.get("allChars", 0)))
1008
+
1009
+ # Maximal
1010
+ self.maxTotalWords.setText("{0:n}".format(data.get("allWords", 0)))
1011
+ self.maxHeaderWords.setText("{0:n}".format(data.get("titleWords", 0)))
1012
+ self.maxTextWords.setText("{0:n}".format(data.get("textWords", 0)))
1013
+ self.maxTitleCount.setText("{0:n}".format(data.get("titleCount", 0)))
1014
+ self.maxParCount.setText("{0:n}".format(data.get("paragraphCount", 0)))
1015
+
1016
+ self.maxTotalChars.setText("{0:n}".format(data.get("allChars", 0)))
1017
+ self.maxHeaderChars.setText("{0:n}".format(data.get("titleChars", 0)))
1018
+ self.maxTextChars.setText("{0:n}".format(data.get("textChars", 0)))
1019
+
1020
+ self.maxTotalWordChars.setText("{0:n}".format(data.get("allWordChars", 0)))
1021
+ self.maxHeaderWordChars.setText("{0:n}".format(data.get("titleWordChars", 0)))
1022
+ self.maxTextWordChars.setText("{0:n}".format(data.get("textWordChars", 0)))
1023
+
1024
+ return
1025
+
1026
+ ##
1027
+ # Private Slots
1028
+ ##
1029
+
1030
+ @pyqtSlot(bool)
1031
+ def _toggleView(self, state: bool) -> None:
1032
+ """Toggle minimal or maximal view."""
1033
+ ignored = QSizePolicy.Policy.Ignored
1034
+ expanded = QSizePolicy.Policy.Expanding
1035
+ if state:
1036
+ self.mainStack.setCurrentWidget(self.maxWidget)
1037
+ self.maxWidget.setSizePolicy(expanded, expanded)
1038
+ self.minWidget.setSizePolicy(ignored, ignored)
1039
+ else:
1040
+ self.mainStack.setCurrentWidget(self.minWidget)
1041
+ self.maxWidget.setSizePolicy(ignored, ignored)
1042
+ self.minWidget.setSizePolicy(expanded, expanded)
1043
+ self.maxWidget.adjustSize()
1044
+ self.minWidget.adjustSize()
1045
+ self.mainStack.adjustSize()
1046
+ self.adjustSize()
1047
+ return
1048
+
1049
+ ##
1050
+ # Internal Functions
1051
+ ##
1052
+
1053
+ def _buildMinimal(self) -> None:
1054
+ """Build the minimal stats page."""
1055
+ mPx = CONFIG.pxInt(8)
1056
+
1057
+ self.lblWordCount = QLabel(self.tr("Words"), self)
1058
+ self.minWordCount = QLabel(self)
1059
+
1060
+ self.lblCharCount = QLabel(self.tr("Characters"), self)
1061
+ self.minCharCount = QLabel(self)
1062
+
1063
+ # Assemble
1064
+ self.minLayout = QHBoxLayout()
1065
+ self.minLayout.addWidget(self.lblWordCount)
1066
+ self.minLayout.addWidget(self.minWordCount)
1067
+ self.minLayout.addSpacing(mPx)
1068
+ self.minLayout.addWidget(self.lblCharCount)
1069
+ self.minLayout.addWidget(self.minCharCount)
1070
+ self.minLayout.addStretch(1)
1071
+ self.minLayout.setSpacing(mPx)
1072
+ self.minLayout.setContentsMargins(0, 0, 0, 0)
1073
+
1074
+ self.minWidget.setLayout(self.minLayout)
1075
+
1076
+ return
1077
+
1078
+ def _buildMaximal(self) -> None:
1079
+ """Build the maximal stats page."""
1080
+ hPx = CONFIG.pxInt(12)
1081
+ vPx = CONFIG.pxInt(4)
1082
+
1083
+ # Left Column
1084
+ self.maxTotalWords = QLabel(self)
1085
+ self.maxHeaderWords = QLabel(self)
1086
+ self.maxTextWords = QLabel(self)
1087
+ self.maxTitleCount = QLabel(self)
1088
+ self.maxParCount = QLabel(self)
1089
+
1090
+ self.maxTotalWords.setAlignment(QtAlignRight)
1091
+ self.maxHeaderWords.setAlignment(QtAlignRight)
1092
+ self.maxTextWords.setAlignment(QtAlignRight)
1093
+ self.maxTitleCount.setAlignment(QtAlignRight)
1094
+ self.maxParCount.setAlignment(QtAlignRight)
1095
+
1096
+ self.leftForm = QFormLayout()
1097
+ self.leftForm.addRow(self.tr("Words"), self.maxTotalWords)
1098
+ self.leftForm.addRow(self.tr("Heading Words"), self.maxHeaderWords)
1099
+ self.leftForm.addRow(self.tr("Body Text Words"), self.maxTextWords)
1100
+ self.leftForm.addRow("", QLabel(self))
1101
+ self.leftForm.addRow(self.tr("Headings"), self.maxTitleCount)
1102
+ self.leftForm.addRow(self.tr("Paragraphs"), self.maxParCount)
1103
+ self.leftForm.setHorizontalSpacing(hPx)
1104
+ self.leftForm.setVerticalSpacing(vPx)
1105
+
1106
+ # Right Column
1107
+ self.maxTotalChars = QLabel(self)
1108
+ self.maxHeaderChars = QLabel(self)
1109
+ self.maxTextChars = QLabel(self)
1110
+
1111
+ self.maxTotalWordChars = QLabel(self)
1112
+ self.maxHeaderWordChars = QLabel(self)
1113
+ self.maxTextWordChars = QLabel(self)
1114
+
1115
+ self.maxTotalChars.setAlignment(QtAlignRight)
1116
+ self.maxHeaderChars.setAlignment(QtAlignRight)
1117
+ self.maxTextChars.setAlignment(QtAlignRight)
1118
+
1119
+ self.maxTotalWordChars.setAlignment(QtAlignRight)
1120
+ self.maxHeaderWordChars.setAlignment(QtAlignRight)
1121
+ self.maxTextWordChars.setAlignment(QtAlignRight)
1122
+
1123
+ self.rightForm = QFormLayout()
1124
+ self.rightForm.addRow(self.tr("Characters"), self.maxTotalChars)
1125
+ self.rightForm.addRow(self.tr("Heading Characters"), self.maxHeaderChars)
1126
+ self.rightForm.addRow(self.tr("Body Text Characters"), self.maxTextChars)
1127
+ self.rightForm.addRow(self.tr("Characters, No Spaces"), self.maxTotalWordChars)
1128
+ self.rightForm.addRow(self.tr("Heading Characters, No Spaces"), self.maxHeaderWordChars)
1129
+ self.rightForm.addRow(self.tr("Body Text Characters, No Spaces"), self.maxTextWordChars)
1130
+ self.rightForm.setHorizontalSpacing(hPx)
1131
+ self.rightForm.setVerticalSpacing(vPx)
1132
+
1133
+ # Assemble
1134
+ self.maxLayout = QHBoxLayout()
1135
+ self.maxLayout.addLayout(self.leftForm)
1136
+ self.maxLayout.addLayout(self.rightForm)
1137
+ self.maxLayout.addStretch(1)
1138
+ self.maxLayout.setSpacing(CONFIG.pxInt(32))
1139
+ self.maxLayout.setContentsMargins(0, 0, 0, 0)
1140
+
1141
+ self.maxWidget.setLayout(self.maxLayout)
1142
+
1143
+ return
1144
+
1145
+ # END Class _StatsWidget