novelWriter 2.1.1__py3-none-any.whl → 2.2rc1__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 (109) hide show
  1. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +105 -76
  3. novelwriter/__init__.py +6 -24
  4. novelwriter/assets/i18n/project_de_DE.json +10 -0
  5. novelwriter/assets/i18n/project_en_GB.json +11 -0
  6. novelwriter/assets/i18n/project_en_US.json +10 -0
  7. novelwriter/assets/i18n/project_ja_JP.json +11 -1
  8. novelwriter/assets/i18n/project_nb_NO.json +10 -0
  9. novelwriter/assets/i18n/project_nn_NO.json +10 -0
  10. novelwriter/assets/icons/novelwriter.ico +0 -0
  11. novelwriter/assets/icons/novelwriter.svg +8 -183
  12. novelwriter/assets/icons/typicons_dark/icons.conf +17 -2
  13. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  14. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  15. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  16. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  17. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  18. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +8 -0
  21. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +8 -0
  22. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +4 -0
  23. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +5 -0
  24. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +5 -0
  25. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +5 -0
  26. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  27. novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +4 -0
  28. novelwriter/assets/icons/typicons_light/icons.conf +17 -2
  29. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  30. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  31. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  32. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  33. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  34. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +4 -0
  35. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +4 -0
  36. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +8 -0
  37. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +8 -0
  38. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +5 -0
  40. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +5 -0
  41. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +5 -0
  42. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +4 -0
  44. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  45. novelwriter/assets/icons/x-novelwriter-project.svg +7 -206
  46. novelwriter/assets/manual.pdf +0 -0
  47. novelwriter/assets/sample.zip +0 -0
  48. novelwriter/assets/syntax/default_dark.conf +1 -0
  49. novelwriter/assets/syntax/default_light.conf +1 -0
  50. novelwriter/assets/syntax/grey_dark.conf +1 -0
  51. novelwriter/assets/syntax/grey_light.conf +1 -0
  52. novelwriter/assets/syntax/light_owl.conf +1 -0
  53. novelwriter/assets/syntax/night_owl.conf +1 -0
  54. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  55. novelwriter/assets/syntax/solarized_light.conf +1 -0
  56. novelwriter/assets/syntax/tomorrow.conf +1 -0
  57. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  58. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  59. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  60. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  61. novelwriter/assets/text/credits_en.htm +7 -0
  62. novelwriter/assets/text/release_notes.htm +7 -37
  63. novelwriter/common.py +22 -1
  64. novelwriter/config.py +27 -42
  65. novelwriter/constants.py +45 -7
  66. novelwriter/core/buildsettings.py +40 -24
  67. novelwriter/core/coretools.py +8 -1
  68. novelwriter/core/docbuild.py +2 -6
  69. novelwriter/core/index.py +264 -175
  70. novelwriter/core/options.py +8 -3
  71. novelwriter/core/project.py +2 -2
  72. novelwriter/core/projectdata.py +3 -3
  73. novelwriter/core/tohtml.py +60 -59
  74. novelwriter/core/tokenizer.py +110 -70
  75. novelwriter/core/tomd.py +51 -38
  76. novelwriter/core/toodt.py +184 -147
  77. novelwriter/dialogs/preferences.py +75 -106
  78. novelwriter/dialogs/projsettings.py +101 -110
  79. novelwriter/dialogs/updates.py +25 -14
  80. novelwriter/enum.py +28 -3
  81. novelwriter/extensions/novelselector.py +1 -1
  82. novelwriter/gui/doceditor.py +1345 -1235
  83. novelwriter/gui/dochighlight.py +98 -62
  84. novelwriter/gui/docviewer.py +151 -340
  85. novelwriter/gui/docviewerpanel.py +457 -0
  86. novelwriter/gui/editordocument.py +126 -0
  87. novelwriter/gui/mainmenu.py +350 -300
  88. novelwriter/gui/noveltree.py +101 -125
  89. novelwriter/gui/outline.py +154 -171
  90. novelwriter/gui/projtree.py +480 -380
  91. novelwriter/gui/sidebar.py +106 -75
  92. novelwriter/gui/statusbar.py +1 -1
  93. novelwriter/gui/theme.py +114 -75
  94. novelwriter/guimain.py +353 -254
  95. novelwriter/shared.py +36 -3
  96. novelwriter/tools/dictionaries.py +268 -0
  97. novelwriter/tools/manusbuild.py +17 -6
  98. novelwriter/tools/manuscript.py +11 -3
  99. novelwriter/tools/manussettings.py +0 -14
  100. novelwriter/tools/projwizard.py +16 -2
  101. novelwriter/tools/writingstats.py +1 -1
  102. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  103. novelwriter/assets/icons/typicons_dark/typ_th-menu.svg +0 -4
  104. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  105. novelwriter/assets/icons/typicons_light/typ_th-menu.svg +0 -4
  106. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
  107. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
  108. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
  109. {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,8 @@ Created: 2020-04-25 [0.4.5] GuiDocEditHeader
10
10
  Rewritten: 2020-06-15 [0.9] GuiDocEditSearch
11
11
  Created: 2020-06-27 [0.10] GuiDocEditFooter
12
12
  Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
13
+ Created: 2023-11-06 [2.2b1] MetaCompleter
14
+ Created: 2023-11-07 [2.2b1] GuiDocToolBar
13
15
 
14
16
  This file is a part of novelWriter
15
17
  Copyright 2018–2023, Veronica Berglyd Olsen
@@ -37,24 +39,28 @@ from time import time
37
39
  from typing import TYPE_CHECKING
38
40
 
39
41
  from PyQt5.QtCore import (
40
- Qt, QSize, QTimer, pyqtSlot, pyqtSignal, QRegExp, QRegularExpression,
41
- QPointF, QObject, QRunnable, QPropertyAnimation
42
+ pyqtSignal, pyqtSlot, QObject, QPoint, QRegExp, QRegularExpression,
43
+ QRunnable, QSize, Qt, QTimer
42
44
  )
43
45
  from PyQt5.QtGui import (
44
- QFontMetrics, QTextCursor, QTextOption, QKeySequence, QFont, QColor,
45
- QPalette, QTextDocument, QCursor, QPixmap
46
+ QColor, QCursor, QFont, QKeyEvent, QKeySequence, QMouseEvent, QPalette,
47
+ QPixmap, QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption
46
48
  )
47
49
  from PyQt5.QtWidgets import (
48
- QAction, qApp, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
49
- QPushButton, QShortcut, QTextEdit, QToolBar, QToolButton, QWidget
50
+ QAction, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
51
+ QPlainTextEdit, QPushButton, QShortcut, QToolBar, QToolButton, QVBoxLayout,
52
+ QWidget, qApp
50
53
  )
51
54
 
52
55
  from novelwriter import CONFIG, SHARED
53
- from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass
56
+ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
54
57
  from novelwriter.common import minmax, transferCase
55
- from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
58
+ from novelwriter.constants import nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst
59
+ from novelwriter.core.item import NWItem
56
60
  from novelwriter.core.index import countWords
61
+ from novelwriter.core.document import NWDocument
57
62
  from novelwriter.gui.dochighlight import GuiDocHighlighter
63
+ from novelwriter.gui.editordocument import GuiTextDocument
58
64
  from novelwriter.extensions.wheeleventfilter import WheelEventFilter
59
65
 
60
66
  if TYPE_CHECKING: # pragma: no cover
@@ -63,11 +69,22 @@ if TYPE_CHECKING: # pragma: no cover
63
69
  logger = logging.getLogger(__name__)
64
70
 
65
71
 
66
- class GuiDocEditor(QTextEdit):
72
+ class _SelectAction(Enum):
73
+
74
+ NO_DECISION = 0
75
+ KEEP_SELECTION = 1
76
+ KEEP_POSITION = 2
77
+ MOVE_AFTER = 3
78
+
79
+ # END Class _SelectAction
80
+
81
+
82
+ class GuiDocEditor(QPlainTextEdit):
83
+ """Gui Widget: Main Document Editor"""
67
84
 
68
85
  MOVE_KEYS = (
69
- Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down,
70
- Qt.Key_PageUp, Qt.Key_PageDown
86
+ Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down,
87
+ Qt.Key.Key_PageUp, Qt.Key.Key_PageDown
71
88
  )
72
89
 
73
90
  # Custom Signals
@@ -77,8 +94,13 @@ class GuiDocEditor(QTextEdit):
77
94
  loadDocumentTagRequest = pyqtSignal(str, Enum)
78
95
  novelStructureChanged = pyqtSignal()
79
96
  novelItemMetaChanged = pyqtSignal(str)
97
+ spellCheckStateChanged = pyqtSignal(bool)
98
+ closeDocumentRequest = pyqtSignal()
99
+ toggleFocusModeRequest = pyqtSignal()
100
+ requestProjectItemSelected = pyqtSignal(str, bool)
101
+ requestProjectItemRenamed = pyqtSignal(str, str)
80
102
 
81
- def __init__(self, mainGui: GuiMain):
103
+ def __init__(self, mainGui: GuiMain) -> None:
82
104
  super().__init__(parent=mainGui)
83
105
 
84
106
  logger.debug("Create: GuiDocEditor")
@@ -90,22 +112,14 @@ class GuiDocEditor(QTextEdit):
90
112
  self._nwItem = None
91
113
 
92
114
  self._docChanged = False # Flag for changed status of document
93
- self._docHandle = None # The handle of the open file
94
-
95
- self._spellCheck = False # Flag for spell checking enabled
96
- self._nonWord = "\"'" # Characters to not include in spell checking
115
+ self._docHandle = None # The handle of the open document
97
116
  self._vpMargin = 0 # The editor viewport margin, set during init
98
117
 
99
118
  # Document Variables
100
- self._charCount = 0 # Character count
101
- self._wordCount = 0 # Word count
102
- self._paraCount = 0 # Paragraph count
103
- self._lastEdit = 0 # Time stamp of last edit
104
- self._lastActive = 0.0 # Time stamp of last activity
119
+ self._lastEdit = 0.0 # Timestamp of last edit
120
+ self._lastActive = 0.0 # Timestamp of last activity
105
121
  self._lastFind = None # Position of the last found search word
106
- self._bigDoc = False # Flag for very large document size
107
122
  self._doReplace = False # Switch to temporarily disable auto-replace
108
- self._queuePos = None # Used for delayed change of cursor position
109
123
 
110
124
  # Typography Cache
111
125
  self._typPadChar = " "
@@ -120,45 +134,54 @@ class GuiDocEditor(QTextEdit):
120
134
  self._typPadBefore = ""
121
135
  self._typPadAfter = ""
122
136
 
123
- # Core Elements and Signals
124
- qDoc = self.document()
125
- qDoc.contentsChange.connect(self._docChange)
126
- qDoc.documentLayout().documentSizeChanged.connect(self._docSizeChanged)
137
+ # Completer
138
+ self._completer = MetaCompleter(self)
139
+ self._completer.complete.connect(self._insertCompletion)
140
+
141
+ # Create Custom Document
142
+ self._qDocument = GuiTextDocument(self)
143
+ self.setDocument(self._qDocument)
144
+
145
+ # Connect Signals
146
+ self._qDocument.contentsChange.connect(self._docChange)
127
147
  self.selectionChanged.connect(self._updateSelectedStatus)
148
+ self.spellCheckStateChanged.connect(self._qDocument.setSpellCheckState)
128
149
 
129
150
  # Document Title
130
151
  self.docHeader = GuiDocEditHeader(self)
131
152
  self.docFooter = GuiDocEditFooter(self)
132
153
  self.docSearch = GuiDocEditSearch(self)
154
+ self.docToolBar = GuiDocToolBar(self)
133
155
 
134
- # Syntax
135
- self.highLight = GuiDocHighlighter(qDoc)
156
+ # Connect Signals
157
+ self.docHeader.closeDocumentRequest.connect(self._closeCurrentDocument)
158
+ self.docHeader.toggleToolBarRequest.connect(self._toggleToolBarVisibility)
159
+ self.docToolBar.requestDocAction.connect(self.docAction)
136
160
 
137
161
  # Context Menu
138
- self.setContextMenuPolicy(Qt.CustomContextMenu)
162
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
139
163
  self.customContextMenuRequested.connect(self._openContextMenu)
140
164
 
141
165
  # Editor Settings
142
166
  self.setMinimumWidth(CONFIG.pxInt(300))
143
- self.setAcceptRichText(False)
144
167
  self.setAutoFillBackground(True)
145
- self.setFrameStyle(QFrame.NoFrame)
168
+ self.setFrameStyle(QFrame.Shape.NoFrame)
146
169
 
147
170
  # Custom Shortcuts
148
171
  self.keyContext = QShortcut(self)
149
172
  self.keyContext.setKey("Ctrl+.")
150
- self.keyContext.setContext(Qt.WidgetShortcut)
151
- self.keyContext.activated.connect(self._openSpellContext)
173
+ self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
174
+ self.keyContext.activated.connect(self._openContextFromCursor)
152
175
 
153
176
  self.followTag1 = QShortcut(self)
154
- self.followTag1.setKey(Qt.Key_Return | Qt.ControlModifier)
155
- self.followTag1.setContext(Qt.WidgetShortcut)
156
- self.followTag1.activated.connect(self._followTag)
177
+ self.followTag1.setKey(Qt.Key.Key_Return | Qt.KeyboardModifier.ControlModifier)
178
+ self.followTag1.setContext(Qt.ShortcutContext.WidgetShortcut)
179
+ self.followTag1.activated.connect(self._processTag)
157
180
 
158
181
  self.followTag2 = QShortcut(self)
159
- self.followTag2.setKey(Qt.Key_Enter | Qt.ControlModifier)
160
- self.followTag2.setContext(Qt.WidgetShortcut)
161
- self.followTag2.activated.connect(self._followTag)
182
+ self.followTag2.setKey(Qt.Key.Key_Enter | Qt.KeyboardModifier.ControlModifier)
183
+ self.followTag2.setContext(Qt.ShortcutContext.WidgetShortcut)
184
+ self.followTag2.activated.connect(self._processTag)
162
185
 
163
186
  # Set Up Document Word Counter
164
187
  self.wcTimerDoc = QTimer()
@@ -213,13 +236,13 @@ class GuiDocEditor(QTextEdit):
213
236
  @property
214
237
  def isEmpty(self) -> bool:
215
238
  """Check if the current document is empty."""
216
- return self.document().isEmpty()
239
+ return self._qDocument.isEmpty()
217
240
 
218
241
  ##
219
242
  # Methods
220
243
  ##
221
244
 
222
- def clearEditor(self):
245
+ def clearEditor(self) -> None:
223
246
  """Clear the current document and reset all document-related
224
247
  flags and counters.
225
248
  """
@@ -230,63 +253,51 @@ class GuiDocEditor(QTextEdit):
230
253
  self.wcTimerSel.stop()
231
254
 
232
255
  self._docHandle = None
233
- self._charCount = 0
234
- self._wordCount = 0
235
- self._paraCount = 0
236
- self._lastEdit = 0
256
+ self._lastEdit = 0.0
237
257
  self._lastActive = 0.0
238
258
  self._lastFind = None
239
- self._bigDoc = False
240
259
  self._doReplace = False
241
- self._queuePos = None
242
260
 
243
261
  self.setDocumentChanged(False)
244
262
  self.docHeader.setTitleFromHandle(self._docHandle)
245
263
  self.docFooter.setHandle(self._docHandle)
264
+ self.docToolBar.setVisible(False)
246
265
 
247
- return True
266
+ return
248
267
 
249
- def updateTheme(self):
250
- """Update theme elements
251
- """
268
+ def updateTheme(self) -> None:
269
+ """Update theme elements."""
252
270
  self.docSearch.updateTheme()
253
271
  self.docHeader.updateTheme()
254
272
  self.docFooter.updateTheme()
273
+ self.docToolBar.updateTheme()
255
274
  return
256
275
 
257
- def updateSyntaxColours(self):
258
- """Update the syntax highlighting theme.
259
- """
276
+ def updateSyntaxColours(self) -> None:
277
+ """Update the syntax highlighting theme."""
260
278
  mainPalette = self.palette()
261
- mainPalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
262
- mainPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack))
263
- mainPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
279
+ mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
280
+ mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
281
+ mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
264
282
  self.setPalette(mainPalette)
265
283
 
266
284
  docPalette = self.viewport().palette()
267
- docPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack))
268
- docPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
285
+ docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
286
+ docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
269
287
  self.viewport().setPalette(docPalette)
270
288
 
271
289
  self.docHeader.matchColours()
272
290
  self.docFooter.matchColours()
273
291
 
274
- self.highLight.initHighlighter()
292
+ self._qDocument.syntaxHighlighter.initHighlighter()
275
293
 
276
294
  return
277
295
 
278
- def initEditor(self):
296
+ def initEditor(self) -> None:
279
297
  """Initialise or re-initialise the editor with the user's
280
298
  settings. This function is both called when the editor is
281
299
  created, and when the user changes the main editor preferences.
282
300
  """
283
- # Some Constants
284
- self._nonWord = (
285
- "\"'"
286
- f"{CONFIG.fmtSQuoteOpen}{CONFIG.fmtSQuoteClose}"
287
- f"{CONFIG.fmtDQuoteOpen}{CONFIG.fmtDQuoteClose}"
288
- )
289
-
290
301
  # Typography
291
302
  if CONFIG.fmtPadThin:
292
303
  self._typPadChar = nwUnicode.U_THNBSP
@@ -316,34 +327,33 @@ class GuiDocEditor(QTextEdit):
316
327
  # Set default text margins
317
328
  # Due to cursor visibility, a part of the margin must be
318
329
  # allocated to the document itself. See issue #1112.
319
- cW = self.cursorWidth()
320
- qDoc = self.document()
321
- qDoc.setDocumentMargin(cW)
322
- self._vpMargin = max(CONFIG.getTextMargin() - cW, 0)
330
+ self._qDocument.setDocumentMargin(4)
331
+ self._vpMargin = max(CONFIG.getTextMargin() - 4, 0)
323
332
  self.setViewportMargins(self._vpMargin, self._vpMargin, self._vpMargin, self._vpMargin)
324
333
 
325
334
  # Also set the document text options for the document text flow
326
- theOpt = QTextOption()
335
+ options = QTextOption()
327
336
 
328
337
  if CONFIG.doJustify:
329
- theOpt.setAlignment(Qt.AlignJustify)
338
+ options.setAlignment(Qt.AlignmentFlag.AlignJustify)
330
339
  if CONFIG.showTabsNSpaces:
331
- theOpt.setFlags(theOpt.flags() | QTextOption.ShowTabsAndSpaces)
340
+ options.setFlags(options.flags() | QTextOption.Flag.ShowTabsAndSpaces)
332
341
  if CONFIG.showLineEndings:
333
- theOpt.setFlags(theOpt.flags() | QTextOption.ShowLineAndParagraphSeparators)
342
+ options.setFlags(options.flags() | QTextOption.Flag.ShowLineAndParagraphSeparators)
334
343
 
335
- qDoc.setDefaultTextOption(theOpt)
344
+ self._qDocument.setDefaultTextOption(options)
336
345
 
337
- # Scroll bars
346
+ # Scrolling
347
+ self.setCenterOnScroll(CONFIG.scrollPastEnd)
338
348
  if CONFIG.hideVScroll:
339
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
349
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
340
350
  else:
341
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
351
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
342
352
 
343
353
  if CONFIG.hideHScroll:
344
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
354
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
345
355
  else:
346
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
356
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
347
357
 
348
358
  # Refresh the tab stops
349
359
  self.setTabStopDistance(CONFIG.getTabWidth())
@@ -358,14 +368,12 @@ class GuiDocEditor(QTextEdit):
358
368
  if self._docHandle is None:
359
369
  self.clearEditor()
360
370
  else:
361
- self.redrawText()
362
- if not self._bigDoc:
363
- self.highLight.rehighlight()
371
+ self._qDocument.syntaxHighlighter.rehighlight()
364
372
 
365
- return True
373
+ return
366
374
 
367
- def loadText(self, tHandle, tLine=None):
368
- """Load text from a document into the editor. If we have an io
375
+ def loadText(self, tHandle: str, tLine=None) -> bool:
376
+ """Load text from a document into the editor. If we have an I/O
369
377
  error, we must handle this and clear the editor so that we don't
370
378
  risk overwriting the file if it exists. This can for instance
371
379
  happen of the file contains binary elements or an encoding that
@@ -376,131 +384,73 @@ class GuiDocEditor(QTextEdit):
376
384
  self._nwDocument = SHARED.project.storage.getDocument(tHandle)
377
385
  self._nwItem = self._nwDocument.nwItem
378
386
 
379
- theDoc = self._nwDocument.readDocument()
380
- if theDoc is None:
381
- # There was an io error
387
+ docText = self._nwDocument.readDocument()
388
+ if docText is None:
389
+ # There was an I/O error
382
390
  self.clearEditor()
383
391
  return False
384
392
 
385
- docSize = len(theDoc)
386
- if docSize > nwConst.MAX_DOCSIZE:
387
- SHARED.error(self.tr(
388
- "The document you are trying to open is too big. "
389
- "The document size is {0} MB. "
390
- "The maximum size allowed is {1} MB."
391
- ).format(
392
- f"{docSize/1.0e6:.2f}",
393
- f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
394
- ))
395
- self.clearEditor()
396
- return False
397
-
398
- qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
399
- self.highLight.setHandle(tHandle)
400
-
401
- # Check that the document is not too big for full, initial spell
402
- # checking. If it is too big, we switch to only check as we type
403
- self._checkDocSize(docSize)
404
- spTemp = self.highLight.spellCheck
405
- if self._bigDoc:
406
- self.highLight.setSpellCheck(False)
393
+ qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
394
+ self._docHandle = tHandle
407
395
 
408
- bfTime = time()
409
396
  self._allowAutoReplace(False)
410
- self.setPlainText(theDoc)
411
- qApp.processEvents()
412
-
397
+ self._qDocument.setTextContent(docText, tHandle)
413
398
  self._allowAutoReplace(True)
414
- afTime = time()
415
- logger.debug("Document highlighted in %.3f ms", 1000*(afTime-bfTime))
399
+ qApp.processEvents()
416
400
 
417
401
  self._lastEdit = time()
418
402
  self._lastActive = time()
419
403
  self._runDocCounter()
420
404
  self.wcTimerDoc.start()
421
- self._docHandle = tHandle
422
405
 
423
406
  self.setReadOnly(False)
424
407
  self.docHeader.setTitleFromHandle(self._docHandle)
425
408
  self.docFooter.setHandle(self._docHandle)
426
409
  self.updateDocMargins()
427
- self.highLight.setSpellCheck(spTemp)
428
410
 
429
411
  if tLine is None and self._nwItem is not None:
430
- # For large documents, we queue the repositioning until the
431
- # document layout has grown past the point we want to move
432
- # the cursor to. This makes the loading significantly
433
- # faster.
434
- if docSize > 50000:
435
- self._queuePos = self._nwItem.cursorPos
436
- else:
437
- self.setCursorPosition(self._nwItem.cursorPos)
412
+ self.setCursorPosition(self._nwItem.cursorPos)
438
413
  elif isinstance(tLine, int):
439
414
  self.setCursorLine(tLine)
440
415
 
441
- if CONFIG.scrollPastEnd > 0:
442
- fSize = QFontMetrics(self.font()).lineSpacing()
443
- docFrame = self.document().rootFrame().frameFormat()
444
- docFrame.setBottomMargin(round(CONFIG.scrollPastEnd * fSize))
445
- self.document().rootFrame().setFrameFormat(docFrame)
446
-
447
416
  self.docFooter.updateLineCount()
448
417
 
449
- qApp.processEvents()
450
- self.document().clearUndoRedoStacks()
451
- self.setDocumentChanged(False)
452
- qApp.restoreOverrideCursor()
453
-
454
418
  # This is a hack to fix invisible cursor on an empty document
455
- if self.document().characterCount() <= 1:
419
+ if self._qDocument.characterCount() <= 1:
456
420
  self.setPlainText("\n")
457
421
  self.setPlainText("")
458
422
  self.setCursorPosition(0)
459
423
 
424
+ qApp.processEvents()
425
+ self.setDocumentChanged(False)
426
+ self._qDocument.clearUndoRedoStacks()
427
+ self.docToolBar.setVisible(CONFIG.showEditToolBar)
428
+
429
+ qApp.restoreOverrideCursor()
430
+
460
431
  # Update the status bar
461
432
  if self._nwItem is not None:
462
433
  self.statusMessage.emit(self.tr("Opened Document: {0}").format(self._nwItem.itemName))
463
434
 
464
435
  return True
465
436
 
466
- def updateTagHighLighting(self):
467
- """Rerun the syntax highlighter on all meta data lines.
468
- """
469
- self.highLight.rehighlightByType(GuiDocHighlighter.BLOCK_META)
437
+ def updateTagHighLighting(self) -> None:
438
+ """Rerun the syntax highlighter on all meta data lines."""
439
+ self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
470
440
  return
471
441
 
472
- def redrawText(self):
473
- """Redraw the text by marking the document content as "dirty".
474
- """
475
- self.document().markContentsDirty(0, self.document().characterCount())
476
- self.updateDocMargins()
477
- return
478
-
479
- def replaceText(self, theText):
442
+ def replaceText(self, text: str) -> None:
480
443
  """Replace the text of the current document with the provided
481
444
  text. This also clears undo history.
482
445
  """
483
- docSize = len(theText)
484
- if docSize > nwConst.MAX_DOCSIZE:
485
- SHARED.error(self.tr(
486
- "The text you are trying to add is too big. "
487
- "The text size is {0} MB. "
488
- "The maximum size allowed is {1} MB."
489
- ).format(
490
- f"{docSize/1.0e6:.2f}",
491
- f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
492
- ))
493
- return False
494
-
495
- qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
496
- self.setPlainText(theText)
446
+ qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
447
+ self.setPlainText(text)
497
448
  self.updateDocMargins()
498
449
  self.setDocumentChanged(True)
499
450
  qApp.restoreOverrideCursor()
451
+ return
500
452
 
501
- return True
502
-
503
- def saveText(self):
453
+ def saveText(self) -> bool:
504
454
  """Save the text currently in the editor to the NWDocument
505
455
  object, and update the NWItem meta data.
506
456
  """
@@ -516,14 +466,9 @@ class GuiDocEditor(QTextEdit):
516
466
  return False
517
467
 
518
468
  docText = self.getText()
519
-
520
469
  cC, wC, pC = countWords(docText)
521
470
  self._updateDocCounts(cC, wC, pC)
522
471
 
523
- self._nwItem.setCharCount(self._charCount)
524
- self._nwItem.setWordCount(self._wordCount)
525
- self._nwItem.setParaCount(self._paraCount)
526
-
527
472
  self.saveCursorPosition()
528
473
  if not self._nwDocument.writeDocument(docText):
529
474
  saveOk = False
@@ -557,10 +502,7 @@ class GuiDocEditor(QTextEdit):
557
502
  else:
558
503
  self.novelStructureChanged.emit()
559
504
 
560
- # ToDo: This should be a signal
561
505
  if oldHeader != newHeader:
562
- self.mainGui.projView.setTreeItemValues(tHandle)
563
- self.mainGui.itemDetails.updateViewBox(tHandle)
564
506
  self.docFooter.updateInfo()
565
507
 
566
508
  # Update the status bar
@@ -568,7 +510,34 @@ class GuiDocEditor(QTextEdit):
568
510
 
569
511
  return True
570
512
 
571
- def updateDocMargins(self):
513
+ def cursorIsVisible(self) -> bool:
514
+ """Check if the cursor is visible in the editor."""
515
+ return (
516
+ 0 < self.cursorRect().top()
517
+ and self.cursorRect().bottom() < self.viewport().height()
518
+ )
519
+
520
+ def ensureCursorVisibleNoCentre(self) -> None:
521
+ """Ensure cursor is visible, but don't force it to centre."""
522
+ cT = self.cursorRect().top()
523
+ cB = self.cursorRect().bottom()
524
+ vH = self.viewport().height()
525
+ if cT < 0:
526
+ count = 0
527
+ vBar = self.verticalScrollBar()
528
+ while self.cursorRect().top() < 0 and count < 100000:
529
+ vBar.setValue(vBar.value() - 1)
530
+ count += 1
531
+ elif cB > vH:
532
+ count = 0
533
+ vBar = self.verticalScrollBar()
534
+ while self.cursorRect().bottom() > vH and count < 100000:
535
+ vBar.setValue(vBar.value() + 1)
536
+ count += 1
537
+ qApp.processEvents()
538
+ return
539
+
540
+ def updateDocMargins(self) -> None:
572
541
  """Automatically adjust the margins so the text is centred if
573
542
  we have a text width set or we're in Focus Mode. Otherwise, just
574
543
  ensure the margins are set correctly.
@@ -594,6 +563,7 @@ class GuiDocEditor(QTextEdit):
594
563
  fY = wH - fH - tB - sH
595
564
  self.docHeader.setGeometry(tB, tB, tW, tH)
596
565
  self.docFooter.setGeometry(tB, fY, tW, fH)
566
+ self.docToolBar.move(0, tH)
597
567
 
598
568
  rH = 0
599
569
  if self.docSearch.isVisible():
@@ -612,19 +582,19 @@ class GuiDocEditor(QTextEdit):
612
582
  # Getters
613
583
  ##
614
584
 
615
- def getText(self):
585
+ def getText(self) -> str:
616
586
  """Get the text content of the current document. This method uses
617
587
  QTextDocument->toRawText instead of toPlainText. The former preserves
618
588
  non-breaking spaces, the latter does not. We still want to get rid of
619
589
  paragraph and line separators though.
620
590
  See: https://doc.qt.io/qt-5/qtextdocument.html#toPlainText
621
591
  """
622
- theText = self.document().toRawText()
623
- theText = theText.replace(nwUnicode.U_LSEP, "\n") # Line separators
624
- theText = theText.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
625
- return theText
592
+ text = self._qDocument.toRawText()
593
+ text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
594
+ text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
595
+ return text
626
596
 
627
- def getCursorPosition(self):
597
+ def getCursorPosition(self) -> int:
628
598
  """Find the cursor position in the document. If the editor has a
629
599
  selection, return the position of the end of the selection.
630
600
  """
@@ -634,64 +604,40 @@ class GuiDocEditor(QTextEdit):
634
604
  # Setters
635
605
  ##
636
606
 
637
- def setDocumentChanged(self, bValue):
607
+ def setDocumentChanged(self, state: bool) -> bool:
638
608
  """Keep track of the document changed variable, and emit the
639
609
  document change signal.
640
610
  """
641
- self._docChanged = bValue
611
+ self._docChanged = state
642
612
  self.editedStatusChanged.emit(self._docChanged)
643
613
  return self._docChanged
644
614
 
645
- def setCursorPosition(self, position):
646
- """Move the cursor to a given position in the document.
647
- """
648
- if not isinstance(position, int):
649
- return False
650
-
651
- nChars = self.document().characterCount()
652
- if nChars > 1:
653
- theCursor = self.textCursor()
654
- theCursor.setPosition(minmax(position, 0, nChars-1))
655
- self.setTextCursor(theCursor)
656
-
657
- # By default, the editor scrolls so the cursor is on the
658
- # last line, so we must correct it. The user setting for
659
- # auto-scroll is used to determine the scroll distance. This
660
- # makes it compatible with the typewriter scrolling feature
661
- # when it is enabled. By default, it's 30% of viewport.
662
- vPos = self.verticalScrollBar().value()
663
- cPos = self.cursorRect().topLeft().y()
664
- mPos = int(CONFIG.autoScrollPos*0.01 * self.viewport().height())
665
- if cPos > mPos:
666
- # Only scroll if the cursor is past the auto-scroll limit
667
- self.verticalScrollBar().setValue(max(0, vPos + cPos - mPos))
668
-
615
+ def setCursorPosition(self, position: int) -> None:
616
+ """Move the cursor to a given position in the document."""
617
+ nChars = self._qDocument.characterCount()
618
+ if nChars > 1 and isinstance(position, int):
619
+ cursor = self.textCursor()
620
+ cursor.setPosition(minmax(position, 0, nChars-1))
621
+ self.setTextCursor(cursor)
622
+ self.centerCursor()
669
623
  self.docFooter.updateLineCount()
624
+ return
670
625
 
671
- return True
672
-
673
- def saveCursorPosition(self):
674
- """Save the cursor position to the current project item object.
675
- """
626
+ def saveCursorPosition(self) -> None:
627
+ """Save the cursor position to the current project item."""
676
628
  if self._nwItem is not None:
677
629
  cursPos = self.getCursorPosition()
678
630
  self._nwItem.setCursorPos(cursPos)
679
631
  return
680
632
 
681
- def setCursorLine(self, lineNo):
682
- """Move the cursor to a given line in the document.
683
- """
684
- if not isinstance(lineNo, int):
685
- return False
686
-
687
- lineIdx = lineNo - 1 # Block index is 0 offset, lineNo is 1 offset
688
- if lineIdx >= 0:
689
- theBlock = self.document().findBlockByLineNumber(lineIdx)
690
- if theBlock:
691
- self.setCursorPosition(theBlock.position())
692
- logger.debug("Cursor moved to line %d", lineNo)
693
-
694
- return True
633
+ def setCursorLine(self, line: int | None) -> None:
634
+ """Move the cursor to a given line in the document."""
635
+ if isinstance(line, int) and line > 0:
636
+ block = self._qDocument.findBlockByNumber(line - 1)
637
+ if block:
638
+ self.setCursorPosition(block.position())
639
+ logger.debug("Cursor moved to line %d", line)
640
+ return
695
641
 
696
642
  ##
697
643
  # Spell Checking
@@ -700,30 +646,25 @@ class GuiDocEditor(QTextEdit):
700
646
  def toggleSpellCheck(self, state: bool | None) -> None:
701
647
  """This is the main spell check setting function, and this one
702
648
  should call all other setSpellCheck functions in other classes.
703
- If the spell check mode (theMode) is not defined (None), then
704
- toggle the current status saved in this class.
649
+ If the spell check state is not defined (None), then toggle the
650
+ current status saved in this class.
705
651
  """
706
652
  if state is None:
707
- state = not self._spellCheck
653
+ state = not SHARED.project.data.spellCheck
708
654
 
709
- if not CONFIG.hasEnchant:
710
- if state:
711
- SHARED.info(self.tr(
712
- "Spell checking requires the package PyEnchant. "
713
- "It does not appear to be installed."
714
- ))
655
+ if SHARED.spelling.spellLanguage is None:
715
656
  state = False
716
657
 
717
- if SHARED.spelling.spellLanguage is None:
658
+ if state and not CONFIG.hasEnchant:
659
+ SHARED.info(self.tr(
660
+ "Spell checking requires the package PyEnchant. "
661
+ "It does not appear to be installed."
662
+ ))
718
663
  state = False
719
664
 
720
- self._spellCheck = state
721
- self.mainGui.mainMenu.setSpellCheck(state)
722
665
  SHARED.project.data.setSpellCheck(state)
723
- self.highLight.setSpellCheck(state)
724
- if not self._bigDoc or state is False:
725
- # We don't run the spell checker automatically on big docs
726
- self.spellCheckDocument()
666
+ self.spellCheckStateChanged.emit(state)
667
+ self.spellCheckDocument()
727
668
 
728
669
  logger.debug("Spell check is set to '%s'", str(state))
729
670
 
@@ -737,12 +678,8 @@ class GuiDocEditor(QTextEdit):
737
678
  """
738
679
  logger.debug("Running spell checker")
739
680
  start = time()
740
- qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
741
- if self._bigDoc:
742
- # This is much faster for large documents
743
- self.setPlainText(self.getText())
744
- else:
745
- self.highLight.rehighlight()
681
+ qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
682
+ self._qDocument.syntaxHighlighter.rehighlight()
746
683
  qApp.restoreOverrideCursor()
747
684
  logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
748
685
  self.statusMessage.emit(self.tr("Spell check complete"))
@@ -752,7 +689,7 @@ class GuiDocEditor(QTextEdit):
752
689
  # General Class Methods
753
690
  ##
754
691
 
755
- def docAction(self, theAction):
692
+ def docAction(self, action: nwDocAction) -> bool:
756
693
  """Perform an action on the current document based on an action
757
694
  flag. This is just a single entry point wrapper function to
758
695
  ensure all the feature functions get the correct information
@@ -763,71 +700,83 @@ class GuiDocEditor(QTextEdit):
763
700
  logger.error("No document open")
764
701
  return False
765
702
 
766
- if not isinstance(theAction, nwDocAction):
703
+ if not isinstance(action, nwDocAction):
767
704
  logger.error("Not a document action")
768
705
  return False
769
706
 
770
- logger.debug("Requesting action: %s", theAction.name)
707
+ logger.debug("Requesting action: %s", action.name)
771
708
 
772
709
  self._allowAutoReplace(False)
773
- if theAction == nwDocAction.UNDO:
710
+ if action == nwDocAction.UNDO:
774
711
  self.undo()
775
- elif theAction == nwDocAction.REDO:
712
+ elif action == nwDocAction.REDO:
776
713
  self.redo()
777
- elif theAction == nwDocAction.CUT:
714
+ elif action == nwDocAction.CUT:
778
715
  self.cut()
779
- elif theAction == nwDocAction.COPY:
716
+ elif action == nwDocAction.COPY:
780
717
  self.copy()
781
- elif theAction == nwDocAction.PASTE:
718
+ elif action == nwDocAction.PASTE:
782
719
  self.paste()
783
- elif theAction == nwDocAction.EMPH:
720
+ elif action == nwDocAction.EMPH:
784
721
  self._toggleFormat(1, "_")
785
- elif theAction == nwDocAction.STRONG:
722
+ elif action == nwDocAction.STRONG:
786
723
  self._toggleFormat(2, "*")
787
- elif theAction == nwDocAction.STRIKE:
724
+ elif action == nwDocAction.STRIKE:
788
725
  self._toggleFormat(2, "~")
789
- elif theAction == nwDocAction.S_QUOTE:
726
+ elif action == nwDocAction.S_QUOTE:
790
727
  self._wrapSelection(self._typSQuoteO, self._typSQuoteC)
791
- elif theAction == nwDocAction.D_QUOTE:
728
+ elif action == nwDocAction.D_QUOTE:
792
729
  self._wrapSelection(self._typDQuoteO, self._typDQuoteC)
793
- elif theAction == nwDocAction.SEL_ALL:
794
- self._makeSelection(QTextCursor.Document)
795
- elif theAction == nwDocAction.SEL_PARA:
796
- self._makeSelection(QTextCursor.BlockUnderCursor)
797
- elif theAction == nwDocAction.BLOCK_H1:
730
+ elif action == nwDocAction.SEL_ALL:
731
+ self._makeSelection(QTextCursor.SelectionType.Document)
732
+ elif action == nwDocAction.SEL_PARA:
733
+ self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor)
734
+ elif action == nwDocAction.BLOCK_H1:
798
735
  self._formatBlock(nwDocAction.BLOCK_H1)
799
- elif theAction == nwDocAction.BLOCK_H2:
736
+ elif action == nwDocAction.BLOCK_H2:
800
737
  self._formatBlock(nwDocAction.BLOCK_H2)
801
- elif theAction == nwDocAction.BLOCK_H3:
738
+ elif action == nwDocAction.BLOCK_H3:
802
739
  self._formatBlock(nwDocAction.BLOCK_H3)
803
- elif theAction == nwDocAction.BLOCK_H4:
740
+ elif action == nwDocAction.BLOCK_H4:
804
741
  self._formatBlock(nwDocAction.BLOCK_H4)
805
- elif theAction == nwDocAction.BLOCK_COM:
742
+ elif action == nwDocAction.BLOCK_COM:
806
743
  self._formatBlock(nwDocAction.BLOCK_COM)
807
- elif theAction == nwDocAction.BLOCK_TXT:
744
+ elif action == nwDocAction.BLOCK_TXT:
808
745
  self._formatBlock(nwDocAction.BLOCK_TXT)
809
- elif theAction == nwDocAction.BLOCK_TTL:
746
+ elif action == nwDocAction.BLOCK_TTL:
810
747
  self._formatBlock(nwDocAction.BLOCK_TTL)
811
- elif theAction == nwDocAction.BLOCK_UNN:
748
+ elif action == nwDocAction.BLOCK_UNN:
812
749
  self._formatBlock(nwDocAction.BLOCK_UNN)
813
- elif theAction == nwDocAction.REPL_SNG:
750
+ elif action == nwDocAction.REPL_SNG:
814
751
  self._replaceQuotes("'", self._typSQuoteO, self._typSQuoteC)
815
- elif theAction == nwDocAction.REPL_DBL:
752
+ elif action == nwDocAction.REPL_DBL:
816
753
  self._replaceQuotes("\"", self._typDQuoteO, self._typDQuoteC)
817
- elif theAction == nwDocAction.RM_BREAKS:
754
+ elif action == nwDocAction.RM_BREAKS:
818
755
  self._removeInParLineBreaks()
819
- elif theAction == nwDocAction.ALIGN_L:
756
+ elif action == nwDocAction.ALIGN_L:
820
757
  self._formatBlock(nwDocAction.ALIGN_L)
821
- elif theAction == nwDocAction.ALIGN_C:
758
+ elif action == nwDocAction.ALIGN_C:
822
759
  self._formatBlock(nwDocAction.ALIGN_C)
823
- elif theAction == nwDocAction.ALIGN_R:
760
+ elif action == nwDocAction.ALIGN_R:
824
761
  self._formatBlock(nwDocAction.ALIGN_R)
825
- elif theAction == nwDocAction.INDENT_L:
762
+ elif action == nwDocAction.INDENT_L:
826
763
  self._formatBlock(nwDocAction.INDENT_L)
827
- elif theAction == nwDocAction.INDENT_R:
764
+ elif action == nwDocAction.INDENT_R:
828
765
  self._formatBlock(nwDocAction.INDENT_R)
766
+ elif action == nwDocAction.SC_ITALIC:
767
+ self._wrapSelection(nwShortcode.ITALIC_O, nwShortcode.ITALIC_C)
768
+ elif action == nwDocAction.SC_BOLD:
769
+ self._wrapSelection(nwShortcode.BOLD_O, nwShortcode.BOLD_C)
770
+ elif action == nwDocAction.SC_STRIKE:
771
+ self._wrapSelection(nwShortcode.STRIKE_O, nwShortcode.STRIKE_C)
772
+ elif action == nwDocAction.SC_ULINE:
773
+ self._wrapSelection(nwShortcode.ULINE_O, nwShortcode.ULINE_C)
774
+ elif action == nwDocAction.SC_SUP:
775
+ self._wrapSelection(nwShortcode.SUP_O, nwShortcode.SUP_C)
776
+ elif action == nwDocAction.SC_SUB:
777
+ self._wrapSelection(nwShortcode.SUB_O, nwShortcode.SUB_C)
829
778
  else:
830
- logger.debug("Unknown or unsupported document action '%s'", str(theAction))
779
+ logger.debug("Unknown or unsupported document action '%s'", str(action))
831
780
  self._allowAutoReplace(True)
832
781
  return False
833
782
 
@@ -836,37 +785,33 @@ class GuiDocEditor(QTextEdit):
836
785
 
837
786
  return True
838
787
 
839
- def anyFocus(self):
840
- """Check if any widget or child widget has focus.
841
- """
788
+ def anyFocus(self) -> bool:
789
+ """Check if any widget or child widget has focus."""
842
790
  if self.hasFocus():
843
791
  return True
844
792
  if self.isAncestorOf(qApp.focusWidget()):
845
793
  return True
846
794
  return False
847
795
 
848
- def revealLocation(self):
796
+ def revealLocation(self) -> None:
849
797
  """Tell the user where on the file system the file in the editor
850
798
  is saved.
851
799
  """
852
- if self._nwDocument is None:
853
- logger.error("No document open")
854
- return False
855
- SHARED.info(
856
- "<br>".join([
857
- self.tr("Document Details"),
858
- ""*40,
859
- self.tr("Created: {0}").format(self._nwDocument.createdDate),
860
- self.tr("Updated: {0}").format(self._nwDocument.updatedDate),
861
- ]),
862
- details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation),
863
- log=False
864
- )
800
+ if isinstance(self._nwDocument, NWDocument):
801
+ SHARED.info(
802
+ "<br>".join([
803
+ self.tr("Document Details"),
804
+ ""*40,
805
+ self.tr("Created: {0}").format(self._nwDocument.createdDate),
806
+ self.tr("Updated: {0}").format(self._nwDocument.updatedDate),
807
+ ]),
808
+ details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation),
809
+ log=False
810
+ )
865
811
  return
866
812
 
867
- def insertText(self, theInsert):
868
- """Insert a specific type of text at the cursor position.
869
- """
813
+ def insertText(self, insert: str | nwDocInsert) -> bool:
814
+ """Insert a specific type of text at the cursor position."""
870
815
  if self._docHandle is None:
871
816
  logger.error("No document open")
872
817
  return False
@@ -874,31 +819,35 @@ class GuiDocEditor(QTextEdit):
874
819
  newBlock = False
875
820
  goAfter = False
876
821
 
877
- if isinstance(theInsert, str):
878
- theText = theInsert
879
- elif isinstance(theInsert, nwDocInsert):
880
- if theInsert == nwDocInsert.QUOTE_LS:
881
- theText = self._typSQuoteO
882
- elif theInsert == nwDocInsert.QUOTE_RS:
883
- theText = self._typSQuoteC
884
- elif theInsert == nwDocInsert.QUOTE_LD:
885
- theText = self._typDQuoteO
886
- elif theInsert == nwDocInsert.QUOTE_RD:
887
- theText = self._typDQuoteC
888
- elif theInsert == nwDocInsert.SYNOPSIS:
889
- theText = "% Synopsis: "
822
+ if isinstance(insert, str):
823
+ text = insert
824
+ elif isinstance(insert, nwDocInsert):
825
+ if insert == nwDocInsert.QUOTE_LS:
826
+ text = self._typSQuoteO
827
+ elif insert == nwDocInsert.QUOTE_RS:
828
+ text = self._typSQuoteC
829
+ elif insert == nwDocInsert.QUOTE_LD:
830
+ text = self._typDQuoteO
831
+ elif insert == nwDocInsert.QUOTE_RD:
832
+ text = self._typDQuoteC
833
+ elif insert == nwDocInsert.SYNOPSIS:
834
+ text = "% Synopsis: "
835
+ newBlock = True
836
+ goAfter = True
837
+ elif insert == nwDocInsert.SHORT:
838
+ text = "% Short: "
890
839
  newBlock = True
891
840
  goAfter = True
892
- elif theInsert == nwDocInsert.NEW_PAGE:
893
- theText = "[NEW PAGE]"
841
+ elif insert == nwDocInsert.NEW_PAGE:
842
+ text = "[newpage]"
894
843
  newBlock = True
895
844
  goAfter = False
896
- elif theInsert == nwDocInsert.VSPACE_S:
897
- theText = "[VSPACE]"
845
+ elif insert == nwDocInsert.VSPACE_S:
846
+ text = "[vspace]"
898
847
  newBlock = True
899
848
  goAfter = False
900
- elif theInsert == nwDocInsert.VSPACE_M:
901
- theText = "[VSPACE:2]"
849
+ elif insert == nwDocInsert.VSPACE_M:
850
+ text = "[vspace:2]"
902
851
  newBlock = True
903
852
  goAfter = False
904
853
  else:
@@ -907,79 +856,55 @@ class GuiDocEditor(QTextEdit):
907
856
  return False
908
857
 
909
858
  if newBlock:
910
- self.insertNewBlock(theText, defaultAfter=goAfter)
859
+ self.insertNewBlock(text, defaultAfter=goAfter)
911
860
  else:
912
- theCursor = self.textCursor()
913
- theCursor.beginEditBlock()
914
- theCursor.insertText(theText)
915
- theCursor.endEditBlock()
861
+ cursor = self.textCursor()
862
+ cursor.beginEditBlock()
863
+ cursor.insertText(text)
864
+ cursor.endEditBlock()
916
865
 
917
866
  return True
918
867
 
919
- def insertNewBlock(self, theText, defaultAfter=True):
920
- """Insert a piece of text on a blank line.
921
- """
922
- theCursor = self.textCursor()
923
- theBlock = theCursor.block()
924
- if not theBlock.isValid():
868
+ def insertNewBlock(self, text: str, defaultAfter: bool = True) -> bool:
869
+ """Insert a piece of text on a blank line."""
870
+ cursor = self.textCursor()
871
+ block = cursor.block()
872
+ if not block.isValid():
925
873
  logger.error("Not a valid text block")
926
874
  return False
927
875
 
928
- sPos = theBlock.position()
929
- sLen = theBlock.length()
876
+ sPos = block.position()
877
+ sLen = block.length()
930
878
 
931
- theCursor.beginEditBlock()
879
+ cursor.beginEditBlock()
932
880
 
933
881
  if sLen > 1 and defaultAfter:
934
- theCursor.setPosition(sPos + sLen - 1)
935
- theCursor.insertText("\n")
882
+ cursor.setPosition(sPos + sLen - 1)
883
+ cursor.insertText("\n")
936
884
  else:
937
- theCursor.setPosition(sPos)
885
+ cursor.setPosition(sPos)
938
886
 
939
- theCursor.insertText(theText)
887
+ cursor.insertText(text)
940
888
 
941
889
  if sLen > 1 and not defaultAfter:
942
- theCursor.insertText("\n")
890
+ cursor.insertText("\n")
943
891
 
944
- theCursor.endEditBlock()
892
+ cursor.endEditBlock()
945
893
 
946
- self.setTextCursor(theCursor)
894
+ self.setTextCursor(cursor)
947
895
 
948
896
  return True
949
897
 
950
- def insertKeyWord(self, keyWord):
951
- """Insert a keyword in the text editor, at the cursor position.
952
- If the insert line is not blank, a new line is started.
953
- """
954
- if keyWord not in nwKeyWords.VALID_KEYS:
955
- logger.error("Invalid keyword '%s'", keyWord)
956
- return False
957
-
958
- logger.debug("Inserting keyword '%s'", keyWord)
959
- theState = self.insertNewBlock("%s: " % keyWord)
960
-
961
- return theState
962
-
963
- def closeSearch(self):
964
- """Close the search box.
965
- """
898
+ def closeSearch(self) -> bool:
899
+ """Close the search box."""
966
900
  self.docSearch.closeSearch()
967
901
  return self.docSearch.isVisible()
968
902
 
969
- def toggleSearch(self):
970
- """Toggle the visibility of the search box.
971
- """
972
- if self.docSearch.isVisible():
973
- self.docSearch.closeSearch()
974
- else:
975
- self.beginSearch()
976
- return
977
-
978
903
  ##
979
904
  # Document Events and Maintenance
980
905
  ##
981
906
 
982
- def keyPressEvent(self, keyEvent):
907
+ def keyPressEvent(self, event: QKeyEvent) -> None:
983
908
  """Intercept key press events.
984
909
  We need to intercept a few key sequences:
985
910
  * The return and enter keys redirect here even if the search
@@ -990,49 +915,40 @@ class GuiDocEditor(QTextEdit):
990
915
  * We also handle automatic scrolling here.
991
916
  """
992
917
  self._lastActive = time()
993
- isReturn = keyEvent.key() == Qt.Key_Return
994
- isReturn |= keyEvent.key() == Qt.Key_Enter
918
+ isReturn = event.key() == Qt.Key.Key_Return
919
+ isReturn |= event.key() == Qt.Key.Key_Enter
995
920
  if isReturn and self.docSearch.anyFocus():
996
921
  return
997
- elif keyEvent == QKeySequence.Redo:
922
+ elif event == QKeySequence.StandardKey.Redo:
998
923
  self.docAction(nwDocAction.REDO)
999
924
  return
1000
- elif keyEvent == QKeySequence.Undo:
925
+ elif event == QKeySequence.StandardKey.Undo:
1001
926
  self.docAction(nwDocAction.UNDO)
1002
927
  return
1003
- elif keyEvent == QKeySequence.SelectAll:
928
+ elif event == QKeySequence.StandardKey.SelectAll:
1004
929
  self.docAction(nwDocAction.SEL_ALL)
1005
930
  return
1006
931
 
1007
932
  if CONFIG.autoScroll:
1008
-
1009
- cOld = self.cursorRect().center().y()
1010
- super().keyPressEvent(keyEvent)
1011
-
1012
- kMod = keyEvent.modifiers()
1013
- okMod = kMod == Qt.NoModifier or kMod == Qt.ShiftModifier
1014
- okKey = keyEvent.key() not in self.MOVE_KEYS
1015
- if okMod and okKey:
1016
- cNew = self.cursorRect().center().y()
1017
- cMov = cNew - cOld
933
+ cPos = self.cursorRect().topLeft().y()
934
+ super().keyPressEvent(event)
935
+ nPos = self.cursorRect().topLeft().y()
936
+ kMod = event.modifiers()
937
+ okMod = kMod in (Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.ShiftModifier)
938
+ okKey = event.key() not in self.MOVE_KEYS
939
+ if nPos != cPos and okMod and okKey:
1018
940
  mPos = CONFIG.autoScrollPos*0.01 * self.viewport().height()
1019
- if abs(cMov) > 0 and cOld > mPos:
1020
- # Move the scroll bar
941
+ if cPos > mPos:
1021
942
  vBar = self.verticalScrollBar()
1022
- doAnim = QPropertyAnimation(vBar, b"value", self)
1023
- doAnim.setDuration(120)
1024
- doAnim.setStartValue(vBar.value())
1025
- doAnim.setEndValue(vBar.value() + cMov)
1026
- doAnim.start()
1027
-
943
+ vBar.setValue(vBar.value() + (1 if nPos > cPos else -1))
1028
944
  else:
1029
- super().keyPressEvent(keyEvent)
945
+ super().keyPressEvent(event)
1030
946
 
1031
947
  self.docFooter.updateLineCount()
1032
948
 
1033
949
  return
1034
950
 
1035
- def focusNextPrevChild(self, toNext):
951
+ def focusNextPrevChild(self, next: bool) -> bool:
1036
952
  """Capture the focus request from the tab key on the text
1037
953
  editor. If the editor has focus, we do not change focus and
1038
954
  allow the editor to insert a tab. If the search bar has focus,
@@ -1041,29 +957,26 @@ class GuiDocEditor(QTextEdit):
1041
957
  if self.hasFocus():
1042
958
  return False
1043
959
  elif self.docSearch.isVisible():
1044
- return self.docSearch.cycleFocus(toNext)
960
+ return self.docSearch.cycleFocus(next)
1045
961
  return True
1046
962
 
1047
- def mouseReleaseEvent(self, theEvent):
963
+ def mouseReleaseEvent(self, event: QMouseEvent) -> None:
1048
964
  """If the mouse button is released and the control key is
1049
965
  pressed, check if we're clicking on a tag, and trigger the
1050
966
  follow tag function.
1051
967
  """
1052
- if qApp.keyboardModifiers() == Qt.ControlModifier:
1053
- theCursor = self.cursorForPosition(theEvent.pos())
1054
- self._followTag(theCursor)
1055
-
1056
- super().mouseReleaseEvent(theEvent)
968
+ if qApp.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier:
969
+ self._processTag(self.cursorForPosition(event.pos()))
970
+ super().mouseReleaseEvent(event)
1057
971
  self.docFooter.updateLineCount()
1058
-
1059
972
  return
1060
973
 
1061
- def resizeEvent(self, theEvent):
974
+ def resizeEvent(self, event: QResizeEvent) -> None:
1062
975
  """If the text editor is resized, we must make sure the document
1063
976
  has its margins adjusted according to user preferences.
1064
977
  """
1065
978
  self.updateDocMargins()
1066
- super().resizeEvent(theEvent)
979
+ super().resizeEvent(event)
1067
980
  return
1068
981
 
1069
982
  ##
@@ -1071,7 +984,7 @@ class GuiDocEditor(QTextEdit):
1071
984
  ##
1072
985
 
1073
986
  @pyqtSlot(str)
1074
- def updateDocInfo(self, tHandle):
987
+ def updateDocInfo(self, tHandle: str) -> None:
1075
988
  """Called when an item label is changed to check if the document
1076
989
  title bar needs updating,
1077
990
  """
@@ -1081,168 +994,185 @@ class GuiDocEditor(QTextEdit):
1081
994
  self.updateDocMargins()
1082
995
  return
1083
996
 
997
+ @pyqtSlot(str)
998
+ def insertKeyWord(self, keyword: str) -> bool:
999
+ """Insert a keyword in the text editor, at the cursor position.
1000
+ If the insert line is not blank, a new line is started.
1001
+ """
1002
+ if keyword not in nwKeyWords.VALID_KEYS:
1003
+ logger.error("Invalid keyword '%s'", keyword)
1004
+ return False
1005
+ logger.debug("Inserting keyword '%s'", keyword)
1006
+ state = self.insertNewBlock("%s: " % keyword)
1007
+ return state
1008
+
1009
+ @pyqtSlot()
1010
+ def toggleSearch(self) -> None:
1011
+ """Toggle the visibility of the search box."""
1012
+ if self.docSearch.isVisible():
1013
+ self.docSearch.closeSearch()
1014
+ else:
1015
+ self.beginSearch()
1016
+ return
1017
+
1084
1018
  ##
1085
1019
  # Private Slots
1086
1020
  ##
1087
1021
 
1088
1022
  @pyqtSlot(int, int, int)
1089
- def _docChange(self, thePos, chrRem, chrAdd):
1023
+ def _docChange(self, pos: int, removed: int, added: int) -> None:
1090
1024
  """Triggered by QTextDocument->contentsChanged. This also
1091
1025
  triggers the syntax highlighter.
1092
1026
  """
1093
1027
  self._lastEdit = time()
1094
1028
  self._lastFind = None
1095
1029
 
1096
- if self.document().characterCount() > nwConst.MAX_DOCSIZE:
1097
- SHARED.error(self.tr(
1098
- "The document has grown too big and you cannot add more text to it. "
1099
- "The maximum size of a single novelWriter document is {0} MB."
1100
- ).format(
1101
- f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
1102
- ))
1103
- self.undo()
1104
- return
1105
-
1106
1030
  if not self._docChanged:
1107
- self.setDocumentChanged(chrRem != 0 or chrAdd != 0)
1031
+ self.setDocumentChanged(removed != 0 or added != 0)
1108
1032
 
1109
1033
  if not self.wcTimerDoc.isActive():
1110
1034
  self.wcTimerDoc.start()
1111
1035
 
1112
- if self._doReplace and chrAdd == 1:
1113
- self._docAutoReplace(self.document().findBlock(thePos))
1114
-
1115
- return
1116
-
1117
- @pyqtSlot("QPoint")
1118
- def _openContextMenu(self, thePos):
1119
- """Triggered by right click to open the context menu. Also
1120
- triggered by the Ctrl+. shortcut.
1121
- """
1122
- userCursor = self.textCursor()
1123
- userSelection = userCursor.hasSelection()
1124
- posCursor = self.cursorForPosition(thePos)
1125
-
1126
- mnuContext = QMenu(self)
1127
-
1128
- # Follow, Cut, Copy and Paste
1129
- # ===========================
1036
+ block = self._qDocument.findBlock(pos)
1037
+ if not block.isValid():
1038
+ return
1130
1039
 
1131
- if self._followTag(theCursor=posCursor, loadTag=False):
1132
- mnuTag = QAction(self.tr("Follow Tag"), mnuContext)
1133
- mnuTag.triggered.connect(lambda: self._followTag(theCursor=posCursor))
1134
- mnuContext.addAction(mnuTag)
1135
- mnuContext.addSeparator()
1040
+ text = block.text()
1041
+ if text.startswith("@") and added + removed == 1:
1042
+ # Only run on single keypresses, otherwise it will trigger
1043
+ # at unwanted times when other changes are made to the document
1044
+ cursor = self.textCursor()
1045
+ bPos = cursor.positionInBlock()
1046
+ if bPos > 0:
1047
+ show = self._completer.updateText(text, bPos)
1048
+ point = self.cursorRect().bottomRight()
1049
+ self._completer.move(self.viewport().mapToGlobal(point))
1050
+ self._completer.setVisible(show)
1051
+ else:
1052
+ self._completer.setVisible(False)
1136
1053
 
1137
- if userSelection:
1138
- mnuCut = QAction(self.tr("Cut"), mnuContext)
1139
- mnuCut.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
1140
- mnuContext.addAction(mnuCut)
1054
+ if self._doReplace and added == 1:
1055
+ self._docAutoReplace(text)
1141
1056
 
1142
- mnuCopy = QAction(self.tr("Copy"), mnuContext)
1143
- mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
1144
- mnuContext.addAction(mnuCopy)
1057
+ return
1145
1058
 
1146
- mnuPaste = QAction(self.tr("Paste"), mnuContext)
1147
- mnuPaste.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
1148
- mnuContext.addAction(mnuPaste)
1059
+ @pyqtSlot(int, int, str)
1060
+ def _insertCompletion(self, pos: int, length: int, text: str) -> None:
1061
+ """Insert choice from the completer menu."""
1062
+ cursor = self.textCursor()
1063
+ block = cursor.block()
1064
+ if block.isValid():
1065
+ pos += block.position()
1066
+ cursor.setPosition(pos, QTextCursor.MoveMode.MoveAnchor)
1067
+ cursor.setPosition(pos + length, QTextCursor.MoveMode.KeepAnchor)
1068
+ cursor.insertText(text)
1069
+ self._completer.hide()
1070
+ return
1149
1071
 
1150
- mnuContext.addSeparator()
1072
+ @pyqtSlot("QPoint")
1073
+ def _openContextMenu(self, pos: QPoint) -> None:
1074
+ """Open the editor context menu at a given coordinate."""
1075
+ uCursor = self.textCursor()
1076
+ pCursor = self.cursorForPosition(pos)
1077
+ pBlock = pCursor.block()
1078
+
1079
+ ctxMenu = QMenu(self)
1080
+ ctxMenu.setObjectName("ContextMenu")
1081
+ if pBlock.userState() == GuiDocHighlighter.BLOCK_TITLE:
1082
+ action = ctxMenu.addAction(self.tr("Set as Document Name"))
1083
+ action.triggered.connect(lambda: self._emitRenameItem(pBlock))
1084
+
1085
+ # Follow
1086
+ status = self._processTag(cursor=pCursor, follow=False)
1087
+ if status == nwTrinary.POSITIVE:
1088
+ action = ctxMenu.addAction(self.tr("Follow Tag"))
1089
+ action.triggered.connect(lambda: self._processTag(cursor=pCursor, follow=True))
1090
+ ctxMenu.addSeparator()
1091
+ elif status == nwTrinary.NEGATIVE:
1092
+ action = ctxMenu.addAction(self.tr("Create Note for Tag"))
1093
+ action.triggered.connect(lambda: self._processTag(cursor=pCursor, create=True))
1094
+ ctxMenu.addSeparator()
1095
+
1096
+ # Cut, Copy and Paste
1097
+ if uCursor.hasSelection():
1098
+ action = ctxMenu.addAction(self.tr("Cut"))
1099
+ action.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
1100
+ action = ctxMenu.addAction(self.tr("Copy"))
1101
+ action.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
1102
+
1103
+ action = ctxMenu.addAction(self.tr("Paste"))
1104
+ action.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
1105
+ ctxMenu.addSeparator()
1151
1106
 
1152
1107
  # Selections
1153
- # ==========
1154
-
1155
- mnuSelAll = QAction(self.tr("Select All"), mnuContext)
1156
- mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
1157
- mnuContext.addAction(mnuSelAll)
1158
-
1159
- mnuSelWord = QAction(self.tr("Select Word"), mnuContext)
1160
- mnuSelWord.triggered.connect(
1161
- lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos)
1108
+ action = ctxMenu.addAction(self.tr("Select All"))
1109
+ action.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
1110
+ action = ctxMenu.addAction(self.tr("Select Word"))
1111
+ action.triggered.connect(
1112
+ lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, pos)
1162
1113
  )
1163
- mnuContext.addAction(mnuSelWord)
1164
-
1165
- mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext)
1166
- mnuSelPara.triggered.connect(
1167
- lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos)
1114
+ action = ctxMenu.addAction(self.tr("Select Paragraph"))
1115
+ action.triggered.connect(lambda: self._makePosSelection(
1116
+ QTextCursor.SelectionType.BlockUnderCursor, pos)
1168
1117
  )
1169
- mnuContext.addAction(mnuSelPara)
1170
1118
 
1171
1119
  # Spell Checking
1172
- # ==============
1173
-
1174
- posCursor = self.cursorForPosition(thePos)
1175
- spellCheck = self._spellCheck
1176
- theWord = ""
1177
-
1178
- if posCursor.block().text().startswith("@"):
1179
- spellCheck = False
1180
-
1181
- if spellCheck:
1182
- posCursor.select(QTextCursor.WordUnderCursor)
1183
- theWord = posCursor.selectedText().strip().strip(self._nonWord)
1184
- spellCheck &= theWord != ""
1185
-
1186
- if spellCheck:
1187
- logger.debug("Looking up '%s' in the dictionary", theWord)
1188
- spellCheck &= not SHARED.spelling.checkWord(theWord)
1189
-
1190
- if spellCheck:
1191
- mnuContext.addSeparator()
1192
- mnuHead = QAction(self.tr("Spelling Suggestion(s)"), mnuContext)
1193
- mnuContext.addAction(mnuHead)
1194
-
1195
- theSuggest = SHARED.spelling.suggestWords(theWord)[:15]
1196
- if len(theSuggest) > 0:
1197
- for aWord in theSuggest:
1198
- mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext)
1199
- mnuWord.triggered.connect(
1200
- lambda thePos, aWord=aWord: self._correctWord(posCursor, aWord)
1201
- )
1202
- mnuContext.addAction(mnuWord)
1203
- else:
1204
- mnuHead = QAction(
1205
- "%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions")), mnuContext
1120
+ if SHARED.project.data.spellCheck:
1121
+ word, cPos, cLen, suggest = self._qDocument.spellErrorAtPos(pCursor.position())
1122
+ if word and cPos >= 0 and cLen > 0:
1123
+ logger.debug("Word '%s' is misspelled", word)
1124
+ block = pCursor.block()
1125
+ sCursor = self.textCursor()
1126
+ sCursor.setPosition(block.position() + cPos)
1127
+ sCursor.movePosition(
1128
+ QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, cLen
1206
1129
  )
1207
- mnuContext.addAction(mnuHead)
1130
+ if suggest:
1131
+ ctxMenu.addSeparator()
1132
+ ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
1133
+ for option in suggest[:15]:
1134
+ action = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
1135
+ action.triggered.connect(
1136
+ lambda _, option=option: self._correctWord(sCursor, option)
1137
+ )
1138
+ else:
1139
+ ctxMenu.addAction("%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions")))
1208
1140
 
1209
- mnuContext.addSeparator()
1210
- mnuAdd = QAction(self.tr("Add Word to Dictionary"), mnuContext)
1211
- mnuAdd.triggered.connect(lambda thePos: self._addWord(posCursor))
1212
- mnuContext.addAction(mnuAdd)
1141
+ ctxMenu.addSeparator()
1142
+ action = ctxMenu.addAction(self.tr("Add Word to Dictionary"))
1143
+ action.triggered.connect(lambda: self._addWord(word, block))
1213
1144
 
1214
- # Open the context menu
1215
- mnuContext.exec_(self.viewport().mapToGlobal(thePos))
1145
+ # Execute the context menu
1146
+ ctxMenu.exec_(self.viewport().mapToGlobal(pos))
1216
1147
 
1217
1148
  return
1218
1149
 
1219
1150
  @pyqtSlot("QTextCursor", str)
1220
- def _correctWord(self, theCursor, theWord):
1151
+ def _correctWord(self, cursor: QTextCursor, word: str) -> None:
1221
1152
  """Slot for the spell check context menu triggering the
1222
1153
  replacement of a word with the word from the dictionary.
1223
1154
  """
1224
- xPos = theCursor.selectionStart()
1225
- theCursor.beginEditBlock()
1226
- theCursor.removeSelectedText()
1227
- theCursor.insertText(theWord)
1228
- theCursor.endEditBlock()
1229
- theCursor.setPosition(xPos)
1230
- self.setTextCursor(theCursor)
1155
+ pos = cursor.selectionStart()
1156
+ cursor.beginEditBlock()
1157
+ cursor.removeSelectedText()
1158
+ cursor.insertText(word)
1159
+ cursor.endEditBlock()
1160
+ cursor.setPosition(pos)
1161
+ self.setTextCursor(cursor)
1231
1162
  return
1232
1163
 
1233
- @pyqtSlot("QTextCursor")
1234
- def _addWord(self, theCursor):
1164
+ @pyqtSlot(str, "QTextBlock")
1165
+ def _addWord(self, word: str, block: QTextBlock) -> None:
1235
1166
  """Slot for the spell check context menu triggered when the user
1236
1167
  wants to add a word to the project dictionary.
1237
1168
  """
1238
- theWord = theCursor.selectedText().strip().strip(self._nonWord)
1239
- logger.debug("Added '%s' to project dictionary", theWord)
1240
- SHARED.spelling.addWord(theWord)
1241
- self.highLight.rehighlightBlock(theCursor.block())
1169
+ logger.debug("Added '%s' to project dictionary", word)
1170
+ SHARED.spelling.addWord(word)
1171
+ self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1242
1172
  return
1243
1173
 
1244
1174
  @pyqtSlot()
1245
- def _runDocCounter(self):
1175
+ def _runDocCounter(self) -> None:
1246
1176
  """Decide whether to run the word counter, or not due to
1247
1177
  inactivity.
1248
1178
  """
@@ -1253,39 +1183,32 @@ class GuiDocEditor(QTextEdit):
1253
1183
  logger.debug("Word counter is busy")
1254
1184
  return
1255
1185
 
1256
- if time() - self._lastEdit < 5 * self.wcInterval:
1186
+ if time() - self._lastEdit < 5.0 * self.wcInterval:
1257
1187
  logger.debug("Running word counter")
1258
- self.mainGui.threadPool.start(self.wCounterDoc)
1188
+ SHARED.runInThreadPool(self.wCounterDoc)
1259
1189
 
1260
1190
  return
1261
1191
 
1262
1192
  @pyqtSlot(int, int, int)
1263
- def _updateDocCounts(self, cCount, wCount, pCount):
1264
- """Slot for the word counter's finished signal
1265
- """
1193
+ def _updateDocCounts(self, cCount: int, wCount: int, pCount: int) -> None:
1194
+ """Process the word counter's finished signal."""
1266
1195
  if self._docHandle is None or self._nwItem is None:
1267
1196
  return
1268
1197
 
1269
1198
  logger.debug("Updating word count")
1270
1199
 
1271
- self._charCount = cCount
1272
- self._wordCount = wCount
1273
- self._paraCount = pCount
1274
-
1275
1200
  self._nwItem.setCharCount(cCount)
1276
1201
  self._nwItem.setWordCount(wCount)
1277
1202
  self._nwItem.setParaCount(pCount)
1278
1203
 
1279
1204
  # Must not be emitted if docHandle is None!
1280
1205
  self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
1281
-
1282
- self._checkDocSize(self.document().characterCount())
1283
1206
  self.docFooter.updateCounts()
1284
1207
 
1285
1208
  return
1286
1209
 
1287
1210
  @pyqtSlot()
1288
- def _updateSelectedStatus(self):
1211
+ def _updateSelectedStatus(self) -> None:
1289
1212
  """The user made a change in text selection. Forward this
1290
1213
  information to the footer, and start the selection word counter.
1291
1214
  """
@@ -1293,18 +1216,15 @@ class GuiDocEditor(QTextEdit):
1293
1216
  if not self.wcTimerSel.isActive():
1294
1217
  self.wcTimerSel.start()
1295
1218
  self.docFooter.setHasSelection(True)
1296
-
1297
1219
  else:
1298
1220
  self.wcTimerSel.stop()
1299
1221
  self.docFooter.setHasSelection(False)
1300
1222
  self.docFooter.updateCounts()
1301
-
1302
1223
  return
1303
1224
 
1304
1225
  @pyqtSlot()
1305
- def _runSelCounter(self):
1306
- """Update the selection word count.
1307
- """
1226
+ def _runSelCounter(self) -> None:
1227
+ """Update the selection word count."""
1308
1228
  if self._docHandle is None:
1309
1229
  return
1310
1230
 
@@ -1312,14 +1232,13 @@ class GuiDocEditor(QTextEdit):
1312
1232
  logger.debug("Selection word counter is busy")
1313
1233
  return
1314
1234
 
1315
- self.mainGui.threadPool.start(self.wCounterSel)
1235
+ SHARED.runInThreadPool(self.wCounterSel)
1316
1236
 
1317
1237
  return
1318
1238
 
1319
1239
  @pyqtSlot(int, int, int)
1320
- def _updateSelCounts(self, cCount, wCount, pCount):
1321
- """Slot for the word counter's finished signal
1322
- """
1240
+ def _updateSelCounts(self, cCount: int, wCount: int, pCount: int) -> None:
1241
+ """Update the counts on the counter's finished signal."""
1323
1242
  if self._docHandle is None or self._nwItem is None:
1324
1243
  return
1325
1244
 
@@ -1329,51 +1248,44 @@ class GuiDocEditor(QTextEdit):
1329
1248
 
1330
1249
  return
1331
1250
 
1332
- @pyqtSlot("QSizeF")
1333
- def _docSizeChanged(self, theSize):
1334
- """Called whenever the underlying document layout size changes.
1335
- This is used to queue the repositioning of the cursor for very
1336
- large documents to ensure the region where the cursor is being
1337
- moved to has been drawn before the move is made.
1338
- """
1339
- if self._queuePos is not None:
1340
- thePos = self.document().documentLayout().hitTest(
1341
- QPointF(theSize.width(), theSize.height()), Qt.FuzzyHit
1342
- )
1343
- if self._queuePos <= thePos:
1344
- logger.debug("Allowed cursor move to %d <= %d", self._queuePos, thePos)
1345
- self.setCursorPosition(self._queuePos)
1346
- self._queuePos = None
1347
- else:
1348
- logger.debug("Denied cursor move to %d > %d", self._queuePos, thePos)
1251
+ @pyqtSlot()
1252
+ def _closeCurrentDocument(self) -> None:
1253
+ """Close the document. Forwarded to the main Gui."""
1254
+ self.closeDocumentRequest.emit()
1255
+ self.docToolBar.setVisible(False)
1256
+ return
1349
1257
 
1258
+ @pyqtSlot()
1259
+ def _toggleToolBarVisibility(self) -> None:
1260
+ """Toggle the visibility of the tool bar."""
1261
+ state = not self.docToolBar.isVisible()
1262
+ self.docToolBar.setVisible(state)
1263
+ CONFIG.showEditToolBar = state
1350
1264
  return
1351
1265
 
1352
1266
  ##
1353
1267
  # Search & Replace
1354
1268
  ##
1355
1269
 
1356
- def beginSearch(self):
1357
- """Set the selected text as the search text for the search bar.
1358
- """
1359
- theCursor = self.textCursor()
1360
- if theCursor.hasSelection():
1361
- self.docSearch.setSearchText(theCursor.selectedText())
1270
+ def beginSearch(self) -> None:
1271
+ """Set the selected text as the search text."""
1272
+ cursor = self.textCursor()
1273
+ if cursor.hasSelection():
1274
+ self.docSearch.setSearchText(cursor.selectedText())
1362
1275
  else:
1363
1276
  self.docSearch.setSearchText(None)
1364
1277
  resS, _ = self.findAllOccurences()
1365
1278
  self.docSearch.setResultCount(None, len(resS))
1366
1279
  return
1367
1280
 
1368
- def beginReplace(self):
1369
- """Initialise the search box and reset the replace text box.
1370
- """
1281
+ def beginReplace(self) -> None:
1282
+ """Initialise the search box and reset the replace text box."""
1371
1283
  self.beginSearch()
1372
1284
  self.docSearch.setReplaceText("")
1373
1285
  self.updateDocMargins()
1374
1286
  return
1375
1287
 
1376
- def findNext(self, goBack=False):
1288
+ def findNext(self, goBack: bool = False) -> None:
1377
1289
  """Search for the next or previous occurrence of the search bar
1378
1290
  text in the document. Wrap around if not found and loop is
1379
1291
  enabled, or continue to next file if next file is enabled.
@@ -1387,7 +1299,7 @@ class GuiDocEditor(QTextEdit):
1387
1299
  return
1388
1300
 
1389
1301
  resS, resE = self.findAllOccurences()
1390
- if len(resS) == 0:
1302
+ if len(resS) == 0 and self._docHandle:
1391
1303
  self.docSearch.setResultCount(0, 0)
1392
1304
  self._lastFind = None
1393
1305
  if self.docSearch.doNextFile and not goBack:
@@ -1397,8 +1309,8 @@ class GuiDocEditor(QTextEdit):
1397
1309
  self.beginSearch()
1398
1310
  return
1399
1311
 
1400
- theCursor = self.textCursor()
1401
- resIdx = bisect.bisect_left(resS, theCursor.position())
1312
+ cursor = self.textCursor()
1313
+ resIdx = bisect.bisect_left(resS, cursor.position())
1402
1314
 
1403
1315
  doLoop = self.docSearch.doLoop
1404
1316
  maxIdx = len(resS) - 1
@@ -1409,7 +1321,7 @@ class GuiDocEditor(QTextEdit):
1409
1321
  if resIdx < 0:
1410
1322
  resIdx = maxIdx if doLoop else 0
1411
1323
 
1412
- if resIdx > maxIdx:
1324
+ if resIdx > maxIdx and self._docHandle:
1413
1325
  if self.docSearch.doNextFile and not goBack:
1414
1326
  self.mainGui.openNextDocument(
1415
1327
  self._docHandle, wrapAround=self.docSearch.doLoop
@@ -1419,9 +1331,9 @@ class GuiDocEditor(QTextEdit):
1419
1331
  else:
1420
1332
  resIdx = 0 if doLoop else maxIdx
1421
1333
 
1422
- theCursor.setPosition(resS[resIdx], QTextCursor.MoveAnchor)
1423
- theCursor.setPosition(resE[resIdx], QTextCursor.KeepAnchor)
1424
- self.setTextCursor(theCursor)
1334
+ cursor.setPosition(resS[resIdx], QTextCursor.MoveMode.MoveAnchor)
1335
+ cursor.setPosition(resE[resIdx], QTextCursor.MoveMode.KeepAnchor)
1336
+ self.setTextCursor(cursor)
1425
1337
 
1426
1338
  self.docFooter.updateLineCount()
1427
1339
  self.docSearch.setResultCount(resIdx + 1, len(resS))
@@ -1429,113 +1341,113 @@ class GuiDocEditor(QTextEdit):
1429
1341
 
1430
1342
  return
1431
1343
 
1432
- def findAllOccurences(self):
1344
+ def findAllOccurences(self) -> tuple[list[int], list[int]]:
1433
1345
  """Create a list of all search results of the current search
1434
1346
  text in the document.
1435
1347
  """
1436
1348
  resS = []
1437
1349
  resE = []
1438
- theCursor = self.textCursor()
1439
- hasSelection = theCursor.hasSelection()
1350
+ cursor = self.textCursor()
1351
+ hasSelection = cursor.hasSelection()
1440
1352
  if hasSelection:
1441
- origA = theCursor.selectionStart()
1442
- origB = theCursor.selectionEnd()
1353
+ origA = cursor.selectionStart()
1354
+ origB = cursor.selectionEnd()
1443
1355
  else:
1444
- origA = theCursor.position()
1445
- origB = theCursor.position()
1356
+ origA = cursor.position()
1357
+ origB = cursor.position()
1446
1358
 
1447
1359
  findOpt = QTextDocument.FindFlag(0)
1448
1360
  if self.docSearch.isCaseSense:
1449
- findOpt |= QTextDocument.FindCaseSensitively
1361
+ findOpt |= QTextDocument.FindFlag.FindCaseSensitively
1450
1362
  if self.docSearch.isWholeWord:
1451
- findOpt |= QTextDocument.FindWholeWords
1363
+ findOpt |= QTextDocument.FindFlag.FindWholeWords
1452
1364
 
1453
1365
  searchFor = self.docSearch.getSearchObject()
1454
- theCursor.setPosition(0)
1455
- self.setTextCursor(theCursor)
1366
+ cursor.setPosition(0)
1367
+ self.setTextCursor(cursor)
1456
1368
 
1457
1369
  # Search up to a maximum of 1000, and make sure certain special
1458
- # searches like a regex search for .* turns into an infinite loop
1370
+ # searches like a regex search for .* don't loop infinitely
1459
1371
  while self.find(searchFor, findOpt) and len(resE) <= 1000:
1460
- theCursor = self.textCursor()
1461
- if theCursor.hasSelection():
1462
- resS.append(theCursor.selectionStart())
1463
- resE.append(theCursor.selectionEnd())
1372
+ cursor = self.textCursor()
1373
+ if cursor.hasSelection():
1374
+ resS.append(cursor.selectionStart())
1375
+ resE.append(cursor.selectionEnd())
1464
1376
  else:
1465
1377
  logger.warning("The search returned an empty result")
1466
1378
  break
1467
1379
 
1468
1380
  if hasSelection:
1469
- theCursor.setPosition(origA, QTextCursor.MoveAnchor)
1470
- theCursor.setPosition(origB, QTextCursor.KeepAnchor)
1381
+ cursor.setPosition(origA, QTextCursor.MoveMode.MoveAnchor)
1382
+ cursor.setPosition(origB, QTextCursor.MoveMode.KeepAnchor)
1471
1383
  else:
1472
- theCursor.setPosition(origA)
1384
+ cursor.setPosition(origA)
1473
1385
 
1474
- self.setTextCursor(theCursor)
1386
+ self.setTextCursor(cursor)
1475
1387
 
1476
1388
  return resS, resE
1477
1389
 
1478
- def replaceNext(self):
1390
+ def replaceNext(self) -> None:
1479
1391
  """Search for the next occurrence of the search bar text in the
1480
1392
  document and replace it with the replace text. Call search next
1481
1393
  automatically when done.
1482
1394
  """
1483
1395
  if not self.anyFocus():
1484
1396
  logger.debug("Editor does not have focus")
1485
- return False
1397
+ return
1486
1398
 
1487
1399
  if not self.docSearch.isVisible():
1488
1400
  # The search tool is not active, so we activate it.
1489
1401
  self.beginSearch()
1490
1402
  return
1491
1403
 
1492
- theCursor = self.textCursor()
1493
- if not theCursor.hasSelection():
1404
+ cursor = self.textCursor()
1405
+ if not cursor.hasSelection():
1494
1406
  # We have no text selected at all, so just make this a
1495
1407
  # regular find next call.
1496
1408
  self.findNext()
1497
1409
  return
1498
1410
 
1499
- if self._lastFind is None and theCursor.hasSelection():
1411
+ if self._lastFind is None and cursor.hasSelection():
1500
1412
  # If we have a selection but no search, it may have been the
1501
1413
  # text we triggered the search with, in which case we search
1502
1414
  # again from the beginning of that selection to make sure we
1503
1415
  # have a valid result.
1504
- sPos = theCursor.selectionStart()
1505
- theCursor.clearSelection()
1506
- theCursor.setPosition(sPos)
1507
- self.setTextCursor(theCursor)
1416
+ sPos = cursor.selectionStart()
1417
+ cursor.clearSelection()
1418
+ cursor.setPosition(sPos)
1419
+ self.setTextCursor(cursor)
1508
1420
  self.findNext()
1509
- theCursor = self.textCursor()
1421
+ cursor = self.textCursor()
1510
1422
 
1511
1423
  if self._lastFind is None:
1512
1424
  # In case the above didn't find a result, we give up here.
1513
1425
  return
1514
1426
 
1515
- searchFor = self.docSearch.getSearchText()
1516
- replWith = self.docSearch.getReplaceText()
1427
+ searchFor = self.docSearch.searchText
1428
+ replWith = self.docSearch.replaceText
1517
1429
 
1518
1430
  if self.docSearch.doMatchCap:
1519
- replWith = transferCase(theCursor.selectedText(), replWith)
1431
+ replWith = transferCase(cursor.selectedText(), replWith)
1520
1432
 
1521
1433
  # Make sure the selected text was selected by an actual find
1522
1434
  # call, and not the user.
1523
1435
  try:
1524
- isFind = self._lastFind[0] == theCursor.selectionStart()
1525
- isFind &= self._lastFind[1] == theCursor.selectionEnd()
1436
+ isFind = self._lastFind[0] == cursor.selectionStart()
1437
+ isFind &= self._lastFind[1] == cursor.selectionEnd()
1526
1438
  except Exception:
1527
1439
  isFind = False
1528
1440
 
1529
1441
  if isFind:
1530
- theCursor.beginEditBlock()
1531
- theCursor.removeSelectedText()
1532
- theCursor.insertText(replWith)
1533
- theCursor.endEditBlock()
1534
- theCursor.setPosition(theCursor.selectionEnd())
1535
- self.setTextCursor(theCursor)
1442
+ cursor.beginEditBlock()
1443
+ cursor.removeSelectedText()
1444
+ cursor.insertText(replWith)
1445
+ cursor.endEditBlock()
1446
+ cursor.setPosition(cursor.selectionEnd())
1447
+ self.setTextCursor(cursor)
1536
1448
  logger.debug(
1537
1449
  "Replaced occurrence of '%s' with '%s' on line %d",
1538
- searchFor, replWith, theCursor.blockNumber()
1450
+ searchFor, replWith, cursor.blockNumber()
1539
1451
  )
1540
1452
  else:
1541
1453
  logger.error("The selected text is not a search result, skipping replace")
@@ -1548,128 +1460,143 @@ class GuiDocEditor(QTextEdit):
1548
1460
  # Internal Functions : Text Manipulation
1549
1461
  ##
1550
1462
 
1551
- def _toggleFormat(self, fLen, fChar):
1463
+ def _toggleFormat(self, fLen: int, fChar: str) -> bool:
1552
1464
  """Toggle the formatting of a specific type for a piece of text.
1553
1465
  If more than one block is selected, the formatting is applied to
1554
1466
  the first block.
1555
1467
  """
1556
- theCursor = self._autoSelect()
1557
- if not theCursor.hasSelection():
1558
- logger.warning("No selection made, nothing to do")
1468
+ cursor = self.textCursor()
1469
+ posO = cursor.position()
1470
+ if cursor.hasSelection():
1471
+ select = _SelectAction.KEEP_SELECTION
1472
+ else:
1473
+ cursor = self._autoSelect()
1474
+ if cursor.hasSelection() and posO == cursor.selectionEnd():
1475
+ select = _SelectAction.MOVE_AFTER
1476
+ else:
1477
+ select = _SelectAction.KEEP_POSITION
1478
+
1479
+ posS = cursor.selectionStart()
1480
+ posE = cursor.selectionEnd()
1481
+ if self._qDocument.characterAt(posO - 1) == fChar:
1482
+ logger.warning("Format repetition, cancelling action")
1483
+ cursor.clearSelection()
1484
+ cursor.setPosition(posO)
1485
+ self.setTextCursor(cursor)
1559
1486
  return False
1560
1487
 
1561
- posS = theCursor.selectionStart()
1562
- posE = theCursor.selectionEnd()
1563
-
1564
- blockS = self.document().findBlock(posS)
1565
- blockE = self.document().findBlock(posE)
1566
-
1488
+ blockS = self._qDocument.findBlock(posS)
1489
+ blockE = self._qDocument.findBlock(posE)
1567
1490
  if blockS != blockE:
1568
1491
  posE = blockS.position() + blockS.length() - 1
1569
- theCursor.clearSelection()
1570
- theCursor.setPosition(posS, QTextCursor.MoveAnchor)
1571
- theCursor.setPosition(posE, QTextCursor.KeepAnchor)
1572
- self.setTextCursor(theCursor)
1492
+ cursor.clearSelection()
1493
+ cursor.setPosition(posS, QTextCursor.MoveMode.MoveAnchor)
1494
+ cursor.setPosition(posE, QTextCursor.MoveMode.KeepAnchor)
1495
+ self.setTextCursor(cursor)
1573
1496
 
1574
1497
  numB = 0
1575
1498
  for n in range(fLen):
1576
- if self.document().characterAt(posS-n-1) == fChar:
1499
+ if self._qDocument.characterAt(posS-n-1) == fChar:
1577
1500
  numB += 1
1578
1501
  else:
1579
1502
  break
1580
1503
 
1581
1504
  numA = 0
1582
1505
  for n in range(fLen):
1583
- if self.document().characterAt(posE+n) == fChar:
1506
+ if self._qDocument.characterAt(posE+n) == fChar:
1584
1507
  numA += 1
1585
1508
  else:
1586
1509
  break
1587
1510
 
1588
1511
  if fLen == min(numA, numB):
1589
- self._clearSurrounding(theCursor, fLen)
1590
- else:
1591
- self._wrapSelection(fChar*fLen)
1592
-
1593
- return True
1594
-
1595
- def _clearSurrounding(self, theCursor, nChars):
1596
- """Clear n characters before and after the cursor.
1597
- """
1598
- if not theCursor.hasSelection():
1599
- logger.warning("No selection made, nothing to do")
1600
- return False
1512
+ cursor.clearSelection()
1513
+ cursor.beginEditBlock()
1514
+ cursor.setPosition(posS)
1515
+ for i in range(fLen):
1516
+ cursor.deletePreviousChar()
1517
+ cursor.setPosition(posE)
1518
+ for i in range(fLen):
1519
+ cursor.deletePreviousChar()
1520
+ cursor.endEditBlock()
1521
+ cursor.clearSelection()
1522
+ cursor.setPosition(posO - fLen)
1523
+ self.setTextCursor(cursor)
1601
1524
 
1602
- posS = theCursor.selectionStart()
1603
- posE = theCursor.selectionEnd()
1604
- theCursor.clearSelection()
1605
- theCursor.beginEditBlock()
1606
- theCursor.setPosition(posS)
1607
- for i in range(nChars):
1608
- theCursor.deletePreviousChar()
1609
- theCursor.setPosition(posE)
1610
- for i in range(nChars):
1611
- theCursor.deletePreviousChar()
1612
- theCursor.endEditBlock()
1613
- theCursor.clearSelection()
1525
+ else:
1526
+ self._wrapSelection(fChar*fLen, pos=posO, select=select)
1614
1527
 
1615
1528
  return True
1616
1529
 
1617
- def _wrapSelection(self, tBefore, tAfter=None):
1530
+ def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None,
1531
+ select: _SelectAction = _SelectAction.NO_DECISION) -> bool:
1618
1532
  """Wrap the selected text in whatever is in tBefore and tAfter.
1619
1533
  If there is no selection, the autoSelect setting decides the
1620
1534
  action. AutoSelect will select the word under the cursor before
1621
1535
  wrapping it. If this feature is disabled, nothing is done.
1622
1536
  """
1623
- if tAfter is None:
1624
- tAfter = tBefore
1537
+ if after is None:
1538
+ after = before
1625
1539
 
1626
- theCursor = self._autoSelect()
1627
- if not theCursor.hasSelection():
1628
- logger.warning("No selection made, nothing to do")
1629
- return False
1540
+ cursor = self.textCursor()
1541
+ posO = pos if isinstance(pos, int) else cursor.position()
1542
+ if select == _SelectAction.NO_DECISION:
1543
+ if cursor.hasSelection():
1544
+ select = _SelectAction.KEEP_SELECTION
1545
+ else:
1546
+ cursor = self._autoSelect()
1547
+ if cursor.hasSelection() and posO == cursor.selectionEnd():
1548
+ select = _SelectAction.MOVE_AFTER
1549
+ else:
1550
+ select = _SelectAction.KEEP_POSITION
1630
1551
 
1631
- posS = theCursor.selectionStart()
1632
- posE = theCursor.selectionEnd()
1552
+ posS = cursor.selectionStart()
1553
+ posE = cursor.selectionEnd()
1633
1554
 
1634
- qDoc = self.document()
1635
- blockS = qDoc.findBlock(posS)
1636
- blockE = qDoc.findBlock(posE)
1555
+ blockS = self._qDocument.findBlock(posS)
1556
+ blockE = self._qDocument.findBlock(posE)
1637
1557
  if blockS != blockE:
1638
1558
  posE = blockS.position() + blockS.length() - 1
1639
1559
 
1640
- theCursor.clearSelection()
1641
- theCursor.beginEditBlock()
1642
- theCursor.setPosition(posE)
1643
- theCursor.insertText(tAfter)
1644
- theCursor.setPosition(posS)
1645
- theCursor.insertText(tBefore)
1646
- theCursor.endEditBlock()
1560
+ cursor.clearSelection()
1561
+ cursor.beginEditBlock()
1562
+ cursor.setPosition(posE)
1563
+ cursor.insertText(after)
1564
+ cursor.setPosition(posS)
1565
+ cursor.insertText(before)
1566
+ cursor.endEditBlock()
1567
+
1568
+ if select == _SelectAction.MOVE_AFTER:
1569
+ cursor.setPosition(posE + len(before + after))
1570
+ elif select == _SelectAction.KEEP_SELECTION:
1571
+ cursor.setPosition(posE + len(before), QTextCursor.MoveMode.MoveAnchor)
1572
+ cursor.setPosition(posS + len(before), QTextCursor.MoveMode.KeepAnchor)
1573
+ elif select == _SelectAction.KEEP_POSITION:
1574
+ cursor.setPosition(posO + len(before))
1647
1575
 
1648
- theCursor.setPosition(posE + len(tBefore), QTextCursor.MoveAnchor)
1649
- theCursor.setPosition(posS + len(tBefore), QTextCursor.KeepAnchor)
1650
- self.setTextCursor(theCursor)
1576
+ self.setTextCursor(cursor)
1651
1577
 
1652
1578
  return True
1653
1579
 
1654
- def _replaceQuotes(self, sQuote, oQuote, cQuote):
1655
- """Replace all straight quotes in the selected text.
1656
- """
1657
- theCursor = self.textCursor()
1658
- if not theCursor.hasSelection():
1580
+ def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> bool:
1581
+ """Replace all straight quotes in the selected text."""
1582
+ cursor = self.textCursor()
1583
+ if not cursor.hasSelection():
1659
1584
  SHARED.error(self.tr("Please select some text before calling replace quotes."))
1660
1585
  return False
1661
1586
 
1662
- posS = theCursor.selectionStart()
1663
- posE = theCursor.selectionEnd()
1587
+ posS = cursor.selectionStart()
1588
+ posE = cursor.selectionEnd()
1664
1589
  closeCheck = (
1665
1590
  " ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP
1666
1591
  )
1667
1592
 
1668
1593
  self._allowAutoReplace(False)
1669
1594
  for posC in range(posS, posE+1):
1670
- theCursor.setPosition(posC)
1671
- theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
1672
- selText = theCursor.selectedText()
1595
+ cursor.setPosition(posC)
1596
+ cursor.movePosition(
1597
+ QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, 2
1598
+ )
1599
+ selText = cursor.selectedText()
1673
1600
 
1674
1601
  nS = len(selText)
1675
1602
  if nS == 2:
@@ -1684,169 +1611,170 @@ class GuiDocEditor(QTextEdit):
1684
1611
  if cC != sQuote:
1685
1612
  continue
1686
1613
 
1687
- theCursor.clearSelection()
1688
- theCursor.setPosition(posC)
1614
+ cursor.clearSelection()
1615
+ cursor.setPosition(posC)
1689
1616
  if pC in closeCheck:
1690
- theCursor.beginEditBlock()
1691
- theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
1692
- theCursor.insertText(oQuote)
1693
- theCursor.endEditBlock()
1617
+ cursor.beginEditBlock()
1618
+ cursor.movePosition(
1619
+ QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, 1
1620
+ )
1621
+ cursor.insertText(oQuote)
1622
+ cursor.endEditBlock()
1694
1623
  else:
1695
- theCursor.beginEditBlock()
1696
- theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
1697
- theCursor.insertText(cQuote)
1698
- theCursor.endEditBlock()
1624
+ cursor.beginEditBlock()
1625
+ cursor.movePosition(
1626
+ QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, 1
1627
+ )
1628
+ cursor.insertText(cQuote)
1629
+ cursor.endEditBlock()
1699
1630
 
1700
1631
  self._allowAutoReplace(True)
1701
1632
 
1702
1633
  return True
1703
1634
 
1704
- def _formatBlock(self, docAction):
1705
- """Change the block format of the block under the cursor.
1706
- """
1707
- theCursor = self.textCursor()
1708
- theBlock = theCursor.block()
1709
- if not theBlock.isValid():
1710
- logger.debug("Invalid block selected for action '%s'", str(docAction))
1635
+ def _formatBlock(self, action: nwDocAction) -> bool:
1636
+ """Change the block format of the block under the cursor."""
1637
+ cursor = self.textCursor()
1638
+ block = cursor.block()
1639
+ if not block.isValid():
1640
+ logger.debug("Invalid block selected for action '%s'", str(action))
1711
1641
  return False
1712
1642
 
1713
1643
  # Remove existing format first, if any
1714
- theText = theBlock.text()
1715
- hasText = len(theText) > 0
1716
- if theText.startswith("@"):
1644
+ setText = block.text()
1645
+ hasText = len(setText) > 0
1646
+ if setText.startswith("@"):
1717
1647
  logger.error("Cannot apply block format to keyword/value line")
1718
1648
  return False
1719
- elif theText.startswith("% "):
1720
- newText = theText[2:]
1649
+ elif setText.startswith("% "):
1650
+ newText = setText[2:]
1721
1651
  cOffset = 2
1722
- if docAction == nwDocAction.BLOCK_COM:
1723
- docAction = nwDocAction.BLOCK_TXT
1724
- elif theText.startswith("%"):
1725
- newText = theText[1:]
1652
+ if action == nwDocAction.BLOCK_COM:
1653
+ action = nwDocAction.BLOCK_TXT
1654
+ elif setText.startswith("%"):
1655
+ newText = setText[1:]
1726
1656
  cOffset = 1
1727
- if docAction == nwDocAction.BLOCK_COM:
1728
- docAction = nwDocAction.BLOCK_TXT
1729
- elif theText.startswith("# "):
1730
- newText = theText[2:]
1657
+ if action == nwDocAction.BLOCK_COM:
1658
+ action = nwDocAction.BLOCK_TXT
1659
+ elif setText.startswith("# "):
1660
+ newText = setText[2:]
1731
1661
  cOffset = 2
1732
- elif theText.startswith("## "):
1733
- newText = theText[3:]
1662
+ elif setText.startswith("## "):
1663
+ newText = setText[3:]
1734
1664
  cOffset = 3
1735
- elif theText.startswith("### "):
1736
- newText = theText[4:]
1665
+ elif setText.startswith("### "):
1666
+ newText = setText[4:]
1737
1667
  cOffset = 4
1738
- elif theText.startswith("#### "):
1739
- newText = theText[5:]
1668
+ elif setText.startswith("#### "):
1669
+ newText = setText[5:]
1740
1670
  cOffset = 5
1741
- elif theText.startswith("#! "):
1742
- newText = theText[3:]
1671
+ elif setText.startswith("#! "):
1672
+ newText = setText[3:]
1743
1673
  cOffset = 3
1744
- elif theText.startswith("##! "):
1745
- newText = theText[4:]
1674
+ elif setText.startswith("##! "):
1675
+ newText = setText[4:]
1746
1676
  cOffset = 4
1747
- elif theText.startswith(">> "):
1748
- newText = theText[3:]
1677
+ elif setText.startswith(">> "):
1678
+ newText = setText[3:]
1749
1679
  cOffset = 3
1750
- elif theText.startswith("> ") and docAction != nwDocAction.INDENT_R:
1751
- newText = theText[2:]
1680
+ elif setText.startswith("> ") and action != nwDocAction.INDENT_R:
1681
+ newText = setText[2:]
1752
1682
  cOffset = 2
1753
- elif theText.startswith(">>"):
1754
- newText = theText[2:]
1683
+ elif setText.startswith(">>"):
1684
+ newText = setText[2:]
1755
1685
  cOffset = 2
1756
- elif theText.startswith(">") and docAction != nwDocAction.INDENT_R:
1757
- newText = theText[1:]
1686
+ elif setText.startswith(">") and action != nwDocAction.INDENT_R:
1687
+ newText = setText[1:]
1758
1688
  cOffset = 1
1759
1689
  else:
1760
- newText = theText
1690
+ newText = setText
1761
1691
  cOffset = 0
1762
1692
 
1763
1693
  # Also remove formatting tags at the end
1764
- if theText.endswith(" <<"):
1694
+ if setText.endswith(" <<"):
1765
1695
  newText = newText[:-3]
1766
- elif theText.endswith(" <") and docAction != nwDocAction.INDENT_L:
1696
+ elif setText.endswith(" <") and action != nwDocAction.INDENT_L:
1767
1697
  newText = newText[:-2]
1768
- elif theText.endswith("<<"):
1698
+ elif setText.endswith("<<"):
1769
1699
  newText = newText[:-2]
1770
- elif theText.endswith("<") and docAction != nwDocAction.INDENT_L:
1700
+ elif setText.endswith("<") and action != nwDocAction.INDENT_L:
1771
1701
  newText = newText[:-1]
1772
1702
 
1773
1703
  # Apply new format
1774
- if docAction == nwDocAction.BLOCK_COM:
1775
- theText = "% "+newText
1704
+ if action == nwDocAction.BLOCK_COM:
1705
+ setText = "% "+newText
1776
1706
  cOffset -= 2
1777
- elif docAction == nwDocAction.BLOCK_H1:
1778
- theText = "# "+newText
1707
+ elif action == nwDocAction.BLOCK_H1:
1708
+ setText = "# "+newText
1779
1709
  cOffset -= 2
1780
- elif docAction == nwDocAction.BLOCK_H2:
1781
- theText = "## "+newText
1710
+ elif action == nwDocAction.BLOCK_H2:
1711
+ setText = "## "+newText
1782
1712
  cOffset -= 3
1783
- elif docAction == nwDocAction.BLOCK_H3:
1784
- theText = "### "+newText
1713
+ elif action == nwDocAction.BLOCK_H3:
1714
+ setText = "### "+newText
1785
1715
  cOffset -= 4
1786
- elif docAction == nwDocAction.BLOCK_H4:
1787
- theText = "#### "+newText
1716
+ elif action == nwDocAction.BLOCK_H4:
1717
+ setText = "#### "+newText
1788
1718
  cOffset -= 5
1789
- elif docAction == nwDocAction.BLOCK_TTL:
1790
- theText = "#! "+newText
1719
+ elif action == nwDocAction.BLOCK_TTL:
1720
+ setText = "#! "+newText
1791
1721
  cOffset -= 3
1792
- elif docAction == nwDocAction.BLOCK_UNN:
1793
- theText = "##! "+newText
1722
+ elif action == nwDocAction.BLOCK_UNN:
1723
+ setText = "##! "+newText
1794
1724
  cOffset -= 4
1795
- elif docAction == nwDocAction.ALIGN_L:
1796
- theText = newText+" <<"
1797
- elif docAction == nwDocAction.ALIGN_C:
1798
- theText = ">> "+newText+" <<"
1725
+ elif action == nwDocAction.ALIGN_L:
1726
+ setText = newText+" <<"
1727
+ elif action == nwDocAction.ALIGN_C:
1728
+ setText = ">> "+newText+" <<"
1799
1729
  cOffset -= 3
1800
- elif docAction == nwDocAction.ALIGN_R:
1801
- theText = ">> "+newText
1730
+ elif action == nwDocAction.ALIGN_R:
1731
+ setText = ">> "+newText
1802
1732
  cOffset -= 3
1803
- elif docAction == nwDocAction.INDENT_L:
1804
- theText = "> "+newText
1733
+ elif action == nwDocAction.INDENT_L:
1734
+ setText = "> "+newText
1805
1735
  cOffset -= 2
1806
- elif docAction == nwDocAction.INDENT_R:
1807
- theText = newText+" <"
1808
- elif docAction == nwDocAction.BLOCK_TXT:
1809
- theText = newText
1736
+ elif action == nwDocAction.INDENT_R:
1737
+ setText = newText+" <"
1738
+ elif action == nwDocAction.BLOCK_TXT:
1739
+ setText = newText
1810
1740
  else:
1811
- logger.error("Unknown or unsupported block format requested: '%s'", str(docAction))
1741
+ logger.error("Unknown or unsupported block format requested: '%s'", str(action))
1812
1742
  return False
1813
1743
 
1814
1744
  # Replace the block text
1815
- theCursor.beginEditBlock()
1816
- posO = theCursor.position()
1817
- theCursor.select(QTextCursor.BlockUnderCursor)
1818
- posS = theCursor.selectionStart()
1819
- theCursor.removeSelectedText()
1820
- theCursor.setPosition(posS)
1745
+ cursor.beginEditBlock()
1746
+ posO = cursor.position()
1747
+ cursor.select(QTextCursor.SelectionType.BlockUnderCursor)
1748
+ posS = cursor.selectionStart()
1749
+ cursor.removeSelectedText()
1750
+ cursor.setPosition(posS)
1821
1751
 
1822
1752
  if posS > 0 and hasText:
1823
1753
  # If the block already had text, we must insert a new block
1824
1754
  # first before we can add back the text to it.
1825
- theCursor.insertBlock()
1755
+ cursor.insertBlock()
1826
1756
 
1827
- theCursor.insertText(theText)
1757
+ cursor.insertText(setText)
1828
1758
 
1829
1759
  if posO - cOffset >= 0:
1830
- theCursor.setPosition(posO - cOffset)
1760
+ cursor.setPosition(posO - cOffset)
1831
1761
 
1832
- theCursor.endEditBlock()
1833
- self.setTextCursor(theCursor)
1762
+ cursor.endEditBlock()
1763
+ self.setTextCursor(cursor)
1834
1764
 
1835
1765
  return True
1836
1766
 
1837
- def _removeInParLineBreaks(self):
1838
- """Strip line breaks within paragraphs in the selected text.
1839
- """
1840
- theCursor = self.textCursor()
1841
- theDoc = self.document()
1767
+ def _removeInParLineBreaks(self) -> None:
1768
+ """Strip line breaks within paragraphs in the selected text."""
1769
+ cursor = self.textCursor()
1842
1770
 
1843
1771
  iS = 0
1844
- iE = theDoc.blockCount() - 1
1772
+ iE = self._qDocument.blockCount() - 1
1845
1773
  rS = 0
1846
- rE = theDoc.characterCount()
1847
- if theCursor.hasSelection():
1848
- sBlock = theDoc.findBlock(theCursor.selectionStart())
1849
- eBlock = theDoc.findBlock(theCursor.selectionEnd())
1774
+ rE = self._qDocument.characterCount()
1775
+ if cursor.hasSelection():
1776
+ sBlock = self._qDocument.findBlock(cursor.selectionStart())
1777
+ eBlock = self._qDocument.findBlock(cursor.selectionEnd())
1850
1778
  iS = sBlock.blockNumber()
1851
1779
  iE = eBlock.blockNumber()
1852
1780
  rS = sBlock.position()
@@ -1856,7 +1784,7 @@ class GuiDocEditor(QTextEdit):
1856
1784
  currPar = []
1857
1785
  cleanText = ""
1858
1786
  for i in range(iS, iE+1):
1859
- cBlock = theDoc.findBlockByNumber(i)
1787
+ cBlock = self._qDocument.findBlockByNumber(i)
1860
1788
  cText = cBlock.text()
1861
1789
  if cText.strip() == "":
1862
1790
  if currPar:
@@ -1873,171 +1801,187 @@ class GuiDocEditor(QTextEdit):
1873
1801
  cleanText += " ".join(currPar) + "\n\n"
1874
1802
 
1875
1803
  # Replace the text with the cleaned up text
1876
- theCursor.beginEditBlock()
1877
- theCursor.clearSelection()
1878
- theCursor.setPosition(rS)
1879
- theCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS)
1880
- theCursor.insertText(cleanText.rstrip() + "\n")
1881
- theCursor.endEditBlock()
1804
+ cursor.beginEditBlock()
1805
+ cursor.clearSelection()
1806
+ cursor.setPosition(rS)
1807
+ cursor.movePosition(
1808
+ QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, rE-rS
1809
+ )
1810
+ cursor.insertText(cleanText.rstrip() + "\n")
1811
+ cursor.endEditBlock()
1882
1812
 
1883
- return True
1813
+ return
1884
1814
 
1885
1815
  ##
1886
1816
  # Internal Functions
1887
1817
  ##
1888
1818
 
1889
- def _followTag(self, theCursor=None, loadTag=True):
1819
+ def _processTag(self, cursor: QTextCursor | None = None,
1820
+ follow: bool = True, create: bool = False) -> nwTrinary:
1890
1821
  """Activated by Ctrl+Enter. Checks that we're in a block
1891
1822
  starting with '@'. We then find the tag under the cursor and
1892
1823
  check that it is not the tag itself. If all this is fine, we
1893
1824
  have a tag and can tell the document viewer to try and find and
1894
1825
  load the file where the tag is defined.
1895
1826
  """
1896
- if theCursor is None:
1897
- theCursor = self.textCursor()
1827
+ if cursor is None:
1828
+ cursor = self.textCursor()
1898
1829
 
1899
- theBlock = theCursor.block()
1900
- theText = theBlock.text()
1830
+ block = cursor.block()
1831
+ text = block.text()
1832
+ if len(text) == 0:
1833
+ return nwTrinary.NEUTRAL
1901
1834
 
1902
- if len(theText) == 0:
1903
- return False
1904
-
1905
- if theText.startswith("@"):
1835
+ if text.startswith("@") and isinstance(self._nwItem, NWItem):
1906
1836
 
1907
- isGood, tBits, tPos = SHARED.project.index.scanThis(theText)
1837
+ isGood, tBits, tPos = SHARED.project.index.scanThis(text)
1908
1838
  if not isGood:
1909
- return False
1839
+ return nwTrinary.NEUTRAL
1910
1840
 
1911
- theTag = ""
1912
- cPos = theCursor.selectionStart() - theBlock.position()
1913
- for sTag, sPos in zip(reversed(tBits), reversed(tPos)):
1841
+ tag = ""
1842
+ exist = False
1843
+ cPos = cursor.selectionStart() - block.position()
1844
+ tExist = SHARED.project.index.checkThese(tBits, self._nwItem)
1845
+ for sTag, sPos, sExist in zip(reversed(tBits), reversed(tPos), reversed(tExist)):
1914
1846
  if cPos >= sPos:
1915
1847
  # The cursor is between the start of two tags
1916
1848
  if cPos <= sPos + len(sTag):
1917
1849
  # The cursor is inside or at the edge of the tag
1918
- theTag = sTag
1850
+ tag = sTag
1851
+ exist = sExist
1919
1852
  break
1920
1853
 
1921
- if not theTag or theTag.startswith("@"):
1854
+ if not tag or tag.startswith("@"):
1922
1855
  # The keyword cannot be looked up, so we ignore that
1923
- return False
1924
-
1925
- if loadTag:
1926
- logger.debug("Attempting to follow tag '%s'", theTag)
1927
- self.loadDocumentTagRequest.emit(theTag, nwDocMode.VIEW)
1928
- else:
1929
- logger.debug("Potential tag '%s'", theTag)
1930
-
1931
- return True
1932
-
1933
- return False
1934
-
1935
- def _openSpellContext(self):
1936
- """Open the spell check context menu at the current point of the
1937
- cursor.
1938
- """
1856
+ return nwTrinary.NEUTRAL
1857
+
1858
+ if follow and exist:
1859
+ logger.debug("Attempting to follow tag '%s'", tag)
1860
+ self.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
1861
+ elif create and not exist:
1862
+ if SHARED.question(self.tr(
1863
+ "Do you want to create a new project note for the tag '{0}'?"
1864
+ ).format(tag)):
1865
+ itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS)
1866
+ if SHARED.mainGui.projView.createNewNote(tag, itemClass):
1867
+ self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1868
+ else:
1869
+ SHARED.error(self.tr(
1870
+ "Could not create note in a root folder for '{0}'. "
1871
+ "If one doesn't exist, you must create one first."
1872
+ ).format(trConst(nwLabels.CLASS_NAME[itemClass])))
1873
+
1874
+ return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
1875
+
1876
+ return nwTrinary.NEUTRAL
1877
+
1878
+ def _emitRenameItem(self, block: QTextBlock) -> None:
1879
+ """Emit a signal to request an item be renamed."""
1880
+ if self._docHandle:
1881
+ text = block.text().lstrip("#").lstrip("!").strip()
1882
+ self.requestProjectItemRenamed.emit(self._docHandle, text)
1883
+ return
1884
+
1885
+ def _openContextFromCursor(self) -> None:
1886
+ """Open the spell check context menu at the cursor."""
1939
1887
  self._openContextMenu(self.cursorRect().center())
1940
1888
  return
1941
1889
 
1942
- def _docAutoReplace(self, theBlock):
1943
- """Auto-replace text elements based on main configuration.
1944
- """
1945
- if not theBlock.isValid():
1946
- return
1890
+ def _docAutoReplace(self, text: str) -> None:
1891
+ """Auto-replace text elements based on main configuration."""
1892
+ cursor = self.textCursor()
1893
+ tPos = cursor.positionInBlock()
1894
+ tLen = len(text)
1947
1895
 
1948
- theText = theBlock.text()
1949
- theCursor = self.textCursor()
1950
- thePos = theCursor.positionInBlock()
1951
- theLen = len(theText)
1952
-
1953
- if theLen < 1 or thePos-1 > theLen:
1896
+ if tLen < 1 or tPos-1 > tLen:
1954
1897
  return
1955
1898
 
1956
- theOne = theText[thePos-1:thePos]
1957
- theTwo = theText[thePos-2:thePos]
1958
- theThree = theText[thePos-3:thePos]
1899
+ tOne = text[tPos-1:tPos]
1900
+ tTwo = text[tPos-2:tPos]
1901
+ tThree = text[tPos-3:tPos]
1959
1902
 
1960
- if not theOne:
1961
- # Sorry, Neo and Zathras
1903
+ if not tOne:
1962
1904
  return
1963
1905
 
1964
1906
  nDelete = 0
1965
- tInsert = theOne
1907
+ tInsert = tOne
1966
1908
 
1967
- if self._typRepDQuote and theTwo[:1].isspace() and theTwo.endswith('"'):
1909
+ if self._typRepDQuote and tTwo[:1].isspace() and tTwo.endswith('"'):
1968
1910
  nDelete = 1
1969
1911
  tInsert = self._typDQuoteO
1970
1912
 
1971
- elif self._typRepDQuote and theOne == '"':
1913
+ elif self._typRepDQuote and tOne == '"':
1972
1914
  nDelete = 1
1973
- if thePos == 1:
1915
+ if tPos == 1:
1974
1916
  tInsert = self._typDQuoteO
1975
- elif thePos == 2 and theTwo == '>"':
1917
+ elif tPos == 2 and tTwo == '>"':
1976
1918
  tInsert = self._typDQuoteO
1977
- elif thePos == 3 and theThree == '>>"':
1919
+ elif tPos == 3 and tThree == '>>"':
1978
1920
  tInsert = self._typDQuoteO
1979
1921
  else:
1980
1922
  tInsert = self._typDQuoteC
1981
1923
 
1982
- elif self._typRepSQuote and theTwo[:1].isspace() and theTwo.endswith("'"):
1924
+ elif self._typRepSQuote and tTwo[:1].isspace() and tTwo.endswith("'"):
1983
1925
  nDelete = 1
1984
1926
  tInsert = self._typSQuoteO
1985
1927
 
1986
- elif self._typRepSQuote and theOne == "'":
1928
+ elif self._typRepSQuote and tOne == "'":
1987
1929
  nDelete = 1
1988
- if thePos == 1:
1930
+ if tPos == 1:
1989
1931
  tInsert = self._typSQuoteO
1990
- elif thePos == 2 and theTwo == ">'":
1932
+ elif tPos == 2 and tTwo == ">'":
1991
1933
  tInsert = self._typSQuoteO
1992
- elif thePos == 3 and theThree == ">>'":
1934
+ elif tPos == 3 and tThree == ">>'":
1993
1935
  tInsert = self._typSQuoteO
1994
1936
  else:
1995
1937
  tInsert = self._typSQuoteC
1996
1938
 
1997
- elif self._typRepDash and theThree == "---":
1939
+ elif self._typRepDash and tThree == "---":
1998
1940
  nDelete = 3
1999
1941
  tInsert = nwUnicode.U_EMDASH
2000
1942
 
2001
- elif self._typRepDash and theTwo == "--":
1943
+ elif self._typRepDash and tTwo == "--":
2002
1944
  nDelete = 2
2003
1945
  tInsert = nwUnicode.U_ENDASH
2004
1946
 
2005
- elif self._typRepDash and theTwo == nwUnicode.U_ENDASH + "-":
1947
+ elif self._typRepDash and tTwo == nwUnicode.U_ENDASH + "-":
2006
1948
  nDelete = 2
2007
1949
  tInsert = nwUnicode.U_EMDASH
2008
1950
 
2009
- elif self._typRepDots and theThree == "...":
1951
+ elif self._typRepDots and tThree == "...":
2010
1952
  nDelete = 3
2011
1953
  tInsert = nwUnicode.U_HELLIP
2012
1954
 
2013
- elif theOne == nwUnicode.U_LSEP:
1955
+ elif tOne == nwUnicode.U_LSEP:
2014
1956
  # This resolves issue #1150
2015
1957
  nDelete = 1
2016
1958
  tInsert = nwUnicode.U_PSEP
2017
1959
 
2018
1960
  tCheck = tInsert
2019
1961
  if self._typPadBefore and tCheck in self._typPadBefore:
2020
- if self._allowSpaceBeforeColon(theText, tCheck):
1962
+ if self._allowSpaceBeforeColon(text, tCheck):
2021
1963
  nDelete = max(nDelete, 1)
2022
- chkPos = thePos - nDelete - 1
2023
- if chkPos >= 0 and theText[chkPos].isspace():
1964
+ chkPos = tPos - nDelete - 1
1965
+ if chkPos >= 0 and text[chkPos].isspace():
2024
1966
  # Strip existing space before inserting a new (#1061)
2025
1967
  nDelete += 1
2026
1968
  tInsert = self._typPadChar + tInsert
2027
1969
 
2028
1970
  if self._typPadAfter and tCheck in self._typPadAfter:
2029
- if self._allowSpaceBeforeColon(theText, tCheck):
1971
+ if self._allowSpaceBeforeColon(text, tCheck):
2030
1972
  nDelete = max(nDelete, 1)
2031
1973
  tInsert = tInsert + self._typPadChar
2032
1974
 
2033
1975
  if nDelete > 0:
2034
- theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
2035
- theCursor.insertText(tInsert)
1976
+ cursor.movePosition(
1977
+ QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, nDelete
1978
+ )
1979
+ cursor.insertText(tInsert)
2036
1980
 
2037
1981
  return
2038
1982
 
2039
1983
  @staticmethod
2040
- def _allowSpaceBeforeColon(text, char):
1984
+ def _allowSpaceBeforeColon(text: str, char: str) -> bool:
2041
1985
  """Special checker function only used by the insert space
2042
1986
  feature for French, Spanish, etc, so it doesn't insert a
2043
1987
  space before colons in meta data lines. See issue #1090.
@@ -2050,95 +1994,78 @@ class GuiDocEditor(QTextEdit):
2050
1994
  return False
2051
1995
  return True
2052
1996
 
2053
- def _checkDocSize(self, theSize):
2054
- """Check if document size crosses the big document limit set in
2055
- config. If so, we will set the big document flag to True.
2056
- """
2057
- bigLim = round(CONFIG.bigDocLimit*1000)
2058
- newState = theSize > bigLim
2059
-
2060
- if newState != self._bigDoc:
2061
- if newState:
2062
- logger.info(
2063
- f"The document size is {theSize:n} > {bigLim:n}, "
2064
- f"big doc mode has been enabled"
2065
- )
2066
- else:
2067
- logger.info(
2068
- f"The document size is {theSize:n} <= {bigLim:n}, "
2069
- f"big doc mode has been disabled"
2070
- )
1997
+ def _autoSelect(self) -> QTextCursor:
1998
+ """Return a cursor which may or may not have a selection based
1999
+ on user settings and document action. The selection will be the
2000
+ word closest to the cursor consisting of alphanumerical unicode
2001
+ characters.
2002
+ """
2003
+ cursor = self.textCursor()
2004
+ if CONFIG.autoSelect and not cursor.hasSelection():
2005
+ cPos = cursor.position()
2006
+ bPos = cursor.block().position()
2007
+ bLen = cursor.block().length()
2008
+
2009
+ # Scan backwards
2010
+ sPos = cPos
2011
+ for i in range(cPos - bPos):
2012
+ sPos = cPos - i - 1
2013
+ if not self._qDocument.characterAt(sPos).isalnum():
2014
+ sPos += 1
2015
+ break
2071
2016
 
2072
- self._bigDoc = newState
2017
+ # Scan forwards
2018
+ ePos = cPos
2019
+ for i in range(bPos + bLen - cPos):
2020
+ ePos = cPos + i
2021
+ if not self._qDocument.characterAt(ePos).isalnum():
2022
+ break
2073
2023
 
2074
- return
2024
+ if ePos - sPos <= 0:
2025
+ # No selection possible
2026
+ return cursor
2075
2027
 
2076
- def _autoSelect(self):
2077
- """Return a cursor which may or may not have a selection based
2078
- on user settings and document action.
2079
- """
2080
- theCursor = self.textCursor()
2081
- if CONFIG.autoSelect and not theCursor.hasSelection():
2082
- theCursor.select(QTextCursor.WordUnderCursor)
2083
- posS = theCursor.selectionStart()
2084
- posE = theCursor.selectionEnd()
2085
-
2086
- # Underscore counts as a part of the word, so check that the
2087
- # selection isn't wrapped in italics markers.
2088
- reSelect = False
2089
- qDoc = self.document()
2090
- if qDoc.characterAt(posS) == "_":
2091
- posS += 1
2092
- reSelect = True
2093
- if qDoc.characterAt(posE) == "_":
2094
- posE -= 1
2095
- reSelect = True
2096
- if reSelect:
2097
- theCursor.clearSelection()
2098
- theCursor.setPosition(posS, QTextCursor.MoveAnchor)
2099
- theCursor.setPosition(posE-1, QTextCursor.KeepAnchor)
2100
-
2101
- self.setTextCursor(theCursor)
2102
-
2103
- return theCursor
2104
-
2105
- def _makeSelection(self, selMode):
2106
- """Wrapper function to select text based on a selection mode.
2107
- """
2108
- theCursor = self.textCursor()
2109
- theCursor.clearSelection()
2110
- theCursor.select(selMode)
2028
+ cursor.clearSelection()
2029
+ cursor.setPosition(sPos, QTextCursor.MoveMode.MoveAnchor)
2030
+ cursor.setPosition(ePos, QTextCursor.MoveMode.KeepAnchor)
2031
+
2032
+ self.setTextCursor(cursor)
2111
2033
 
2112
- if selMode == QTextCursor.WordUnderCursor:
2113
- theCursor = self._autoSelect()
2034
+ return cursor
2114
2035
 
2115
- elif selMode == QTextCursor.BlockUnderCursor:
2036
+ def _makeSelection(self, mode: QTextCursor.SelectionType) -> None:
2037
+ """Select text based on selection mode."""
2038
+ cursor = self.textCursor()
2039
+ cursor.clearSelection()
2040
+ cursor.select(mode)
2041
+
2042
+ if mode == QTextCursor.SelectionType.WordUnderCursor:
2043
+ cursor = self._autoSelect()
2044
+
2045
+ elif mode == QTextCursor.SelectionType.BlockUnderCursor:
2116
2046
  # This selection mode also selects the preceding paragraph
2117
2047
  # separator, which we want to avoid.
2118
- posS = theCursor.selectionStart()
2119
- posE = theCursor.selectionEnd()
2120
- selTxt = theCursor.selectedText()
2048
+ posS = cursor.selectionStart()
2049
+ posE = cursor.selectionEnd()
2050
+ selTxt = cursor.selectedText()
2121
2051
  if selTxt.startswith(nwUnicode.U_PSEP):
2122
- theCursor.setPosition(posS+1, QTextCursor.MoveAnchor)
2123
- theCursor.setPosition(posE, QTextCursor.KeepAnchor)
2052
+ cursor.setPosition(posS+1, QTextCursor.MoveMode.MoveAnchor)
2053
+ cursor.setPosition(posE, QTextCursor.MoveMode.KeepAnchor)
2124
2054
 
2125
- self.setTextCursor(theCursor)
2055
+ self.setTextCursor(cursor)
2126
2056
 
2127
2057
  return
2128
2058
 
2129
- def _makePosSelection(self, selMode, thePos):
2130
- """Wrapper function to select text based on selection mode, but
2131
- first move cursor to given position.
2132
- """
2133
- theCursor = self.cursorForPosition(thePos)
2134
- self.setTextCursor(theCursor)
2135
- self._makeSelection(selMode)
2059
+ def _makePosSelection(self, mode: QTextCursor.SelectionType, pos: QPoint) -> None:
2060
+ """Select text based on selection mode, but first move cursor."""
2061
+ cursor = self.cursorForPosition(pos)
2062
+ self.setTextCursor(cursor)
2063
+ self._makeSelection(mode)
2136
2064
  return
2137
2065
 
2138
- def _allowAutoReplace(self, theState):
2139
- """Enable/disable the auto-replace feature temporarily.
2140
- """
2141
- if theState:
2066
+ def _allowAutoReplace(self, state: bool) -> None:
2067
+ """Enable/disable the auto-replace feature temporarily."""
2068
+ if state:
2142
2069
  self._doReplace = CONFIG.doReplace
2143
2070
  else:
2144
2071
  self._doReplace = False
@@ -2147,6 +2074,85 @@ class GuiDocEditor(QTextEdit):
2147
2074
  # END Class GuiDocEditor
2148
2075
 
2149
2076
 
2077
+ class MetaCompleter(QMenu):
2078
+ """GuiWidget: Meta Completer Menu
2079
+
2080
+ This is a context menu with options populated from the user's
2081
+ defined tags. It also helps to type the meta data keyword on a new
2082
+ line starting with an @. The updateText function should be called on
2083
+ every keystroke on a line starting with @.
2084
+ """
2085
+
2086
+ complete = pyqtSignal(int, int, str)
2087
+
2088
+ def __init__(self, parent: QWidget) -> None:
2089
+ super().__init__(parent=parent)
2090
+ return
2091
+
2092
+ def updateText(self, text: str, pos: int) -> bool:
2093
+ """Update the menu options based on the line of text."""
2094
+ self.clear()
2095
+ kw, sep, _ = text.partition(":")
2096
+ if pos <= len(kw):
2097
+ offset = 0
2098
+ length = len(kw.rstrip())
2099
+ suffix = "" if sep else ":"
2100
+ options = list(filter(
2101
+ lambda x: x.startswith(kw.rstrip()), nwKeyWords.VALID_KEYS
2102
+ ))
2103
+ else:
2104
+ status, tBits, tPos = SHARED.project.index.scanThis(text)
2105
+ if not status:
2106
+ return False
2107
+ index = bisect.bisect_right(tPos, pos) - 1
2108
+ lookup = tBits[index].lower() if index > 0 else ""
2109
+ offset = tPos[index] if lookup else pos
2110
+ length = len(lookup)
2111
+ suffix = ""
2112
+ options = list(filter(
2113
+ lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
2114
+ nwKeyWords.KEY_CLASS.get(kw.strip(), nwItemClass.NO_CLASS)
2115
+ )
2116
+ ))[:15]
2117
+
2118
+ if not options:
2119
+ return False
2120
+
2121
+ for value in sorted(options):
2122
+ rep = value + suffix
2123
+ action = self.addAction(value)
2124
+ action.triggered.connect(lambda _, r=rep: self._emitComplete(offset, length, r))
2125
+
2126
+ return True
2127
+
2128
+ ##
2129
+ # Events
2130
+ ##
2131
+
2132
+ def keyPressEvent(self, event: QKeyEvent) -> None:
2133
+ """Capture keypresses and forward most of them to the editor."""
2134
+ parent = self.parent()
2135
+ if event.key() in (
2136
+ Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Return,
2137
+ Qt.Key.Key_Enter, Qt.Key.Key_Escape
2138
+ ):
2139
+ super().keyPressEvent(event)
2140
+ elif isinstance(parent, GuiDocEditor):
2141
+ parent.keyPressEvent(event)
2142
+ return
2143
+
2144
+ ##
2145
+ # Internal Functions
2146
+ ##
2147
+
2148
+ def _emitComplete(self, pos: int, length: int, value: str):
2149
+ """Emit the signal to indicate a selection has been made."""
2150
+ self.complete.emit(pos, length, value)
2151
+ return
2152
+
2153
+ # END Class MetaCompleter
2154
+
2155
+
2150
2156
  # =============================================================================================== #
2151
2157
  # The Off-GUI Thread Word Counter
2152
2158
  # A runnable for the word counter to be run in the thread pool off the main GUI thread.
@@ -2154,7 +2160,7 @@ class GuiDocEditor(QTextEdit):
2154
2160
 
2155
2161
  class BackgroundWordCounter(QRunnable):
2156
2162
 
2157
- def __init__(self, docEditor, forSelection=False):
2163
+ def __init__(self, docEditor: GuiDocEditor, forSelection: bool = False) -> None:
2158
2164
  super().__init__()
2159
2165
 
2160
2166
  self._docEditor = docEditor
@@ -2165,21 +2171,21 @@ class BackgroundWordCounter(QRunnable):
2165
2171
 
2166
2172
  return
2167
2173
 
2168
- def isRunning(self):
2174
+ def isRunning(self) -> bool:
2169
2175
  return self._isRunning
2170
2176
 
2171
2177
  @pyqtSlot()
2172
- def run(self):
2178
+ def run(self) -> None:
2173
2179
  """Overloaded run function for the word counter, forwarding the
2174
2180
  call to the function that does the actual counting.
2175
2181
  """
2176
2182
  self._isRunning = True
2177
2183
  if self._forSelection:
2178
- theText = self._docEditor.textCursor().selectedText()
2184
+ text = self._docEditor.textCursor().selectedText()
2179
2185
  else:
2180
- theText = self._docEditor.getText()
2186
+ text = self._docEditor.getText()
2181
2187
 
2182
- cC, wC, pC = countWords(theText)
2188
+ cC, wC, pC = countWords(text)
2183
2189
  self.signals.countsReady.emit(cC, wC, pC)
2184
2190
  self._isRunning = False
2185
2191
 
@@ -2197,6 +2203,145 @@ class BackgroundWordCounterSignals(QObject):
2197
2203
  # END Class BackgroundWordCounterSignals
2198
2204
 
2199
2205
 
2206
+ # =============================================================================================== #
2207
+ # The Formatting and Options Fold Out Menu
2208
+ # Only used by DocEditor, and is opened by the first button in the header
2209
+ # =============================================================================================== #
2210
+
2211
+ class GuiDocToolBar(QWidget):
2212
+
2213
+ requestDocAction = pyqtSignal(nwDocAction)
2214
+
2215
+ def __init__(self, docEditor: GuiDocEditor) -> None:
2216
+ super().__init__(parent=docEditor)
2217
+
2218
+ logger.debug("Create: GuiDocToolBar")
2219
+
2220
+ cM = CONFIG.pxInt(4)
2221
+ tPx = int(0.8*SHARED.theme.fontPixelSize)
2222
+ iconSize = QSize(tPx, tPx)
2223
+ self.setContentsMargins(0, 0, 0, 0)
2224
+
2225
+ # General Buttons
2226
+ # ===============
2227
+
2228
+ self.tbMode = QToolButton(self)
2229
+ self.tbMode.setToolTip(self.tr("Toggle Markdown or Shortcodes Mode"))
2230
+ self.tbMode.setIconSize(iconSize)
2231
+ self.tbMode.setCheckable(True)
2232
+ self.tbMode.setChecked(CONFIG.useShortcodes)
2233
+ self.tbMode.toggled.connect(self._toggleFormatMode)
2234
+
2235
+ self.tbBold = QToolButton(self)
2236
+ self.tbBold.setIconSize(iconSize)
2237
+ self.tbBold.clicked.connect(self._formatBold)
2238
+
2239
+ self.tbItalic = QToolButton(self)
2240
+ self.tbItalic.setIconSize(iconSize)
2241
+ self.tbItalic.clicked.connect(self._formatItalic)
2242
+
2243
+ self.tbStrike = QToolButton(self)
2244
+ self.tbStrike.setIconSize(iconSize)
2245
+ self.tbStrike.clicked.connect(self._formatStrike)
2246
+
2247
+ self.tbUnderline = QToolButton(self)
2248
+ self.tbUnderline.setIconSize(iconSize)
2249
+ self.tbUnderline.clicked.connect(
2250
+ lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE)
2251
+ )
2252
+
2253
+ self.tbSuperscript = QToolButton(self)
2254
+ self.tbSuperscript.setIconSize(iconSize)
2255
+ self.tbSuperscript.clicked.connect(
2256
+ lambda: self.requestDocAction.emit(nwDocAction.SC_SUP)
2257
+ )
2258
+
2259
+ self.tbSubscript = QToolButton(self)
2260
+ self.tbSubscript.setIconSize(iconSize)
2261
+ self.tbSubscript.clicked.connect(
2262
+ lambda: self.requestDocAction.emit(nwDocAction.SC_SUB)
2263
+ )
2264
+
2265
+ # Assemble
2266
+ # ========
2267
+
2268
+ self.outerBox = QVBoxLayout()
2269
+ self.outerBox.addWidget(self.tbMode)
2270
+ self.outerBox.addWidget(self.tbBold)
2271
+ self.outerBox.addWidget(self.tbItalic)
2272
+ self.outerBox.addWidget(self.tbStrike)
2273
+ self.outerBox.addWidget(self.tbUnderline)
2274
+ self.outerBox.addWidget(self.tbSuperscript)
2275
+ self.outerBox.addWidget(self.tbSubscript)
2276
+ self.outerBox.setContentsMargins(cM, cM, cM, cM)
2277
+ self.outerBox.setSpacing(cM)
2278
+
2279
+ self.setLayout(self.outerBox)
2280
+ self.updateTheme()
2281
+
2282
+ # Starts as Invisible
2283
+ self.setVisible(False)
2284
+
2285
+ logger.debug("Ready: GuiDocToolBar")
2286
+
2287
+ return
2288
+
2289
+ def updateTheme(self) -> None:
2290
+ """Initialise GUI elements that depend on specific settings."""
2291
+ palette = QPalette()
2292
+ palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
2293
+ palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
2294
+ palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
2295
+ self.setPalette(palette)
2296
+
2297
+ tPx = int(0.8*SHARED.theme.fontPixelSize)
2298
+ self.tbMode.setIcon(SHARED.theme.getToggleIcon("fmt_mode", (tPx, tPx)))
2299
+ self.tbBold.setIcon(SHARED.theme.getIcon("fmt_bold"))
2300
+ self.tbItalic.setIcon(SHARED.theme.getIcon("fmt_italic"))
2301
+ self.tbStrike.setIcon(SHARED.theme.getIcon("fmt_strike"))
2302
+ self.tbUnderline.setIcon(SHARED.theme.getIcon("fmt_underline"))
2303
+ self.tbSuperscript.setIcon(SHARED.theme.getIcon("fmt_superscript"))
2304
+ self.tbSubscript.setIcon(SHARED.theme.getIcon("fmt_subscript"))
2305
+
2306
+ return
2307
+
2308
+ ##
2309
+ # Private Slots
2310
+ ##
2311
+
2312
+ @pyqtSlot(bool)
2313
+ def _toggleFormatMode(self, checked: bool) -> None:
2314
+ """Toggle the formatting mode."""
2315
+ CONFIG.useShortcodes = checked
2316
+ return
2317
+
2318
+ @pyqtSlot()
2319
+ def _formatBold(self):
2320
+ """Call the bold format action."""
2321
+ self.requestDocAction.emit(
2322
+ nwDocAction.SC_BOLD if self.tbMode.isChecked() else nwDocAction.STRONG
2323
+ )
2324
+ return
2325
+
2326
+ @pyqtSlot()
2327
+ def _formatItalic(self):
2328
+ """Call the italic format action."""
2329
+ self.requestDocAction.emit(
2330
+ nwDocAction.SC_ITALIC if self.tbMode.isChecked() else nwDocAction.EMPH
2331
+ )
2332
+ return
2333
+
2334
+ @pyqtSlot()
2335
+ def _formatStrike(self):
2336
+ """Call the strikethrough format action."""
2337
+ self.requestDocAction.emit(
2338
+ nwDocAction.SC_STRIKE if self.tbMode.isChecked() else nwDocAction.STRIKE
2339
+ )
2340
+ return
2341
+
2342
+ # END Class GuiDocToolBar
2343
+
2344
+
2200
2345
  # =============================================================================================== #
2201
2346
  # The Embedded Document Search/Replace Feature
2202
2347
  # Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport
@@ -2204,13 +2349,12 @@ class BackgroundWordCounterSignals(QObject):
2204
2349
 
2205
2350
  class GuiDocEditSearch(QFrame):
2206
2351
 
2207
- def __init__(self, docEditor):
2352
+ def __init__(self, docEditor: GuiDocEditor) -> None:
2208
2353
  super().__init__(parent=docEditor)
2209
2354
 
2210
2355
  logger.debug("Create: GuiDocEditSearch")
2211
2356
 
2212
2357
  self.docEditor = docEditor
2213
- self.mainGui = docEditor.mainGui
2214
2358
 
2215
2359
  self.repVisible = False
2216
2360
  self.isCaseSense = CONFIG.searchCase
@@ -2227,7 +2371,7 @@ class GuiDocEditSearch(QFrame):
2227
2371
 
2228
2372
  self.setContentsMargins(0, 0, 0, 0)
2229
2373
  self.setAutoFillBackground(True)
2230
- self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain)
2374
+ self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain)
2231
2375
 
2232
2376
  self.mainBox = QGridLayout(self)
2233
2377
  self.setLayout(self.mainBox)
@@ -2246,7 +2390,7 @@ class GuiDocEditSearch(QFrame):
2246
2390
  self.replaceBox.returnPressed.connect(self._doReplace)
2247
2391
 
2248
2392
  self.searchOpt = QToolBar(self)
2249
- self.searchOpt.setToolButtonStyle(Qt.ToolButtonIconOnly)
2393
+ self.searchOpt.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2250
2394
  self.searchOpt.setIconSize(QSize(tPx, tPx))
2251
2395
  self.searchOpt.setContentsMargins(0, 0, 0, 0)
2252
2396
 
@@ -2308,7 +2452,7 @@ class GuiDocEditSearch(QFrame):
2308
2452
  bPx = self.searchBox.sizeHint().height()
2309
2453
 
2310
2454
  self.showReplace = QToolButton(self)
2311
- self.showReplace.setArrowType(Qt.RightArrow)
2455
+ self.showReplace.setArrowType(Qt.ArrowType.RightArrow)
2312
2456
  self.showReplace.setCheckable(True)
2313
2457
  self.showReplace.toggled.connect(self._doToggleReplace)
2314
2458
 
@@ -2322,8 +2466,8 @@ class GuiDocEditSearch(QFrame):
2322
2466
  self.replaceButton.setToolTip(self.tr("Find and replace in current document"))
2323
2467
  self.replaceButton.clicked.connect(self._doReplace)
2324
2468
 
2325
- self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, Qt.AlignLeft)
2326
- self.mainBox.addWidget(self.searchOpt, 0, 2, 1, 3, Qt.AlignRight)
2469
+ self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, Qt.AlignmentFlag.AlignLeft)
2470
+ self.mainBox.addWidget(self.searchOpt, 0, 2, 1, 3, Qt.AlignmentFlag.AlignRight)
2327
2471
  self.mainBox.addWidget(self.showReplace, 1, 0, 1, 1)
2328
2472
  self.mainBox.addWidget(self.searchBox, 1, 1, 1, 2)
2329
2473
  self.mainBox.addWidget(self.searchButton, 1, 3, 1, 1)
@@ -2353,9 +2497,94 @@ class GuiDocEditSearch(QFrame):
2353
2497
 
2354
2498
  return
2355
2499
 
2356
- def updateTheme(self):
2357
- """Update theme elements.
2500
+ ##
2501
+ # Properties
2502
+ ##
2503
+
2504
+ @property
2505
+ def searchText(self) -> str:
2506
+ """Return the current search text."""
2507
+ return self.searchBox.text()
2508
+
2509
+ @property
2510
+ def replaceText(self) -> str:
2511
+ """Return the current replace text."""
2512
+ return self.replaceBox.text()
2513
+
2514
+ ##
2515
+ # Getters
2516
+ ##
2517
+
2518
+ def getSearchObject(self) -> str | QRegularExpression | QRegExp:
2519
+ """Return the current search text either as text or as a regular
2520
+ expression object.
2521
+ """
2522
+ text = self.searchBox.text()
2523
+ if self.isRegEx:
2524
+ # Using the Unicode-capable QRegularExpression class was
2525
+ # only added in Qt 5.13. Otherwise, 5.3 and up supports
2526
+ # only the QRegExp class.
2527
+ if CONFIG.verQtValue >= 0x050d00:
2528
+ rxOpt = QRegularExpression.PatternOption.UseUnicodePropertiesOption
2529
+ if not self.isCaseSense:
2530
+ rxOpt |= QRegularExpression.PatternOption.CaseInsensitiveOption
2531
+ regEx = QRegularExpression(text, rxOpt)
2532
+ self._alertSearchValid(regEx.isValid())
2533
+ return regEx
2534
+ else: # pragma: no cover
2535
+ # >= 50300 to < 51300
2536
+ if self.isCaseSense:
2537
+ rxOpt = Qt.CaseSensitivity.CaseSensitive
2538
+ else:
2539
+ rxOpt = Qt.CaseSensitivity.CaseInsensitive
2540
+ regEx = QRegExp(text, rxOpt)
2541
+ self._alertSearchValid(regEx.isValid())
2542
+ return regEx
2543
+
2544
+ return text
2545
+
2546
+ ##
2547
+ # Setters
2548
+ ##
2549
+
2550
+ def setSearchText(self, text: str | None) -> None:
2551
+ """Open the search bar and set the search text to the text
2552
+ provided, if any.
2358
2553
  """
2554
+ if not self.isVisible():
2555
+ self.setVisible(True)
2556
+ if text is not None:
2557
+ self.searchBox.setText(text)
2558
+ self.searchBox.setFocus()
2559
+ self.searchBox.selectAll()
2560
+ if self.isRegEx:
2561
+ self._alertSearchValid(True)
2562
+ return
2563
+
2564
+ def setReplaceText(self, text: str) -> None:
2565
+ """Set the replace text."""
2566
+ self.showReplace.setChecked(True)
2567
+ self.replaceBox.setFocus()
2568
+ self.replaceBox.setText(text)
2569
+ return
2570
+
2571
+ def setResultCount(self, currRes: int | None, resCount: int | None) -> None:
2572
+ """Set the count values for the current search."""
2573
+ sCurrRes = "?" if currRes is None else str(currRes)
2574
+ sResCount = "?" if resCount is None else "1000+" if resCount > 1000 else str(resCount)
2575
+ minWidth = SHARED.theme.getTextWidth(f"{sResCount}//{sResCount}", self.boxFont)
2576
+ self.resultLabel.setText(f"{sCurrRes}/{sResCount}")
2577
+ self.resultLabel.setMinimumWidth(minWidth)
2578
+ self.adjustSize()
2579
+ self.docEditor.updateDocMargins()
2580
+ return
2581
+
2582
+ ##
2583
+ # Methods
2584
+ ##
2585
+
2586
+ def updateTheme(self) -> None:
2587
+ """Update theme elements."""
2359
2588
  qPalette = qApp.palette()
2360
2589
  self.setPalette(qPalette)
2361
2590
  self.searchBox.setPalette(qPalette)
@@ -2396,9 +2625,8 @@ class GuiDocEditSearch(QFrame):
2396
2625
 
2397
2626
  return
2398
2627
 
2399
- def closeSearch(self):
2400
- """Close the search box.
2401
- """
2628
+ def closeSearch(self) -> None:
2629
+ """Close the search box."""
2402
2630
  CONFIG.searchCase = self.isCaseSense
2403
2631
  CONFIG.searchWord = self.isWholeWord
2404
2632
  CONFIG.searchRegEx = self.isRegEx
@@ -2413,7 +2641,7 @@ class GuiDocEditSearch(QFrame):
2413
2641
 
2414
2642
  return
2415
2643
 
2416
- def cycleFocus(self, toNext):
2644
+ def cycleFocus(self, next: bool) -> bool:
2417
2645
  """The tab key just alternates focus between the two input
2418
2646
  boxes, if the replace box is visible.
2419
2647
  """
@@ -2426,184 +2654,96 @@ class GuiDocEditSearch(QFrame):
2426
2654
  return True
2427
2655
  return False
2428
2656
 
2429
- def anyFocus(self):
2430
- """Return True if any of the input boxes have focus.
2431
- """
2657
+ def anyFocus(self) -> bool:
2658
+ """Return True if any of the input boxes have focus."""
2432
2659
  return self.searchBox.hasFocus() | self.replaceBox.hasFocus()
2433
2660
 
2434
2661
  ##
2435
- # Get and Set Functions
2436
- ##
2437
-
2438
- def setSearchText(self, theText):
2439
- """Open the search bar and set the search text to the text
2440
- provided, if any.
2441
- """
2442
- if not self.isVisible():
2443
- self.setVisible(True)
2444
- if theText is not None:
2445
- self.searchBox.setText(theText)
2446
- self.searchBox.setFocus()
2447
- self.searchBox.selectAll()
2448
- if self.isRegEx:
2449
- self._alertSearchValid(True)
2450
- return True
2451
-
2452
- def setReplaceText(self, theText):
2453
- """Set the replace text.
2454
- """
2455
- self.showReplace.setChecked(True)
2456
- self.replaceBox.setFocus()
2457
- self.replaceBox.setText(theText)
2458
- return True
2459
-
2460
- def setResultCount(self, currRes, resCount):
2461
- """Set the count values for the current search.
2462
- """
2463
- currRes = "?" if currRes is None else currRes
2464
- resCount = "?" if resCount is None else "1000+" if resCount > 1000 else resCount
2465
- minWidth = SHARED.theme.getTextWidth(f"{resCount}//{resCount}", self.boxFont)
2466
- self.resultLabel.setText(f"{currRes}/{resCount}")
2467
- self.resultLabel.setMinimumWidth(minWidth)
2468
- self.adjustSize()
2469
- self.docEditor.updateDocMargins()
2470
- return
2471
-
2472
- def getSearchObject(self):
2473
- """Return the current search text either as text or as a regular
2474
- expression object.
2475
- """
2476
- theText = self.searchBox.text()
2477
- if self.isRegEx:
2478
- # Using the Unicode-capable QRegularExpression class was
2479
- # only added in Qt 5.13. Otherwise, 5.3 and up supports
2480
- # only the QRegExp class.
2481
- if CONFIG.verQtValue >= 0x050d00:
2482
- rxOpt = QRegularExpression.UseUnicodePropertiesOption
2483
- if not self.isCaseSense:
2484
- rxOpt |= QRegularExpression.CaseInsensitiveOption
2485
- theRegEx = QRegularExpression(theText, rxOpt)
2486
- self._alertSearchValid(theRegEx.isValid())
2487
- return theRegEx
2488
-
2489
- else: # pragma: no cover
2490
- # >= 50300 to < 51300
2491
- if self.isCaseSense:
2492
- rxOpt = Qt.CaseSensitive
2493
- else:
2494
- rxOpt = Qt.CaseInsensitive
2495
- theRegEx = QRegExp(theText, rxOpt)
2496
- self._alertSearchValid(theRegEx.isValid())
2497
- return theRegEx
2498
-
2499
- return theText
2500
-
2501
- def getSearchText(self):
2502
- """Return the current search text.
2503
- """
2504
- return self.searchBox.text()
2505
-
2506
- def getReplaceText(self):
2507
- """Return the current replace text.
2508
- """
2509
- return self.replaceBox.text()
2510
-
2511
- ##
2512
- # Slots
2662
+ # Private Slots
2513
2663
  ##
2514
2664
 
2515
2665
  @pyqtSlot()
2516
- def _doClose(self):
2517
- """Hide the search/replace bar.
2518
- """
2666
+ def _doClose(self) -> None:
2667
+ """Hide the search/replace bar."""
2519
2668
  self.closeSearch()
2520
2669
  return
2521
2670
 
2522
2671
  @pyqtSlot()
2523
- def _doSearch(self):
2524
- """Call the search action function for the document editor.
2525
- """
2672
+ def _doSearch(self) -> None:
2673
+ """Call the search action function for the document editor."""
2526
2674
  modKey = qApp.keyboardModifiers()
2527
- if modKey == Qt.ShiftModifier:
2675
+ if modKey == Qt.KeyboardModifier.ShiftModifier:
2528
2676
  self.docEditor.findNext(goBack=True)
2529
2677
  else:
2530
2678
  self.docEditor.findNext()
2531
2679
  return
2532
2680
 
2533
2681
  @pyqtSlot()
2534
- def _doReplace(self):
2535
- """Call the replace action function for the document editor.
2536
- """
2682
+ def _doReplace(self) -> None:
2683
+ """Call the replace action function for the document editor."""
2537
2684
  self.docEditor.replaceNext()
2538
2685
  return
2539
2686
 
2540
2687
  @pyqtSlot(bool)
2541
- def _doToggleReplace(self, theState):
2542
- """Toggle the show/hide of the replace box.
2543
- """
2544
- if theState:
2545
- self.showReplace.setArrowType(Qt.DownArrow)
2688
+ def _doToggleReplace(self, state: bool) -> None:
2689
+ """Toggle the show/hide of the replace box."""
2690
+ if state:
2691
+ self.showReplace.setArrowType(Qt.ArrowType.DownArrow)
2546
2692
  else:
2547
- self.showReplace.setArrowType(Qt.RightArrow)
2548
- self.replaceBox.setVisible(theState)
2549
- self.replaceButton.setVisible(theState)
2550
- self.repVisible = theState
2693
+ self.showReplace.setArrowType(Qt.ArrowType.RightArrow)
2694
+ self.replaceBox.setVisible(state)
2695
+ self.replaceButton.setVisible(state)
2696
+ self.repVisible = state
2551
2697
  self.adjustSize()
2552
2698
  self.docEditor.updateDocMargins()
2553
2699
  return
2554
2700
 
2555
2701
  @pyqtSlot(bool)
2556
- def _doToggleCase(self, theState):
2557
- """Enable/disable case sensitive mode.
2558
- """
2559
- self.isCaseSense = theState
2702
+ def _doToggleCase(self, state: bool) -> None:
2703
+ """Enable/disable case sensitive mode."""
2704
+ self.isCaseSense = state
2560
2705
  return
2561
2706
 
2562
2707
  @pyqtSlot(bool)
2563
- def _doToggleWord(self, theState):
2564
- """Enable/disable whole word search mode.
2565
- """
2566
- self.isWholeWord = theState
2708
+ def _doToggleWord(self, state: bool) -> None:
2709
+ """Enable/disable whole word search mode."""
2710
+ self.isWholeWord = state
2567
2711
  return
2568
2712
 
2569
2713
  @pyqtSlot(bool)
2570
- def _doToggleRegEx(self, theState):
2571
- """Enable/disable regular expression search mode.
2572
- """
2573
- self.isRegEx = theState
2714
+ def _doToggleRegEx(self, state: bool) -> None:
2715
+ """Enable/disable regular expression search mode."""
2716
+ self.isRegEx = state
2574
2717
  return
2575
2718
 
2576
2719
  @pyqtSlot(bool)
2577
- def _doToggleLoop(self, theState):
2578
- """Enable/disable looping the search.
2579
- """
2580
- self.doLoop = theState
2720
+ def _doToggleLoop(self, state: bool) -> None:
2721
+ """Enable/disable looping the search."""
2722
+ self.doLoop = state
2581
2723
  return
2582
2724
 
2583
2725
  @pyqtSlot(bool)
2584
- def _doToggleProject(self, theState):
2585
- """Enable/disable continuing search in next project file.
2586
- """
2587
- self.doNextFile = theState
2726
+ def _doToggleProject(self, state: bool) -> None:
2727
+ """Enable/disable continuing search in next project file."""
2728
+ self.doNextFile = state
2588
2729
  return
2589
2730
 
2590
2731
  @pyqtSlot(bool)
2591
- def _doToggleMatchCap(self, theState):
2592
- """Enable/disable preserving capitalisation when replacing.
2593
- """
2594
- self.doMatchCap = theState
2732
+ def _doToggleMatchCap(self, state: bool) -> None:
2733
+ """Enable/disable preserving capitalisation when replacing."""
2734
+ self.doMatchCap = state
2595
2735
  return
2596
2736
 
2597
2737
  ##
2598
2738
  # Internal Functions
2599
2739
  ##
2600
2740
 
2601
- def _alertSearchValid(self, isValid):
2741
+ def _alertSearchValid(self, isValid: bool) -> None:
2602
2742
  """Highlight the search box to indicate the search string is or
2603
2743
  isn't valid. Take the colour from the replace box.
2604
2744
  """
2605
2745
  qPalette = self.replaceBox.palette()
2606
- qPalette.setColor(QPalette.Base, self.rxCol[isValid])
2746
+ qPalette.setColor(QPalette.ColorRole.Base, self.rxCol[isValid])
2607
2747
  self.searchBox.setPalette(qPalette)
2608
2748
  return
2609
2749
 
@@ -2617,7 +2757,10 @@ class GuiDocEditSearch(QFrame):
2617
2757
 
2618
2758
  class GuiDocEditHeader(QWidget):
2619
2759
 
2620
- def __init__(self, docEditor):
2760
+ closeDocumentRequest = pyqtSignal()
2761
+ toggleToolBarRequest = pyqtSignal()
2762
+
2763
+ def __init__(self, docEditor: GuiDocEditor) -> None:
2621
2764
  super().__init__(parent=docEditor)
2622
2765
 
2623
2766
  logger.debug("Create: GuiDocEditHeader")
@@ -2629,67 +2772,68 @@ class GuiDocEditHeader(QWidget):
2629
2772
 
2630
2773
  fPx = int(0.9*SHARED.theme.fontPixelSize)
2631
2774
  hSp = CONFIG.pxInt(6)
2775
+ iconSize = QSize(fPx, fPx)
2632
2776
 
2633
2777
  # Main Widget Settings
2634
2778
  self.setAutoFillBackground(True)
2635
2779
 
2636
2780
  # Title Label
2637
- self.theTitle = QLabel()
2638
- self.theTitle.setText("")
2639
- self.theTitle.setIndent(0)
2640
- self.theTitle.setMargin(0)
2641
- self.theTitle.setContentsMargins(0, 0, 0, 0)
2642
- self.theTitle.setAutoFillBackground(True)
2643
- self.theTitle.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
2644
- self.theTitle.setFixedHeight(fPx)
2645
-
2646
- lblFont = self.theTitle.font()
2781
+ self.itemTitle = QLabel()
2782
+ self.itemTitle.setText("")
2783
+ self.itemTitle.setIndent(0)
2784
+ self.itemTitle.setMargin(0)
2785
+ self.itemTitle.setContentsMargins(0, 0, 0, 0)
2786
+ self.itemTitle.setAutoFillBackground(True)
2787
+ self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
2788
+ self.itemTitle.setFixedHeight(fPx)
2789
+
2790
+ lblFont = self.itemTitle.font()
2647
2791
  lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
2648
- self.theTitle.setFont(lblFont)
2792
+ self.itemTitle.setFont(lblFont)
2649
2793
 
2650
2794
  # Buttons
2651
- self.editButton = QToolButton(self)
2652
- self.editButton.setContentsMargins(0, 0, 0, 0)
2653
- self.editButton.setIconSize(QSize(fPx, fPx))
2654
- self.editButton.setFixedSize(fPx, fPx)
2655
- self.editButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
2656
- self.editButton.setVisible(False)
2657
- self.editButton.setToolTip(self.tr("Edit document label"))
2658
- self.editButton.clicked.connect(self._editDocument)
2795
+ self.tbButton = QToolButton(self)
2796
+ self.tbButton.setContentsMargins(0, 0, 0, 0)
2797
+ self.tbButton.setIconSize(iconSize)
2798
+ self.tbButton.setFixedSize(fPx, fPx)
2799
+ self.tbButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2800
+ self.tbButton.setVisible(False)
2801
+ self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
2802
+ self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
2659
2803
 
2660
2804
  self.searchButton = QToolButton(self)
2661
2805
  self.searchButton.setContentsMargins(0, 0, 0, 0)
2662
- self.searchButton.setIconSize(QSize(fPx, fPx))
2806
+ self.searchButton.setIconSize(iconSize)
2663
2807
  self.searchButton.setFixedSize(fPx, fPx)
2664
- self.searchButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
2808
+ self.searchButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2665
2809
  self.searchButton.setVisible(False)
2666
- self.searchButton.setToolTip(self.tr("Search document"))
2667
- self.searchButton.clicked.connect(self._searchDocument)
2810
+ self.searchButton.setToolTip(self.tr("Search"))
2811
+ self.searchButton.clicked.connect(self.docEditor.toggleSearch)
2668
2812
 
2669
2813
  self.minmaxButton = QToolButton(self)
2670
2814
  self.minmaxButton.setContentsMargins(0, 0, 0, 0)
2671
- self.minmaxButton.setIconSize(QSize(fPx, fPx))
2815
+ self.minmaxButton.setIconSize(iconSize)
2672
2816
  self.minmaxButton.setFixedSize(fPx, fPx)
2673
- self.minmaxButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
2817
+ self.minmaxButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2674
2818
  self.minmaxButton.setVisible(False)
2675
2819
  self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
2676
- self.minmaxButton.clicked.connect(self._minmaxDocument)
2820
+ self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit())
2677
2821
 
2678
2822
  self.closeButton = QToolButton(self)
2679
2823
  self.closeButton.setContentsMargins(0, 0, 0, 0)
2680
- self.closeButton.setIconSize(QSize(fPx, fPx))
2824
+ self.closeButton.setIconSize(iconSize)
2681
2825
  self.closeButton.setFixedSize(fPx, fPx)
2682
- self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
2826
+ self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
2683
2827
  self.closeButton.setVisible(False)
2684
- self.closeButton.setToolTip(self.tr("Close the document"))
2828
+ self.closeButton.setToolTip(self.tr("Close"))
2685
2829
  self.closeButton.clicked.connect(self._closeDocument)
2686
2830
 
2687
2831
  # Assemble Layout
2688
2832
  self.outerBox = QHBoxLayout()
2689
2833
  self.outerBox.setSpacing(hSp)
2690
- self.outerBox.addWidget(self.editButton, 0)
2834
+ self.outerBox.addWidget(self.tbButton, 0)
2691
2835
  self.outerBox.addWidget(self.searchButton, 0)
2692
- self.outerBox.addWidget(self.theTitle, 1)
2836
+ self.outerBox.addWidget(self.itemTitle, 1)
2693
2837
  self.outerBox.addWidget(self.minmaxButton, 0)
2694
2838
  self.outerBox.addWidget(self.closeButton, 0)
2695
2839
  self.setLayout(self.outerBox)
@@ -2711,10 +2855,9 @@ class GuiDocEditHeader(QWidget):
2711
2855
  # Methods
2712
2856
  ##
2713
2857
 
2714
- def updateTheme(self):
2715
- """Update theme elements.
2716
- """
2717
- self.editButton.setIcon(SHARED.theme.getIcon("edit"))
2858
+ def updateTheme(self) -> None:
2859
+ """Update theme elements."""
2860
+ self.tbButton.setIcon(SHARED.theme.getIcon("menu"))
2718
2861
  self.searchButton.setIcon(SHARED.theme.getIcon("search"))
2719
2862
  self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
2720
2863
  self.closeButton.setIcon(SHARED.theme.getIcon("close"))
@@ -2724,7 +2867,7 @@ class GuiDocEditHeader(QWidget):
2724
2867
  "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
2725
2868
  ).format(*SHARED.theme.colText)
2726
2869
 
2727
- self.editButton.setStyleSheet(buttonStyle)
2870
+ self.tbButton.setStyleSheet(buttonStyle)
2728
2871
  self.searchButton.setStyleSheet(buttonStyle)
2729
2872
  self.minmaxButton.setStyleSheet(buttonStyle)
2730
2873
  self.closeButton.setStyleSheet(buttonStyle)
@@ -2733,28 +2876,28 @@ class GuiDocEditHeader(QWidget):
2733
2876
 
2734
2877
  return
2735
2878
 
2736
- def matchColours(self):
2879
+ def matchColours(self) -> None:
2737
2880
  """Update the colours of the widget to match those of the syntax
2738
2881
  theme rather than the main GUI.
2739
2882
  """
2740
- thePalette = QPalette()
2741
- thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
2742
- thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
2743
- thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
2883
+ palette = QPalette()
2884
+ palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
2885
+ palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
2886
+ palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
2744
2887
 
2745
- self.setPalette(thePalette)
2746
- self.theTitle.setPalette(thePalette)
2888
+ self.setPalette(palette)
2889
+ self.itemTitle.setPalette(palette)
2747
2890
 
2748
2891
  return
2749
2892
 
2750
- def setTitleFromHandle(self, tHandle):
2893
+ def setTitleFromHandle(self, tHandle: str | None) -> bool:
2751
2894
  """Set the document title from the handle, or alternatively, set
2752
2895
  the whole document path within the project.
2753
2896
  """
2754
2897
  self._docHandle = tHandle
2755
2898
  if tHandle is None:
2756
- self.theTitle.setText("")
2757
- self.editButton.setVisible(False)
2899
+ self.itemTitle.setText("")
2900
+ self.tbButton.setVisible(False)
2758
2901
  self.searchButton.setVisible(False)
2759
2902
  self.closeButton.setVisible(False)
2760
2903
  self.minmaxButton.setVisible(False)
@@ -2769,21 +2912,21 @@ class GuiDocEditHeader(QWidget):
2769
2912
  if nwItem is not None:
2770
2913
  tTitle.append(nwItem.itemName)
2771
2914
  sSep = " %s " % nwUnicode.U_RSAQUO
2772
- self.theTitle.setText(sSep.join(tTitle))
2915
+ self.itemTitle.setText(sSep.join(tTitle))
2773
2916
  else:
2774
2917
  nwItem = pTree[tHandle]
2775
2918
  if nwItem is None:
2776
2919
  return False
2777
- self.theTitle.setText(nwItem.itemName)
2920
+ self.itemTitle.setText(nwItem.itemName)
2778
2921
 
2779
- self.editButton.setVisible(True)
2922
+ self.tbButton.setVisible(True)
2780
2923
  self.searchButton.setVisible(True)
2781
2924
  self.closeButton.setVisible(True)
2782
2925
  self.minmaxButton.setVisible(True)
2783
2926
 
2784
2927
  return True
2785
2928
 
2786
- def updateFocusMode(self):
2929
+ def updateFocusMode(self) -> None:
2787
2930
  """Update the minimise/maximise icon of the Focus Mode button.
2788
2931
  This function is called by the GuiMain class via the
2789
2932
  toggleFocusMode function and should not be activated directly.
@@ -2795,50 +2938,29 @@ class GuiDocEditHeader(QWidget):
2795
2938
  return
2796
2939
 
2797
2940
  ##
2798
- # Slots
2941
+ # Private Slots
2799
2942
  ##
2800
2943
 
2801
2944
  @pyqtSlot()
2802
- def _editDocument(self):
2803
- """Open the edit item dialog from the main GUI.
2804
- """
2805
- self.mainGui.editItemLabel(self._docHandle)
2806
- return
2807
-
2808
- @pyqtSlot()
2809
- def _searchDocument(self):
2810
- """Toggle the visibility of the search box.
2811
- """
2812
- self.docEditor.toggleSearch()
2813
- return
2814
-
2815
- @pyqtSlot()
2816
- def _closeDocument(self):
2817
- """Trigger the close editor on the main window.
2818
- """
2819
- self.mainGui.closeDocEditor()
2820
- self.editButton.setVisible(False)
2945
+ def _closeDocument(self) -> None:
2946
+ """Trigger the close editor on the main window."""
2947
+ self.closeDocumentRequest.emit()
2948
+ self.tbButton.setVisible(False)
2821
2949
  self.searchButton.setVisible(False)
2822
2950
  self.closeButton.setVisible(False)
2823
2951
  self.minmaxButton.setVisible(False)
2824
2952
  return
2825
2953
 
2826
- @pyqtSlot()
2827
- def _minmaxDocument(self):
2828
- """Switch on or off Focus Mode.
2829
- """
2830
- self.mainGui.toggleFocusMode()
2831
- return
2832
-
2833
2954
  ##
2834
2955
  # Events
2835
2956
  ##
2836
2957
 
2837
- def mousePressEvent(self, theEvent):
2958
+ def mousePressEvent(self, event: QMouseEvent):
2838
2959
  """Capture a click on the title and ensure that the item is
2839
2960
  selected in the project tree.
2840
2961
  """
2841
- self.mainGui.projView.setSelectedHandle(self._docHandle, doScroll=True)
2962
+ if event.button() == Qt.MouseButton.LeftButton:
2963
+ self.docEditor.requestProjectItemSelected.emit(self._docHandle, True)
2842
2964
  return
2843
2965
 
2844
2966
  # END Class GuiDocEditHeader
@@ -2851,15 +2973,14 @@ class GuiDocEditHeader(QWidget):
2851
2973
 
2852
2974
  class GuiDocEditFooter(QWidget):
2853
2975
 
2854
- def __init__(self, docEditor):
2976
+ def __init__(self, docEditor: GuiDocEditor) -> None:
2855
2977
  super().__init__(parent=docEditor)
2856
2978
 
2857
2979
  logger.debug("Create: GuiDocEditFooter")
2858
2980
 
2859
2981
  self.docEditor = docEditor
2860
- self.mainGui = docEditor.mainGui
2861
2982
 
2862
- self._theItem = None
2983
+ self._tItem = None
2863
2984
  self._docHandle = None
2864
2985
 
2865
2986
  self._docSelection = False
@@ -2876,11 +2997,13 @@ class GuiDocEditFooter(QWidget):
2876
2997
  self.setContentsMargins(0, 0, 0, 0)
2877
2998
  self.setAutoFillBackground(True)
2878
2999
 
3000
+ alLeftTop = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
3001
+
2879
3002
  # Status
2880
3003
  self.statusIcon = QLabel("")
2881
3004
  self.statusIcon.setContentsMargins(0, 0, 0, 0)
2882
3005
  self.statusIcon.setFixedHeight(self.sPx)
2883
- self.statusIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
3006
+ self.statusIcon.setAlignment(alLeftTop)
2884
3007
 
2885
3008
  self.statusText = QLabel(self.tr("Status"))
2886
3009
  self.statusText.setIndent(0)
@@ -2888,14 +3011,14 @@ class GuiDocEditFooter(QWidget):
2888
3011
  self.statusText.setContentsMargins(0, 0, 0, 0)
2889
3012
  self.statusText.setAutoFillBackground(True)
2890
3013
  self.statusText.setFixedHeight(fPx)
2891
- self.statusText.setAlignment(Qt.AlignLeft | Qt.AlignTop)
3014
+ self.statusText.setAlignment(alLeftTop)
2892
3015
  self.statusText.setFont(lblFont)
2893
3016
 
2894
3017
  # Lines
2895
3018
  self.linesIcon = QLabel("")
2896
3019
  self.linesIcon.setContentsMargins(0, 0, 0, 0)
2897
3020
  self.linesIcon.setFixedHeight(self.sPx)
2898
- self.linesIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
3021
+ self.linesIcon.setAlignment(alLeftTop)
2899
3022
 
2900
3023
  self.linesText = QLabel("")
2901
3024
  self.linesText.setIndent(0)
@@ -2903,14 +3026,14 @@ class GuiDocEditFooter(QWidget):
2903
3026
  self.linesText.setContentsMargins(0, 0, 0, 0)
2904
3027
  self.linesText.setAutoFillBackground(True)
2905
3028
  self.linesText.setFixedHeight(fPx)
2906
- self.linesText.setAlignment(Qt.AlignLeft | Qt.AlignTop)
3029
+ self.linesText.setAlignment(alLeftTop)
2907
3030
  self.linesText.setFont(lblFont)
2908
3031
 
2909
3032
  # Words
2910
3033
  self.wordsIcon = QLabel("")
2911
3034
  self.wordsIcon.setContentsMargins(0, 0, 0, 0)
2912
3035
  self.wordsIcon.setFixedHeight(self.sPx)
2913
- self.wordsIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
3036
+ self.wordsIcon.setAlignment(alLeftTop)
2914
3037
 
2915
3038
  self.wordsText = QLabel("")
2916
3039
  self.wordsText.setIndent(0)
@@ -2918,7 +3041,7 @@ class GuiDocEditFooter(QWidget):
2918
3041
  self.wordsText.setContentsMargins(0, 0, 0, 0)
2919
3042
  self.wordsText.setAutoFillBackground(True)
2920
3043
  self.wordsText.setFixedHeight(fPx)
2921
- self.wordsText.setAlignment(Qt.AlignLeft | Qt.AlignTop)
3044
+ self.wordsText.setAlignment(alLeftTop)
2922
3045
  self.wordsText.setFont(lblFont)
2923
3046
 
2924
3047
  # Assemble Layout
@@ -2954,41 +3077,37 @@ class GuiDocEditFooter(QWidget):
2954
3077
  # Methods
2955
3078
  ##
2956
3079
 
2957
- def updateTheme(self):
2958
- """Update theme elements.
2959
- """
3080
+ def updateTheme(self) -> None:
3081
+ """Update theme elements."""
2960
3082
  self.linesIcon.setPixmap(SHARED.theme.getPixmap("status_lines", (self.sPx, self.sPx)))
2961
3083
  self.wordsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (self.sPx, self.sPx)))
2962
-
2963
3084
  self.matchColours()
2964
-
2965
3085
  return
2966
3086
 
2967
- def matchColours(self):
3087
+ def matchColours(self) -> None:
2968
3088
  """Update the colours of the widget to match those of the syntax
2969
3089
  theme rather than the main GUI.
2970
3090
  """
2971
- thePalette = QPalette()
2972
- thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
2973
- thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
2974
- thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
3091
+ palette = QPalette()
3092
+ palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
3093
+ palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
3094
+ palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
2975
3095
 
2976
- self.setPalette(thePalette)
2977
- self.statusText.setPalette(thePalette)
2978
- self.linesText.setPalette(thePalette)
2979
- self.wordsText.setPalette(thePalette)
3096
+ self.setPalette(palette)
3097
+ self.statusText.setPalette(palette)
3098
+ self.linesText.setPalette(palette)
3099
+ self.wordsText.setPalette(palette)
2980
3100
 
2981
3101
  return
2982
3102
 
2983
- def setHandle(self, tHandle):
2984
- """Set the handle that will populate the footer's data.
2985
- """
3103
+ def setHandle(self, tHandle: str | None) -> None:
3104
+ """Set the handle that will populate the footer's data."""
2986
3105
  self._docHandle = tHandle
2987
3106
  if self._docHandle is None:
2988
3107
  logger.debug("No handle set, so clearing the editor footer")
2989
- self._theItem = None
3108
+ self._tItem = None
2990
3109
  else:
2991
- self._theItem = SHARED.project.tree[self._docHandle]
3110
+ self._tItem = SHARED.project.tree[self._docHandle]
2992
3111
 
2993
3112
  self.setHasSelection(False)
2994
3113
  self.updateInfo()
@@ -2996,49 +3115,44 @@ class GuiDocEditFooter(QWidget):
2996
3115
 
2997
3116
  return
2998
3117
 
2999
- def setHasSelection(self, hasSelection):
3118
+ def setHasSelection(self, hasSelection: bool) -> None:
3000
3119
  """Toggle the word counter mode between full count and selection
3001
3120
  count mode.
3002
3121
  """
3003
3122
  self._docSelection = hasSelection
3004
3123
  return
3005
3124
 
3006
- def updateInfo(self):
3007
- """Update the content of text labels.
3008
- """
3009
- if self._theItem is None:
3125
+ def updateInfo(self) -> None:
3126
+ """Update the content of text labels."""
3127
+ if self._tItem is None:
3010
3128
  sIcon = QPixmap()
3011
3129
  sText = ""
3012
3130
  else:
3013
- theStatus, theIcon = self._theItem.getImportStatus(incIcon=True)
3014
- sIcon = theIcon.pixmap(self.sPx, self.sPx)
3015
- sText = f"{theStatus} / {self._theItem.describeMe()}"
3131
+ status, icon = self._tItem.getImportStatus(incIcon=True)
3132
+ sIcon = icon.pixmap(self.sPx, self.sPx)
3133
+ sText = f"{status} / {self._tItem.describeMe()}"
3016
3134
 
3017
3135
  self.statusIcon.setPixmap(sIcon)
3018
3136
  self.statusText.setText(sText)
3019
3137
 
3020
3138
  return
3021
3139
 
3022
- def updateLineCount(self):
3023
- """Update the line counter.
3024
- """
3025
- if self._theItem is None:
3140
+ def updateLineCount(self) -> None:
3141
+ """Update the line counter."""
3142
+ if self._tItem is None:
3026
3143
  iLine = 0
3027
3144
  iDist = 0
3028
3145
  else:
3029
- theCursor = self.docEditor.textCursor()
3030
- iLine = theCursor.blockNumber() + 1
3031
- iDist = 100*iLine/self.docEditor.document().blockCount()
3032
-
3146
+ cursor = self.docEditor.textCursor()
3147
+ iLine = cursor.blockNumber() + 1
3148
+ iDist = 100*iLine/self.docEditor._qDocument.blockCount()
3033
3149
  self.linesText.setText(
3034
3150
  self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
3035
3151
  )
3036
-
3037
3152
  return
3038
3153
 
3039
- def updateCounts(self, wCount=None, cCount=None):
3040
- """Select which word count display mode to use.
3041
- """
3154
+ def updateCounts(self, wCount: int | None = None, cCount: int | None = None) -> None:
3155
+ """Select which word count display mode to use."""
3042
3156
  if self._docSelection:
3043
3157
  self._updateSelectionWordCounts(wCount, cCount)
3044
3158
  else:
@@ -3049,40 +3163,36 @@ class GuiDocEditFooter(QWidget):
3049
3163
  # Internal Functions
3050
3164
  ##
3051
3165
 
3052
- def _updateWordCounts(self):
3053
- """Update the word count for the whole document.
3054
- """
3055
- if self._theItem is None:
3166
+ def _updateWordCounts(self) -> None:
3167
+ """Update the word count for the whole document."""
3168
+ if self._tItem is None:
3056
3169
  wCount = 0
3057
3170
  wDiff = 0
3058
3171
  else:
3059
- wCount = self._theItem.wordCount
3060
- wDiff = wCount - self._theItem.initCount
3172
+ wCount = self._tItem.wordCount
3173
+ wDiff = wCount - self._tItem.initCount
3061
3174
 
3062
3175
  self.wordsText.setText(
3063
3176
  self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
3064
3177
  )
3065
3178
 
3066
- byteSize = self.docEditor.document().characterCount()
3179
+ byteSize = self.docEditor._qDocument.characterCount()
3067
3180
  self.wordsText.setToolTip(
3068
3181
  self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
3069
3182
  )
3070
3183
 
3071
3184
  return
3072
3185
 
3073
- def _updateSelectionWordCounts(self, wCount, cCount):
3074
- """Update the word count for a selection.
3075
- """
3186
+ def _updateSelectionWordCounts(self, wCount: int | None, cCount: int | None) -> None:
3187
+ """Update the word count for a selection."""
3076
3188
  if wCount is None or cCount is None:
3077
3189
  return
3078
-
3079
3190
  self.wordsText.setText(
3080
3191
  self.tr("Words: {0} selected").format(f"{wCount:n}")
3081
3192
  )
3082
3193
  self.wordsText.setToolTip(
3083
3194
  self.tr("Character count: {0}").format(f"{cCount:n}")
3084
3195
  )
3085
-
3086
3196
  return
3087
3197
 
3088
3198
  # END Class GuiDocEditFooter