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.
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/METADATA +3 -3
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/RECORD +105 -76
- novelwriter/__init__.py +6 -24
- novelwriter/assets/i18n/project_de_DE.json +10 -0
- novelwriter/assets/i18n/project_en_GB.json +11 -0
- novelwriter/assets/i18n/project_en_US.json +10 -0
- novelwriter/assets/i18n/project_ja_JP.json +11 -1
- novelwriter/assets/i18n/project_nb_NO.json +10 -0
- novelwriter/assets/i18n/project_nn_NO.json +10 -0
- novelwriter/assets/icons/novelwriter.ico +0 -0
- novelwriter/assets/icons/novelwriter.svg +8 -183
- novelwriter/assets/icons/typicons_dark/icons.conf +17 -2
- novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +8 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +8 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +4 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +5 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +5 -0
- novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +5 -0
- novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +4 -0
- novelwriter/assets/icons/typicons_light/icons.conf +17 -2
- novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +8 -0
- novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +8 -0
- novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +4 -0
- novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +5 -0
- novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +5 -0
- novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +5 -0
- novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +4 -0
- novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
- novelwriter/assets/icons/x-novelwriter-project.svg +7 -206
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/default_dark.conf +1 -0
- novelwriter/assets/syntax/default_light.conf +1 -0
- novelwriter/assets/syntax/grey_dark.conf +1 -0
- novelwriter/assets/syntax/grey_light.conf +1 -0
- novelwriter/assets/syntax/light_owl.conf +1 -0
- novelwriter/assets/syntax/night_owl.conf +1 -0
- novelwriter/assets/syntax/solarized_dark.conf +1 -0
- novelwriter/assets/syntax/solarized_light.conf +1 -0
- novelwriter/assets/syntax/tomorrow.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
- novelwriter/assets/text/credits_en.htm +7 -0
- novelwriter/assets/text/release_notes.htm +7 -37
- novelwriter/common.py +22 -1
- novelwriter/config.py +27 -42
- novelwriter/constants.py +45 -7
- novelwriter/core/buildsettings.py +40 -24
- novelwriter/core/coretools.py +8 -1
- novelwriter/core/docbuild.py +2 -6
- novelwriter/core/index.py +264 -175
- novelwriter/core/options.py +8 -3
- novelwriter/core/project.py +2 -2
- novelwriter/core/projectdata.py +3 -3
- novelwriter/core/tohtml.py +60 -59
- novelwriter/core/tokenizer.py +110 -70
- novelwriter/core/tomd.py +51 -38
- novelwriter/core/toodt.py +184 -147
- novelwriter/dialogs/preferences.py +75 -106
- novelwriter/dialogs/projsettings.py +101 -110
- novelwriter/dialogs/updates.py +25 -14
- novelwriter/enum.py +28 -3
- novelwriter/extensions/novelselector.py +1 -1
- novelwriter/gui/doceditor.py +1345 -1235
- novelwriter/gui/dochighlight.py +98 -62
- novelwriter/gui/docviewer.py +151 -340
- novelwriter/gui/docviewerpanel.py +457 -0
- novelwriter/gui/editordocument.py +126 -0
- novelwriter/gui/mainmenu.py +350 -300
- novelwriter/gui/noveltree.py +101 -125
- novelwriter/gui/outline.py +154 -171
- novelwriter/gui/projtree.py +480 -380
- novelwriter/gui/sidebar.py +106 -75
- novelwriter/gui/statusbar.py +1 -1
- novelwriter/gui/theme.py +114 -75
- novelwriter/guimain.py +353 -254
- novelwriter/shared.py +36 -3
- novelwriter/tools/dictionaries.py +268 -0
- novelwriter/tools/manusbuild.py +17 -6
- novelwriter/tools/manuscript.py +11 -3
- novelwriter/tools/manussettings.py +0 -14
- novelwriter/tools/projwizard.py +16 -2
- novelwriter/tools/writingstats.py +1 -1
- novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_th-menu.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_th-menu.svg +0 -4
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/WHEEL +0 -0
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.1.1.dist-info → novelWriter-2.2rc1.dist-info}/top_level.txt +0 -0
novelwriter/gui/doceditor.py
CHANGED
@@ -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
|
-
|
41
|
-
|
42
|
+
pyqtSignal, pyqtSlot, QObject, QPoint, QRegExp, QRegularExpression,
|
43
|
+
QRunnable, QSize, Qt, QTimer
|
42
44
|
)
|
43
45
|
from PyQt5.QtGui import (
|
44
|
-
|
45
|
-
|
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,
|
49
|
-
QPushButton, QShortcut,
|
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
|
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
|
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
|
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.
|
101
|
-
self.
|
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
|
-
#
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
#
|
135
|
-
self.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
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.
|
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
|
-
|
320
|
-
|
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
|
-
|
335
|
+
options = QTextOption()
|
327
336
|
|
328
337
|
if CONFIG.doJustify:
|
329
|
-
|
338
|
+
options.setAlignment(Qt.AlignmentFlag.AlignJustify)
|
330
339
|
if CONFIG.showTabsNSpaces:
|
331
|
-
|
340
|
+
options.setFlags(options.flags() | QTextOption.Flag.ShowTabsAndSpaces)
|
332
341
|
if CONFIG.showLineEndings:
|
333
|
-
|
342
|
+
options.setFlags(options.flags() | QTextOption.Flag.ShowLineAndParagraphSeparators)
|
334
343
|
|
335
|
-
|
344
|
+
self._qDocument.setDefaultTextOption(options)
|
336
345
|
|
337
|
-
#
|
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.
|
362
|
-
if not self._bigDoc:
|
363
|
-
self.highLight.rehighlight()
|
371
|
+
self._qDocument.syntaxHighlighter.rehighlight()
|
364
372
|
|
365
|
-
return
|
373
|
+
return
|
366
374
|
|
367
|
-
def loadText(self, tHandle, tLine=None):
|
368
|
-
"""Load text from a document into the editor. If we have an
|
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
|
-
|
380
|
-
if
|
381
|
-
# There was an
|
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
|
-
|
386
|
-
|
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.
|
411
|
-
qApp.processEvents()
|
412
|
-
|
397
|
+
self._qDocument.setTextContent(docText, tHandle)
|
413
398
|
self._allowAutoReplace(True)
|
414
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
484
|
-
|
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
|
-
|
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
|
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
|
-
|
623
|
-
|
624
|
-
|
625
|
-
return
|
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,
|
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 =
|
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
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
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
|
-
|
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,
|
682
|
-
"""Move the cursor to a given line in the document.
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
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
|
704
|
-
|
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
|
653
|
+
state = not SHARED.project.data.spellCheck
|
708
654
|
|
709
|
-
if
|
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
|
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.
|
724
|
-
|
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
|
-
|
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,
|
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(
|
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",
|
707
|
+
logger.debug("Requesting action: %s", action.name)
|
771
708
|
|
772
709
|
self._allowAutoReplace(False)
|
773
|
-
if
|
710
|
+
if action == nwDocAction.UNDO:
|
774
711
|
self.undo()
|
775
|
-
elif
|
712
|
+
elif action == nwDocAction.REDO:
|
776
713
|
self.redo()
|
777
|
-
elif
|
714
|
+
elif action == nwDocAction.CUT:
|
778
715
|
self.cut()
|
779
|
-
elif
|
716
|
+
elif action == nwDocAction.COPY:
|
780
717
|
self.copy()
|
781
|
-
elif
|
718
|
+
elif action == nwDocAction.PASTE:
|
782
719
|
self.paste()
|
783
|
-
elif
|
720
|
+
elif action == nwDocAction.EMPH:
|
784
721
|
self._toggleFormat(1, "_")
|
785
|
-
elif
|
722
|
+
elif action == nwDocAction.STRONG:
|
786
723
|
self._toggleFormat(2, "*")
|
787
|
-
elif
|
724
|
+
elif action == nwDocAction.STRIKE:
|
788
725
|
self._toggleFormat(2, "~")
|
789
|
-
elif
|
726
|
+
elif action == nwDocAction.S_QUOTE:
|
790
727
|
self._wrapSelection(self._typSQuoteO, self._typSQuoteC)
|
791
|
-
elif
|
728
|
+
elif action == nwDocAction.D_QUOTE:
|
792
729
|
self._wrapSelection(self._typDQuoteO, self._typDQuoteC)
|
793
|
-
elif
|
794
|
-
self._makeSelection(QTextCursor.Document)
|
795
|
-
elif
|
796
|
-
self._makeSelection(QTextCursor.BlockUnderCursor)
|
797
|
-
elif
|
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
|
736
|
+
elif action == nwDocAction.BLOCK_H2:
|
800
737
|
self._formatBlock(nwDocAction.BLOCK_H2)
|
801
|
-
elif
|
738
|
+
elif action == nwDocAction.BLOCK_H3:
|
802
739
|
self._formatBlock(nwDocAction.BLOCK_H3)
|
803
|
-
elif
|
740
|
+
elif action == nwDocAction.BLOCK_H4:
|
804
741
|
self._formatBlock(nwDocAction.BLOCK_H4)
|
805
|
-
elif
|
742
|
+
elif action == nwDocAction.BLOCK_COM:
|
806
743
|
self._formatBlock(nwDocAction.BLOCK_COM)
|
807
|
-
elif
|
744
|
+
elif action == nwDocAction.BLOCK_TXT:
|
808
745
|
self._formatBlock(nwDocAction.BLOCK_TXT)
|
809
|
-
elif
|
746
|
+
elif action == nwDocAction.BLOCK_TTL:
|
810
747
|
self._formatBlock(nwDocAction.BLOCK_TTL)
|
811
|
-
elif
|
748
|
+
elif action == nwDocAction.BLOCK_UNN:
|
812
749
|
self._formatBlock(nwDocAction.BLOCK_UNN)
|
813
|
-
elif
|
750
|
+
elif action == nwDocAction.REPL_SNG:
|
814
751
|
self._replaceQuotes("'", self._typSQuoteO, self._typSQuoteC)
|
815
|
-
elif
|
752
|
+
elif action == nwDocAction.REPL_DBL:
|
816
753
|
self._replaceQuotes("\"", self._typDQuoteO, self._typDQuoteC)
|
817
|
-
elif
|
754
|
+
elif action == nwDocAction.RM_BREAKS:
|
818
755
|
self._removeInParLineBreaks()
|
819
|
-
elif
|
756
|
+
elif action == nwDocAction.ALIGN_L:
|
820
757
|
self._formatBlock(nwDocAction.ALIGN_L)
|
821
|
-
elif
|
758
|
+
elif action == nwDocAction.ALIGN_C:
|
822
759
|
self._formatBlock(nwDocAction.ALIGN_C)
|
823
|
-
elif
|
760
|
+
elif action == nwDocAction.ALIGN_R:
|
824
761
|
self._formatBlock(nwDocAction.ALIGN_R)
|
825
|
-
elif
|
762
|
+
elif action == nwDocAction.INDENT_L:
|
826
763
|
self._formatBlock(nwDocAction.INDENT_L)
|
827
|
-
elif
|
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(
|
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
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
self.tr("
|
861
|
-
|
862
|
-
|
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,
|
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(
|
878
|
-
|
879
|
-
elif isinstance(
|
880
|
-
if
|
881
|
-
|
882
|
-
elif
|
883
|
-
|
884
|
-
elif
|
885
|
-
|
886
|
-
elif
|
887
|
-
|
888
|
-
elif
|
889
|
-
|
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
|
893
|
-
|
841
|
+
elif insert == nwDocInsert.NEW_PAGE:
|
842
|
+
text = "[newpage]"
|
894
843
|
newBlock = True
|
895
844
|
goAfter = False
|
896
|
-
elif
|
897
|
-
|
845
|
+
elif insert == nwDocInsert.VSPACE_S:
|
846
|
+
text = "[vspace]"
|
898
847
|
newBlock = True
|
899
848
|
goAfter = False
|
900
|
-
elif
|
901
|
-
|
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(
|
859
|
+
self.insertNewBlock(text, defaultAfter=goAfter)
|
911
860
|
else:
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
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,
|
920
|
-
"""Insert a piece of text on a blank line.
|
921
|
-
|
922
|
-
|
923
|
-
|
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 =
|
929
|
-
sLen =
|
876
|
+
sPos = block.position()
|
877
|
+
sLen = block.length()
|
930
878
|
|
931
|
-
|
879
|
+
cursor.beginEditBlock()
|
932
880
|
|
933
881
|
if sLen > 1 and defaultAfter:
|
934
|
-
|
935
|
-
|
882
|
+
cursor.setPosition(sPos + sLen - 1)
|
883
|
+
cursor.insertText("\n")
|
936
884
|
else:
|
937
|
-
|
885
|
+
cursor.setPosition(sPos)
|
938
886
|
|
939
|
-
|
887
|
+
cursor.insertText(text)
|
940
888
|
|
941
889
|
if sLen > 1 and not defaultAfter:
|
942
|
-
|
890
|
+
cursor.insertText("\n")
|
943
891
|
|
944
|
-
|
892
|
+
cursor.endEditBlock()
|
945
893
|
|
946
|
-
self.setTextCursor(
|
894
|
+
self.setTextCursor(cursor)
|
947
895
|
|
948
896
|
return True
|
949
897
|
|
950
|
-
def
|
951
|
-
"""
|
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,
|
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 =
|
994
|
-
isReturn |=
|
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
|
922
|
+
elif event == QKeySequence.StandardKey.Redo:
|
998
923
|
self.docAction(nwDocAction.REDO)
|
999
924
|
return
|
1000
|
-
elif
|
925
|
+
elif event == QKeySequence.StandardKey.Undo:
|
1001
926
|
self.docAction(nwDocAction.UNDO)
|
1002
927
|
return
|
1003
|
-
elif
|
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
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
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
|
1020
|
-
# Move the scroll bar
|
941
|
+
if cPos > mPos:
|
1021
942
|
vBar = self.verticalScrollBar()
|
1022
|
-
|
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(
|
945
|
+
super().keyPressEvent(event)
|
1030
946
|
|
1031
947
|
self.docFooter.updateLineCount()
|
1032
948
|
|
1033
949
|
return
|
1034
950
|
|
1035
|
-
def focusNextPrevChild(self,
|
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(
|
960
|
+
return self.docSearch.cycleFocus(next)
|
1045
961
|
return True
|
1046
962
|
|
1047
|
-
def mouseReleaseEvent(self,
|
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
|
-
|
1054
|
-
|
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,
|
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(
|
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,
|
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(
|
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
|
-
|
1113
|
-
|
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
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
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
|
1138
|
-
|
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
|
-
|
1143
|
-
mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
|
1144
|
-
mnuContext.addAction(mnuCopy)
|
1057
|
+
return
|
1145
1058
|
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
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
|
-
|
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
|
-
|
1156
|
-
|
1157
|
-
|
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
|
-
|
1164
|
-
|
1165
|
-
|
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
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
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
|
-
|
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
|
-
|
1210
|
-
|
1211
|
-
|
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
|
-
#
|
1215
|
-
|
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,
|
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
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
self.setTextCursor(
|
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("
|
1234
|
-
def _addWord(self,
|
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
|
-
|
1239
|
-
|
1240
|
-
|
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
|
-
|
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
|
-
"""
|
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
|
-
|
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
|
-
"""
|
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(
|
1333
|
-
def
|
1334
|
-
"""
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
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
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
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
|
-
|
1401
|
-
resIdx = bisect.bisect_left(resS,
|
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
|
-
|
1423
|
-
|
1424
|
-
self.setTextCursor(
|
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
|
-
|
1439
|
-
hasSelection =
|
1350
|
+
cursor = self.textCursor()
|
1351
|
+
hasSelection = cursor.hasSelection()
|
1440
1352
|
if hasSelection:
|
1441
|
-
origA =
|
1442
|
-
origB =
|
1353
|
+
origA = cursor.selectionStart()
|
1354
|
+
origB = cursor.selectionEnd()
|
1443
1355
|
else:
|
1444
|
-
origA =
|
1445
|
-
origB =
|
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
|
-
|
1455
|
-
self.setTextCursor(
|
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 .*
|
1370
|
+
# searches like a regex search for .* don't loop infinitely
|
1459
1371
|
while self.find(searchFor, findOpt) and len(resE) <= 1000:
|
1460
|
-
|
1461
|
-
if
|
1462
|
-
resS.append(
|
1463
|
-
resE.append(
|
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
|
-
|
1470
|
-
|
1381
|
+
cursor.setPosition(origA, QTextCursor.MoveMode.MoveAnchor)
|
1382
|
+
cursor.setPosition(origB, QTextCursor.MoveMode.KeepAnchor)
|
1471
1383
|
else:
|
1472
|
-
|
1384
|
+
cursor.setPosition(origA)
|
1473
1385
|
|
1474
|
-
self.setTextCursor(
|
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
|
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
|
-
|
1493
|
-
if not
|
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
|
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 =
|
1505
|
-
|
1506
|
-
|
1507
|
-
self.setTextCursor(
|
1416
|
+
sPos = cursor.selectionStart()
|
1417
|
+
cursor.clearSelection()
|
1418
|
+
cursor.setPosition(sPos)
|
1419
|
+
self.setTextCursor(cursor)
|
1508
1420
|
self.findNext()
|
1509
|
-
|
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.
|
1516
|
-
replWith = self.docSearch.
|
1427
|
+
searchFor = self.docSearch.searchText
|
1428
|
+
replWith = self.docSearch.replaceText
|
1517
1429
|
|
1518
1430
|
if self.docSearch.doMatchCap:
|
1519
|
-
replWith = transferCase(
|
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] ==
|
1525
|
-
isFind &= self._lastFind[1] ==
|
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
|
-
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
1534
|
-
|
1535
|
-
self.setTextCursor(
|
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,
|
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
|
-
|
1557
|
-
|
1558
|
-
|
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
|
-
|
1562
|
-
|
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
|
-
|
1570
|
-
|
1571
|
-
|
1572
|
-
self.setTextCursor(
|
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.
|
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.
|
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
|
-
|
1590
|
-
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
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
|
-
|
1603
|
-
|
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,
|
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
|
1624
|
-
|
1537
|
+
if after is None:
|
1538
|
+
after = before
|
1625
1539
|
|
1626
|
-
|
1627
|
-
if
|
1628
|
-
|
1629
|
-
|
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 =
|
1632
|
-
posE =
|
1552
|
+
posS = cursor.selectionStart()
|
1553
|
+
posE = cursor.selectionEnd()
|
1633
1554
|
|
1634
|
-
|
1635
|
-
|
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
|
-
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1644
|
-
|
1645
|
-
|
1646
|
-
|
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
|
-
|
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
|
-
|
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 =
|
1663
|
-
posE =
|
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
|
-
|
1671
|
-
|
1672
|
-
|
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
|
-
|
1688
|
-
|
1614
|
+
cursor.clearSelection()
|
1615
|
+
cursor.setPosition(posC)
|
1689
1616
|
if pC in closeCheck:
|
1690
|
-
|
1691
|
-
|
1692
|
-
|
1693
|
-
|
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
|
-
|
1696
|
-
|
1697
|
-
|
1698
|
-
|
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,
|
1705
|
-
"""Change the block format of the block under the cursor.
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
|
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
|
-
|
1715
|
-
hasText = len(
|
1716
|
-
if
|
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
|
1720
|
-
newText =
|
1649
|
+
elif setText.startswith("% "):
|
1650
|
+
newText = setText[2:]
|
1721
1651
|
cOffset = 2
|
1722
|
-
if
|
1723
|
-
|
1724
|
-
elif
|
1725
|
-
newText =
|
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
|
1728
|
-
|
1729
|
-
elif
|
1730
|
-
newText =
|
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
|
1733
|
-
newText =
|
1662
|
+
elif setText.startswith("## "):
|
1663
|
+
newText = setText[3:]
|
1734
1664
|
cOffset = 3
|
1735
|
-
elif
|
1736
|
-
newText =
|
1665
|
+
elif setText.startswith("### "):
|
1666
|
+
newText = setText[4:]
|
1737
1667
|
cOffset = 4
|
1738
|
-
elif
|
1739
|
-
newText =
|
1668
|
+
elif setText.startswith("#### "):
|
1669
|
+
newText = setText[5:]
|
1740
1670
|
cOffset = 5
|
1741
|
-
elif
|
1742
|
-
newText =
|
1671
|
+
elif setText.startswith("#! "):
|
1672
|
+
newText = setText[3:]
|
1743
1673
|
cOffset = 3
|
1744
|
-
elif
|
1745
|
-
newText =
|
1674
|
+
elif setText.startswith("##! "):
|
1675
|
+
newText = setText[4:]
|
1746
1676
|
cOffset = 4
|
1747
|
-
elif
|
1748
|
-
newText =
|
1677
|
+
elif setText.startswith(">> "):
|
1678
|
+
newText = setText[3:]
|
1749
1679
|
cOffset = 3
|
1750
|
-
elif
|
1751
|
-
newText =
|
1680
|
+
elif setText.startswith("> ") and action != nwDocAction.INDENT_R:
|
1681
|
+
newText = setText[2:]
|
1752
1682
|
cOffset = 2
|
1753
|
-
elif
|
1754
|
-
newText =
|
1683
|
+
elif setText.startswith(">>"):
|
1684
|
+
newText = setText[2:]
|
1755
1685
|
cOffset = 2
|
1756
|
-
elif
|
1757
|
-
newText =
|
1686
|
+
elif setText.startswith(">") and action != nwDocAction.INDENT_R:
|
1687
|
+
newText = setText[1:]
|
1758
1688
|
cOffset = 1
|
1759
1689
|
else:
|
1760
|
-
newText =
|
1690
|
+
newText = setText
|
1761
1691
|
cOffset = 0
|
1762
1692
|
|
1763
1693
|
# Also remove formatting tags at the end
|
1764
|
-
if
|
1694
|
+
if setText.endswith(" <<"):
|
1765
1695
|
newText = newText[:-3]
|
1766
|
-
elif
|
1696
|
+
elif setText.endswith(" <") and action != nwDocAction.INDENT_L:
|
1767
1697
|
newText = newText[:-2]
|
1768
|
-
elif
|
1698
|
+
elif setText.endswith("<<"):
|
1769
1699
|
newText = newText[:-2]
|
1770
|
-
elif
|
1700
|
+
elif setText.endswith("<") and action != nwDocAction.INDENT_L:
|
1771
1701
|
newText = newText[:-1]
|
1772
1702
|
|
1773
1703
|
# Apply new format
|
1774
|
-
if
|
1775
|
-
|
1704
|
+
if action == nwDocAction.BLOCK_COM:
|
1705
|
+
setText = "% "+newText
|
1776
1706
|
cOffset -= 2
|
1777
|
-
elif
|
1778
|
-
|
1707
|
+
elif action == nwDocAction.BLOCK_H1:
|
1708
|
+
setText = "# "+newText
|
1779
1709
|
cOffset -= 2
|
1780
|
-
elif
|
1781
|
-
|
1710
|
+
elif action == nwDocAction.BLOCK_H2:
|
1711
|
+
setText = "## "+newText
|
1782
1712
|
cOffset -= 3
|
1783
|
-
elif
|
1784
|
-
|
1713
|
+
elif action == nwDocAction.BLOCK_H3:
|
1714
|
+
setText = "### "+newText
|
1785
1715
|
cOffset -= 4
|
1786
|
-
elif
|
1787
|
-
|
1716
|
+
elif action == nwDocAction.BLOCK_H4:
|
1717
|
+
setText = "#### "+newText
|
1788
1718
|
cOffset -= 5
|
1789
|
-
elif
|
1790
|
-
|
1719
|
+
elif action == nwDocAction.BLOCK_TTL:
|
1720
|
+
setText = "#! "+newText
|
1791
1721
|
cOffset -= 3
|
1792
|
-
elif
|
1793
|
-
|
1722
|
+
elif action == nwDocAction.BLOCK_UNN:
|
1723
|
+
setText = "##! "+newText
|
1794
1724
|
cOffset -= 4
|
1795
|
-
elif
|
1796
|
-
|
1797
|
-
elif
|
1798
|
-
|
1725
|
+
elif action == nwDocAction.ALIGN_L:
|
1726
|
+
setText = newText+" <<"
|
1727
|
+
elif action == nwDocAction.ALIGN_C:
|
1728
|
+
setText = ">> "+newText+" <<"
|
1799
1729
|
cOffset -= 3
|
1800
|
-
elif
|
1801
|
-
|
1730
|
+
elif action == nwDocAction.ALIGN_R:
|
1731
|
+
setText = ">> "+newText
|
1802
1732
|
cOffset -= 3
|
1803
|
-
elif
|
1804
|
-
|
1733
|
+
elif action == nwDocAction.INDENT_L:
|
1734
|
+
setText = "> "+newText
|
1805
1735
|
cOffset -= 2
|
1806
|
-
elif
|
1807
|
-
|
1808
|
-
elif
|
1809
|
-
|
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(
|
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
|
-
|
1816
|
-
posO =
|
1817
|
-
|
1818
|
-
posS =
|
1819
|
-
|
1820
|
-
|
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
|
-
|
1755
|
+
cursor.insertBlock()
|
1826
1756
|
|
1827
|
-
|
1757
|
+
cursor.insertText(setText)
|
1828
1758
|
|
1829
1759
|
if posO - cOffset >= 0:
|
1830
|
-
|
1760
|
+
cursor.setPosition(posO - cOffset)
|
1831
1761
|
|
1832
|
-
|
1833
|
-
self.setTextCursor(
|
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 =
|
1772
|
+
iE = self._qDocument.blockCount() - 1
|
1845
1773
|
rS = 0
|
1846
|
-
rE =
|
1847
|
-
if
|
1848
|
-
sBlock =
|
1849
|
-
eBlock =
|
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 =
|
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
|
-
|
1877
|
-
|
1878
|
-
|
1879
|
-
|
1880
|
-
|
1881
|
-
|
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
|
1813
|
+
return
|
1884
1814
|
|
1885
1815
|
##
|
1886
1816
|
# Internal Functions
|
1887
1817
|
##
|
1888
1818
|
|
1889
|
-
def
|
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
|
1897
|
-
|
1827
|
+
if cursor is None:
|
1828
|
+
cursor = self.textCursor()
|
1898
1829
|
|
1899
|
-
|
1900
|
-
|
1830
|
+
block = cursor.block()
|
1831
|
+
text = block.text()
|
1832
|
+
if len(text) == 0:
|
1833
|
+
return nwTrinary.NEUTRAL
|
1901
1834
|
|
1902
|
-
if
|
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(
|
1837
|
+
isGood, tBits, tPos = SHARED.project.index.scanThis(text)
|
1908
1838
|
if not isGood:
|
1909
|
-
return
|
1839
|
+
return nwTrinary.NEUTRAL
|
1910
1840
|
|
1911
|
-
|
1912
|
-
|
1913
|
-
|
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
|
-
|
1850
|
+
tag = sTag
|
1851
|
+
exist = sExist
|
1919
1852
|
break
|
1920
1853
|
|
1921
|
-
if not
|
1854
|
+
if not tag or tag.startswith("@"):
|
1922
1855
|
# The keyword cannot be looked up, so we ignore that
|
1923
|
-
return
|
1924
|
-
|
1925
|
-
if
|
1926
|
-
logger.debug("Attempting to follow tag '%s'",
|
1927
|
-
self.loadDocumentTagRequest.emit(
|
1928
|
-
|
1929
|
-
|
1930
|
-
|
1931
|
-
|
1932
|
-
|
1933
|
-
|
1934
|
-
|
1935
|
-
|
1936
|
-
|
1937
|
-
|
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,
|
1943
|
-
"""Auto-replace text elements based on main configuration.
|
1944
|
-
|
1945
|
-
|
1946
|
-
|
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
|
-
|
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
|
-
|
1957
|
-
|
1958
|
-
|
1899
|
+
tOne = text[tPos-1:tPos]
|
1900
|
+
tTwo = text[tPos-2:tPos]
|
1901
|
+
tThree = text[tPos-3:tPos]
|
1959
1902
|
|
1960
|
-
if not
|
1961
|
-
# Sorry, Neo and Zathras
|
1903
|
+
if not tOne:
|
1962
1904
|
return
|
1963
1905
|
|
1964
1906
|
nDelete = 0
|
1965
|
-
tInsert =
|
1907
|
+
tInsert = tOne
|
1966
1908
|
|
1967
|
-
if self._typRepDQuote and
|
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
|
1913
|
+
elif self._typRepDQuote and tOne == '"':
|
1972
1914
|
nDelete = 1
|
1973
|
-
if
|
1915
|
+
if tPos == 1:
|
1974
1916
|
tInsert = self._typDQuoteO
|
1975
|
-
elif
|
1917
|
+
elif tPos == 2 and tTwo == '>"':
|
1976
1918
|
tInsert = self._typDQuoteO
|
1977
|
-
elif
|
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
|
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
|
1928
|
+
elif self._typRepSQuote and tOne == "'":
|
1987
1929
|
nDelete = 1
|
1988
|
-
if
|
1930
|
+
if tPos == 1:
|
1989
1931
|
tInsert = self._typSQuoteO
|
1990
|
-
elif
|
1932
|
+
elif tPos == 2 and tTwo == ">'":
|
1991
1933
|
tInsert = self._typSQuoteO
|
1992
|
-
elif
|
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
|
1939
|
+
elif self._typRepDash and tThree == "---":
|
1998
1940
|
nDelete = 3
|
1999
1941
|
tInsert = nwUnicode.U_EMDASH
|
2000
1942
|
|
2001
|
-
elif self._typRepDash and
|
1943
|
+
elif self._typRepDash and tTwo == "--":
|
2002
1944
|
nDelete = 2
|
2003
1945
|
tInsert = nwUnicode.U_ENDASH
|
2004
1946
|
|
2005
|
-
elif self._typRepDash and
|
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
|
1951
|
+
elif self._typRepDots and tThree == "...":
|
2010
1952
|
nDelete = 3
|
2011
1953
|
tInsert = nwUnicode.U_HELLIP
|
2012
1954
|
|
2013
|
-
elif
|
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(
|
1962
|
+
if self._allowSpaceBeforeColon(text, tCheck):
|
2021
1963
|
nDelete = max(nDelete, 1)
|
2022
|
-
chkPos =
|
2023
|
-
if chkPos >= 0 and
|
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(
|
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
|
-
|
2035
|
-
|
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
|
2054
|
-
"""
|
2055
|
-
|
2056
|
-
|
2057
|
-
|
2058
|
-
|
2059
|
-
|
2060
|
-
if
|
2061
|
-
|
2062
|
-
|
2063
|
-
|
2064
|
-
|
2065
|
-
|
2066
|
-
|
2067
|
-
|
2068
|
-
|
2069
|
-
|
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
|
-
|
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
|
-
|
2024
|
+
if ePos - sPos <= 0:
|
2025
|
+
# No selection possible
|
2026
|
+
return cursor
|
2075
2027
|
|
2076
|
-
|
2077
|
-
|
2078
|
-
|
2079
|
-
|
2080
|
-
|
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
|
-
|
2113
|
-
theCursor = self._autoSelect()
|
2034
|
+
return cursor
|
2114
2035
|
|
2115
|
-
|
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 =
|
2119
|
-
posE =
|
2120
|
-
selTxt =
|
2048
|
+
posS = cursor.selectionStart()
|
2049
|
+
posE = cursor.selectionEnd()
|
2050
|
+
selTxt = cursor.selectedText()
|
2121
2051
|
if selTxt.startswith(nwUnicode.U_PSEP):
|
2122
|
-
|
2123
|
-
|
2052
|
+
cursor.setPosition(posS+1, QTextCursor.MoveMode.MoveAnchor)
|
2053
|
+
cursor.setPosition(posE, QTextCursor.MoveMode.KeepAnchor)
|
2124
2054
|
|
2125
|
-
self.setTextCursor(
|
2055
|
+
self.setTextCursor(cursor)
|
2126
2056
|
|
2127
2057
|
return
|
2128
2058
|
|
2129
|
-
def _makePosSelection(self,
|
2130
|
-
"""
|
2131
|
-
|
2132
|
-
|
2133
|
-
|
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,
|
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
|
-
|
2184
|
+
text = self._docEditor.textCursor().selectedText()
|
2179
2185
|
else:
|
2180
|
-
|
2186
|
+
text = self._docEditor.getText()
|
2181
2187
|
|
2182
|
-
cC, wC, pC = countWords(
|
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
|
-
|
2357
|
-
|
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,
|
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
|
-
#
|
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,
|
2542
|
-
"""Toggle the show/hide of the replace box.
|
2543
|
-
|
2544
|
-
|
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(
|
2549
|
-
self.replaceButton.setVisible(
|
2550
|
-
self.repVisible =
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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
|
-
|
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.
|
2638
|
-
self.
|
2639
|
-
self.
|
2640
|
-
self.
|
2641
|
-
self.
|
2642
|
-
self.
|
2643
|
-
self.
|
2644
|
-
self.
|
2645
|
-
|
2646
|
-
lblFont = self.
|
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.
|
2792
|
+
self.itemTitle.setFont(lblFont)
|
2649
2793
|
|
2650
2794
|
# Buttons
|
2651
|
-
self.
|
2652
|
-
self.
|
2653
|
-
self.
|
2654
|
-
self.
|
2655
|
-
self.
|
2656
|
-
self.
|
2657
|
-
self.
|
2658
|
-
self.
|
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(
|
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
|
2667
|
-
self.searchButton.clicked.connect(self.
|
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(
|
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.
|
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(
|
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
|
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.
|
2834
|
+
self.outerBox.addWidget(self.tbButton, 0)
|
2691
2835
|
self.outerBox.addWidget(self.searchButton, 0)
|
2692
|
-
self.outerBox.addWidget(self.
|
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.
|
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
|
-
|
2741
|
-
|
2742
|
-
|
2743
|
-
|
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(
|
2746
|
-
self.
|
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.
|
2757
|
-
self.
|
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.
|
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.
|
2920
|
+
self.itemTitle.setText(nwItem.itemName)
|
2778
2921
|
|
2779
|
-
self.
|
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
|
2803
|
-
"""
|
2804
|
-
|
2805
|
-
self.
|
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,
|
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
|
-
|
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.
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
-
|
2972
|
-
|
2973
|
-
|
2974
|
-
|
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(
|
2977
|
-
self.statusText.setPalette(
|
2978
|
-
self.linesText.setPalette(
|
2979
|
-
self.wordsText.setPalette(
|
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.
|
3108
|
+
self._tItem = None
|
2990
3109
|
else:
|
2991
|
-
self.
|
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
|
-
|
3014
|
-
sIcon =
|
3015
|
-
sText = f"{
|
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
|
-
|
3030
|
-
iLine =
|
3031
|
-
iDist = 100*iLine/self.docEditor.
|
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.
|
3060
|
-
wDiff = wCount - self.
|
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.
|
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
|