novelWriter 2.3rc1__py3-none-any.whl → 2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/METADATA +5 -6
  2. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/RECORD +119 -109
  3. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/WHEEL +1 -1
  4. novelWriter-2.4.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +17 -10
  6. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  7. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  8. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  9. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  10. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  11. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  12. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  13. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  16. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  17. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  18. novelwriter/assets/icons/none.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  21. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  22. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  23. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  24. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  25. novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
  27. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  28. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  29. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  30. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  31. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  32. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  33. novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
  34. novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
  35. novelwriter/assets/manual.pdf +0 -0
  36. novelwriter/assets/sample.zip +0 -0
  37. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  38. novelwriter/assets/syntax/default_dark.conf +1 -0
  39. novelwriter/assets/syntax/default_light.conf +1 -0
  40. novelwriter/assets/syntax/grey_dark.conf +1 -0
  41. novelwriter/assets/syntax/grey_light.conf +1 -0
  42. novelwriter/assets/syntax/light_owl.conf +1 -0
  43. novelwriter/assets/syntax/night_owl.conf +1 -0
  44. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  45. novelwriter/assets/syntax/solarized_light.conf +1 -0
  46. novelwriter/assets/syntax/tango.conf +23 -0
  47. novelwriter/assets/syntax/tomorrow.conf +1 -0
  48. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  49. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  50. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  51. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  52. novelwriter/assets/text/credits_en.htm +25 -23
  53. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  54. novelwriter/common.py +12 -4
  55. novelwriter/config.py +47 -16
  56. novelwriter/constants.py +5 -6
  57. novelwriter/core/buildsettings.py +64 -44
  58. novelwriter/core/coretools.py +97 -13
  59. novelwriter/core/docbuild.py +74 -7
  60. novelwriter/core/document.py +24 -3
  61. novelwriter/core/index.py +31 -112
  62. novelwriter/core/project.py +11 -15
  63. novelwriter/core/projectxml.py +3 -2
  64. novelwriter/core/sessions.py +2 -2
  65. novelwriter/core/spellcheck.py +3 -3
  66. novelwriter/core/status.py +6 -5
  67. novelwriter/core/storage.py +16 -6
  68. novelwriter/core/tohtml.py +22 -25
  69. novelwriter/core/tokenizer.py +417 -237
  70. novelwriter/core/tomd.py +17 -8
  71. novelwriter/core/toodt.py +386 -351
  72. novelwriter/core/tree.py +8 -8
  73. novelwriter/dialogs/about.py +10 -12
  74. novelwriter/dialogs/docmerge.py +17 -14
  75. novelwriter/dialogs/docsplit.py +20 -19
  76. novelwriter/dialogs/editlabel.py +5 -4
  77. novelwriter/dialogs/preferences.py +32 -40
  78. novelwriter/dialogs/projectsettings.py +31 -28
  79. novelwriter/dialogs/quotes.py +10 -9
  80. novelwriter/dialogs/wordlist.py +18 -15
  81. novelwriter/enum.py +17 -14
  82. novelwriter/error.py +14 -12
  83. novelwriter/extensions/circularprogress.py +12 -8
  84. novelwriter/extensions/configlayout.py +23 -3
  85. novelwriter/extensions/modified.py +51 -2
  86. novelwriter/extensions/pagedsidebar.py +16 -14
  87. novelwriter/extensions/simpleprogress.py +3 -1
  88. novelwriter/extensions/statusled.py +3 -1
  89. novelwriter/extensions/switch.py +10 -9
  90. novelwriter/extensions/switchbox.py +14 -13
  91. novelwriter/extensions/versioninfo.py +1 -1
  92. novelwriter/gui/doceditor.py +433 -496
  93. novelwriter/gui/dochighlight.py +54 -33
  94. novelwriter/gui/docviewer.py +162 -175
  95. novelwriter/gui/docviewerpanel.py +20 -37
  96. novelwriter/gui/editordocument.py +15 -4
  97. novelwriter/gui/itemdetails.py +51 -54
  98. novelwriter/gui/mainmenu.py +37 -17
  99. novelwriter/gui/noveltree.py +31 -37
  100. novelwriter/gui/outline.py +120 -98
  101. novelwriter/gui/projtree.py +114 -112
  102. novelwriter/gui/search.py +362 -0
  103. novelwriter/gui/sidebar.py +36 -45
  104. novelwriter/gui/statusbar.py +14 -14
  105. novelwriter/gui/theme.py +116 -34
  106. novelwriter/guimain.py +216 -207
  107. novelwriter/shared.py +31 -6
  108. novelwriter/text/counting.py +138 -0
  109. novelwriter/tools/dictionaries.py +15 -14
  110. novelwriter/tools/lipsum.py +20 -17
  111. novelwriter/tools/manusbuild.py +43 -35
  112. novelwriter/tools/manuscript.py +381 -104
  113. novelwriter/tools/manussettings.py +263 -125
  114. novelwriter/tools/noveldetails.py +21 -19
  115. novelwriter/tools/welcome.py +59 -57
  116. novelwriter/tools/writingstats.py +61 -55
  117. novelwriter/types.py +90 -0
  118. novelWriter-2.3rc1.dist-info/entry_points.txt +0 -5
  119. novelwriter/core/__init__.py +0 -3
  120. novelwriter/dialogs/__init__.py +0 -3
  121. novelwriter/extensions/__init__.py +0 -3
  122. novelwriter/gui/__init__.py +0 -3
  123. novelwriter/tools/__init__.py +0 -3
  124. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/LICENSE.md +0 -0
  125. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/top_level.txt +0 -0
@@ -40,28 +40,34 @@ from typing import TYPE_CHECKING
40
40
 
41
41
  from PyQt5.QtCore import (
42
42
  pyqtSignal, pyqtSlot, QObject, QPoint, QRegExp, QRegularExpression,
43
- QRunnable, QSize, Qt, QTimer
43
+ QRunnable, Qt, QTimer
44
44
  )
45
45
  from PyQt5.QtGui import (
46
46
  QColor, QCursor, QFont, QKeyEvent, QKeySequence, QMouseEvent, QPalette,
47
47
  QPixmap, QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption
48
48
  )
49
49
  from PyQt5.QtWidgets import (
50
- QAction, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
51
- QPlainTextEdit, QPushButton, QShortcut, QToolBar, QToolButton, QVBoxLayout,
52
- QWidget, qApp
50
+ QAction, QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit,
51
+ QMenu, QPlainTextEdit, QShortcut, QToolBar, QVBoxLayout, QWidget
53
52
  )
54
53
 
55
54
  from novelwriter import CONFIG, SHARED
56
- from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
57
55
  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
56
+ from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
61
57
  from novelwriter.core.document import NWDocument
62
- from novelwriter.gui.dochighlight import GuiDocHighlighter
63
- from novelwriter.gui.editordocument import GuiTextDocument
58
+ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
64
59
  from novelwriter.extensions.eventfilters import WheelEventFilter
60
+ from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton
61
+ from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE
62
+ from novelwriter.gui.editordocument import GuiTextDocument
63
+ from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
64
+ from novelwriter.text.counting import standardCounter
65
+ from novelwriter.tools.lipsum import GuiLipsum
66
+ from novelwriter.types import (
67
+ QtAlignCenterTop, QtAlignJustify, QtAlignLeft, QtAlignLeftTop,
68
+ QtAlignRight, QtKeepAnchor, QtModCtrl, QtMouseLeft, QtModeNone, QtModShift,
69
+ QtMoveAnchor, QtMoveLeft, QtMoveRight
70
+ )
65
71
 
66
72
  if TYPE_CHECKING: # pragma: no cover
67
73
  from novelwriter.guimain import GuiMain
@@ -90,6 +96,7 @@ class GuiDocEditor(QPlainTextEdit):
90
96
  # Custom Signals
91
97
  statusMessage = pyqtSignal(str)
92
98
  docCountsChanged = pyqtSignal(str, int, int, int)
99
+ docTextChanged = pyqtSignal(str, float)
93
100
  editedStatusChanged = pyqtSignal(bool)
94
101
  loadDocumentTagRequest = pyqtSignal(str, Enum)
95
102
  novelStructureChanged = pyqtSignal()
@@ -99,6 +106,7 @@ class GuiDocEditor(QPlainTextEdit):
99
106
  toggleFocusModeRequest = pyqtSignal()
100
107
  requestProjectItemSelected = pyqtSignal(str, bool)
101
108
  requestProjectItemRenamed = pyqtSignal(str, str)
109
+ requestNewNoteCreation = pyqtSignal(str, nwItemClass)
102
110
 
103
111
  def __init__(self, mainGui: GuiMain) -> None:
104
112
  super().__init__(parent=mainGui)
@@ -142,9 +150,10 @@ class GuiDocEditor(QPlainTextEdit):
142
150
  self._qDocument = GuiTextDocument(self)
143
151
  self.setDocument(self._qDocument)
144
152
 
145
- # Connect Signals
153
+ # Connect Editor and Document Signals
146
154
  self._qDocument.contentsChange.connect(self._docChange)
147
155
  self.selectionChanged.connect(self._updateSelectedStatus)
156
+ self.cursorPositionChanged.connect(self._cursorMoved)
148
157
  self.spellCheckStateChanged.connect(self._qDocument.setSpellCheckState)
149
158
 
150
159
  # Document Title
@@ -153,7 +162,7 @@ class GuiDocEditor(QPlainTextEdit):
153
162
  self.docSearch = GuiDocEditSearch(self)
154
163
  self.docToolBar = GuiDocToolBar(self)
155
164
 
156
- # Connect Signals
165
+ # Connect Widget Signals
157
166
  self.docHeader.closeDocumentRequest.connect(self._closeCurrentDocument)
158
167
  self.docHeader.toggleToolBarRequest.connect(self._toggleToolBarVisibility)
159
168
  self.docToolBar.requestDocAction.connect(self.docAction)
@@ -174,28 +183,28 @@ class GuiDocEditor(QPlainTextEdit):
174
183
  self.keyContext.activated.connect(self._openContextFromCursor)
175
184
 
176
185
  self.followTag1 = QShortcut(self)
177
- self.followTag1.setKey(Qt.Key.Key_Return | Qt.KeyboardModifier.ControlModifier)
186
+ self.followTag1.setKey(Qt.Key.Key_Return | QtModCtrl)
178
187
  self.followTag1.setContext(Qt.ShortcutContext.WidgetShortcut)
179
188
  self.followTag1.activated.connect(self._processTag)
180
189
 
181
190
  self.followTag2 = QShortcut(self)
182
- self.followTag2.setKey(Qt.Key.Key_Enter | Qt.KeyboardModifier.ControlModifier)
191
+ self.followTag2.setKey(Qt.Key.Key_Enter | QtModCtrl)
183
192
  self.followTag2.setContext(Qt.ShortcutContext.WidgetShortcut)
184
193
  self.followTag2.activated.connect(self._processTag)
185
194
 
186
195
  # Set Up Document Word Counter
187
- self.wcTimerDoc = QTimer()
188
- self.wcTimerDoc.timeout.connect(self._runDocCounter)
189
- self.wcTimerDoc.setInterval(5000)
196
+ self.timerDoc = QTimer(self)
197
+ self.timerDoc.timeout.connect(self._runDocumentTasks)
198
+ self.timerDoc.setInterval(5000)
190
199
 
191
200
  self.wCounterDoc = BackgroundWordCounter(self)
192
201
  self.wCounterDoc.setAutoDelete(False)
193
202
  self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
194
203
 
195
204
  # Set Up Selection Word Counter
196
- self.wcTimerSel = QTimer()
197
- self.wcTimerSel.timeout.connect(self._runSelCounter)
198
- self.wcTimerSel.setInterval(500)
205
+ self.timerSel = QTimer(self)
206
+ self.timerSel.timeout.connect(self._runSelCounter)
207
+ self.timerSel.setInterval(500)
199
208
 
200
209
  self.wCounterSel = BackgroundWordCounter(self, forSelection=True)
201
210
  self.wCounterSel.setAutoDelete(False)
@@ -205,6 +214,10 @@ class GuiDocEditor(QPlainTextEdit):
205
214
  self.wheelEventFilter = WheelEventFilter(self)
206
215
  self.installEventFilter(self.wheelEventFilter)
207
216
 
217
+ # Function Mapping
218
+ self.closeSearch = self.docSearch.closeSearch
219
+ self.searchVisible = self.docSearch.isVisible
220
+
208
221
  # Finalise
209
222
  self.updateSyntaxColours()
210
223
  self.initEditor()
@@ -248,8 +261,8 @@ class GuiDocEditor(QPlainTextEdit):
248
261
  self._nwDocument = None
249
262
  self.setReadOnly(True)
250
263
  self.clear()
251
- self.wcTimerDoc.stop()
252
- self.wcTimerSel.stop()
264
+ self.timerDoc.stop()
265
+ self.timerSel.stop()
253
266
 
254
267
  self._docHandle = None
255
268
  self._lastEdit = 0.0
@@ -258,7 +271,7 @@ class GuiDocEditor(QPlainTextEdit):
258
271
  self._doReplace = False
259
272
 
260
273
  self.setDocumentChanged(False)
261
- self.docHeader.setTitleFromHandle(self._docHandle)
274
+ self.docHeader.clearHeader()
262
275
  self.docFooter.setHandle(self._docHandle)
263
276
  self.docToolBar.setVisible(False)
264
277
 
@@ -334,7 +347,7 @@ class GuiDocEditor(QPlainTextEdit):
334
347
  options = QTextOption()
335
348
 
336
349
  if CONFIG.doJustify:
337
- options.setAlignment(Qt.AlignmentFlag.AlignJustify)
350
+ options.setAlignment(QtAlignJustify)
338
351
  if CONFIG.showTabsNSpaces:
339
352
  options.setFlags(options.flags() | QTextOption.Flag.ShowTabsAndSpaces)
340
353
  if CONFIG.showLineEndings:
@@ -362,13 +375,13 @@ class GuiDocEditor(QPlainTextEdit):
362
375
  # which makes it read only.
363
376
  if self._docHandle:
364
377
  self._qDocument.syntaxHighlighter.rehighlight()
365
- self.docHeader.setTitleFromHandle(self._docHandle)
378
+ self.docHeader.setHandle(self._docHandle)
366
379
  else:
367
380
  self.clearEditor()
368
381
 
369
382
  return
370
383
 
371
- def loadText(self, tHandle: str, tLine=None) -> bool:
384
+ def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
372
385
  """Load text from a document into the editor. If we have an I/O
373
386
  error, we must handle this and clear the editor so that we don't
374
387
  risk overwriting the file if it exists. This can for instance
@@ -386,22 +399,20 @@ class GuiDocEditor(QPlainTextEdit):
386
399
  self.clearEditor()
387
400
  return False
388
401
 
389
- qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
402
+ QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
390
403
  self._docHandle = tHandle
391
404
 
392
405
  self._allowAutoReplace(False)
393
406
  self._qDocument.setTextContent(docText, tHandle)
394
407
  self._allowAutoReplace(True)
395
- qApp.processEvents()
408
+ QApplication.processEvents()
396
409
 
397
410
  self._lastEdit = time()
398
411
  self._lastActive = time()
399
- self._runDocCounter()
400
- self.wcTimerDoc.start()
412
+ self._runDocumentTasks()
413
+ self.timerDoc.start()
401
414
 
402
415
  self.setReadOnly(False)
403
- self.docHeader.setTitleFromHandle(self._docHandle)
404
- self.docFooter.setHandle(self._docHandle)
405
416
  self.updateDocMargins()
406
417
 
407
418
  if tLine is None and self._nwItem is not None:
@@ -409,7 +420,8 @@ class GuiDocEditor(QPlainTextEdit):
409
420
  elif isinstance(tLine, int):
410
421
  self.setCursorLine(tLine)
411
422
 
412
- self.docFooter.updateLineCount()
423
+ self.docHeader.setHandle(tHandle)
424
+ self.docFooter.setHandle(tHandle)
413
425
 
414
426
  # This is a hack to fix invisible cursor on an empty document
415
427
  if self._qDocument.characterCount() <= 1:
@@ -417,12 +429,12 @@ class GuiDocEditor(QPlainTextEdit):
417
429
  self.setPlainText("")
418
430
  self.setCursorPosition(0)
419
431
 
420
- qApp.processEvents()
432
+ QApplication.processEvents()
421
433
  self.setDocumentChanged(False)
422
434
  self._qDocument.clearUndoRedoStacks()
423
435
  self.docToolBar.setVisible(CONFIG.showEditToolBar)
424
436
 
425
- qApp.restoreOverrideCursor()
437
+ QApplication.restoreOverrideCursor()
426
438
 
427
439
  # Update the status bar
428
440
  if self._nwItem is not None:
@@ -432,18 +444,18 @@ class GuiDocEditor(QPlainTextEdit):
432
444
 
433
445
  def updateTagHighLighting(self) -> None:
434
446
  """Rerun the syntax highlighter on all meta data lines."""
435
- self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
447
+ self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META)
436
448
  return
437
449
 
438
450
  def replaceText(self, text: str) -> None:
439
451
  """Replace the text of the current document with the provided
440
452
  text. This also clears undo history.
441
453
  """
442
- qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
454
+ QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
443
455
  self.setPlainText(text)
444
456
  self.updateDocMargins()
445
457
  self.setDocumentChanged(True)
446
- qApp.restoreOverrideCursor()
458
+ QApplication.restoreOverrideCursor()
447
459
  return
448
460
 
449
461
  def saveText(self) -> bool:
@@ -462,7 +474,7 @@ class GuiDocEditor(QPlainTextEdit):
462
474
  return False
463
475
 
464
476
  docText = self.getText()
465
- cC, wC, pC = countWords(docText)
477
+ cC, wC, pC = standardCounter(docText)
466
478
  self._updateDocCounts(cC, wC, pC)
467
479
 
468
480
  self.saveCursorPosition()
@@ -485,6 +497,7 @@ class GuiDocEditor(QPlainTextEdit):
485
497
  return False
486
498
 
487
499
  self.setDocumentChanged(False)
500
+ self.docTextChanged.emit(self._docHandle, self._lastEdit)
488
501
 
489
502
  oldHeader = self._nwItem.mainHeading
490
503
  oldCount = SHARED.project.index.getHandleHeaderCount(tHandle)
@@ -530,7 +543,7 @@ class GuiDocEditor(QPlainTextEdit):
530
543
  while self.cursorRect().bottom() > vH and count < 100000:
531
544
  vBar.setValue(vBar.value() + 1)
532
545
  count += 1
533
- qApp.processEvents()
546
+ QApplication.processEvents()
534
547
  return
535
548
 
536
549
  def updateDocMargins(self) -> None:
@@ -548,8 +561,8 @@ class GuiDocEditor(QPlainTextEdit):
548
561
  sH = hBar.height() if hBar.isVisible() else 0
549
562
 
550
563
  tM = self._vpMargin
551
- if CONFIG.textWidth > 0 or self.mainGui.isFocusMode:
552
- tW = CONFIG.getTextWidth(self.mainGui.isFocusMode)
564
+ if CONFIG.textWidth > 0 or SHARED.focusMode:
565
+ tW = CONFIG.getTextWidth(SHARED.focusMode)
553
566
  tM = max((wW - sW - tW)//2, self._vpMargin)
554
567
 
555
568
  tB = self.frameWidth()
@@ -590,6 +603,15 @@ class GuiDocEditor(QPlainTextEdit):
590
603
  text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
591
604
  return text
592
605
 
606
+ def getSelectedText(self) -> str:
607
+ """Get currently selected text."""
608
+ if (cursor := self.textCursor()).hasSelection():
609
+ text = cursor.selectedText()
610
+ text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
611
+ text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
612
+ return text
613
+ return ""
614
+
593
615
  def getCursorPosition(self) -> int:
594
616
  """Find the cursor position in the document. If the editor has a
595
617
  selection, return the position of the end of the selection.
@@ -600,13 +622,15 @@ class GuiDocEditor(QPlainTextEdit):
600
622
  # Setters
601
623
  ##
602
624
 
603
- def setDocumentChanged(self, state: bool) -> bool:
625
+ def setDocumentChanged(self, state: bool) -> None:
604
626
  """Keep track of the document changed variable, and emit the
605
627
  document change signal.
606
628
  """
607
- self._docChanged = state
608
- self.editedStatusChanged.emit(self._docChanged)
609
- return self._docChanged
629
+ if self._docChanged != state:
630
+ logger.debug("Document changed status is '%s'", state)
631
+ self._docChanged = state
632
+ self.editedStatusChanged.emit(self._docChanged)
633
+ return
610
634
 
611
635
  def setCursorPosition(self, position: int) -> None:
612
636
  """Move the cursor to a given position in the document."""
@@ -616,7 +640,6 @@ class GuiDocEditor(QPlainTextEdit):
616
640
  cursor.setPosition(minmax(position, 0, nChars-1))
617
641
  self.setTextCursor(cursor)
618
642
  self.centerCursor()
619
- self.docFooter.updateLineCount()
620
643
  return
621
644
 
622
645
  def saveCursorPosition(self) -> None:
@@ -635,6 +658,15 @@ class GuiDocEditor(QPlainTextEdit):
635
658
  logger.debug("Cursor moved to line %d", line)
636
659
  return
637
660
 
661
+ def setCursorSelection(self, start: int, length: int) -> None:
662
+ """Make a text selection."""
663
+ if start >= 0 and length > 0:
664
+ cursor = self.textCursor()
665
+ cursor.setPosition(start, QtMoveAnchor)
666
+ cursor.setPosition(start + length, QtKeepAnchor)
667
+ self.setTextCursor(cursor)
668
+ return
669
+
638
670
  ##
639
671
  # Spell Checking
640
672
  ##
@@ -674,9 +706,9 @@ class GuiDocEditor(QPlainTextEdit):
674
706
  """
675
707
  logger.debug("Running spell checker")
676
708
  start = time()
677
- qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
709
+ QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
678
710
  self._qDocument.syntaxHighlighter.rehighlight()
679
- qApp.restoreOverrideCursor()
711
+ QApplication.restoreOverrideCursor()
680
712
  logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
681
713
  self.statusMessage.emit(self.tr("Spell check complete"))
682
714
  return
@@ -745,6 +777,8 @@ class GuiDocEditor(QPlainTextEdit):
745
777
  self._formatBlock(nwDocAction.BLOCK_TTL)
746
778
  elif action == nwDocAction.BLOCK_UNN:
747
779
  self._formatBlock(nwDocAction.BLOCK_UNN)
780
+ elif action == nwDocAction.BLOCK_HSC:
781
+ self._formatBlock(nwDocAction.BLOCK_HSC)
748
782
  elif action == nwDocAction.REPL_SNG:
749
783
  self._replaceQuotes("'", self._typSQuoteO, self._typSQuoteC)
750
784
  elif action == nwDocAction.REPL_DBL:
@@ -769,6 +803,8 @@ class GuiDocEditor(QPlainTextEdit):
769
803
  self._wrapSelection(nwShortcode.STRIKE_O, nwShortcode.STRIKE_C)
770
804
  elif action == nwDocAction.SC_ULINE:
771
805
  self._wrapSelection(nwShortcode.ULINE_O, nwShortcode.ULINE_C)
806
+ elif action == nwDocAction.SC_MARK:
807
+ self._wrapSelection(nwShortcode.MARK_O, nwShortcode.MARK_C)
772
808
  elif action == nwDocAction.SC_SUP:
773
809
  self._wrapSelection(nwShortcode.SUP_O, nwShortcode.SUP_C)
774
810
  elif action == nwDocAction.SC_SUB:
@@ -785,11 +821,7 @@ class GuiDocEditor(QPlainTextEdit):
785
821
 
786
822
  def anyFocus(self) -> bool:
787
823
  """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
824
+ return self.hasFocus() or self.isAncestorOf(QApplication.focusWidget())
793
825
 
794
826
  def revealLocation(self) -> None:
795
827
  """Tell the user where on the file system the file in the editor
@@ -898,11 +930,6 @@ class GuiDocEditor(QPlainTextEdit):
898
930
 
899
931
  return True
900
932
 
901
- def closeSearch(self) -> bool:
902
- """Close the search box."""
903
- self.docSearch.closeSearch()
904
- return self.docSearch.isVisible()
905
-
906
933
  ##
907
934
  # Document Events and Maintenance
908
935
  ##
@@ -937,7 +964,7 @@ class GuiDocEditor(QPlainTextEdit):
937
964
  super().keyPressEvent(event)
938
965
  nPos = self.cursorRect().topLeft().y()
939
966
  kMod = event.modifiers()
940
- okMod = kMod in (Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.ShiftModifier)
967
+ okMod = kMod in (QtModeNone, QtModShift)
941
968
  okKey = event.key() not in self.MOVE_KEYS
942
969
  if nPos != cPos and okMod and okKey:
943
970
  mPos = CONFIG.autoScrollPos*0.01 * self.viewport().height()
@@ -947,8 +974,6 @@ class GuiDocEditor(QPlainTextEdit):
947
974
  else:
948
975
  super().keyPressEvent(event)
949
976
 
950
- self.docFooter.updateLineCount()
951
-
952
977
  return
953
978
 
954
979
  def focusNextPrevChild(self, next: bool) -> bool:
@@ -960,7 +985,7 @@ class GuiDocEditor(QPlainTextEdit):
960
985
  if self.hasFocus():
961
986
  return False
962
987
  elif self.docSearch.isVisible():
963
- return self.docSearch.cycleFocus(next)
988
+ return self.docSearch.cycleFocus()
964
989
  return True
965
990
 
966
991
  def mouseReleaseEvent(self, event: QMouseEvent) -> None:
@@ -968,10 +993,9 @@ class GuiDocEditor(QPlainTextEdit):
968
993
  pressed, check if we're clicking on a tag, and trigger the
969
994
  follow tag function.
970
995
  """
971
- if qApp.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier:
996
+ if QApplication.keyboardModifiers() == QtModCtrl:
972
997
  self._processTag(self.cursorForPosition(event.pos()))
973
998
  super().mouseReleaseEvent(event)
974
- self.docFooter.updateLineCount()
975
999
  return
976
1000
 
977
1001
  def resizeEvent(self, event: QResizeEvent) -> None:
@@ -991,8 +1015,8 @@ class GuiDocEditor(QPlainTextEdit):
991
1015
  """Called when an item label is changed to check if the document
992
1016
  title bar needs updating,
993
1017
  """
994
- if tHandle == self._docHandle:
995
- self.docHeader.setTitleFromHandle(self._docHandle)
1018
+ if tHandle and tHandle == self._docHandle:
1019
+ self.docHeader.setHandle(tHandle)
996
1020
  self.docFooter.updateInfo()
997
1021
  self.updateDocMargins()
998
1022
  return
@@ -1012,8 +1036,8 @@ class GuiDocEditor(QPlainTextEdit):
1012
1036
  @pyqtSlot()
1013
1037
  def toggleSearch(self) -> None:
1014
1038
  """Toggle the visibility of the search box."""
1015
- if self.docSearch.isVisible():
1016
- self.docSearch.closeSearch()
1039
+ if self.searchVisible():
1040
+ self.closeSearch()
1017
1041
  else:
1018
1042
  self.beginSearch()
1019
1043
  return
@@ -1033,32 +1057,35 @@ class GuiDocEditor(QPlainTextEdit):
1033
1057
  if not self._docChanged:
1034
1058
  self.setDocumentChanged(removed != 0 or added != 0)
1035
1059
 
1036
- if not self.wcTimerDoc.isActive():
1037
- self.wcTimerDoc.start()
1038
-
1039
- block = self._qDocument.findBlock(pos)
1040
- if not block.isValid():
1041
- return
1060
+ if not self.timerDoc.isActive():
1061
+ self.timerDoc.start()
1042
1062
 
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)
1063
+ if (block := self._qDocument.findBlock(pos)).isValid():
1064
+ text = block.text()
1065
+ if text.startswith("@") and added + removed == 1:
1066
+ # Only run on single keypresses, otherwise it will trigger
1067
+ # at unwanted times when other changes are made to the document
1068
+ cursor = self.textCursor()
1069
+ bPos = cursor.positionInBlock()
1070
+ if bPos > 0:
1071
+ show = self._completer.updateText(text, bPos)
1072
+ point = self.cursorRect().bottomRight()
1073
+ self._completer.move(self.viewport().mapToGlobal(point))
1074
+ self._completer.setVisible(show)
1075
+ else:
1076
+ self._completer.setVisible(False)
1056
1077
 
1057
1078
  if self._doReplace and added == 1:
1058
1079
  self._docAutoReplace(text)
1059
1080
 
1060
1081
  return
1061
1082
 
1083
+ @pyqtSlot()
1084
+ def _cursorMoved(self) -> None:
1085
+ """Triggered when the cursor moved in the editor."""
1086
+ self.docFooter.updateLineCount(self.textCursor())
1087
+ return
1088
+
1062
1089
  @pyqtSlot(int, int, str)
1063
1090
  def _insertCompletion(self, pos: int, length: int, text: str) -> None:
1064
1091
  """Insert choice from the completer menu."""
@@ -1066,8 +1093,8 @@ class GuiDocEditor(QPlainTextEdit):
1066
1093
  block = cursor.block()
1067
1094
  if block.isValid():
1068
1095
  pos += block.position()
1069
- cursor.setPosition(pos, QTextCursor.MoveMode.MoveAnchor)
1070
- cursor.setPosition(pos + length, QTextCursor.MoveMode.KeepAnchor)
1096
+ cursor.setPosition(pos, QtMoveAnchor)
1097
+ cursor.setPosition(pos + length, QtKeepAnchor)
1071
1098
  cursor.insertText(text)
1072
1099
  self._completer.hide()
1073
1100
  return
@@ -1081,7 +1108,7 @@ class GuiDocEditor(QPlainTextEdit):
1081
1108
 
1082
1109
  ctxMenu = QMenu(self)
1083
1110
  ctxMenu.setObjectName("ContextMenu")
1084
- if pBlock.userState() == GuiDocHighlighter.BLOCK_TITLE:
1111
+ if pBlock.userState() == BLOCK_TITLE:
1085
1112
  action = ctxMenu.addAction(self.tr("Set as Document Name"))
1086
1113
  action.triggered.connect(lambda: self._emitRenameItem(pBlock))
1087
1114
 
@@ -1127,9 +1154,7 @@ class GuiDocEditor(QPlainTextEdit):
1127
1154
  block = pCursor.block()
1128
1155
  sCursor = self.textCursor()
1129
1156
  sCursor.setPosition(block.position() + cPos)
1130
- sCursor.movePosition(
1131
- QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, cLen
1132
- )
1157
+ sCursor.movePosition(QtMoveRight, QtKeepAnchor, cLen)
1133
1158
  if suggest:
1134
1159
  ctxMenu.addSeparator()
1135
1160
  ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
@@ -1146,7 +1171,7 @@ class GuiDocEditor(QPlainTextEdit):
1146
1171
  action.triggered.connect(lambda: self._addWord(word, block))
1147
1172
 
1148
1173
  # Execute the context menu
1149
- ctxMenu.exec_(self.viewport().mapToGlobal(pos))
1174
+ ctxMenu.exec(self.viewport().mapToGlobal(pos))
1150
1175
  ctxMenu.deleteLater()
1151
1176
 
1152
1177
  return
@@ -1176,39 +1201,36 @@ class GuiDocEditor(QPlainTextEdit):
1176
1201
  return
1177
1202
 
1178
1203
  @pyqtSlot()
1179
- def _runDocCounter(self) -> None:
1180
- """Decide whether to run the word counter, or not due to
1181
- inactivity.
1182
- """
1204
+ def _runDocumentTasks(self) -> None:
1205
+ """Run timer document tasks."""
1183
1206
  if self._docHandle is None:
1184
1207
  return
1185
1208
 
1186
- if self.wCounterDoc.isRunning():
1187
- logger.debug("Word counter is busy")
1188
- return
1189
-
1190
1209
  if time() - self._lastEdit < 25.0:
1191
- logger.debug("Running word counter")
1192
- SHARED.runInThreadPool(self.wCounterDoc)
1210
+ logger.debug("Running document tasks")
1211
+ if not self.wCounterDoc.isRunning():
1212
+ SHARED.runInThreadPool(self.wCounterDoc)
1213
+
1214
+ self.docHeader.setOutline({
1215
+ block.blockNumber(): block.text()
1216
+ for block in self._qDocument.iterBlockByType(BLOCK_TITLE, maxCount=30)
1217
+ })
1218
+
1219
+ if self._docChanged:
1220
+ self.docTextChanged.emit(self._docHandle, self._lastEdit)
1193
1221
 
1194
1222
  return
1195
1223
 
1196
1224
  @pyqtSlot(int, int, int)
1197
1225
  def _updateDocCounts(self, cCount: int, wCount: int, pCount: int) -> None:
1198
1226
  """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
-
1227
+ if self._docHandle and self._nwItem:
1228
+ logger.debug("Updating word count")
1229
+ self._nwItem.setCharCount(cCount)
1230
+ self._nwItem.setWordCount(wCount)
1231
+ self._nwItem.setParaCount(pCount)
1232
+ self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
1233
+ self.docFooter.updateWordCount(wCount, False)
1212
1234
  return
1213
1235
 
1214
1236
  @pyqtSlot()
@@ -1217,13 +1239,11 @@ class GuiDocEditor(QPlainTextEdit):
1217
1239
  information to the footer, and start the selection word counter.
1218
1240
  """
1219
1241
  if self.textCursor().hasSelection():
1220
- if not self.wcTimerSel.isActive():
1221
- self.wcTimerSel.start()
1222
- self.docFooter.setHasSelection(True)
1242
+ if not self.timerSel.isActive():
1243
+ self.timerSel.start()
1223
1244
  else:
1224
- self.wcTimerSel.stop()
1225
- self.docFooter.setHasSelection(False)
1226
- self.docFooter.updateCounts()
1245
+ self.timerSel.stop()
1246
+ self.docFooter.updateWordCount(0, False)
1227
1247
  return
1228
1248
 
1229
1249
  @pyqtSlot()
@@ -1243,13 +1263,10 @@ class GuiDocEditor(QPlainTextEdit):
1243
1263
  @pyqtSlot(int, int, int)
1244
1264
  def _updateSelCounts(self, cCount: int, wCount: int, pCount: int) -> None:
1245
1265
  """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
-
1266
+ if self._docHandle and self._nwItem:
1267
+ logger.debug("User selected %d words", wCount)
1268
+ self.docFooter.updateWordCount(wCount, True)
1269
+ self.timerSel.stop()
1253
1270
  return
1254
1271
 
1255
1272
  @pyqtSlot()
@@ -1273,11 +1290,7 @@ class GuiDocEditor(QPlainTextEdit):
1273
1290
 
1274
1291
  def beginSearch(self) -> None:
1275
1292
  """Set the selected text as the search text."""
1276
- cursor = self.textCursor()
1277
- if cursor.hasSelection():
1278
- self.docSearch.setSearchText(cursor.selectedText())
1279
- else:
1280
- self.docSearch.setSearchText(None)
1293
+ self.docSearch.setSearchText(self.getSelectedText() or None)
1281
1294
  resS, _ = self.findAllOccurences()
1282
1295
  self.docSearch.setResultCount(None, len(resS))
1283
1296
  return
@@ -1306,17 +1319,18 @@ class GuiDocEditor(QPlainTextEdit):
1306
1319
  if len(resS) == 0 and self._docHandle:
1307
1320
  self.docSearch.setResultCount(0, 0)
1308
1321
  self._lastFind = None
1309
- if self.docSearch.doNextFile and not goBack:
1322
+ if CONFIG.searchNextFile and not goBack:
1310
1323
  self.mainGui.openNextDocument(
1311
- self._docHandle, wrapAround=self.docSearch.doLoop
1324
+ self._docHandle, wrapAround=CONFIG.searchLoop
1312
1325
  )
1313
1326
  self.beginSearch()
1327
+ self.setFocus()
1314
1328
  return
1315
1329
 
1316
1330
  cursor = self.textCursor()
1317
1331
  resIdx = bisect.bisect_left(resS, cursor.position())
1318
1332
 
1319
- doLoop = self.docSearch.doLoop
1333
+ doLoop = CONFIG.searchLoop
1320
1334
  maxIdx = len(resS) - 1
1321
1335
 
1322
1336
  if goBack:
@@ -1326,20 +1340,20 @@ class GuiDocEditor(QPlainTextEdit):
1326
1340
  resIdx = maxIdx if doLoop else 0
1327
1341
 
1328
1342
  if resIdx > maxIdx and self._docHandle:
1329
- if self.docSearch.doNextFile and not goBack:
1343
+ if CONFIG.searchNextFile and not goBack:
1330
1344
  self.mainGui.openNextDocument(
1331
- self._docHandle, wrapAround=self.docSearch.doLoop
1345
+ self._docHandle, wrapAround=CONFIG.searchLoop
1332
1346
  )
1333
1347
  self.beginSearch()
1348
+ self.setFocus()
1334
1349
  return
1335
1350
  else:
1336
1351
  resIdx = 0 if doLoop else maxIdx
1337
1352
 
1338
- cursor.setPosition(resS[resIdx], QTextCursor.MoveMode.MoveAnchor)
1339
- cursor.setPosition(resE[resIdx], QTextCursor.MoveMode.KeepAnchor)
1353
+ cursor.setPosition(resS[resIdx], QtMoveAnchor)
1354
+ cursor.setPosition(resE[resIdx], QtKeepAnchor)
1340
1355
  self.setTextCursor(cursor)
1341
1356
 
1342
- self.docFooter.updateLineCount()
1343
1357
  self.docSearch.setResultCount(resIdx + 1, len(resS))
1344
1358
  self._lastFind = (resS[resIdx], resE[resIdx])
1345
1359
 
@@ -1361,18 +1375,19 @@ class GuiDocEditor(QPlainTextEdit):
1361
1375
  origB = cursor.position()
1362
1376
 
1363
1377
  findOpt = QTextDocument.FindFlag(0)
1364
- if self.docSearch.isCaseSense:
1378
+ if CONFIG.searchCase:
1365
1379
  findOpt |= QTextDocument.FindFlag.FindCaseSensitively
1366
- if self.docSearch.isWholeWord:
1380
+ if CONFIG.searchWord:
1367
1381
  findOpt |= QTextDocument.FindFlag.FindWholeWords
1368
1382
 
1369
1383
  searchFor = self.docSearch.getSearchObject()
1370
1384
  cursor.setPosition(0)
1371
1385
  self.setTextCursor(cursor)
1372
1386
 
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:
1387
+ # Search up to a maximum of MAX_SEARCH_RESULT, and make sure
1388
+ # certain special searches like a regex search for .* don't loop
1389
+ # infinitely
1390
+ while self.find(searchFor, findOpt) and len(resE) <= nwConst.MAX_SEARCH_RESULT:
1376
1391
  cursor = self.textCursor()
1377
1392
  if cursor.hasSelection():
1378
1393
  resS.append(cursor.selectionStart())
@@ -1382,8 +1397,8 @@ class GuiDocEditor(QPlainTextEdit):
1382
1397
  break
1383
1398
 
1384
1399
  if hasSelection:
1385
- cursor.setPosition(origA, QTextCursor.MoveMode.MoveAnchor)
1386
- cursor.setPosition(origB, QTextCursor.MoveMode.KeepAnchor)
1400
+ cursor.setPosition(origA, QtMoveAnchor)
1401
+ cursor.setPosition(origB, QtKeepAnchor)
1387
1402
  else:
1388
1403
  cursor.setPosition(origA)
1389
1404
 
@@ -1431,18 +1446,12 @@ class GuiDocEditor(QPlainTextEdit):
1431
1446
  searchFor = self.docSearch.searchText
1432
1447
  replWith = self.docSearch.replaceText
1433
1448
 
1434
- if self.docSearch.doMatchCap:
1449
+ if CONFIG.searchMatchCap:
1435
1450
  replWith = transferCase(cursor.selectedText(), replWith)
1436
1451
 
1437
1452
  # Make sure the selected text was selected by an actual find
1438
1453
  # 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:
1454
+ if self._lastFind == (cursor.selectionStart(), cursor.selectionEnd()):
1446
1455
  cursor.beginEditBlock()
1447
1456
  cursor.removeSelectedText()
1448
1457
  cursor.insertText(replWith)
@@ -1451,10 +1460,8 @@ class GuiDocEditor(QPlainTextEdit):
1451
1460
  self.setTextCursor(cursor)
1452
1461
  logger.debug(
1453
1462
  "Replaced occurrence of '%s' with '%s' on line %d",
1454
- searchFor, replWith, cursor.blockNumber()
1463
+ searchFor, replWith, cursor.blockNumber() + 1
1455
1464
  )
1456
- else:
1457
- logger.error("The selected text is not a search result, skipping replace")
1458
1465
 
1459
1466
  self.findNext()
1460
1467
 
@@ -1464,7 +1471,7 @@ class GuiDocEditor(QPlainTextEdit):
1464
1471
  # Internal Functions : Text Manipulation
1465
1472
  ##
1466
1473
 
1467
- def _toggleFormat(self, fLen: int, fChar: str) -> bool:
1474
+ def _toggleFormat(self, fLen: int, fChar: str) -> None:
1468
1475
  """Toggle the formatting of a specific type for a piece of text.
1469
1476
  If more than one block is selected, the formatting is applied to
1470
1477
  the first block.
@@ -1482,20 +1489,20 @@ class GuiDocEditor(QPlainTextEdit):
1482
1489
 
1483
1490
  posS = cursor.selectionStart()
1484
1491
  posE = cursor.selectionEnd()
1485
- if self._qDocument.characterAt(posO - 1) == fChar:
1492
+ if posS == posE and self._qDocument.characterAt(posO - 1) == fChar:
1486
1493
  logger.warning("Format repetition, cancelling action")
1487
1494
  cursor.clearSelection()
1488
1495
  cursor.setPosition(posO)
1489
1496
  self.setTextCursor(cursor)
1490
- return False
1497
+ return
1491
1498
 
1492
1499
  blockS = self._qDocument.findBlock(posS)
1493
1500
  blockE = self._qDocument.findBlock(posE)
1494
1501
  if blockS != blockE:
1495
1502
  posE = blockS.position() + blockS.length() - 1
1496
1503
  cursor.clearSelection()
1497
- cursor.setPosition(posS, QTextCursor.MoveMode.MoveAnchor)
1498
- cursor.setPosition(posE, QTextCursor.MoveMode.KeepAnchor)
1504
+ cursor.setPosition(posS, QtMoveAnchor)
1505
+ cursor.setPosition(posE, QtKeepAnchor)
1499
1506
  self.setTextCursor(cursor)
1500
1507
 
1501
1508
  numB = 0
@@ -1513,7 +1520,6 @@ class GuiDocEditor(QPlainTextEdit):
1513
1520
  break
1514
1521
 
1515
1522
  if fLen == min(numA, numB):
1516
- cursor.clearSelection()
1517
1523
  cursor.beginEditBlock()
1518
1524
  cursor.setPosition(posS)
1519
1525
  for i in range(fLen):
@@ -1522,17 +1528,19 @@ class GuiDocEditor(QPlainTextEdit):
1522
1528
  for i in range(fLen):
1523
1529
  cursor.deletePreviousChar()
1524
1530
  cursor.endEditBlock()
1525
- cursor.clearSelection()
1526
- cursor.setPosition(posO - fLen)
1527
- self.setTextCursor(cursor)
1531
+
1532
+ if select != _SelectAction.KEEP_SELECTION:
1533
+ cursor.clearSelection()
1534
+ cursor.setPosition(posO - fLen)
1535
+ self.setTextCursor(cursor)
1528
1536
 
1529
1537
  else:
1530
1538
  self._wrapSelection(fChar*fLen, pos=posO, select=select)
1531
1539
 
1532
- return True
1540
+ return
1533
1541
 
1534
1542
  def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None,
1535
- select: _SelectAction = _SelectAction.NO_DECISION) -> bool:
1543
+ select: _SelectAction = _SelectAction.NO_DECISION) -> None:
1536
1544
  """Wrap the selected text in whatever is in tBefore and tAfter.
1537
1545
  If there is no selection, the autoSelect setting decides the
1538
1546
  action. AutoSelect will select the word under the cursor before
@@ -1572,21 +1580,21 @@ class GuiDocEditor(QPlainTextEdit):
1572
1580
  if select == _SelectAction.MOVE_AFTER:
1573
1581
  cursor.setPosition(posE + len(before + after))
1574
1582
  elif select == _SelectAction.KEEP_SELECTION:
1575
- cursor.setPosition(posE + len(before), QTextCursor.MoveMode.MoveAnchor)
1576
- cursor.setPosition(posS + len(before), QTextCursor.MoveMode.KeepAnchor)
1583
+ cursor.setPosition(posS + len(before), QtMoveAnchor)
1584
+ cursor.setPosition(posE + len(before), QtKeepAnchor)
1577
1585
  elif select == _SelectAction.KEEP_POSITION:
1578
1586
  cursor.setPosition(posO + len(before))
1579
1587
 
1580
1588
  self.setTextCursor(cursor)
1581
1589
 
1582
- return True
1590
+ return
1583
1591
 
1584
- def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> bool:
1592
+ def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> None:
1585
1593
  """Replace all straight quotes in the selected text."""
1586
1594
  cursor = self.textCursor()
1587
1595
  if not cursor.hasSelection():
1588
1596
  SHARED.error(self.tr("Please select some text before calling replace quotes."))
1589
- return False
1597
+ return
1590
1598
 
1591
1599
  posS = cursor.selectionStart()
1592
1600
  posE = cursor.selectionEnd()
@@ -1595,9 +1603,7 @@ class GuiDocEditor(QPlainTextEdit):
1595
1603
  self._allowAutoReplace(False)
1596
1604
  for posC in range(posS, posE+1):
1597
1605
  cursor.setPosition(posC)
1598
- cursor.movePosition(
1599
- QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, 2
1600
- )
1606
+ cursor.movePosition(QtMoveLeft, QtKeepAnchor, 2)
1601
1607
  selText = cursor.selectedText()
1602
1608
 
1603
1609
  nS = len(selText)
@@ -1617,22 +1623,18 @@ class GuiDocEditor(QPlainTextEdit):
1617
1623
  cursor.setPosition(posC)
1618
1624
  if pC in closeCheck:
1619
1625
  cursor.beginEditBlock()
1620
- cursor.movePosition(
1621
- QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, 1
1622
- )
1626
+ cursor.movePosition(QtMoveLeft, QtKeepAnchor, 1)
1623
1627
  cursor.insertText(oQuote)
1624
1628
  cursor.endEditBlock()
1625
1629
  else:
1626
1630
  cursor.beginEditBlock()
1627
- cursor.movePosition(
1628
- QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, 1
1629
- )
1631
+ cursor.movePosition(QtMoveLeft, QtKeepAnchor, 1)
1630
1632
  cursor.insertText(cQuote)
1631
1633
  cursor.endEditBlock()
1632
1634
 
1633
1635
  self._allowAutoReplace(True)
1634
1636
 
1635
- return True
1637
+ return
1636
1638
 
1637
1639
  def _processBlockFormat(
1638
1640
  self, action: nwDocAction, text: str, toggle: bool = True
@@ -1670,6 +1672,9 @@ class GuiDocEditor(QPlainTextEdit):
1670
1672
  elif text.startswith("##! "):
1671
1673
  temp = text[4:]
1672
1674
  offset = 4
1675
+ elif text.startswith("###! "):
1676
+ temp = text[5:]
1677
+ offset = 5
1673
1678
  elif text.startswith(">> "):
1674
1679
  temp = text[3:]
1675
1680
  offset = 3
@@ -1721,6 +1726,9 @@ class GuiDocEditor(QPlainTextEdit):
1721
1726
  elif action == nwDocAction.BLOCK_UNN:
1722
1727
  text = f"##! {temp}"
1723
1728
  offset -= 4
1729
+ elif action == nwDocAction.BLOCK_HSC:
1730
+ text = f"###! {temp}"
1731
+ offset -= 5
1724
1732
  elif action == nwDocAction.ALIGN_L:
1725
1733
  text = f"{temp} <<"
1726
1734
  elif action == nwDocAction.ALIGN_C:
@@ -1836,9 +1844,7 @@ class GuiDocEditor(QPlainTextEdit):
1836
1844
  cursor.beginEditBlock()
1837
1845
  cursor.clearSelection()
1838
1846
  cursor.setPosition(rS)
1839
- cursor.movePosition(
1840
- QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, rE-rS
1841
- )
1847
+ cursor.movePosition(QtMoveRight, QtKeepAnchor, rE-rS)
1842
1848
  cursor.insertText(cleanText.rstrip() + "\n")
1843
1849
  cursor.endEditBlock()
1844
1850
 
@@ -1867,7 +1873,10 @@ class GuiDocEditor(QPlainTextEdit):
1867
1873
  if text.startswith("@") and self._docHandle:
1868
1874
 
1869
1875
  isGood, tBits, tPos = SHARED.project.index.scanThis(text)
1870
- if not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY:
1876
+ if (
1877
+ not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY
1878
+ or tBits[0] not in nwKeyWords.VALID_KEYS
1879
+ ):
1871
1880
  return nwTrinary.NEUTRAL
1872
1881
 
1873
1882
  tag = ""
@@ -1895,13 +1904,9 @@ class GuiDocEditor(QPlainTextEdit):
1895
1904
  "Do you want to create a new project note for the tag '{0}'?"
1896
1905
  ).format(tag)):
1897
1906
  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])))
1907
+ self.requestNewNoteCreation.emit(tag, itemClass)
1908
+ QApplication.processEvents()
1909
+ self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1905
1910
 
1906
1911
  return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
1907
1912
 
@@ -2005,9 +2010,7 @@ class GuiDocEditor(QPlainTextEdit):
2005
2010
  tInsert = tInsert + self._typPadChar
2006
2011
 
2007
2012
  if nDelete > 0:
2008
- cursor.movePosition(
2009
- QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, nDelete
2010
- )
2013
+ cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete)
2011
2014
  cursor.insertText(tInsert)
2012
2015
 
2013
2016
  return
@@ -2063,8 +2066,8 @@ class GuiDocEditor(QPlainTextEdit):
2063
2066
  return cursor
2064
2067
 
2065
2068
  cursor.clearSelection()
2066
- cursor.setPosition(sPos, QTextCursor.MoveMode.MoveAnchor)
2067
- cursor.setPosition(ePos, QTextCursor.MoveMode.KeepAnchor)
2069
+ cursor.setPosition(sPos, QtMoveAnchor)
2070
+ cursor.setPosition(ePos, QtKeepAnchor)
2068
2071
 
2069
2072
  self.setTextCursor(cursor)
2070
2073
 
@@ -2088,8 +2091,8 @@ class GuiDocEditor(QPlainTextEdit):
2088
2091
  posE = cursor.selectionEnd()
2089
2092
  selTxt = cursor.selectedText()
2090
2093
  if selTxt.startswith(nwUnicode.U_PSEP):
2091
- cursor.setPosition(posS+1, QTextCursor.MoveMode.MoveAnchor)
2092
- cursor.setPosition(posE, QTextCursor.MoveMode.KeepAnchor)
2094
+ cursor.setPosition(posS+1, QtMoveAnchor)
2095
+ cursor.setPosition(posE, QtKeepAnchor)
2093
2096
 
2094
2097
  self.setTextCursor(cursor)
2095
2098
 
@@ -2184,7 +2187,7 @@ class MetaCompleter(QMenu):
2184
2187
  # Internal Functions
2185
2188
  ##
2186
2189
 
2187
- def _emitComplete(self, pos: int, length: int, value: str):
2190
+ def _emitComplete(self, pos: int, length: int, value: str) -> None:
2188
2191
  """Emit the signal to indicate a selection has been made."""
2189
2192
  self.complete.emit(pos, length, value)
2190
2193
  return
@@ -2220,11 +2223,11 @@ class BackgroundWordCounter(QRunnable):
2220
2223
  """
2221
2224
  self._isRunning = True
2222
2225
  if self._forSelection:
2223
- text = self._docEditor.textCursor().selectedText()
2226
+ text = self._docEditor.getSelectedText()
2224
2227
  else:
2225
2228
  text = self._docEditor.getText()
2226
2229
 
2227
- cC, wC, pC = countWords(text)
2230
+ cC, wC, pC = standardCounter(text)
2228
2231
  self.signals.countsReady.emit(cC, wC, pC)
2229
2232
  self._isRunning = False
2230
2233
 
@@ -2256,72 +2259,68 @@ class GuiDocToolBar(QWidget):
2256
2259
 
2257
2260
  logger.debug("Create: GuiDocToolBar")
2258
2261
 
2262
+ iSz = SHARED.theme.baseIconSize
2259
2263
  cM = CONFIG.pxInt(4)
2260
- tPx = int(0.8*SHARED.theme.fontPixelSize)
2261
- iconSize = QSize(tPx, tPx)
2262
2264
  self.setContentsMargins(0, 0, 0, 0)
2263
2265
 
2264
2266
  # General Buttons
2265
2267
  # ===============
2266
2268
 
2267
- self.tbBoldMD = QToolButton(self)
2268
- self.tbBoldMD.setIconSize(iconSize)
2269
+ self.tbBoldMD = NIconToolButton(self, iSz)
2269
2270
  self.tbBoldMD.setToolTip(self.tr("Markdown Bold"))
2270
2271
  self.tbBoldMD.clicked.connect(
2271
2272
  lambda: self.requestDocAction.emit(nwDocAction.MD_BOLD)
2272
2273
  )
2273
2274
 
2274
- self.tbItalicMD = QToolButton(self)
2275
- self.tbItalicMD.setIconSize(iconSize)
2275
+ self.tbItalicMD = NIconToolButton(self, iSz)
2276
2276
  self.tbItalicMD.setToolTip(self.tr("Markdown Italic"))
2277
2277
  self.tbItalicMD.clicked.connect(
2278
2278
  lambda: self.requestDocAction.emit(nwDocAction.MD_ITALIC)
2279
2279
  )
2280
2280
 
2281
- self.tbStrikeMD = QToolButton(self)
2282
- self.tbStrikeMD.setIconSize(iconSize)
2281
+ self.tbStrikeMD = NIconToolButton(self, iSz)
2283
2282
  self.tbStrikeMD.setToolTip(self.tr("Markdown Strikethrough"))
2284
2283
  self.tbStrikeMD.clicked.connect(
2285
2284
  lambda: self.requestDocAction.emit(nwDocAction.MD_STRIKE)
2286
2285
  )
2287
2286
 
2288
- self.tbBold = QToolButton(self)
2289
- self.tbBold.setIconSize(iconSize)
2287
+ self.tbBold = NIconToolButton(self, iSz)
2290
2288
  self.tbBold.setToolTip(self.tr("Shortcode Bold"))
2291
2289
  self.tbBold.clicked.connect(
2292
2290
  lambda: self.requestDocAction.emit(nwDocAction.SC_BOLD)
2293
2291
  )
2294
2292
 
2295
- self.tbItalic = QToolButton(self)
2296
- self.tbItalic.setIconSize(iconSize)
2293
+ self.tbItalic = NIconToolButton(self, iSz)
2297
2294
  self.tbItalic.setToolTip(self.tr("Shortcode Italic"))
2298
2295
  self.tbItalic.clicked.connect(
2299
2296
  lambda: self.requestDocAction.emit(nwDocAction.SC_ITALIC)
2300
2297
  )
2301
2298
 
2302
- self.tbStrike = QToolButton(self)
2303
- self.tbStrike.setIconSize(iconSize)
2299
+ self.tbStrike = NIconToolButton(self, iSz)
2304
2300
  self.tbStrike.setToolTip(self.tr("Shortcode Strikethrough"))
2305
2301
  self.tbStrike.clicked.connect(
2306
2302
  lambda: self.requestDocAction.emit(nwDocAction.SC_STRIKE)
2307
2303
  )
2308
2304
 
2309
- self.tbUnderline = QToolButton(self)
2310
- self.tbUnderline.setIconSize(iconSize)
2305
+ self.tbUnderline = NIconToolButton(self, iSz)
2311
2306
  self.tbUnderline.setToolTip(self.tr("Shortcode Underline"))
2312
2307
  self.tbUnderline.clicked.connect(
2313
2308
  lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE)
2314
2309
  )
2315
2310
 
2316
- self.tbSuperscript = QToolButton(self)
2317
- self.tbSuperscript.setIconSize(iconSize)
2311
+ self.tbMark = NIconToolButton(self, iSz)
2312
+ self.tbMark.setToolTip(self.tr("Shortcode Highlight"))
2313
+ self.tbMark.clicked.connect(
2314
+ lambda: self.requestDocAction.emit(nwDocAction.SC_MARK)
2315
+ )
2316
+
2317
+ self.tbSuperscript = NIconToolButton(self, iSz)
2318
2318
  self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript"))
2319
2319
  self.tbSuperscript.clicked.connect(
2320
2320
  lambda: self.requestDocAction.emit(nwDocAction.SC_SUP)
2321
2321
  )
2322
2322
 
2323
- self.tbSubscript = QToolButton(self)
2324
- self.tbSubscript.setIconSize(iconSize)
2323
+ self.tbSubscript = NIconToolButton(self, iSz)
2325
2324
  self.tbSubscript.setToolTip(self.tr("Shortcode Subscript"))
2326
2325
  self.tbSubscript.clicked.connect(
2327
2326
  lambda: self.requestDocAction.emit(nwDocAction.SC_SUB)
@@ -2339,6 +2338,7 @@ class GuiDocToolBar(QWidget):
2339
2338
  self.outerBox.addWidget(self.tbItalic)
2340
2339
  self.outerBox.addWidget(self.tbStrike)
2341
2340
  self.outerBox.addWidget(self.tbUnderline)
2341
+ self.outerBox.addWidget(self.tbMark)
2342
2342
  self.outerBox.addWidget(self.tbSuperscript)
2343
2343
  self.outerBox.addWidget(self.tbSubscript)
2344
2344
  self.outerBox.setContentsMargins(cM, cM, cM, cM)
@@ -2362,15 +2362,16 @@ class GuiDocToolBar(QWidget):
2362
2362
  palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
2363
2363
  self.setPalette(palette)
2364
2364
 
2365
- self.tbBoldMD.setIcon(SHARED.theme.getIcon("fmt_bold-md"))
2366
- self.tbItalicMD.setIcon(SHARED.theme.getIcon("fmt_italic-md"))
2367
- self.tbStrikeMD.setIcon(SHARED.theme.getIcon("fmt_strike-md"))
2368
- self.tbBold.setIcon(SHARED.theme.getIcon("fmt_bold"))
2369
- self.tbItalic.setIcon(SHARED.theme.getIcon("fmt_italic"))
2370
- self.tbStrike.setIcon(SHARED.theme.getIcon("fmt_strike"))
2371
- self.tbUnderline.setIcon(SHARED.theme.getIcon("fmt_underline"))
2372
- self.tbSuperscript.setIcon(SHARED.theme.getIcon("fmt_superscript"))
2373
- self.tbSubscript.setIcon(SHARED.theme.getIcon("fmt_subscript"))
2365
+ self.tbBoldMD.setThemeIcon("fmt_bold-md")
2366
+ self.tbItalicMD.setThemeIcon("fmt_italic-md")
2367
+ self.tbStrikeMD.setThemeIcon("fmt_strike-md")
2368
+ self.tbBold.setThemeIcon("fmt_bold")
2369
+ self.tbItalic.setThemeIcon("fmt_italic")
2370
+ self.tbStrike.setThemeIcon("fmt_strike")
2371
+ self.tbUnderline.setThemeIcon("fmt_underline")
2372
+ self.tbMark.setThemeIcon("fmt_mark")
2373
+ self.tbSuperscript.setThemeIcon("fmt_superscript")
2374
+ self.tbSubscript.setThemeIcon("fmt_subscript")
2374
2375
 
2375
2376
  return
2376
2377
 
@@ -2391,16 +2392,9 @@ class GuiDocEditSearch(QFrame):
2391
2392
 
2392
2393
  self.docEditor = docEditor
2393
2394
 
2394
- self.repVisible = False
2395
- self.isCaseSense = CONFIG.searchCase
2396
- self.isWholeWord = CONFIG.searchWord
2397
- self.isRegEx = CONFIG.searchRegEx
2398
- self.doLoop = CONFIG.searchLoop
2399
- self.doNextFile = CONFIG.searchNextFile
2400
- self.doMatchCap = CONFIG.searchMatchCap
2401
-
2395
+ iSz = SHARED.theme.baseIconSize
2402
2396
  mPx = CONFIG.pxInt(6)
2403
- tPx = int(0.8*SHARED.theme.fontPixelSize)
2397
+
2404
2398
  self.boxFont = SHARED.theme.guiFont
2405
2399
  self.boxFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
2406
2400
 
@@ -2416,54 +2410,54 @@ class GuiDocEditSearch(QFrame):
2416
2410
 
2417
2411
  self.searchBox = QLineEdit(self)
2418
2412
  self.searchBox.setFont(self.boxFont)
2419
- self.searchBox.setPlaceholderText(self.tr("Search"))
2413
+ self.searchBox.setPlaceholderText(self.tr("Search for"))
2420
2414
  self.searchBox.returnPressed.connect(self._doSearch)
2421
2415
 
2422
2416
  self.replaceBox = QLineEdit(self)
2423
2417
  self.replaceBox.setFont(self.boxFont)
2424
- self.replaceBox.setPlaceholderText(self.tr("Replace"))
2418
+ self.replaceBox.setPlaceholderText(self.tr("Replace with"))
2425
2419
  self.replaceBox.returnPressed.connect(self._doReplace)
2426
2420
 
2427
2421
  self.searchOpt = QToolBar(self)
2428
2422
  self.searchOpt.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2429
- self.searchOpt.setIconSize(QSize(tPx, tPx))
2423
+ self.searchOpt.setIconSize(iSz)
2430
2424
  self.searchOpt.setContentsMargins(0, 0, 0, 0)
2431
2425
 
2432
- self.searchLabel = QLabel(self.tr("Search"))
2426
+ self.searchLabel = QLabel(self.tr("Search"), self)
2433
2427
  self.searchLabel.setFont(self.boxFont)
2434
2428
  self.searchLabel.setIndent(CONFIG.pxInt(6))
2435
2429
 
2436
- self.resultLabel = QLabel("?/?")
2430
+ self.resultLabel = QLabel("?/?", self)
2437
2431
  self.resultLabel.setFont(self.boxFont)
2438
2432
  self.resultLabel.setMinimumWidth(SHARED.theme.getTextWidth("?/?", self.boxFont))
2439
2433
 
2440
2434
  self.toggleCase = QAction(self.tr("Case Sensitive"), self)
2441
2435
  self.toggleCase.setCheckable(True)
2442
- self.toggleCase.setChecked(self.isCaseSense)
2436
+ self.toggleCase.setChecked(CONFIG.searchCase)
2443
2437
  self.toggleCase.toggled.connect(self._doToggleCase)
2444
2438
  self.searchOpt.addAction(self.toggleCase)
2445
2439
 
2446
2440
  self.toggleWord = QAction(self.tr("Whole Words Only"), self)
2447
2441
  self.toggleWord.setCheckable(True)
2448
- self.toggleWord.setChecked(self.isWholeWord)
2442
+ self.toggleWord.setChecked(CONFIG.searchWord)
2449
2443
  self.toggleWord.toggled.connect(self._doToggleWord)
2450
2444
  self.searchOpt.addAction(self.toggleWord)
2451
2445
 
2452
2446
  self.toggleRegEx = QAction(self.tr("RegEx Mode"), self)
2453
2447
  self.toggleRegEx.setCheckable(True)
2454
- self.toggleRegEx.setChecked(self.isRegEx)
2448
+ self.toggleRegEx.setChecked(CONFIG.searchRegEx)
2455
2449
  self.toggleRegEx.toggled.connect(self._doToggleRegEx)
2456
2450
  self.searchOpt.addAction(self.toggleRegEx)
2457
2451
 
2458
2452
  self.toggleLoop = QAction(self.tr("Loop Search"), self)
2459
2453
  self.toggleLoop.setCheckable(True)
2460
- self.toggleLoop.setChecked(self.doLoop)
2454
+ self.toggleLoop.setChecked(CONFIG.searchLoop)
2461
2455
  self.toggleLoop.toggled.connect(self._doToggleLoop)
2462
2456
  self.searchOpt.addAction(self.toggleLoop)
2463
2457
 
2464
2458
  self.toggleProject = QAction(self.tr("Search Next File"), self)
2465
2459
  self.toggleProject.setCheckable(True)
2466
- self.toggleProject.setChecked(self.doNextFile)
2460
+ self.toggleProject.setChecked(CONFIG.searchNextFile)
2467
2461
  self.toggleProject.toggled.connect(self._doToggleProject)
2468
2462
  self.searchOpt.addAction(self.toggleProject)
2469
2463
 
@@ -2471,38 +2465,32 @@ class GuiDocEditSearch(QFrame):
2471
2465
 
2472
2466
  self.toggleMatchCap = QAction(self.tr("Preserve Case"), self)
2473
2467
  self.toggleMatchCap.setCheckable(True)
2474
- self.toggleMatchCap.setChecked(self.doMatchCap)
2468
+ self.toggleMatchCap.setChecked(CONFIG.searchMatchCap)
2475
2469
  self.toggleMatchCap.toggled.connect(self._doToggleMatchCap)
2476
2470
  self.searchOpt.addAction(self.toggleMatchCap)
2477
2471
 
2478
2472
  self.searchOpt.addSeparator()
2479
2473
 
2480
2474
  self.cancelSearch = QAction(self.tr("Close Search"), self)
2481
- self.cancelSearch.triggered.connect(self._doClose)
2475
+ self.cancelSearch.triggered.connect(self.closeSearch)
2482
2476
  self.searchOpt.addAction(self.cancelSearch)
2483
2477
 
2484
2478
  # Buttons
2485
2479
  # =======
2486
2480
 
2487
- bPx = self.searchBox.sizeHint().height()
2488
-
2489
- self.showReplace = QToolButton(self)
2490
- self.showReplace.setArrowType(Qt.ArrowType.RightArrow)
2491
- self.showReplace.setCheckable(True)
2481
+ self.showReplace = NIconToggleButton(self, iSz, "unfold")
2492
2482
  self.showReplace.toggled.connect(self._doToggleReplace)
2493
2483
 
2494
- self.searchButton = QPushButton("")
2495
- self.searchButton.setFixedSize(QSize(bPx, bPx))
2484
+ self.searchButton = NIconToolButton(self, iSz)
2496
2485
  self.searchButton.setToolTip(self.tr("Find in current document"))
2497
2486
  self.searchButton.clicked.connect(self._doSearch)
2498
2487
 
2499
- self.replaceButton = QPushButton("")
2500
- self.replaceButton.setFixedSize(QSize(bPx, bPx))
2488
+ self.replaceButton = NIconToolButton(self, iSz)
2501
2489
  self.replaceButton.setToolTip(self.tr("Find and replace in current document"))
2502
2490
  self.replaceButton.clicked.connect(self._doReplace)
2503
2491
 
2504
- self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, Qt.AlignmentFlag.AlignLeft)
2505
- self.mainBox.addWidget(self.searchOpt, 0, 2, 1, 3, Qt.AlignmentFlag.AlignRight)
2492
+ self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, QtAlignLeft)
2493
+ self.mainBox.addWidget(self.searchOpt, 0, 2, 1, 3, QtAlignRight)
2506
2494
  self.mainBox.addWidget(self.showReplace, 1, 0, 1, 1)
2507
2495
  self.mainBox.addWidget(self.searchBox, 1, 1, 1, 2)
2508
2496
  self.mainBox.addWidget(self.searchButton, 1, 3, 1, 1)
@@ -2555,20 +2543,20 @@ class GuiDocEditSearch(QFrame):
2555
2543
  expression object.
2556
2544
  """
2557
2545
  text = self.searchBox.text()
2558
- if self.isRegEx:
2546
+ if CONFIG.searchRegEx:
2559
2547
  # Using the Unicode-capable QRegularExpression class was
2560
2548
  # only added in Qt 5.13. Otherwise, 5.3 and up supports
2561
2549
  # only the QRegExp class.
2562
2550
  if CONFIG.verQtValue >= 0x050d00:
2563
2551
  rxOpt = QRegularExpression.PatternOption.UseUnicodePropertiesOption
2564
- if not self.isCaseSense:
2552
+ if not CONFIG.searchCase:
2565
2553
  rxOpt |= QRegularExpression.PatternOption.CaseInsensitiveOption
2566
2554
  regEx = QRegularExpression(text, rxOpt)
2567
2555
  self._alertSearchValid(regEx.isValid())
2568
2556
  return regEx
2569
2557
  else: # pragma: no cover
2570
2558
  # >= 50300 to < 51300
2571
- if self.isCaseSense:
2559
+ if CONFIG.searchCase:
2572
2560
  rxOpt = Qt.CaseSensitivity.CaseSensitive
2573
2561
  else:
2574
2562
  rxOpt = Qt.CaseSensitivity.CaseInsensitive
@@ -2592,7 +2580,7 @@ class GuiDocEditSearch(QFrame):
2592
2580
  self.searchBox.setText(text)
2593
2581
  self.searchBox.setFocus()
2594
2582
  self.searchBox.selectAll()
2595
- if self.isRegEx:
2583
+ if CONFIG.searchRegEx:
2596
2584
  self._alertSearchValid(True)
2597
2585
  return
2598
2586
 
@@ -2605,8 +2593,10 @@ class GuiDocEditSearch(QFrame):
2605
2593
 
2606
2594
  def setResultCount(self, currRes: int | None, resCount: int | None) -> None:
2607
2595
  """Set the count values for the current search."""
2596
+ lim = nwConst.MAX_SEARCH_RESULT
2597
+ numCount = f"{lim:n}+" if (resCount or 0) > lim else f"{resCount:n}"
2608
2598
  sCurrRes = "?" if currRes is None else str(currRes)
2609
- sResCount = "?" if resCount is None else "1000+" if resCount > 1000 else str(resCount)
2599
+ sResCount = "?" if resCount is None else numCount
2610
2600
  minWidth = SHARED.theme.getTextWidth(f"{sResCount}//{sResCount}", self.boxFont)
2611
2601
  self.resultLabel.setText(f"{sCurrRes}/{sResCount}")
2612
2602
  self.resultLabel.setMinimumWidth(minWidth)
@@ -2620,7 +2610,7 @@ class GuiDocEditSearch(QFrame):
2620
2610
 
2621
2611
  def updateTheme(self) -> None:
2622
2612
  """Update theme elements."""
2623
- qPalette = qApp.palette()
2613
+ qPalette = QApplication.palette()
2624
2614
  self.setPalette(qPalette)
2625
2615
  self.searchBox.setPalette(qPalette)
2626
2616
  self.replaceBox.setPalette(qPalette)
@@ -2633,8 +2623,8 @@ class GuiDocEditSearch(QFrame):
2633
2623
  self.toggleProject.setIcon(SHARED.theme.getIcon("search_project"))
2634
2624
  self.toggleMatchCap.setIcon(SHARED.theme.getIcon("search_preserve"))
2635
2625
  self.cancelSearch.setIcon(SHARED.theme.getIcon("search_cancel"))
2636
- self.searchButton.setIcon(SHARED.theme.getIcon("search"))
2637
- self.replaceButton.setIcon(SHARED.theme.getIcon("search_replace"))
2626
+ self.searchButton.setThemeIcon("search")
2627
+ self.replaceButton.setThemeIcon("search_replace")
2638
2628
 
2639
2629
  # Set stylesheets
2640
2630
  self.searchOpt.setStyleSheet("QToolBar {padding: 0;}")
@@ -2660,57 +2650,43 @@ class GuiDocEditSearch(QFrame):
2660
2650
 
2661
2651
  return
2662
2652
 
2663
- def closeSearch(self) -> None:
2664
- """Close the search box."""
2665
- CONFIG.searchCase = self.isCaseSense
2666
- CONFIG.searchWord = self.isWholeWord
2667
- CONFIG.searchRegEx = self.isRegEx
2668
- CONFIG.searchLoop = self.doLoop
2669
- CONFIG.searchNextFile = self.doNextFile
2670
- CONFIG.searchMatchCap = self.doMatchCap
2671
-
2672
- self.showReplace.setChecked(False)
2673
- self.setVisible(False)
2674
- self.docEditor.updateDocMargins()
2675
- self.docEditor.setFocus()
2676
-
2677
- return
2678
-
2679
- def cycleFocus(self, next: bool) -> bool:
2653
+ def cycleFocus(self) -> bool:
2680
2654
  """The tab key just alternates focus between the two input
2681
2655
  boxes, if the replace box is visible.
2682
2656
  """
2683
- if self.replaceBox.isVisible():
2684
- if self.searchBox.hasFocus():
2685
- self.replaceBox.setFocus()
2686
- return True
2687
- elif self.replaceBox.hasFocus():
2688
- self.searchBox.setFocus()
2689
- return True
2657
+ if self.searchBox.hasFocus():
2658
+ self.replaceBox.setFocus()
2659
+ return True
2660
+ elif self.replaceBox.hasFocus():
2661
+ self.searchBox.setFocus()
2662
+ return True
2690
2663
  return False
2691
2664
 
2692
2665
  def anyFocus(self) -> bool:
2693
2666
  """Return True if any of the input boxes have focus."""
2694
- return self.searchBox.hasFocus() | self.replaceBox.hasFocus()
2667
+ return self.searchBox.hasFocus() or self.replaceBox.hasFocus()
2695
2668
 
2696
2669
  ##
2697
- # Private Slots
2670
+ # Public Slots
2698
2671
  ##
2699
2672
 
2700
2673
  @pyqtSlot()
2701
- def _doClose(self) -> None:
2702
- """Hide the search/replace bar."""
2703
- self.closeSearch()
2674
+ def closeSearch(self) -> None:
2675
+ """Close the search box."""
2676
+ self.showReplace.setChecked(False)
2677
+ self.setVisible(False)
2678
+ self.docEditor.updateDocMargins()
2679
+ self.docEditor.setFocus()
2704
2680
  return
2705
2681
 
2682
+ ##
2683
+ # Private Slots
2684
+ ##
2685
+
2706
2686
  @pyqtSlot()
2707
2687
  def _doSearch(self) -> None:
2708
2688
  """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()
2689
+ self.docEditor.findNext(goBack=(QApplication.keyboardModifiers() == QtModShift))
2714
2690
  return
2715
2691
 
2716
2692
  @pyqtSlot()
@@ -2722,13 +2698,8 @@ class GuiDocEditSearch(QFrame):
2722
2698
  @pyqtSlot(bool)
2723
2699
  def _doToggleReplace(self, state: bool) -> None:
2724
2700
  """Toggle the show/hide of the replace box."""
2725
- if state:
2726
- self.showReplace.setArrowType(Qt.ArrowType.DownArrow)
2727
- else:
2728
- self.showReplace.setArrowType(Qt.ArrowType.RightArrow)
2729
2701
  self.replaceBox.setVisible(state)
2730
2702
  self.replaceButton.setVisible(state)
2731
- self.repVisible = state
2732
2703
  self.adjustSize()
2733
2704
  self.docEditor.updateDocMargins()
2734
2705
  return
@@ -2736,37 +2707,37 @@ class GuiDocEditSearch(QFrame):
2736
2707
  @pyqtSlot(bool)
2737
2708
  def _doToggleCase(self, state: bool) -> None:
2738
2709
  """Enable/disable case sensitive mode."""
2739
- self.isCaseSense = state
2710
+ CONFIG.searchCase = state
2740
2711
  return
2741
2712
 
2742
2713
  @pyqtSlot(bool)
2743
2714
  def _doToggleWord(self, state: bool) -> None:
2744
2715
  """Enable/disable whole word search mode."""
2745
- self.isWholeWord = state
2716
+ CONFIG.searchWord = state
2746
2717
  return
2747
2718
 
2748
2719
  @pyqtSlot(bool)
2749
2720
  def _doToggleRegEx(self, state: bool) -> None:
2750
2721
  """Enable/disable regular expression search mode."""
2751
- self.isRegEx = state
2722
+ CONFIG.searchRegEx = state
2752
2723
  return
2753
2724
 
2754
2725
  @pyqtSlot(bool)
2755
2726
  def _doToggleLoop(self, state: bool) -> None:
2756
2727
  """Enable/disable looping the search."""
2757
- self.doLoop = state
2728
+ CONFIG.searchLoop = state
2758
2729
  return
2759
2730
 
2760
2731
  @pyqtSlot(bool)
2761
2732
  def _doToggleProject(self, state: bool) -> None:
2762
2733
  """Enable/disable continuing search in next project file."""
2763
- self.doNextFile = state
2734
+ CONFIG.searchNextFile = state
2764
2735
  return
2765
2736
 
2766
2737
  @pyqtSlot(bool)
2767
2738
  def _doToggleMatchCap(self, state: bool) -> None:
2768
2739
  """Enable/disable preserving capitalisation when replacing."""
2769
- self.doMatchCap = state
2740
+ CONFIG.searchMatchCap = state
2770
2741
  return
2771
2742
 
2772
2743
  ##
@@ -2801,84 +2772,82 @@ class GuiDocEditHeader(QWidget):
2801
2772
  logger.debug("Create: GuiDocEditHeader")
2802
2773
 
2803
2774
  self.docEditor = docEditor
2804
- self.mainGui = docEditor.mainGui
2805
2775
 
2806
2776
  self._docHandle = None
2777
+ self._docOutline: dict[int, str] = {}
2807
2778
 
2808
- fPx = int(0.9*SHARED.theme.fontPixelSize)
2809
- hSp = CONFIG.pxInt(6)
2810
- iconSize = QSize(fPx, fPx)
2779
+ iPx = SHARED.theme.baseIconHeight
2780
+ iSz = SHARED.theme.baseIconSize
2781
+ mPx = CONFIG.pxInt(4)
2811
2782
 
2812
2783
  # Main Widget Settings
2813
2784
  self.setAutoFillBackground(True)
2814
2785
 
2815
2786
  # Title Label
2816
- self.itemTitle = QLabel()
2817
- self.itemTitle.setText("")
2787
+ self.itemTitle = QLabel("", self)
2818
2788
  self.itemTitle.setIndent(0)
2819
2789
  self.itemTitle.setMargin(0)
2820
2790
  self.itemTitle.setContentsMargins(0, 0, 0, 0)
2821
2791
  self.itemTitle.setAutoFillBackground(True)
2822
- self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
2823
- self.itemTitle.setFixedHeight(fPx)
2792
+ self.itemTitle.setAlignment(QtAlignCenterTop)
2793
+ self.itemTitle.setFixedHeight(iPx)
2824
2794
 
2825
2795
  lblFont = self.itemTitle.font()
2826
2796
  lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
2827
2797
  self.itemTitle.setFont(lblFont)
2828
2798
 
2799
+ # Other Widgets
2800
+ self.outlineMenu = QMenu(self)
2801
+
2829
2802
  # 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)
2803
+ self.tbButton = NIconToolButton(self, iSz)
2835
2804
  self.tbButton.setVisible(False)
2836
2805
  self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
2837
2806
  self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
2838
2807
 
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)
2808
+ self.outlineButton = NIconToolButton(self, iSz)
2809
+ self.outlineButton.setVisible(False)
2810
+ self.outlineButton.setToolTip(self.tr("Outline"))
2811
+ self.outlineButton.setMenu(self.outlineMenu)
2812
+
2813
+ self.searchButton = NIconToolButton(self, iSz)
2844
2814
  self.searchButton.setVisible(False)
2845
2815
  self.searchButton.setToolTip(self.tr("Search"))
2846
2816
  self.searchButton.clicked.connect(self.docEditor.toggleSearch)
2847
2817
 
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)
2818
+ self.minmaxButton = NIconToolButton(self, iSz)
2853
2819
  self.minmaxButton.setVisible(False)
2854
2820
  self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
2855
2821
  self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit())
2856
2822
 
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)
2823
+ self.closeButton = NIconToolButton(self, iSz)
2862
2824
  self.closeButton.setVisible(False)
2863
2825
  self.closeButton.setToolTip(self.tr("Close"))
2864
2826
  self.closeButton.clicked.connect(self._closeDocument)
2865
2827
 
2866
2828
  # Assemble Layout
2867
2829
  self.outerBox = QHBoxLayout()
2868
- self.outerBox.setSpacing(hSp)
2869
2830
  self.outerBox.addWidget(self.tbButton, 0)
2831
+ self.outerBox.addWidget(self.outlineButton, 0)
2870
2832
  self.outerBox.addWidget(self.searchButton, 0)
2833
+ self.outerBox.addSpacing(mPx)
2871
2834
  self.outerBox.addWidget(self.itemTitle, 1)
2835
+ self.outerBox.addSpacing(mPx)
2836
+ self.outerBox.addSpacing(iPx)
2872
2837
  self.outerBox.addWidget(self.minmaxButton, 0)
2873
2838
  self.outerBox.addWidget(self.closeButton, 0)
2839
+ self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
2840
+ self.outerBox.setSpacing(0)
2841
+
2874
2842
  self.setLayout(self.outerBox)
2875
2843
 
2844
+ # Other Signals
2845
+ SHARED.focusModeChanged.connect(self._focusModeChanged)
2846
+
2876
2847
  # Fix Margins and Size
2877
2848
  # This is needed for high DPI systems. See issue #499.
2878
- cM = CONFIG.pxInt(8)
2879
2849
  self.setContentsMargins(0, 0, 0, 0)
2880
- self.outerBox.setContentsMargins(cM, cM, cM, cM)
2881
- self.setMinimumHeight(fPx + 2*cM)
2850
+ self.setMinimumHeight(iPx + 2*mPx)
2882
2851
 
2883
2852
  self.updateTheme()
2884
2853
 
@@ -2890,20 +2859,45 @@ class GuiDocEditHeader(QWidget):
2890
2859
  # Methods
2891
2860
  ##
2892
2861
 
2862
+ def clearHeader(self) -> None:
2863
+ """Clear the header."""
2864
+ self._docHandle = None
2865
+ self._docOutline = {}
2866
+
2867
+ self.itemTitle.setText("")
2868
+ self.outlineMenu.clear()
2869
+ self.tbButton.setVisible(False)
2870
+ self.outlineButton.setVisible(False)
2871
+ self.searchButton.setVisible(False)
2872
+ self.closeButton.setVisible(False)
2873
+ self.minmaxButton.setVisible(False)
2874
+ return
2875
+
2876
+ def setOutline(self, data: dict[int, str]) -> None:
2877
+ """Set the document outline dataset."""
2878
+ if data != self._docOutline:
2879
+ tStart = time()
2880
+ self.outlineMenu.clear()
2881
+ for number, text in data.items():
2882
+ action = self.outlineMenu.addAction(text)
2883
+ action.triggered.connect(
2884
+ lambda _, number=number: self._gotoBlock(number)
2885
+ )
2886
+ self._docOutline = data
2887
+ logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
2888
+ return
2889
+
2893
2890
  def updateTheme(self) -> None:
2894
2891
  """Update theme elements."""
2895
- self.tbButton.setIcon(SHARED.theme.getIcon("menu"))
2896
- self.searchButton.setIcon(SHARED.theme.getIcon("search"))
2897
- self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
2898
- self.closeButton.setIcon(SHARED.theme.getIcon("close"))
2899
-
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())
2892
+ self.tbButton.setThemeIcon("menu")
2893
+ self.outlineButton.setThemeIcon("list")
2894
+ self.searchButton.setThemeIcon("search")
2895
+ self.minmaxButton.setThemeIcon("maximise")
2896
+ self.closeButton.setThemeIcon("close")
2905
2897
 
2898
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
2906
2899
  self.tbButton.setStyleSheet(buttonStyle)
2900
+ self.outlineButton.setStyleSheet(buttonStyle)
2907
2901
  self.searchButton.setStyleSheet(buttonStyle)
2908
2902
  self.minmaxButton.setStyleSheet(buttonStyle)
2909
2903
  self.closeButton.setStyleSheet(buttonStyle)
@@ -2926,51 +2920,25 @@ class GuiDocEditHeader(QWidget):
2926
2920
 
2927
2921
  return
2928
2922
 
2929
- def setTitleFromHandle(self, tHandle: str | None) -> bool:
2923
+ def setHandle(self, tHandle: str) -> None:
2930
2924
  """Set the document title from the handle, or alternatively, set
2931
2925
  the whole document path within the project.
2932
2926
  """
2933
2927
  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
2928
 
2942
- pTree = SHARED.project.tree
2943
2929
  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))
2930
+ self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
2931
+ [name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)]
2932
+ )))
2952
2933
  else:
2953
- nwItem = pTree[tHandle]
2954
- if nwItem is None:
2955
- return False
2956
- self.itemTitle.setText(nwItem.itemName)
2934
+ self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
2957
2935
 
2958
2936
  self.tbButton.setVisible(True)
2959
2937
  self.searchButton.setVisible(True)
2938
+ self.outlineButton.setVisible(True)
2960
2939
  self.closeButton.setVisible(True)
2961
2940
  self.minmaxButton.setVisible(True)
2962
2941
 
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
2942
  return
2975
2943
 
2976
2944
  ##
@@ -2980,23 +2948,32 @@ class GuiDocEditHeader(QWidget):
2980
2948
  @pyqtSlot()
2981
2949
  def _closeDocument(self) -> None:
2982
2950
  """Trigger the close editor on the main window."""
2951
+ self.clearHeader()
2983
2952
  self.closeDocumentRequest.emit()
2984
- self.tbButton.setVisible(False)
2985
- self.searchButton.setVisible(False)
2986
- self.closeButton.setVisible(False)
2987
- self.minmaxButton.setVisible(False)
2953
+ return
2954
+
2955
+ @pyqtSlot(int)
2956
+ def _gotoBlock(self, blockNumber: int) -> None:
2957
+ """Move cursor to a specific heading."""
2958
+ self.docEditor.setCursorLine(blockNumber + 1)
2959
+ return
2960
+
2961
+ @pyqtSlot(bool)
2962
+ def _focusModeChanged(self, focusMode: bool) -> None:
2963
+ """Update minimise/maximise icon of the Focus Mode button."""
2964
+ self.minmaxButton.setThemeIcon("minimise" if focusMode else "maximise")
2988
2965
  return
2989
2966
 
2990
2967
  ##
2991
2968
  # Events
2992
2969
  ##
2993
2970
 
2994
- def mousePressEvent(self, event: QMouseEvent):
2971
+ def mousePressEvent(self, event: QMouseEvent) -> None:
2995
2972
  """Capture a click on the title and ensure that the item is
2996
2973
  selected in the project tree.
2997
2974
  """
2998
- if event.button() == Qt.MouseButton.LeftButton:
2999
- self.docEditor.requestProjectItemSelected.emit(self._docHandle, True)
2975
+ if event.button() == QtMouseLeft:
2976
+ self.docEditor.requestProjectItemSelected.emit(self._docHandle or "", True)
3000
2977
  return
3001
2978
 
3002
2979
  # END Class GuiDocEditHeader
@@ -3009,75 +2986,75 @@ class GuiDocEditHeader(QWidget):
3009
2986
 
3010
2987
  class GuiDocEditFooter(QWidget):
3011
2988
 
3012
- def __init__(self, docEditor: GuiDocEditor) -> None:
3013
- super().__init__(parent=docEditor)
2989
+ def __init__(self, parent: QWidget) -> None:
2990
+ super().__init__(parent=parent)
3014
2991
 
3015
2992
  logger.debug("Create: GuiDocEditFooter")
3016
2993
 
3017
- self.docEditor = docEditor
3018
-
3019
- self._tItem = None
2994
+ self._tItem = None
3020
2995
  self._docHandle = None
3021
2996
 
3022
- self._docSelection = False
3023
-
3024
- self.sPx = int(round(0.9*SHARED.theme.baseIconSize))
2997
+ iPx = round(0.9*SHARED.theme.baseIconHeight)
3025
2998
  fPx = int(0.9*SHARED.theme.fontPixelSize)
2999
+ mPx = CONFIG.pxInt(8)
3026
3000
  bSp = CONFIG.pxInt(4)
3027
3001
  hSp = CONFIG.pxInt(6)
3028
3002
 
3029
3003
  lblFont = self.font()
3030
3004
  lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
3031
3005
 
3006
+ # Cached Translations
3007
+ self._trLineCount = self.tr("Line: {0} ({1})")
3008
+ self._trWordCount = self.tr("Words: {0} ({1})")
3009
+ self._trSelectCount = self.tr("Words: {0} selected")
3010
+
3032
3011
  # Main Widget Settings
3033
3012
  self.setContentsMargins(0, 0, 0, 0)
3034
3013
  self.setAutoFillBackground(True)
3035
3014
 
3036
- alLeftTop = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
3037
-
3038
3015
  # Status
3039
- self.statusIcon = QLabel("")
3016
+ self.statusIcon = QLabel("", self)
3040
3017
  self.statusIcon.setContentsMargins(0, 0, 0, 0)
3041
- self.statusIcon.setFixedHeight(self.sPx)
3042
- self.statusIcon.setAlignment(alLeftTop)
3018
+ self.statusIcon.setFixedHeight(iPx)
3019
+ self.statusIcon.setAlignment(QtAlignLeftTop)
3043
3020
 
3044
- self.statusText = QLabel(self.tr("Status"))
3021
+ self.statusText = QLabel(self.tr("Status"), self)
3045
3022
  self.statusText.setIndent(0)
3046
3023
  self.statusText.setMargin(0)
3047
3024
  self.statusText.setContentsMargins(0, 0, 0, 0)
3048
3025
  self.statusText.setAutoFillBackground(True)
3049
3026
  self.statusText.setFixedHeight(fPx)
3050
- self.statusText.setAlignment(alLeftTop)
3027
+ self.statusText.setAlignment(QtAlignLeftTop)
3051
3028
  self.statusText.setFont(lblFont)
3052
3029
 
3053
3030
  # Lines
3054
- self.linesIcon = QLabel("")
3031
+ self.linesIcon = QLabel("", self)
3055
3032
  self.linesIcon.setContentsMargins(0, 0, 0, 0)
3056
- self.linesIcon.setFixedHeight(self.sPx)
3057
- self.linesIcon.setAlignment(alLeftTop)
3033
+ self.linesIcon.setFixedHeight(iPx)
3034
+ self.linesIcon.setAlignment(QtAlignLeftTop)
3058
3035
 
3059
- self.linesText = QLabel("")
3036
+ self.linesText = QLabel("", self)
3060
3037
  self.linesText.setIndent(0)
3061
3038
  self.linesText.setMargin(0)
3062
3039
  self.linesText.setContentsMargins(0, 0, 0, 0)
3063
3040
  self.linesText.setAutoFillBackground(True)
3064
3041
  self.linesText.setFixedHeight(fPx)
3065
- self.linesText.setAlignment(alLeftTop)
3042
+ self.linesText.setAlignment(QtAlignLeftTop)
3066
3043
  self.linesText.setFont(lblFont)
3067
3044
 
3068
3045
  # Words
3069
- self.wordsIcon = QLabel("")
3046
+ self.wordsIcon = QLabel("", self)
3070
3047
  self.wordsIcon.setContentsMargins(0, 0, 0, 0)
3071
- self.wordsIcon.setFixedHeight(self.sPx)
3072
- self.wordsIcon.setAlignment(alLeftTop)
3048
+ self.wordsIcon.setFixedHeight(iPx)
3049
+ self.wordsIcon.setAlignment(QtAlignLeftTop)
3073
3050
 
3074
- self.wordsText = QLabel("")
3051
+ self.wordsText = QLabel("", self)
3075
3052
  self.wordsText.setIndent(0)
3076
3053
  self.wordsText.setMargin(0)
3077
3054
  self.wordsText.setContentsMargins(0, 0, 0, 0)
3078
3055
  self.wordsText.setAutoFillBackground(True)
3079
3056
  self.wordsText.setFixedHeight(fPx)
3080
- self.wordsText.setAlignment(alLeftTop)
3057
+ self.wordsText.setAlignment(QtAlignLeftTop)
3081
3058
  self.wordsText.setFont(lblFont)
3082
3059
 
3083
3060
  # Assemble Layout
@@ -3091,19 +3068,20 @@ class GuiDocEditFooter(QWidget):
3091
3068
  self.outerBox.addSpacing(hSp)
3092
3069
  self.outerBox.addWidget(self.wordsIcon)
3093
3070
  self.outerBox.addWidget(self.wordsText)
3071
+ self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
3072
+
3094
3073
  self.setLayout(self.outerBox)
3095
3074
 
3096
3075
  # Fix Margins and Size
3097
3076
  # This is needed for high DPI systems. See issue #499.
3098
- cM = CONFIG.pxInt(8)
3099
3077
  self.setContentsMargins(0, 0, 0, 0)
3100
- self.outerBox.setContentsMargins(cM, cM, cM, cM)
3101
- self.setMinimumHeight(fPx + 2*cM)
3078
+ self.setMinimumHeight(fPx + 2*mPx)
3102
3079
 
3103
3080
  # Fix the Colours
3104
3081
  self.updateTheme()
3105
- self.updateLineCount()
3106
- self.updateCounts()
3082
+
3083
+ # Initialise Info
3084
+ self.updateWordCount(0, False)
3107
3085
 
3108
3086
  logger.debug("Ready: GuiDocEditFooter")
3109
3087
 
@@ -3115,8 +3093,9 @@ class GuiDocEditFooter(QWidget):
3115
3093
 
3116
3094
  def updateTheme(self) -> None:
3117
3095
  """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)))
3096
+ iPx = round(0.9*SHARED.theme.baseIconHeight)
3097
+ self.linesIcon.setPixmap(SHARED.theme.getPixmap("status_lines", (iPx, iPx)))
3098
+ self.wordsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (iPx, iPx)))
3120
3099
  self.matchColours()
3121
3100
  return
3122
3101
 
@@ -3145,27 +3124,20 @@ class GuiDocEditFooter(QWidget):
3145
3124
  else:
3146
3125
  self._tItem = SHARED.project.tree[self._docHandle]
3147
3126
 
3148
- self.setHasSelection(False)
3149
3127
  self.updateInfo()
3150
- self.updateCounts()
3128
+ self.updateWordCount(0, False)
3151
3129
 
3152
3130
  return
3153
3131
 
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
3132
  def updateInfo(self) -> None:
3162
3133
  """Update the content of text labels."""
3163
3134
  if self._tItem is None:
3164
3135
  sIcon = QPixmap()
3165
3136
  sText = ""
3166
3137
  else:
3138
+ iPx = round(0.9*SHARED.theme.baseIconHeight)
3167
3139
  status, icon = self._tItem.getImportStatus(incIcon=True)
3168
- sIcon = icon.pixmap(self.sPx, self.sPx)
3140
+ sIcon = icon.pixmap(iPx, iPx)
3169
3141
  sText = f"{status} / {self._tItem.describeMe()}"
3170
3142
 
3171
3143
  self.statusIcon.setPixmap(sIcon)
@@ -3173,62 +3145,27 @@ class GuiDocEditFooter(QWidget):
3173
3145
 
3174
3146
  return
3175
3147
 
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()
3148
+ def updateLineCount(self, cursor: QTextCursor) -> None:
3149
+ """Update the line and document position counter."""
3150
+ cPos = cursor.position() + 1
3151
+ cLine = cursor.blockNumber() + 1
3152
+ cCount = max(cursor.document().characterCount(), 1)
3185
3153
  self.linesText.setText(
3186
- self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
3154
+ self._trLineCount.format(f"{cLine:n}", f"{100*cPos//cCount:d} %")
3187
3155
  )
3188
3156
  return
3189
3157
 
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:
3158
+ def updateWordCount(self, wCount: int, selection: bool) -> None:
3159
+ """Update word counter information."""
3160
+ if selection and wCount:
3161
+ wText = self._trSelectCount.format(f"{wCount:n}")
3162
+ elif self._tItem:
3208
3163
  wCount = self._tItem.wordCount
3209
3164
  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
- )
3165
+ wText = self._trWordCount.format(f"{wCount:n}", f"{wDiff:+n}")
3166
+ else:
3167
+ wText = self._trWordCount.format("0", "+0")
3168
+ self.wordsText.setText(wText)
3232
3169
  return
3233
3170
 
3234
3171
  # END Class GuiDocEditFooter