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
@@ -53,15 +53,17 @@ from PyQt5.QtWidgets import (
53
53
  )
54
54
 
55
55
  from novelwriter import CONFIG, SHARED
56
- from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
57
56
  from novelwriter.common import minmax, transferCase
58
- from novelwriter.constants import nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst
59
- from novelwriter.core.index import countWords
60
- from novelwriter.tools.lipsum import GuiLipsum
57
+ from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
61
58
  from novelwriter.core.document import NWDocument
62
- from novelwriter.gui.dochighlight import GuiDocHighlighter
63
- from novelwriter.gui.editordocument import GuiTextDocument
59
+ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
64
60
  from novelwriter.extensions.eventfilters import WheelEventFilter
61
+ from novelwriter.extensions.modified import NIconToolButton
62
+ from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE
63
+ from novelwriter.gui.editordocument import GuiTextDocument
64
+ from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
65
+ from novelwriter.text.counting import standardCounter
66
+ from novelwriter.tools.lipsum import GuiLipsum
65
67
 
66
68
  if TYPE_CHECKING: # pragma: no cover
67
69
  from novelwriter.guimain import GuiMain
@@ -99,6 +101,7 @@ class GuiDocEditor(QPlainTextEdit):
99
101
  toggleFocusModeRequest = pyqtSignal()
100
102
  requestProjectItemSelected = pyqtSignal(str, bool)
101
103
  requestProjectItemRenamed = pyqtSignal(str, str)
104
+ requestNewNoteCreation = pyqtSignal(str, nwItemClass)
102
105
 
103
106
  def __init__(self, mainGui: GuiMain) -> None:
104
107
  super().__init__(parent=mainGui)
@@ -142,9 +145,10 @@ class GuiDocEditor(QPlainTextEdit):
142
145
  self._qDocument = GuiTextDocument(self)
143
146
  self.setDocument(self._qDocument)
144
147
 
145
- # Connect Signals
148
+ # Connect Editor and Document Signals
146
149
  self._qDocument.contentsChange.connect(self._docChange)
147
150
  self.selectionChanged.connect(self._updateSelectedStatus)
151
+ self.cursorPositionChanged.connect(self._cursorMoved)
148
152
  self.spellCheckStateChanged.connect(self._qDocument.setSpellCheckState)
149
153
 
150
154
  # Document Title
@@ -153,7 +157,7 @@ class GuiDocEditor(QPlainTextEdit):
153
157
  self.docSearch = GuiDocEditSearch(self)
154
158
  self.docToolBar = GuiDocToolBar(self)
155
159
 
156
- # Connect Signals
160
+ # Connect Widget Signals
157
161
  self.docHeader.closeDocumentRequest.connect(self._closeCurrentDocument)
158
162
  self.docHeader.toggleToolBarRequest.connect(self._toggleToolBarVisibility)
159
163
  self.docToolBar.requestDocAction.connect(self.docAction)
@@ -184,18 +188,18 @@ class GuiDocEditor(QPlainTextEdit):
184
188
  self.followTag2.activated.connect(self._processTag)
185
189
 
186
190
  # Set Up Document Word Counter
187
- self.wcTimerDoc = QTimer()
188
- self.wcTimerDoc.timeout.connect(self._runDocCounter)
189
- self.wcTimerDoc.setInterval(5000)
191
+ self.timerDoc = QTimer(self)
192
+ self.timerDoc.timeout.connect(self._runDocumentTasks)
193
+ self.timerDoc.setInterval(5000)
190
194
 
191
195
  self.wCounterDoc = BackgroundWordCounter(self)
192
196
  self.wCounterDoc.setAutoDelete(False)
193
197
  self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
194
198
 
195
199
  # Set Up Selection Word Counter
196
- self.wcTimerSel = QTimer()
197
- self.wcTimerSel.timeout.connect(self._runSelCounter)
198
- self.wcTimerSel.setInterval(500)
200
+ self.timerSel = QTimer(self)
201
+ self.timerSel.timeout.connect(self._runSelCounter)
202
+ self.timerSel.setInterval(500)
199
203
 
200
204
  self.wCounterSel = BackgroundWordCounter(self, forSelection=True)
201
205
  self.wCounterSel.setAutoDelete(False)
@@ -248,8 +252,8 @@ class GuiDocEditor(QPlainTextEdit):
248
252
  self._nwDocument = None
249
253
  self.setReadOnly(True)
250
254
  self.clear()
251
- self.wcTimerDoc.stop()
252
- self.wcTimerSel.stop()
255
+ self.timerDoc.stop()
256
+ self.timerSel.stop()
253
257
 
254
258
  self._docHandle = None
255
259
  self._lastEdit = 0.0
@@ -258,7 +262,7 @@ class GuiDocEditor(QPlainTextEdit):
258
262
  self._doReplace = False
259
263
 
260
264
  self.setDocumentChanged(False)
261
- self.docHeader.setTitleFromHandle(self._docHandle)
265
+ self.docHeader.clearHeader()
262
266
  self.docFooter.setHandle(self._docHandle)
263
267
  self.docToolBar.setVisible(False)
264
268
 
@@ -362,7 +366,7 @@ class GuiDocEditor(QPlainTextEdit):
362
366
  # which makes it read only.
363
367
  if self._docHandle:
364
368
  self._qDocument.syntaxHighlighter.rehighlight()
365
- self.docHeader.setTitleFromHandle(self._docHandle)
369
+ self.docHeader.setHandle(self._docHandle)
366
370
  else:
367
371
  self.clearEditor()
368
372
 
@@ -396,12 +400,10 @@ class GuiDocEditor(QPlainTextEdit):
396
400
 
397
401
  self._lastEdit = time()
398
402
  self._lastActive = time()
399
- self._runDocCounter()
400
- self.wcTimerDoc.start()
403
+ self._runDocumentTasks()
404
+ self.timerDoc.start()
401
405
 
402
406
  self.setReadOnly(False)
403
- self.docHeader.setTitleFromHandle(self._docHandle)
404
- self.docFooter.setHandle(self._docHandle)
405
407
  self.updateDocMargins()
406
408
 
407
409
  if tLine is None and self._nwItem is not None:
@@ -409,7 +411,8 @@ class GuiDocEditor(QPlainTextEdit):
409
411
  elif isinstance(tLine, int):
410
412
  self.setCursorLine(tLine)
411
413
 
412
- self.docFooter.updateLineCount()
414
+ self.docHeader.setHandle(tHandle)
415
+ self.docFooter.setHandle(tHandle)
413
416
 
414
417
  # This is a hack to fix invisible cursor on an empty document
415
418
  if self._qDocument.characterCount() <= 1:
@@ -432,7 +435,7 @@ class GuiDocEditor(QPlainTextEdit):
432
435
 
433
436
  def updateTagHighLighting(self) -> None:
434
437
  """Rerun the syntax highlighter on all meta data lines."""
435
- self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
438
+ self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META)
436
439
  return
437
440
 
438
441
  def replaceText(self, text: str) -> None:
@@ -462,7 +465,7 @@ class GuiDocEditor(QPlainTextEdit):
462
465
  return False
463
466
 
464
467
  docText = self.getText()
465
- cC, wC, pC = countWords(docText)
468
+ cC, wC, pC = standardCounter(docText)
466
469
  self._updateDocCounts(cC, wC, pC)
467
470
 
468
471
  self.saveCursorPosition()
@@ -548,8 +551,8 @@ class GuiDocEditor(QPlainTextEdit):
548
551
  sH = hBar.height() if hBar.isVisible() else 0
549
552
 
550
553
  tM = self._vpMargin
551
- if CONFIG.textWidth > 0 or self.mainGui.isFocusMode:
552
- tW = CONFIG.getTextWidth(self.mainGui.isFocusMode)
554
+ if CONFIG.textWidth > 0 or SHARED.focusMode:
555
+ tW = CONFIG.getTextWidth(SHARED.focusMode)
553
556
  tM = max((wW - sW - tW)//2, self._vpMargin)
554
557
 
555
558
  tB = self.frameWidth()
@@ -600,13 +603,15 @@ class GuiDocEditor(QPlainTextEdit):
600
603
  # Setters
601
604
  ##
602
605
 
603
- def setDocumentChanged(self, state: bool) -> bool:
606
+ def setDocumentChanged(self, state: bool) -> None:
604
607
  """Keep track of the document changed variable, and emit the
605
608
  document change signal.
606
609
  """
607
- self._docChanged = state
608
- self.editedStatusChanged.emit(self._docChanged)
609
- return self._docChanged
610
+ if self._docChanged != state:
611
+ logger.debug("Document changed status is '%s'", state)
612
+ self._docChanged = state
613
+ self.editedStatusChanged.emit(self._docChanged)
614
+ return
610
615
 
611
616
  def setCursorPosition(self, position: int) -> None:
612
617
  """Move the cursor to a given position in the document."""
@@ -616,7 +621,6 @@ class GuiDocEditor(QPlainTextEdit):
616
621
  cursor.setPosition(minmax(position, 0, nChars-1))
617
622
  self.setTextCursor(cursor)
618
623
  self.centerCursor()
619
- self.docFooter.updateLineCount()
620
624
  return
621
625
 
622
626
  def saveCursorPosition(self) -> None:
@@ -635,6 +639,14 @@ class GuiDocEditor(QPlainTextEdit):
635
639
  logger.debug("Cursor moved to line %d", line)
636
640
  return
637
641
 
642
+ def setCursorSelection(self, selStart: int, selLength: int) -> None:
643
+ """Make a text selection."""
644
+ cursor = self.textCursor()
645
+ cursor.setPosition(selStart, QTextCursor.MoveMode.MoveAnchor)
646
+ cursor.setPosition(selStart + selLength, QTextCursor.MoveMode.KeepAnchor)
647
+ self.setTextCursor(cursor)
648
+ return
649
+
638
650
  ##
639
651
  # Spell Checking
640
652
  ##
@@ -745,6 +757,8 @@ class GuiDocEditor(QPlainTextEdit):
745
757
  self._formatBlock(nwDocAction.BLOCK_TTL)
746
758
  elif action == nwDocAction.BLOCK_UNN:
747
759
  self._formatBlock(nwDocAction.BLOCK_UNN)
760
+ elif action == nwDocAction.BLOCK_HSC:
761
+ self._formatBlock(nwDocAction.BLOCK_HSC)
748
762
  elif action == nwDocAction.REPL_SNG:
749
763
  self._replaceQuotes("'", self._typSQuoteO, self._typSQuoteC)
750
764
  elif action == nwDocAction.REPL_DBL:
@@ -769,6 +783,8 @@ class GuiDocEditor(QPlainTextEdit):
769
783
  self._wrapSelection(nwShortcode.STRIKE_O, nwShortcode.STRIKE_C)
770
784
  elif action == nwDocAction.SC_ULINE:
771
785
  self._wrapSelection(nwShortcode.ULINE_O, nwShortcode.ULINE_C)
786
+ elif action == nwDocAction.SC_MARK:
787
+ self._wrapSelection(nwShortcode.MARK_O, nwShortcode.MARK_C)
772
788
  elif action == nwDocAction.SC_SUP:
773
789
  self._wrapSelection(nwShortcode.SUP_O, nwShortcode.SUP_C)
774
790
  elif action == nwDocAction.SC_SUB:
@@ -785,11 +801,7 @@ class GuiDocEditor(QPlainTextEdit):
785
801
 
786
802
  def anyFocus(self) -> bool:
787
803
  """Check if any widget or child widget has focus."""
788
- if self.hasFocus():
789
- return True
790
- if self.isAncestorOf(qApp.focusWidget()):
791
- return True
792
- return False
804
+ return self.hasFocus() or self.isAncestorOf(qApp.focusWidget())
793
805
 
794
806
  def revealLocation(self) -> None:
795
807
  """Tell the user where on the file system the file in the editor
@@ -947,8 +959,6 @@ class GuiDocEditor(QPlainTextEdit):
947
959
  else:
948
960
  super().keyPressEvent(event)
949
961
 
950
- self.docFooter.updateLineCount()
951
-
952
962
  return
953
963
 
954
964
  def focusNextPrevChild(self, next: bool) -> bool:
@@ -971,7 +981,6 @@ class GuiDocEditor(QPlainTextEdit):
971
981
  if qApp.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier:
972
982
  self._processTag(self.cursorForPosition(event.pos()))
973
983
  super().mouseReleaseEvent(event)
974
- self.docFooter.updateLineCount()
975
984
  return
976
985
 
977
986
  def resizeEvent(self, event: QResizeEvent) -> None:
@@ -991,8 +1000,8 @@ class GuiDocEditor(QPlainTextEdit):
991
1000
  """Called when an item label is changed to check if the document
992
1001
  title bar needs updating,
993
1002
  """
994
- if tHandle == self._docHandle:
995
- self.docHeader.setTitleFromHandle(self._docHandle)
1003
+ if tHandle and tHandle == self._docHandle:
1004
+ self.docHeader.setHandle(tHandle)
996
1005
  self.docFooter.updateInfo()
997
1006
  self.updateDocMargins()
998
1007
  return
@@ -1033,32 +1042,35 @@ class GuiDocEditor(QPlainTextEdit):
1033
1042
  if not self._docChanged:
1034
1043
  self.setDocumentChanged(removed != 0 or added != 0)
1035
1044
 
1036
- if not self.wcTimerDoc.isActive():
1037
- self.wcTimerDoc.start()
1038
-
1039
- block = self._qDocument.findBlock(pos)
1040
- if not block.isValid():
1041
- return
1045
+ if not self.timerDoc.isActive():
1046
+ self.timerDoc.start()
1042
1047
 
1043
- text = block.text()
1044
- if text.startswith("@") and added + removed == 1:
1045
- # Only run on single keypresses, otherwise it will trigger
1046
- # at unwanted times when other changes are made to the document
1047
- cursor = self.textCursor()
1048
- bPos = cursor.positionInBlock()
1049
- if bPos > 0:
1050
- show = self._completer.updateText(text, bPos)
1051
- point = self.cursorRect().bottomRight()
1052
- self._completer.move(self.viewport().mapToGlobal(point))
1053
- self._completer.setVisible(show)
1054
- else:
1055
- self._completer.setVisible(False)
1048
+ if (block := self._qDocument.findBlock(pos)).isValid():
1049
+ text = block.text()
1050
+ if text.startswith("@") and added + removed == 1:
1051
+ # Only run on single keypresses, otherwise it will trigger
1052
+ # at unwanted times when other changes are made to the document
1053
+ cursor = self.textCursor()
1054
+ bPos = cursor.positionInBlock()
1055
+ if bPos > 0:
1056
+ show = self._completer.updateText(text, bPos)
1057
+ point = self.cursorRect().bottomRight()
1058
+ self._completer.move(self.viewport().mapToGlobal(point))
1059
+ self._completer.setVisible(show)
1060
+ else:
1061
+ self._completer.setVisible(False)
1056
1062
 
1057
1063
  if self._doReplace and added == 1:
1058
1064
  self._docAutoReplace(text)
1059
1065
 
1060
1066
  return
1061
1067
 
1068
+ @pyqtSlot()
1069
+ def _cursorMoved(self):
1070
+ """Triggered when the cursor moved in the editor."""
1071
+ self.docFooter.updateLineCount(self.textCursor())
1072
+ return
1073
+
1062
1074
  @pyqtSlot(int, int, str)
1063
1075
  def _insertCompletion(self, pos: int, length: int, text: str) -> None:
1064
1076
  """Insert choice from the completer menu."""
@@ -1081,7 +1093,7 @@ class GuiDocEditor(QPlainTextEdit):
1081
1093
 
1082
1094
  ctxMenu = QMenu(self)
1083
1095
  ctxMenu.setObjectName("ContextMenu")
1084
- if pBlock.userState() == GuiDocHighlighter.BLOCK_TITLE:
1096
+ if pBlock.userState() == BLOCK_TITLE:
1085
1097
  action = ctxMenu.addAction(self.tr("Set as Document Name"))
1086
1098
  action.triggered.connect(lambda: self._emitRenameItem(pBlock))
1087
1099
 
@@ -1176,10 +1188,8 @@ class GuiDocEditor(QPlainTextEdit):
1176
1188
  return
1177
1189
 
1178
1190
  @pyqtSlot()
1179
- def _runDocCounter(self) -> None:
1180
- """Decide whether to run the word counter, or not due to
1181
- inactivity.
1182
- """
1191
+ def _runDocumentTasks(self) -> None:
1192
+ """Run timer document tasks."""
1183
1193
  if self._docHandle is None:
1184
1194
  return
1185
1195
 
@@ -1190,25 +1200,23 @@ class GuiDocEditor(QPlainTextEdit):
1190
1200
  if time() - self._lastEdit < 25.0:
1191
1201
  logger.debug("Running word counter")
1192
1202
  SHARED.runInThreadPool(self.wCounterDoc)
1203
+ self.docHeader.setOutline({
1204
+ block.blockNumber(): block.text()
1205
+ for block in self._qDocument.iterBlockByType(BLOCK_TITLE, maxCount=30)
1206
+ })
1193
1207
 
1194
1208
  return
1195
1209
 
1196
1210
  @pyqtSlot(int, int, int)
1197
1211
  def _updateDocCounts(self, cCount: int, wCount: int, pCount: int) -> None:
1198
1212
  """Process the word counter's finished signal."""
1199
- if self._docHandle is None or self._nwItem is None:
1200
- return
1201
-
1202
- logger.debug("Updating word count")
1203
-
1204
- self._nwItem.setCharCount(cCount)
1205
- self._nwItem.setWordCount(wCount)
1206
- self._nwItem.setParaCount(pCount)
1207
-
1208
- # Must not be emitted if docHandle is None!
1209
- self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
1210
- self.docFooter.updateCounts()
1211
-
1213
+ if self._docHandle and self._nwItem:
1214
+ logger.debug("Updating word count")
1215
+ self._nwItem.setCharCount(cCount)
1216
+ self._nwItem.setWordCount(wCount)
1217
+ self._nwItem.setParaCount(pCount)
1218
+ self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
1219
+ self.docFooter.updateWordCount(wCount, False)
1212
1220
  return
1213
1221
 
1214
1222
  @pyqtSlot()
@@ -1217,13 +1225,11 @@ class GuiDocEditor(QPlainTextEdit):
1217
1225
  information to the footer, and start the selection word counter.
1218
1226
  """
1219
1227
  if self.textCursor().hasSelection():
1220
- if not self.wcTimerSel.isActive():
1221
- self.wcTimerSel.start()
1222
- self.docFooter.setHasSelection(True)
1228
+ if not self.timerSel.isActive():
1229
+ self.timerSel.start()
1223
1230
  else:
1224
- self.wcTimerSel.stop()
1225
- self.docFooter.setHasSelection(False)
1226
- self.docFooter.updateCounts()
1231
+ self.timerSel.stop()
1232
+ self.docFooter.updateWordCount(0, False)
1227
1233
  return
1228
1234
 
1229
1235
  @pyqtSlot()
@@ -1243,13 +1249,10 @@ class GuiDocEditor(QPlainTextEdit):
1243
1249
  @pyqtSlot(int, int, int)
1244
1250
  def _updateSelCounts(self, cCount: int, wCount: int, pCount: int) -> None:
1245
1251
  """Update the counts on the counter's finished signal."""
1246
- if self._docHandle is None or self._nwItem is None:
1247
- return
1248
-
1249
- logger.debug("User selected %d words", wCount)
1250
- self.docFooter.updateCounts(wCount=wCount, cCount=cCount)
1251
- self.wcTimerSel.stop()
1252
-
1252
+ if self._docHandle and self._nwItem:
1253
+ logger.debug("User selected %d words", wCount)
1254
+ self.docFooter.updateWordCount(wCount, True)
1255
+ self.timerSel.stop()
1253
1256
  return
1254
1257
 
1255
1258
  @pyqtSlot()
@@ -1311,6 +1314,7 @@ class GuiDocEditor(QPlainTextEdit):
1311
1314
  self._docHandle, wrapAround=self.docSearch.doLoop
1312
1315
  )
1313
1316
  self.beginSearch()
1317
+ self.setFocus()
1314
1318
  return
1315
1319
 
1316
1320
  cursor = self.textCursor()
@@ -1331,6 +1335,7 @@ class GuiDocEditor(QPlainTextEdit):
1331
1335
  self._docHandle, wrapAround=self.docSearch.doLoop
1332
1336
  )
1333
1337
  self.beginSearch()
1338
+ self.setFocus()
1334
1339
  return
1335
1340
  else:
1336
1341
  resIdx = 0 if doLoop else maxIdx
@@ -1339,7 +1344,6 @@ class GuiDocEditor(QPlainTextEdit):
1339
1344
  cursor.setPosition(resE[resIdx], QTextCursor.MoveMode.KeepAnchor)
1340
1345
  self.setTextCursor(cursor)
1341
1346
 
1342
- self.docFooter.updateLineCount()
1343
1347
  self.docSearch.setResultCount(resIdx + 1, len(resS))
1344
1348
  self._lastFind = (resS[resIdx], resE[resIdx])
1345
1349
 
@@ -1370,9 +1374,10 @@ class GuiDocEditor(QPlainTextEdit):
1370
1374
  cursor.setPosition(0)
1371
1375
  self.setTextCursor(cursor)
1372
1376
 
1373
- # Search up to a maximum of 1000, and make sure certain special
1374
- # searches like a regex search for .* don't loop infinitely
1375
- while self.find(searchFor, findOpt) and len(resE) <= 1000:
1377
+ # Search up to a maximum of MAX_SEARCH_RESULT, and make sure
1378
+ # certain special searches like a regex search for .* don't loop
1379
+ # infinitely
1380
+ while self.find(searchFor, findOpt) and len(resE) <= nwConst.MAX_SEARCH_RESULT:
1376
1381
  cursor = self.textCursor()
1377
1382
  if cursor.hasSelection():
1378
1383
  resS.append(cursor.selectionStart())
@@ -1436,13 +1441,7 @@ class GuiDocEditor(QPlainTextEdit):
1436
1441
 
1437
1442
  # Make sure the selected text was selected by an actual find
1438
1443
  # call, and not the user.
1439
- try:
1440
- isFind = self._lastFind[0] == cursor.selectionStart()
1441
- isFind &= self._lastFind[1] == cursor.selectionEnd()
1442
- except Exception:
1443
- isFind = False
1444
-
1445
- if isFind:
1444
+ if self._lastFind == (cursor.selectionStart(), cursor.selectionEnd()):
1446
1445
  cursor.beginEditBlock()
1447
1446
  cursor.removeSelectedText()
1448
1447
  cursor.insertText(replWith)
@@ -1451,10 +1450,8 @@ class GuiDocEditor(QPlainTextEdit):
1451
1450
  self.setTextCursor(cursor)
1452
1451
  logger.debug(
1453
1452
  "Replaced occurrence of '%s' with '%s' on line %d",
1454
- searchFor, replWith, cursor.blockNumber()
1453
+ searchFor, replWith, cursor.blockNumber() + 1
1455
1454
  )
1456
- else:
1457
- logger.error("The selected text is not a search result, skipping replace")
1458
1455
 
1459
1456
  self.findNext()
1460
1457
 
@@ -1670,6 +1667,9 @@ class GuiDocEditor(QPlainTextEdit):
1670
1667
  elif text.startswith("##! "):
1671
1668
  temp = text[4:]
1672
1669
  offset = 4
1670
+ elif text.startswith("###! "):
1671
+ temp = text[5:]
1672
+ offset = 5
1673
1673
  elif text.startswith(">> "):
1674
1674
  temp = text[3:]
1675
1675
  offset = 3
@@ -1721,6 +1721,9 @@ class GuiDocEditor(QPlainTextEdit):
1721
1721
  elif action == nwDocAction.BLOCK_UNN:
1722
1722
  text = f"##! {temp}"
1723
1723
  offset -= 4
1724
+ elif action == nwDocAction.BLOCK_HSC:
1725
+ text = f"###! {temp}"
1726
+ offset -= 5
1724
1727
  elif action == nwDocAction.ALIGN_L:
1725
1728
  text = f"{temp} <<"
1726
1729
  elif action == nwDocAction.ALIGN_C:
@@ -1867,7 +1870,10 @@ class GuiDocEditor(QPlainTextEdit):
1867
1870
  if text.startswith("@") and self._docHandle:
1868
1871
 
1869
1872
  isGood, tBits, tPos = SHARED.project.index.scanThis(text)
1870
- if not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY:
1873
+ if (
1874
+ not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY
1875
+ or tBits[0] not in nwKeyWords.VALID_KEYS
1876
+ ):
1871
1877
  return nwTrinary.NEUTRAL
1872
1878
 
1873
1879
  tag = ""
@@ -1895,13 +1901,9 @@ class GuiDocEditor(QPlainTextEdit):
1895
1901
  "Do you want to create a new project note for the tag '{0}'?"
1896
1902
  ).format(tag)):
1897
1903
  itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS)
1898
- if SHARED.mainGui.projView.createNewNote(tag, itemClass):
1899
- self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1900
- else:
1901
- SHARED.error(self.tr(
1902
- "Could not create note in a root folder for '{0}'. "
1903
- "If one doesn't exist, you must create one first."
1904
- ).format(trConst(nwLabels.CLASS_NAME[itemClass])))
1904
+ self.requestNewNoteCreation.emit(tag, itemClass)
1905
+ qApp.processEvents()
1906
+ self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1905
1907
 
1906
1908
  return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
1907
1909
 
@@ -2224,7 +2226,7 @@ class BackgroundWordCounter(QRunnable):
2224
2226
  else:
2225
2227
  text = self._docEditor.getText()
2226
2228
 
2227
- cC, wC, pC = countWords(text)
2229
+ cC, wC, pC = standardCounter(text)
2228
2230
  self.signals.countsReady.emit(cC, wC, pC)
2229
2231
  self._isRunning = False
2230
2232
 
@@ -2313,6 +2315,13 @@ class GuiDocToolBar(QWidget):
2313
2315
  lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE)
2314
2316
  )
2315
2317
 
2318
+ self.tbMark = QToolButton(self)
2319
+ self.tbMark.setIconSize(iconSize)
2320
+ self.tbMark.setToolTip(self.tr("Shortcode Highlight"))
2321
+ self.tbMark.clicked.connect(
2322
+ lambda: self.requestDocAction.emit(nwDocAction.SC_MARK)
2323
+ )
2324
+
2316
2325
  self.tbSuperscript = QToolButton(self)
2317
2326
  self.tbSuperscript.setIconSize(iconSize)
2318
2327
  self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript"))
@@ -2339,6 +2348,7 @@ class GuiDocToolBar(QWidget):
2339
2348
  self.outerBox.addWidget(self.tbItalic)
2340
2349
  self.outerBox.addWidget(self.tbStrike)
2341
2350
  self.outerBox.addWidget(self.tbUnderline)
2351
+ self.outerBox.addWidget(self.tbMark)
2342
2352
  self.outerBox.addWidget(self.tbSuperscript)
2343
2353
  self.outerBox.addWidget(self.tbSubscript)
2344
2354
  self.outerBox.setContentsMargins(cM, cM, cM, cM)
@@ -2369,6 +2379,7 @@ class GuiDocToolBar(QWidget):
2369
2379
  self.tbItalic.setIcon(SHARED.theme.getIcon("fmt_italic"))
2370
2380
  self.tbStrike.setIcon(SHARED.theme.getIcon("fmt_strike"))
2371
2381
  self.tbUnderline.setIcon(SHARED.theme.getIcon("fmt_underline"))
2382
+ self.tbMark.setIcon(SHARED.theme.getIcon("fmt_mark"))
2372
2383
  self.tbSuperscript.setIcon(SHARED.theme.getIcon("fmt_superscript"))
2373
2384
  self.tbSubscript.setIcon(SHARED.theme.getIcon("fmt_subscript"))
2374
2385
 
@@ -2605,8 +2616,10 @@ class GuiDocEditSearch(QFrame):
2605
2616
 
2606
2617
  def setResultCount(self, currRes: int | None, resCount: int | None) -> None:
2607
2618
  """Set the count values for the current search."""
2619
+ lim = nwConst.MAX_SEARCH_RESULT
2620
+ numCount = f"{lim:n}+" if (resCount or 0) > lim else f"{resCount:n}"
2608
2621
  sCurrRes = "?" if currRes is None else str(currRes)
2609
- sResCount = "?" if resCount is None else "1000+" if resCount > 1000 else str(resCount)
2622
+ sResCount = "?" if resCount is None else numCount
2610
2623
  minWidth = SHARED.theme.getTextWidth(f"{sResCount}//{sResCount}", self.boxFont)
2611
2624
  self.resultLabel.setText(f"{sCurrRes}/{sResCount}")
2612
2625
  self.resultLabel.setMinimumWidth(minWidth)
@@ -2706,11 +2719,9 @@ class GuiDocEditSearch(QFrame):
2706
2719
  @pyqtSlot()
2707
2720
  def _doSearch(self) -> None:
2708
2721
  """Call the search action function for the document editor."""
2709
- modKey = qApp.keyboardModifiers()
2710
- if modKey == Qt.KeyboardModifier.ShiftModifier:
2711
- self.docEditor.findNext(goBack=True)
2712
- else:
2713
- self.docEditor.findNext()
2722
+ self.docEditor.findNext(goBack=(
2723
+ qApp.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier)
2724
+ )
2714
2725
  return
2715
2726
 
2716
2727
  @pyqtSlot()
@@ -2801,84 +2812,81 @@ class GuiDocEditHeader(QWidget):
2801
2812
  logger.debug("Create: GuiDocEditHeader")
2802
2813
 
2803
2814
  self.docEditor = docEditor
2804
- self.mainGui = docEditor.mainGui
2805
2815
 
2806
2816
  self._docHandle = None
2817
+ self._docOutline: dict[int, str] = {}
2807
2818
 
2808
- fPx = int(0.9*SHARED.theme.fontPixelSize)
2809
- hSp = CONFIG.pxInt(6)
2810
- iconSize = QSize(fPx, fPx)
2819
+ iPx = SHARED.theme.baseIconSize
2820
+ mPx = CONFIG.pxInt(4)
2811
2821
 
2812
2822
  # Main Widget Settings
2813
2823
  self.setAutoFillBackground(True)
2814
2824
 
2815
2825
  # Title Label
2816
- self.itemTitle = QLabel()
2817
- self.itemTitle.setText("")
2826
+ self.itemTitle = QLabel("", self)
2818
2827
  self.itemTitle.setIndent(0)
2819
2828
  self.itemTitle.setMargin(0)
2820
2829
  self.itemTitle.setContentsMargins(0, 0, 0, 0)
2821
2830
  self.itemTitle.setAutoFillBackground(True)
2822
2831
  self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
2823
- self.itemTitle.setFixedHeight(fPx)
2832
+ self.itemTitle.setFixedHeight(iPx)
2824
2833
 
2825
2834
  lblFont = self.itemTitle.font()
2826
2835
  lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
2827
2836
  self.itemTitle.setFont(lblFont)
2828
2837
 
2838
+ # Other Widgets
2839
+ self.outlineMenu = QMenu(self)
2840
+
2829
2841
  # Buttons
2830
- self.tbButton = QToolButton(self)
2831
- self.tbButton.setContentsMargins(0, 0, 0, 0)
2832
- self.tbButton.setIconSize(iconSize)
2833
- self.tbButton.setFixedSize(fPx, fPx)
2834
- self.tbButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2842
+ self.tbButton = NIconToolButton(self, iPx)
2835
2843
  self.tbButton.setVisible(False)
2836
2844
  self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
2837
2845
  self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
2838
2846
 
2839
- self.searchButton = QToolButton(self)
2840
- self.searchButton.setContentsMargins(0, 0, 0, 0)
2841
- self.searchButton.setIconSize(iconSize)
2842
- self.searchButton.setFixedSize(fPx, fPx)
2843
- self.searchButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2847
+ self.outlineButton = NIconToolButton(self, iPx)
2848
+ self.outlineButton.setVisible(False)
2849
+ self.outlineButton.setToolTip(self.tr("Outline"))
2850
+ self.outlineButton.setMenu(self.outlineMenu)
2851
+
2852
+ self.searchButton = NIconToolButton(self, iPx)
2844
2853
  self.searchButton.setVisible(False)
2845
2854
  self.searchButton.setToolTip(self.tr("Search"))
2846
2855
  self.searchButton.clicked.connect(self.docEditor.toggleSearch)
2847
2856
 
2848
- self.minmaxButton = QToolButton(self)
2849
- self.minmaxButton.setContentsMargins(0, 0, 0, 0)
2850
- self.minmaxButton.setIconSize(iconSize)
2851
- self.minmaxButton.setFixedSize(fPx, fPx)
2852
- self.minmaxButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2857
+ self.minmaxButton = NIconToolButton(self, iPx)
2853
2858
  self.minmaxButton.setVisible(False)
2854
2859
  self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
2855
2860
  self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit())
2856
2861
 
2857
- self.closeButton = QToolButton(self)
2858
- self.closeButton.setContentsMargins(0, 0, 0, 0)
2859
- self.closeButton.setIconSize(iconSize)
2860
- self.closeButton.setFixedSize(fPx, fPx)
2861
- self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2862
+ self.closeButton = NIconToolButton(self, iPx)
2862
2863
  self.closeButton.setVisible(False)
2863
2864
  self.closeButton.setToolTip(self.tr("Close"))
2864
2865
  self.closeButton.clicked.connect(self._closeDocument)
2865
2866
 
2866
2867
  # Assemble Layout
2867
2868
  self.outerBox = QHBoxLayout()
2868
- self.outerBox.setSpacing(hSp)
2869
2869
  self.outerBox.addWidget(self.tbButton, 0)
2870
+ self.outerBox.addWidget(self.outlineButton, 0)
2870
2871
  self.outerBox.addWidget(self.searchButton, 0)
2872
+ self.outerBox.addSpacing(mPx)
2871
2873
  self.outerBox.addWidget(self.itemTitle, 1)
2874
+ self.outerBox.addSpacing(mPx)
2875
+ self.outerBox.addSpacing(iPx)
2872
2876
  self.outerBox.addWidget(self.minmaxButton, 0)
2873
2877
  self.outerBox.addWidget(self.closeButton, 0)
2878
+ self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
2879
+ self.outerBox.setSpacing(0)
2880
+
2874
2881
  self.setLayout(self.outerBox)
2875
2882
 
2883
+ # Other Signals
2884
+ SHARED.focusModeChanged.connect(self._focusModeChanged)
2885
+
2876
2886
  # Fix Margins and Size
2877
2887
  # This is needed for high DPI systems. See issue #499.
2878
- cM = CONFIG.pxInt(8)
2879
2888
  self.setContentsMargins(0, 0, 0, 0)
2880
- self.outerBox.setContentsMargins(cM, cM, cM, cM)
2881
- self.setMinimumHeight(fPx + 2*cM)
2889
+ self.setMinimumHeight(iPx + 2*mPx)
2882
2890
 
2883
2891
  self.updateTheme()
2884
2892
 
@@ -2890,20 +2898,45 @@ class GuiDocEditHeader(QWidget):
2890
2898
  # Methods
2891
2899
  ##
2892
2900
 
2901
+ def clearHeader(self) -> None:
2902
+ """Clear the header."""
2903
+ self._docHandle = None
2904
+ self._docOutline = {}
2905
+
2906
+ self.itemTitle.setText("")
2907
+ self.outlineMenu.clear()
2908
+ self.tbButton.setVisible(False)
2909
+ self.outlineButton.setVisible(False)
2910
+ self.searchButton.setVisible(False)
2911
+ self.closeButton.setVisible(False)
2912
+ self.minmaxButton.setVisible(False)
2913
+ return
2914
+
2915
+ def setOutline(self, data: dict[int, str]) -> None:
2916
+ """Set the document outline dataset."""
2917
+ if data != self._docOutline:
2918
+ tStart = time()
2919
+ self.outlineMenu.clear()
2920
+ for number, text in data.items():
2921
+ action = self.outlineMenu.addAction(text)
2922
+ action.triggered.connect(
2923
+ lambda _, number=number: self._gotoBlock(number)
2924
+ )
2925
+ self._docOutline = data
2926
+ logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
2927
+ return
2928
+
2893
2929
  def updateTheme(self) -> None:
2894
2930
  """Update theme elements."""
2895
2931
  self.tbButton.setIcon(SHARED.theme.getIcon("menu"))
2932
+ self.outlineButton.setIcon(SHARED.theme.getIcon("list"))
2896
2933
  self.searchButton.setIcon(SHARED.theme.getIcon("search"))
2897
2934
  self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
2898
2935
  self.closeButton.setIcon(SHARED.theme.getIcon("close"))
2899
2936
 
2900
- colText = SHARED.theme.colText
2901
- buttonStyle = (
2902
- "QToolButton {{border: none; background: transparent;}} "
2903
- "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
2904
- ).format(colText.red(), colText.green(), colText.blue())
2905
-
2937
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
2906
2938
  self.tbButton.setStyleSheet(buttonStyle)
2939
+ self.outlineButton.setStyleSheet(buttonStyle)
2907
2940
  self.searchButton.setStyleSheet(buttonStyle)
2908
2941
  self.minmaxButton.setStyleSheet(buttonStyle)
2909
2942
  self.closeButton.setStyleSheet(buttonStyle)
@@ -2926,51 +2959,25 @@ class GuiDocEditHeader(QWidget):
2926
2959
 
2927
2960
  return
2928
2961
 
2929
- def setTitleFromHandle(self, tHandle: str | None) -> bool:
2962
+ def setHandle(self, tHandle: str) -> None:
2930
2963
  """Set the document title from the handle, or alternatively, set
2931
2964
  the whole document path within the project.
2932
2965
  """
2933
2966
  self._docHandle = tHandle
2934
- if tHandle is None:
2935
- self.itemTitle.setText("")
2936
- self.tbButton.setVisible(False)
2937
- self.searchButton.setVisible(False)
2938
- self.closeButton.setVisible(False)
2939
- self.minmaxButton.setVisible(False)
2940
- return True
2941
-
2942
- pTree = SHARED.project.tree
2967
+
2943
2968
  if CONFIG.showFullPath:
2944
- tTitle = []
2945
- tTree = pTree.getItemPath(tHandle)
2946
- for aHandle in reversed(tTree):
2947
- nwItem = pTree[aHandle]
2948
- if nwItem is not None:
2949
- tTitle.append(nwItem.itemName)
2950
- sSep = " %s " % nwUnicode.U_RSAQUO
2951
- self.itemTitle.setText(sSep.join(tTitle))
2969
+ self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
2970
+ [name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)]
2971
+ )))
2952
2972
  else:
2953
- nwItem = pTree[tHandle]
2954
- if nwItem is None:
2955
- return False
2956
- self.itemTitle.setText(nwItem.itemName)
2973
+ self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
2957
2974
 
2958
2975
  self.tbButton.setVisible(True)
2959
2976
  self.searchButton.setVisible(True)
2977
+ self.outlineButton.setVisible(True)
2960
2978
  self.closeButton.setVisible(True)
2961
2979
  self.minmaxButton.setVisible(True)
2962
2980
 
2963
- return True
2964
-
2965
- def updateFocusMode(self) -> None:
2966
- """Update the minimise/maximise icon of the Focus Mode button.
2967
- This function is called by the GuiMain class via the
2968
- toggleFocusMode function and should not be activated directly.
2969
- """
2970
- if self.mainGui.isFocusMode:
2971
- self.minmaxButton.setIcon(SHARED.theme.getIcon("minimise"))
2972
- else:
2973
- self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
2974
2981
  return
2975
2982
 
2976
2983
  ##
@@ -2980,11 +2987,20 @@ class GuiDocEditHeader(QWidget):
2980
2987
  @pyqtSlot()
2981
2988
  def _closeDocument(self) -> None:
2982
2989
  """Trigger the close editor on the main window."""
2990
+ self.clearHeader()
2983
2991
  self.closeDocumentRequest.emit()
2984
- self.tbButton.setVisible(False)
2985
- self.searchButton.setVisible(False)
2986
- self.closeButton.setVisible(False)
2987
- self.minmaxButton.setVisible(False)
2992
+ return
2993
+
2994
+ @pyqtSlot(int)
2995
+ def _gotoBlock(self, blockNumber: int) -> None:
2996
+ """Move cursor to a specific heading."""
2997
+ self.docEditor.setCursorLine(blockNumber + 1)
2998
+ return
2999
+
3000
+ @pyqtSlot(bool)
3001
+ def _focusModeChanged(self, focusMode: bool) -> None:
3002
+ """Update minimise/maximise icon of the Focus Mode button."""
3003
+ self.minmaxButton.setIcon(SHARED.theme.getIcon("minimise" if focusMode else "maximise"))
2988
3004
  return
2989
3005
 
2990
3006
  ##
@@ -2996,7 +3012,7 @@ class GuiDocEditHeader(QWidget):
2996
3012
  selected in the project tree.
2997
3013
  """
2998
3014
  if event.button() == Qt.MouseButton.LeftButton:
2999
- self.docEditor.requestProjectItemSelected.emit(self._docHandle, True)
3015
+ self.docEditor.requestProjectItemSelected.emit(self._docHandle or "", True)
3000
3016
  return
3001
3017
 
3002
3018
  # END Class GuiDocEditHeader
@@ -3009,26 +3025,28 @@ class GuiDocEditHeader(QWidget):
3009
3025
 
3010
3026
  class GuiDocEditFooter(QWidget):
3011
3027
 
3012
- def __init__(self, docEditor: GuiDocEditor) -> None:
3013
- super().__init__(parent=docEditor)
3028
+ def __init__(self, parent: QWidget) -> None:
3029
+ super().__init__(parent=parent)
3014
3030
 
3015
3031
  logger.debug("Create: GuiDocEditFooter")
3016
3032
 
3017
- self.docEditor = docEditor
3018
-
3019
- self._tItem = None
3033
+ self._tItem = None
3020
3034
  self._docHandle = None
3021
3035
 
3022
- self._docSelection = False
3023
-
3024
- self.sPx = int(round(0.9*SHARED.theme.baseIconSize))
3036
+ iPx = round(0.9*SHARED.theme.baseIconSize)
3025
3037
  fPx = int(0.9*SHARED.theme.fontPixelSize)
3038
+ mPx = CONFIG.pxInt(8)
3026
3039
  bSp = CONFIG.pxInt(4)
3027
3040
  hSp = CONFIG.pxInt(6)
3028
3041
 
3029
3042
  lblFont = self.font()
3030
3043
  lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
3031
3044
 
3045
+ # Cached Translations
3046
+ self._trLineCount = self.tr("Line: {0} ({1})")
3047
+ self._trWordCount = self.tr("Words: {0} ({1})")
3048
+ self._trSelectCount = self.tr("Words: {0} selected")
3049
+
3032
3050
  # Main Widget Settings
3033
3051
  self.setContentsMargins(0, 0, 0, 0)
3034
3052
  self.setAutoFillBackground(True)
@@ -3036,9 +3054,9 @@ class GuiDocEditFooter(QWidget):
3036
3054
  alLeftTop = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
3037
3055
 
3038
3056
  # Status
3039
- self.statusIcon = QLabel("")
3057
+ self.statusIcon = QLabel("", self)
3040
3058
  self.statusIcon.setContentsMargins(0, 0, 0, 0)
3041
- self.statusIcon.setFixedHeight(self.sPx)
3059
+ self.statusIcon.setFixedHeight(iPx)
3042
3060
  self.statusIcon.setAlignment(alLeftTop)
3043
3061
 
3044
3062
  self.statusText = QLabel(self.tr("Status"))
@@ -3051,12 +3069,12 @@ class GuiDocEditFooter(QWidget):
3051
3069
  self.statusText.setFont(lblFont)
3052
3070
 
3053
3071
  # Lines
3054
- self.linesIcon = QLabel("")
3072
+ self.linesIcon = QLabel("", self)
3055
3073
  self.linesIcon.setContentsMargins(0, 0, 0, 0)
3056
- self.linesIcon.setFixedHeight(self.sPx)
3074
+ self.linesIcon.setFixedHeight(iPx)
3057
3075
  self.linesIcon.setAlignment(alLeftTop)
3058
3076
 
3059
- self.linesText = QLabel("")
3077
+ self.linesText = QLabel("", self)
3060
3078
  self.linesText.setIndent(0)
3061
3079
  self.linesText.setMargin(0)
3062
3080
  self.linesText.setContentsMargins(0, 0, 0, 0)
@@ -3066,12 +3084,12 @@ class GuiDocEditFooter(QWidget):
3066
3084
  self.linesText.setFont(lblFont)
3067
3085
 
3068
3086
  # Words
3069
- self.wordsIcon = QLabel("")
3087
+ self.wordsIcon = QLabel("", self)
3070
3088
  self.wordsIcon.setContentsMargins(0, 0, 0, 0)
3071
- self.wordsIcon.setFixedHeight(self.sPx)
3089
+ self.wordsIcon.setFixedHeight(iPx)
3072
3090
  self.wordsIcon.setAlignment(alLeftTop)
3073
3091
 
3074
- self.wordsText = QLabel("")
3092
+ self.wordsText = QLabel("", self)
3075
3093
  self.wordsText.setIndent(0)
3076
3094
  self.wordsText.setMargin(0)
3077
3095
  self.wordsText.setContentsMargins(0, 0, 0, 0)
@@ -3091,19 +3109,20 @@ class GuiDocEditFooter(QWidget):
3091
3109
  self.outerBox.addSpacing(hSp)
3092
3110
  self.outerBox.addWidget(self.wordsIcon)
3093
3111
  self.outerBox.addWidget(self.wordsText)
3112
+ self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
3113
+
3094
3114
  self.setLayout(self.outerBox)
3095
3115
 
3096
3116
  # Fix Margins and Size
3097
3117
  # This is needed for high DPI systems. See issue #499.
3098
- cM = CONFIG.pxInt(8)
3099
3118
  self.setContentsMargins(0, 0, 0, 0)
3100
- self.outerBox.setContentsMargins(cM, cM, cM, cM)
3101
- self.setMinimumHeight(fPx + 2*cM)
3119
+ self.setMinimumHeight(fPx + 2*mPx)
3102
3120
 
3103
3121
  # Fix the Colours
3104
3122
  self.updateTheme()
3105
- self.updateLineCount()
3106
- self.updateCounts()
3123
+
3124
+ # Initialise Info
3125
+ self.updateWordCount(0, False)
3107
3126
 
3108
3127
  logger.debug("Ready: GuiDocEditFooter")
3109
3128
 
@@ -3115,8 +3134,9 @@ class GuiDocEditFooter(QWidget):
3115
3134
 
3116
3135
  def updateTheme(self) -> None:
3117
3136
  """Update theme elements."""
3118
- self.linesIcon.setPixmap(SHARED.theme.getPixmap("status_lines", (self.sPx, self.sPx)))
3119
- self.wordsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (self.sPx, self.sPx)))
3137
+ iPx = round(0.9*SHARED.theme.baseIconSize)
3138
+ self.linesIcon.setPixmap(SHARED.theme.getPixmap("status_lines", (iPx, iPx)))
3139
+ self.wordsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (iPx, iPx)))
3120
3140
  self.matchColours()
3121
3141
  return
3122
3142
 
@@ -3145,27 +3165,20 @@ class GuiDocEditFooter(QWidget):
3145
3165
  else:
3146
3166
  self._tItem = SHARED.project.tree[self._docHandle]
3147
3167
 
3148
- self.setHasSelection(False)
3149
3168
  self.updateInfo()
3150
- self.updateCounts()
3169
+ self.updateWordCount(0, False)
3151
3170
 
3152
3171
  return
3153
3172
 
3154
- def setHasSelection(self, hasSelection: bool) -> None:
3155
- """Toggle the word counter mode between full count and selection
3156
- count mode.
3157
- """
3158
- self._docSelection = hasSelection
3159
- return
3160
-
3161
3173
  def updateInfo(self) -> None:
3162
3174
  """Update the content of text labels."""
3163
3175
  if self._tItem is None:
3164
3176
  sIcon = QPixmap()
3165
3177
  sText = ""
3166
3178
  else:
3179
+ iPx = round(0.9*SHARED.theme.baseIconSize)
3167
3180
  status, icon = self._tItem.getImportStatus(incIcon=True)
3168
- sIcon = icon.pixmap(self.sPx, self.sPx)
3181
+ sIcon = icon.pixmap(iPx, iPx)
3169
3182
  sText = f"{status} / {self._tItem.describeMe()}"
3170
3183
 
3171
3184
  self.statusIcon.setPixmap(sIcon)
@@ -3173,62 +3186,27 @@ class GuiDocEditFooter(QWidget):
3173
3186
 
3174
3187
  return
3175
3188
 
3176
- def updateLineCount(self) -> None:
3177
- """Update the line counter."""
3178
- if self._tItem is None:
3179
- iLine = 0
3180
- iDist = 0
3181
- else:
3182
- cursor = self.docEditor.textCursor()
3183
- iLine = cursor.blockNumber() + 1
3184
- iDist = 100*iLine/self.docEditor._qDocument.blockCount()
3189
+ def updateLineCount(self, cursor: QTextCursor) -> None:
3190
+ """Update the line and document position counter."""
3191
+ cPos = cursor.position() + 1
3192
+ cLine = cursor.blockNumber() + 1
3193
+ cCount = max(cursor.document().characterCount(), 1)
3185
3194
  self.linesText.setText(
3186
- self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
3195
+ self._trLineCount.format(f"{cLine:n}", f"{100*cPos//cCount:d} %")
3187
3196
  )
3188
3197
  return
3189
3198
 
3190
- def updateCounts(self, wCount: int | None = None, cCount: int | None = None) -> None:
3191
- """Select which word count display mode to use."""
3192
- if self._docSelection:
3193
- self._updateSelectionWordCounts(wCount, cCount)
3194
- else:
3195
- self._updateWordCounts()
3196
- return
3197
-
3198
- ##
3199
- # Internal Functions
3200
- ##
3201
-
3202
- def _updateWordCounts(self) -> None:
3203
- """Update the word count for the whole document."""
3204
- if self._tItem is None:
3205
- wCount = 0
3206
- wDiff = 0
3207
- else:
3199
+ def updateWordCount(self, wCount: int, selection: bool) -> None:
3200
+ """Update word counter information."""
3201
+ if selection and wCount:
3202
+ wText = self._trSelectCount.format(f"{wCount:n}")
3203
+ elif self._tItem:
3208
3204
  wCount = self._tItem.wordCount
3209
3205
  wDiff = wCount - self._tItem.initCount
3210
-
3211
- self.wordsText.setText(
3212
- self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
3213
- )
3214
-
3215
- byteSize = self.docEditor._qDocument.characterCount()
3216
- self.wordsText.setToolTip(
3217
- self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
3218
- )
3219
-
3220
- return
3221
-
3222
- def _updateSelectionWordCounts(self, wCount: int | None, cCount: int | None) -> None:
3223
- """Update the word count for a selection."""
3224
- if wCount is None or cCount is None:
3225
- return
3226
- self.wordsText.setText(
3227
- self.tr("Words: {0} selected").format(f"{wCount:n}")
3228
- )
3229
- self.wordsText.setToolTip(
3230
- self.tr("Character count: {0}").format(f"{cCount:n}")
3231
- )
3206
+ wText = self._trWordCount.format(f"{wCount:n}", f"{wDiff:+n}")
3207
+ else:
3208
+ wText = self._trWordCount.format("0", "+0")
3209
+ self.wordsText.setText(wText)
3232
3210
  return
3233
3211
 
3234
3212
  # END Class GuiDocEditFooter