novelWriter 2.3.1__py3-none-any.whl → 2.4b1__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 (81) hide show
  1. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/RECORD +81 -70
  3. novelwriter/__init__.py +5 -5
  4. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  5. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  6. novelwriter/assets/icons/typicons_dark/typ_arrow-down.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  9. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  10. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  11. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  12. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  13. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  14. novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  17. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  18. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  19. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  20. novelwriter/assets/manual.pdf +0 -0
  21. novelwriter/assets/sample.zip +0 -0
  22. novelwriter/assets/syntax/default_dark.conf +1 -0
  23. novelwriter/assets/syntax/default_light.conf +1 -0
  24. novelwriter/assets/syntax/grey_dark.conf +1 -0
  25. novelwriter/assets/syntax/grey_light.conf +1 -0
  26. novelwriter/assets/syntax/light_owl.conf +1 -0
  27. novelwriter/assets/syntax/night_owl.conf +1 -0
  28. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  29. novelwriter/assets/syntax/solarized_light.conf +1 -0
  30. novelwriter/assets/syntax/tomorrow.conf +1 -0
  31. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  32. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  33. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  34. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  35. novelwriter/assets/text/credits_en.htm +25 -23
  36. novelwriter/common.py +1 -1
  37. novelwriter/config.py +35 -12
  38. novelwriter/constants.py +5 -6
  39. novelwriter/core/buildsettings.py +60 -40
  40. novelwriter/core/coretools.py +98 -13
  41. novelwriter/core/docbuild.py +74 -7
  42. novelwriter/core/document.py +24 -3
  43. novelwriter/core/index.py +31 -112
  44. novelwriter/core/project.py +10 -15
  45. novelwriter/core/sessions.py +2 -2
  46. novelwriter/core/status.py +4 -4
  47. novelwriter/core/storage.py +8 -2
  48. novelwriter/core/tohtml.py +22 -25
  49. novelwriter/core/tokenizer.py +416 -232
  50. novelwriter/core/tomd.py +17 -8
  51. novelwriter/core/toodt.py +65 -7
  52. novelwriter/core/tree.py +8 -8
  53. novelwriter/dialogs/docsplit.py +7 -8
  54. novelwriter/dialogs/preferences.py +3 -6
  55. novelwriter/enum.py +17 -14
  56. novelwriter/extensions/modified.py +20 -2
  57. novelwriter/extensions/versioninfo.py +1 -1
  58. novelwriter/gui/doceditor.py +257 -279
  59. novelwriter/gui/dochighlight.py +29 -25
  60. novelwriter/gui/docviewer.py +139 -148
  61. novelwriter/gui/docviewerpanel.py +4 -24
  62. novelwriter/gui/editordocument.py +12 -1
  63. novelwriter/gui/itemdetails.py +6 -6
  64. novelwriter/gui/mainmenu.py +37 -16
  65. novelwriter/gui/noveltree.py +11 -19
  66. novelwriter/gui/outline.py +43 -20
  67. novelwriter/gui/projtree.py +35 -43
  68. novelwriter/gui/search.py +316 -0
  69. novelwriter/gui/sidebar.py +25 -30
  70. novelwriter/gui/theme.py +59 -6
  71. novelwriter/guimain.py +176 -173
  72. novelwriter/shared.py +26 -1
  73. novelwriter/text/__init__.py +3 -0
  74. novelwriter/text/counting.py +137 -0
  75. novelwriter/tools/manuscript.py +344 -55
  76. novelwriter/tools/manussettings.py +213 -71
  77. novelwriter/tools/welcome.py +1 -1
  78. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/LICENSE.md +0 -0
  79. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/WHEEL +0 -0
  80. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/entry_points.txt +0 -0
  81. {novelWriter-2.3.1.dist-info → novelWriter-2.4b1.dist-info}/top_level.txt +0 -0
@@ -31,24 +31,27 @@ from typing import TYPE_CHECKING
31
31
  from datetime import datetime
32
32
 
33
33
  from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
34
- from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
34
+ from PyQt5.QtCore import QSize, QTimer, QUrl, Qt, pyqtSignal, pyqtSlot
35
35
  from PyQt5.QtWidgets import (
36
- QAbstractItemView, QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget,
37
- QListWidgetItem, QPushButton, QSplitter, QTextBrowser, QToolButton,
38
- QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, qApp
36
+ QAbstractItemView, QDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel,
37
+ QListWidget, QListWidgetItem, QPushButton, QSizePolicy, QSplitter,
38
+ QStackedWidget, QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem,
39
+ QVBoxLayout, QWidget, qApp
39
40
  )
40
41
  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 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
52
55
 
53
56
  if TYPE_CHECKING: # pragma: no cover
54
57
  from novelwriter.guimain import GuiMain
@@ -100,29 +103,22 @@ class GuiManuscript(QDialog):
100
103
  qPalette.setBrush(QPalette.Window, qPalette.base())
101
104
  self.setPalette(qPalette)
102
105
 
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())
106
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
108
107
 
109
- self.tbAdd = QToolButton(self)
108
+ self.tbAdd = NIconToolButton(self, iPx)
110
109
  self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
111
- self.tbAdd.setIconSize(QSize(iPx, iPx))
112
110
  self.tbAdd.setToolTip(self.tr("Add New Build"))
113
111
  self.tbAdd.setStyleSheet(buttonStyle)
114
112
  self.tbAdd.clicked.connect(self._createNewBuild)
115
113
 
116
- self.tbDel = QToolButton(self)
114
+ self.tbDel = NIconToolButton(self, iPx)
117
115
  self.tbDel.setIcon(SHARED.theme.getIcon("remove"))
118
- self.tbDel.setIconSize(QSize(iPx, iPx))
119
116
  self.tbDel.setToolTip(self.tr("Delete Selected Build"))
120
117
  self.tbDel.setStyleSheet(buttonStyle)
121
118
  self.tbDel.clicked.connect(self._deleteSelectedBuild)
122
119
 
123
- self.tbEdit = QToolButton(self)
120
+ self.tbEdit = NIconToolButton(self, iPx)
124
121
  self.tbEdit.setIcon(SHARED.theme.getIcon("edit"))
125
- self.tbEdit.setIconSize(QSize(iPx, iPx))
126
122
  self.tbEdit.setToolTip(self.tr("Edit Selected Build"))
127
123
  self.tbEdit.setStyleSheet(buttonStyle)
128
124
  self.tbEdit.clicked.connect(self._editSelectedBuild)
@@ -140,21 +136,31 @@ class GuiManuscript(QDialog):
140
136
  # Builds
141
137
  # ======
142
138
 
143
- self.buildList = QListWidget()
139
+ self.buildList = QListWidget(self)
144
140
  self.buildList.setIconSize(QSize(iPx, iPx))
145
141
  self.buildList.doubleClicked.connect(self._editSelectedBuild)
146
142
  self.buildList.currentItemChanged.connect(self._updateBuildDetails)
147
143
  self.buildList.setSelectionMode(QAbstractItemView.SingleSelection)
148
144
  self.buildList.setDragDropMode(QAbstractItemView.InternalMove)
149
145
 
146
+ # Details Tabs
147
+ # ============
148
+
150
149
  self.buildDetails = _DetailsWidget(self)
151
150
  self.buildDetails.setColumnWidth(
152
151
  CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsWidth", 100)),
153
152
  )
154
153
 
154
+ self.buildOutline = _OutlineWidget(self)
155
+
156
+ self.detailsTabs = QTabWidget(self)
157
+ self.detailsTabs.addTab(self.buildDetails, self.tr("Build"))
158
+ self.detailsTabs.addTab(self.buildOutline, self.tr("Outline"))
159
+ self.detailsTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
160
+
155
161
  self.buildSplit = QSplitter(Qt.Orientation.Vertical, self)
156
162
  self.buildSplit.addWidget(self.buildList)
157
- self.buildSplit.addWidget(self.buildDetails)
163
+ self.buildSplit.addWidget(self.detailsTabs)
158
164
  self.buildSplit.setSizes([
159
165
  CONFIG.pxInt(pOptions.getInt("GuiManuscript", "listHeight", 50)),
160
166
  CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsHeight", 50)),
@@ -185,6 +191,15 @@ class GuiManuscript(QDialog):
185
191
  # ============
186
192
 
187
193
  self.docPreview = _PreviewWidget(self)
194
+ self.docStats = _StatsWidget(self)
195
+
196
+ self.docBox = QVBoxLayout()
197
+ self.docBox.addWidget(self.docPreview, 1)
198
+ self.docBox.addWidget(self.docStats, 0)
199
+ self.docBox.setContentsMargins(0, 0, 0, 0)
200
+
201
+ self.docWdiget = QWidget(self)
202
+ self.docWdiget.setLayout(self.docBox)
188
203
 
189
204
  self.controlBox = QVBoxLayout()
190
205
  self.controlBox.addLayout(self.listToolBox, 0)
@@ -192,12 +207,12 @@ class GuiManuscript(QDialog):
192
207
  self.controlBox.addLayout(self.processBox, 0)
193
208
  self.controlBox.setContentsMargins(0, 0, 0, 0)
194
209
 
195
- self.optsWidget = QWidget()
210
+ self.optsWidget = QWidget(self)
196
211
  self.optsWidget.setLayout(self.controlBox)
197
212
 
198
213
  self.mainSplit = QSplitter()
199
214
  self.mainSplit.addWidget(self.optsWidget)
200
- self.mainSplit.addWidget(self.docPreview)
215
+ self.mainSplit.addWidget(self.docWdiget)
201
216
  self.mainSplit.setCollapsible(0, False)
202
217
  self.mainSplit.setCollapsible(1, False)
203
218
  self.mainSplit.setStretchFactor(0, 0)
@@ -213,6 +228,9 @@ class GuiManuscript(QDialog):
213
228
  self.setLayout(self.outerBox)
214
229
  self.setSizeGripEnabled(True)
215
230
 
231
+ # Signals
232
+ self.buildOutline.outlineEntryClicked.connect(self.docPreview.navigateTo)
233
+
216
234
  logger.debug("Ready: GuiManuscript")
217
235
 
218
236
  return
@@ -245,7 +263,7 @@ class GuiManuscript(QDialog):
245
263
  if isinstance(build, BuildSettings):
246
264
  self._updatePreview(data, build)
247
265
  except Exception:
248
- logger.error("Failed to save build cache")
266
+ logger.error("Failed to load build cache")
249
267
  logException()
250
268
  return
251
269
 
@@ -328,6 +346,7 @@ class GuiManuscript(QDialog):
328
346
  return
329
347
 
330
348
  docBuild = NWBuildDocument(SHARED.project, build)
349
+ docBuild.setPreviewMode(True)
331
350
  docBuild.queueAll()
332
351
 
333
352
  self.docPreview.beginNewBuild(len(docBuild))
@@ -340,6 +359,8 @@ class GuiManuscript(QDialog):
340
359
  result = {
341
360
  "uuid": build.buildID,
342
361
  "time": int(time()),
362
+ "stats": buildObj.textStats,
363
+ "outline": buildObj.textOutline,
343
364
  "styles": buildObj.getStyleSheet(),
344
365
  "html": buildObj.fullHTML,
345
366
  }
@@ -395,6 +416,8 @@ class GuiManuscript(QDialog):
395
416
  self.docPreview.setJustify(
396
417
  build.getBool("format.justifyText")
397
418
  )
419
+ self.docStats.updateStats(data.get("stats", {}))
420
+ self.buildOutline.updateOutline(data.get("outline", {}))
398
421
  return
399
422
 
400
423
  def _getSelectedBuild(self) -> BuildSettings | None:
@@ -504,7 +527,7 @@ class _DetailsWidget(QWidget):
504
527
  self.listView = QTreeWidget(self)
505
528
  self.listView.setHeaderLabels([self.tr("Setting"), self.tr("Value")])
506
529
  self.listView.setIndentation(SHARED.theme.baseIconSize)
507
- self.listView.setSelectionMode(QAbstractItemView.NoSelection)
530
+ self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
508
531
 
509
532
  # Assemble
510
533
  self.outerBox = QVBoxLayout()
@@ -592,24 +615,26 @@ class _DetailsWidget(QWidget):
592
615
  hFmt.resetScene()
593
616
  hFmt.incScene()
594
617
  title = self.tr("Title")
618
+ hidden = self.tr("Hidden")
595
619
 
596
620
  item = QTreeWidgetItem()
597
621
  item.setText(0, build.getLabel("headings"))
598
622
  item.setText(1, "")
599
623
  self.listView.addTopLevelItem(item)
600
- entries = [
601
- "headings.fmtTitle", "headings.fmtChapter", "headings.fmtUnnumbered",
602
- "headings.fmtScene", "headings.fmtSection"
603
- ]
604
- for key in entries:
624
+ for hFormat, hHide in [
625
+ ("headings.fmtTitle", "headings.hideTitle"),
626
+ ("headings.fmtChapter", "headings.hideChapter"),
627
+ ("headings.fmtUnnumbered", "headings.hideUnnumbered"),
628
+ ("headings.fmtScene", "headings.hideScene"),
629
+ ("headings.fmtHardScene", "headings.hideHardScene"),
630
+ ("headings.fmtSection", "headings.hideSection"),
631
+ ]:
605
632
  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)
633
+ sub.setText(0, build.getLabel(hFormat))
634
+ if build.getBool(hHide):
635
+ sub.setText(1, f"[{hidden}]")
636
+ else:
637
+ sub.setText(1, hFmt.apply(build.getStr(hFormat), title, 0))
613
638
  item.addChild(sub)
614
639
 
615
640
  # Text Content
@@ -617,11 +642,10 @@ class _DetailsWidget(QWidget):
617
642
  item.setText(0, build.getLabel("text.grpContent"))
618
643
  item.setText(1, "")
619
644
  self.listView.addTopLevelItem(item)
620
- entries = [
645
+ for key in [
621
646
  "text.includeSynopsis", "text.includeComments",
622
647
  "text.includeKeywords", "text.includeBodyText",
623
- ]
624
- for key in entries:
648
+ ]:
625
649
  sub = QTreeWidgetItem()
626
650
  sub.setText(0, build.getLabel(key))
627
651
  sub.setIcon(1, on if build.getBool(key) else off)
@@ -635,6 +659,85 @@ class _DetailsWidget(QWidget):
635
659
  # END Class _DetailsWidget
636
660
 
637
661
 
662
+ class _OutlineWidget(QWidget):
663
+
664
+ D_LINE = Qt.ItemDataRole.UserRole
665
+
666
+ outlineEntryClicked = pyqtSignal(str)
667
+
668
+ def __init__(self, parent: QWidget) -> None:
669
+ super().__init__(parent=parent)
670
+
671
+ self._outline = {}
672
+
673
+ # Tree Widget
674
+ self.listView = QTreeWidget(self)
675
+ self.listView.setHeaderHidden(True)
676
+ self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
677
+ self.listView.itemClicked.connect(self._onItemClick)
678
+
679
+ # Assemble
680
+ self.outerBox = QVBoxLayout()
681
+ self.outerBox.addWidget(self.listView)
682
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
683
+ self.setLayout(self.outerBox)
684
+
685
+ return
686
+
687
+ def updateOutline(self, data: dict[str, str]) -> None:
688
+ """Update the outline."""
689
+ if isinstance(data, dict) and data != self._outline:
690
+ self.listView.clear()
691
+
692
+ tFont = self.font()
693
+ tFont.setBold(True)
694
+ tBrush = self.palette().highlight()
695
+
696
+ hFont = self.font()
697
+ hFont.setBold(True)
698
+ hFont.setUnderline(True)
699
+
700
+ root = self.listView.invisibleRootItem()
701
+ parent = root
702
+ indent = False
703
+ for anchor, entry in data.items():
704
+ prefix, _, text = entry.partition("|")
705
+ if prefix in ("TT", "PT", "CH", "SC", "H1", "H2"):
706
+ item = QTreeWidgetItem([text])
707
+ item.setData(0, self.D_LINE, anchor)
708
+ if prefix == "TT":
709
+ item.setFont(0, tFont)
710
+ item.setForeground(0, tBrush)
711
+ root.addChild(item)
712
+ parent = root
713
+ elif prefix == "PT":
714
+ item.setFont(0, hFont)
715
+ root.addChild(item)
716
+ parent = root
717
+ elif prefix in ("CH", "H1"):
718
+ root.addChild(item)
719
+ parent = item
720
+ elif prefix in ("SC", "H2"):
721
+ parent.addChild(item)
722
+ indent = True
723
+
724
+ self.listView.setIndentation(SHARED.theme.baseIconSize if indent else CONFIG.pxInt(4))
725
+ self._outline = data
726
+
727
+ return
728
+
729
+ ##
730
+ # Private Slots
731
+ ##
732
+
733
+ def _onItemClick(self, item: QTreeWidgetItem) -> None:
734
+ """Process tree item click."""
735
+ self.outlineEntryClicked.emit(str(item.data(0, self.D_LINE)))
736
+ return
737
+
738
+ # END Class _OutlineWidget
739
+
740
+
638
741
  class _PreviewWidget(QTextBrowser):
639
742
 
640
743
  def __init__(self, parent: QWidget) -> None:
@@ -674,7 +777,7 @@ class _PreviewWidget(QTextBrowser):
674
777
  self.ageLabel.setFont(aFont)
675
778
  self.ageLabel.setPalette(aPalette)
676
779
  self.ageLabel.setAutoFillBackground(True)
677
- self.ageLabel.setAlignment(Qt.AlignCenter)
780
+ self.ageLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
678
781
  self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize))
679
782
 
680
783
  # Progress
@@ -692,8 +795,8 @@ class _PreviewWidget(QTextBrowser):
692
795
  self._updateBuildAge()
693
796
 
694
797
  # Age Timer
695
- self.ageTimer = QTimer()
696
- self.ageTimer.setInterval(10)
798
+ self.ageTimer = QTimer(self)
799
+ self.ageTimer.setInterval(10000)
697
800
  self.ageTimer.timeout.connect(self._updateBuildAge)
698
801
  self.ageTimer.start()
699
802
 
@@ -713,9 +816,9 @@ class _PreviewWidget(QTextBrowser):
713
816
  """Enable/disable the justify text option."""
714
817
  pOptions = self.document().defaultTextOption()
715
818
  if state:
716
- pOptions.setAlignment(Qt.AlignJustify)
819
+ pOptions.setAlignment(Qt.AlignmentFlag.AlignJustify)
717
820
  else:
718
- pOptions.setAlignment(Qt.AlignAbsolute)
821
+ pOptions.setAlignment(Qt.AlignmentFlag.AlignAbsolute)
719
822
  self.document().setDefaultTextOption(pOptions)
720
823
  return
721
824
 
@@ -751,23 +854,16 @@ class _PreviewWidget(QTextBrowser):
751
854
  def setContent(self, data: dict) -> None:
752
855
  """Set the content of the preview widget."""
753
856
  sPos = self.verticalScrollBar().value()
754
- qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
857
+ qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
755
858
 
756
859
  self.buildProgress.setCentreText(self.tr("Processing ..."))
757
860
  qApp.processEvents()
758
861
 
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
- ]))
862
+ styles = "\n".join(data.get("styles", []))
765
863
  self.document().setDefaultStyleSheet(styles)
766
864
 
767
865
  html = "".join(data.get("html", []))
768
866
  html = html.replace("\t", "!!tab!!")
769
- html = html.replace("<del>", "<span style='text-decoration: line-through;'>")
770
- html = html.replace("</del>", "</span>")
771
867
  self.setHtml(html)
772
868
  qApp.processEvents()
773
869
  while self.find("!!tab!!"):
@@ -812,6 +908,13 @@ class _PreviewWidget(QTextBrowser):
812
908
  qApp.restoreOverrideCursor()
813
909
  return
814
910
 
911
+ @pyqtSlot(str)
912
+ def navigateTo(self, anchor: str) -> None:
913
+ """Go to a specific #link in the document."""
914
+ logger.debug("Moving to anchor '#%s'", anchor)
915
+ self.setSource(QUrl(f"#{anchor}"))
916
+ return
917
+
815
918
  ##
816
919
  # Private Slots
817
920
  ##
@@ -821,7 +924,7 @@ class _PreviewWidget(QTextBrowser):
821
924
  """Update the build time and the fuzzy age."""
822
925
  if self._docTime > 0:
823
926
  strBuildTime = "%s (%s)" % (
824
- datetime.fromtimestamp(self._docTime).strftime("%x %X"),
927
+ CONFIG.localDateTime(datetime.fromtimestamp(self._docTime)),
825
928
  fuzzyTime(int(time()) - self._docTime)
826
929
  )
827
930
  else:
@@ -859,3 +962,189 @@ class _PreviewWidget(QTextBrowser):
859
962
  return
860
963
 
861
964
  # END Class _PreviewWidget
965
+
966
+
967
+ class _StatsWidget(QWidget):
968
+
969
+ def __init__(self, parent: QWidget) -> None:
970
+ super().__init__(parent=parent)
971
+
972
+ font = self.font()
973
+ font.setPointSizeF(0.9*SHARED.theme.fontPointSize)
974
+ self.setFont(font)
975
+
976
+ self.minWidget = QWidget(self)
977
+ self.maxWidget = QWidget(self)
978
+
979
+ iPx = int(0.6*SHARED.theme.baseIconSize)
980
+ toggleIcon = SHARED.theme.getToggleIcon("unfold", (iPx, iPx))
981
+
982
+ self.toggleButton = NIconToolButton(self, iPx)
983
+ self.toggleButton.setCheckable(True)
984
+ self.toggleButton.setIcon(toggleIcon)
985
+ self.toggleButton.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON))
986
+ self.toggleButton.toggled.connect(self._toggleView)
987
+
988
+ self._buildMinimal()
989
+ self._buildMaximal()
990
+
991
+ self.mainStack = QStackedWidget(self)
992
+ self.mainStack.addWidget(self.minWidget)
993
+ self.mainStack.addWidget(self.maxWidget)
994
+
995
+ self.outerBox = QHBoxLayout()
996
+ self.outerBox.addWidget(self.toggleButton, 0, Qt.AlignmentFlag.AlignTop)
997
+ self.outerBox.addWidget(self.mainStack, 1, Qt.AlignmentFlag.AlignTop)
998
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
999
+
1000
+ self.setLayout(self.outerBox)
1001
+
1002
+ self._toggleView(False)
1003
+
1004
+ return
1005
+
1006
+ def updateStats(self, data: dict[str, int]) -> None:
1007
+ """Update the stats values from a Tokenizer stats dict."""
1008
+ # Minimal
1009
+ self.minWordCount.setText("{0:n}".format(data.get("allWords", 0)))
1010
+ self.minCharCount.setText("{0:n}".format(data.get("allChars", 0)))
1011
+
1012
+ # Maximal
1013
+ self.maxTotalWords.setText("{0:n}".format(data.get("allWords", 0)))
1014
+ self.maxHeaderWords.setText("{0:n}".format(data.get("titleWords", 0)))
1015
+ self.maxTextWords.setText("{0:n}".format(data.get("textWords", 0)))
1016
+ self.maxTitleCount.setText("{0:n}".format(data.get("titleCount", 0)))
1017
+ self.maxParCount.setText("{0:n}".format(data.get("paragraphCount", 0)))
1018
+
1019
+ self.maxTotalChars.setText("{0:n}".format(data.get("allChars", 0)))
1020
+ self.maxHeaderChars.setText("{0:n}".format(data.get("titleChars", 0)))
1021
+ self.maxTextChars.setText("{0:n}".format(data.get("textChars", 0)))
1022
+
1023
+ self.maxTotalWordChars.setText("{0:n}".format(data.get("allWordChars", 0)))
1024
+ self.maxHeaderWordChars.setText("{0:n}".format(data.get("titleWordChars", 0)))
1025
+ self.maxTextWordChars.setText("{0:n}".format(data.get("textWordChars", 0)))
1026
+
1027
+ return
1028
+
1029
+ ##
1030
+ # Private Slots
1031
+ ##
1032
+
1033
+ @pyqtSlot(bool)
1034
+ def _toggleView(self, state: bool) -> None:
1035
+ """Toggle minimal or maximal view."""
1036
+ ignored = QSizePolicy.Policy.Ignored
1037
+ expanded = QSizePolicy.Policy.Expanding
1038
+ if state:
1039
+ self.mainStack.setCurrentWidget(self.maxWidget)
1040
+ self.maxWidget.setSizePolicy(expanded, expanded)
1041
+ self.minWidget.setSizePolicy(ignored, ignored)
1042
+ else:
1043
+ self.mainStack.setCurrentWidget(self.minWidget)
1044
+ self.maxWidget.setSizePolicy(ignored, ignored)
1045
+ self.minWidget.setSizePolicy(expanded, expanded)
1046
+ self.maxWidget.adjustSize()
1047
+ self.minWidget.adjustSize()
1048
+ self.mainStack.adjustSize()
1049
+ self.adjustSize()
1050
+ return
1051
+
1052
+ ##
1053
+ # Internal Functions
1054
+ ##
1055
+
1056
+ def _buildMinimal(self) -> None:
1057
+ """Build the minimal stats page."""
1058
+ mPx = CONFIG.pxInt(8)
1059
+
1060
+ self.lblWordCount = QLabel(self.tr("Words"))
1061
+ self.minWordCount = QLabel(self)
1062
+
1063
+ self.lblCharCount = QLabel(self.tr("Characters"))
1064
+ self.minCharCount = QLabel(self)
1065
+
1066
+ # Assemble
1067
+ self.minLayout = QHBoxLayout()
1068
+ self.minLayout.addWidget(self.lblWordCount)
1069
+ self.minLayout.addWidget(self.minWordCount)
1070
+ self.minLayout.addSpacing(mPx)
1071
+ self.minLayout.addWidget(self.lblCharCount)
1072
+ self.minLayout.addWidget(self.minCharCount)
1073
+ self.minLayout.addStretch(1)
1074
+ self.minLayout.setSpacing(mPx)
1075
+ self.minLayout.setContentsMargins(0, 0, 0, 0)
1076
+
1077
+ self.minWidget.setLayout(self.minLayout)
1078
+
1079
+ return
1080
+
1081
+ def _buildMaximal(self) -> None:
1082
+ """Build the maximal stats page."""
1083
+ hPx = CONFIG.pxInt(12)
1084
+ vPx = CONFIG.pxInt(4)
1085
+
1086
+ alignRight = Qt.AlignmentFlag.AlignRight
1087
+
1088
+ # Left Column
1089
+ self.maxTotalWords = QLabel(self)
1090
+ self.maxHeaderWords = QLabel(self)
1091
+ self.maxTextWords = QLabel(self)
1092
+ self.maxTitleCount = QLabel(self)
1093
+ self.maxParCount = QLabel(self)
1094
+
1095
+ self.maxTotalWords.setAlignment(alignRight)
1096
+ self.maxHeaderWords.setAlignment(alignRight)
1097
+ self.maxTextWords.setAlignment(alignRight)
1098
+ self.maxTitleCount.setAlignment(alignRight)
1099
+ self.maxParCount.setAlignment(alignRight)
1100
+
1101
+ self.leftForm = QFormLayout()
1102
+ self.leftForm.addRow(self.tr("Words"), self.maxTotalWords)
1103
+ self.leftForm.addRow(self.tr("Heading Words"), self.maxHeaderWords)
1104
+ self.leftForm.addRow(self.tr("Body Text Words"), self.maxTextWords)
1105
+ self.leftForm.addRow("", QLabel(self))
1106
+ self.leftForm.addRow(self.tr("Headings"), self.maxTitleCount)
1107
+ self.leftForm.addRow(self.tr("Paragraphs"), self.maxParCount)
1108
+ self.leftForm.setHorizontalSpacing(hPx)
1109
+ self.leftForm.setVerticalSpacing(vPx)
1110
+
1111
+ # Right Column
1112
+ self.maxTotalChars = QLabel(self)
1113
+ self.maxHeaderChars = QLabel(self)
1114
+ self.maxTextChars = QLabel(self)
1115
+
1116
+ self.maxTotalWordChars = QLabel(self)
1117
+ self.maxHeaderWordChars = QLabel(self)
1118
+ self.maxTextWordChars = QLabel(self)
1119
+
1120
+ self.maxTotalChars.setAlignment(alignRight)
1121
+ self.maxHeaderChars.setAlignment(alignRight)
1122
+ self.maxTextChars.setAlignment(alignRight)
1123
+
1124
+ self.maxTotalWordChars.setAlignment(alignRight)
1125
+ self.maxHeaderWordChars.setAlignment(alignRight)
1126
+ self.maxTextWordChars.setAlignment(alignRight)
1127
+
1128
+ self.rightForm = QFormLayout()
1129
+ self.rightForm.addRow(self.tr("Characters"), self.maxTotalChars)
1130
+ self.rightForm.addRow(self.tr("Heading Characters"), self.maxHeaderChars)
1131
+ self.rightForm.addRow(self.tr("Body Text Characters"), self.maxTextChars)
1132
+ self.rightForm.addRow(self.tr("Characters, No Spaces"), self.maxTotalWordChars)
1133
+ self.rightForm.addRow(self.tr("Heading Characters, No Spaces"), self.maxHeaderWordChars)
1134
+ self.rightForm.addRow(self.tr("Body Text Characters, No Spaces"), self.maxTextWordChars)
1135
+ self.rightForm.setHorizontalSpacing(hPx)
1136
+ self.rightForm.setVerticalSpacing(vPx)
1137
+
1138
+ # Assemble
1139
+ self.maxLayout = QHBoxLayout()
1140
+ self.maxLayout.addLayout(self.leftForm)
1141
+ self.maxLayout.addLayout(self.rightForm)
1142
+ self.maxLayout.addStretch(1)
1143
+ self.maxLayout.setSpacing(CONFIG.pxInt(32))
1144
+ self.maxLayout.setContentsMargins(0, 0, 0, 0)
1145
+
1146
+ self.maxWidget.setLayout(self.maxLayout)
1147
+
1148
+ return
1149
+
1150
+ # END Class _StatsWidget