novelWriter 2.5.3__py3-none-any.whl → 2.6__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.5.3.dist-info → novelWriter-2.6.dist-info}/METADATA +2 -2
- {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/RECORD +123 -103
- {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +50 -11
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
- novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
- novelwriter/assets/i18n/project_de_DE.json +2 -0
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/i18n/project_en_US.json +2 -0
- novelwriter/assets/i18n/project_it_IT.json +2 -0
- novelwriter/assets/i18n/project_ja_JP.json +2 -0
- novelwriter/assets/i18n/project_nb_NO.json +2 -0
- novelwriter/assets/i18n/project_nl_NL.json +2 -0
- novelwriter/assets/i18n/project_pl_PL.json +2 -0
- novelwriter/assets/i18n/project_pt_BR.json +2 -0
- novelwriter/assets/i18n/project_zh_CN.json +2 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
- novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -0
- novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/common.py +101 -3
- novelwriter/config.py +30 -17
- novelwriter/constants.py +189 -81
- novelwriter/core/buildsettings.py +74 -40
- novelwriter/core/coretools.py +146 -148
- novelwriter/core/docbuild.py +133 -171
- novelwriter/core/document.py +1 -1
- novelwriter/core/index.py +39 -38
- novelwriter/core/item.py +42 -9
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/options.py +5 -2
- novelwriter/core/project.py +68 -90
- novelwriter/core/projectdata.py +8 -2
- novelwriter/core/projectxml.py +1 -1
- novelwriter/core/sessions.py +1 -1
- novelwriter/core/spellcheck.py +10 -15
- novelwriter/core/status.py +24 -8
- novelwriter/core/storage.py +1 -1
- novelwriter/core/tree.py +269 -288
- novelwriter/dialogs/about.py +1 -1
- novelwriter/dialogs/docmerge.py +8 -18
- novelwriter/dialogs/docsplit.py +1 -1
- novelwriter/dialogs/editlabel.py +1 -1
- novelwriter/dialogs/preferences.py +47 -34
- novelwriter/dialogs/projectsettings.py +149 -99
- novelwriter/dialogs/quotes.py +1 -1
- novelwriter/dialogs/wordlist.py +11 -10
- novelwriter/enum.py +37 -24
- novelwriter/error.py +2 -2
- novelwriter/extensions/configlayout.py +28 -13
- novelwriter/extensions/eventfilters.py +1 -1
- novelwriter/extensions/modified.py +30 -6
- novelwriter/extensions/novelselector.py +4 -3
- novelwriter/extensions/pagedsidebar.py +9 -9
- novelwriter/extensions/progressbars.py +4 -4
- novelwriter/extensions/statusled.py +3 -3
- novelwriter/extensions/switch.py +3 -3
- novelwriter/extensions/switchbox.py +1 -1
- novelwriter/extensions/versioninfo.py +1 -1
- novelwriter/formats/shared.py +156 -0
- novelwriter/formats/todocx.py +1191 -0
- novelwriter/formats/tohtml.py +454 -0
- novelwriter/{core → formats}/tokenizer.py +497 -495
- novelwriter/formats/tomarkdown.py +218 -0
- novelwriter/{core → formats}/toodt.py +312 -433
- novelwriter/formats/toqdoc.py +486 -0
- novelwriter/formats/toraw.py +91 -0
- novelwriter/gui/doceditor.py +347 -287
- novelwriter/gui/dochighlight.py +97 -85
- novelwriter/gui/docviewer.py +90 -33
- novelwriter/gui/docviewerpanel.py +18 -26
- novelwriter/gui/editordocument.py +18 -3
- novelwriter/gui/itemdetails.py +27 -29
- novelwriter/gui/mainmenu.py +130 -64
- novelwriter/gui/noveltree.py +46 -48
- novelwriter/gui/outline.py +202 -256
- novelwriter/gui/projtree.py +590 -1242
- novelwriter/gui/search.py +11 -19
- novelwriter/gui/sidebar.py +8 -7
- novelwriter/gui/statusbar.py +20 -3
- novelwriter/gui/theme.py +11 -6
- novelwriter/guimain.py +101 -201
- novelwriter/shared.py +67 -28
- novelwriter/text/counting.py +3 -1
- novelwriter/text/patterns.py +169 -61
- novelwriter/tools/dictionaries.py +3 -3
- novelwriter/tools/lipsum.py +1 -1
- novelwriter/tools/manusbuild.py +15 -13
- novelwriter/tools/manuscript.py +121 -79
- novelwriter/tools/manussettings.py +424 -291
- novelwriter/tools/noveldetails.py +1 -1
- novelwriter/tools/welcome.py +6 -6
- novelwriter/tools/writingstats.py +4 -4
- novelwriter/types.py +25 -9
- novelwriter/core/tohtml.py +0 -530
- novelwriter/core/tomarkdown.py +0 -252
- novelwriter/core/toqdoc.py +0 -419
- {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/top_level.txt +0 -0
novelwriter/gui/doceditor.py
CHANGED
@@ -14,7 +14,7 @@ Created: 2023-11-06 [2.2b1] MetaCompleter
|
|
14
14
|
Created: 2023-11-07 [2.2b1] GuiDocToolBar
|
15
15
|
|
16
16
|
This file is a part of novelWriter
|
17
|
-
Copyright 2018
|
17
|
+
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
|
18
18
|
|
19
19
|
This program is free software: you can redistribute it and/or modify
|
20
20
|
it under the terms of the GNU General Public License as published by
|
@@ -42,8 +42,9 @@ from PyQt5.QtCore import (
|
|
42
42
|
pyqtSlot
|
43
43
|
)
|
44
44
|
from PyQt5.QtGui import (
|
45
|
-
QColor, QCursor,
|
46
|
-
|
45
|
+
QColor, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent,
|
46
|
+
QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, QTextBlock,
|
47
|
+
QTextCursor, QTextDocument, QTextOption
|
47
48
|
)
|
48
49
|
from PyQt5.QtWidgets import (
|
49
50
|
QAction, QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit,
|
@@ -51,10 +52,13 @@ from PyQt5.QtWidgets import (
|
|
51
52
|
)
|
52
53
|
|
53
54
|
from novelwriter import CONFIG, SHARED
|
54
|
-
from novelwriter.common import minmax, transferCase
|
55
|
+
from novelwriter.common import decodeMimeHandles, fontMatcher, minmax, qtLambda, transferCase
|
55
56
|
from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
|
56
57
|
from novelwriter.core.document import NWDocument
|
57
|
-
from novelwriter.enum import
|
58
|
+
from novelwriter.enum import (
|
59
|
+
nwChange, nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass,
|
60
|
+
nwItemType, nwTrinary
|
61
|
+
)
|
58
62
|
from novelwriter.extensions.configlayout import NColourLabel
|
59
63
|
from novelwriter.extensions.eventfilters import WheelEventFilter
|
60
64
|
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton
|
@@ -66,7 +70,7 @@ from novelwriter.tools.lipsum import GuiLipsum
|
|
66
70
|
from novelwriter.types import (
|
67
71
|
QtAlignCenterTop, QtAlignJustify, QtAlignLeft, QtAlignLeftTop,
|
68
72
|
QtAlignRight, QtKeepAnchor, QtModCtrl, QtModNone, QtModShift, QtMouseLeft,
|
69
|
-
QtMoveAnchor, QtMoveLeft, QtMoveRight
|
73
|
+
QtMoveAnchor, QtMoveLeft, QtMoveRight, QtScrollAlwaysOff, QtScrollAsNeeded
|
70
74
|
)
|
71
75
|
|
72
76
|
logger = logging.getLogger(__name__)
|
@@ -83,26 +87,34 @@ class _SelectAction(Enum):
|
|
83
87
|
class GuiDocEditor(QPlainTextEdit):
|
84
88
|
"""Gui Widget: Main Document Editor"""
|
85
89
|
|
90
|
+
__slots__ = (
|
91
|
+
"_nwDocument", "_nwItem", "_docChanged", "_docHandle", "_vpMargin",
|
92
|
+
"_lastEdit", "_lastActive", "_lastFind", "_doReplace", "_autoReplace",
|
93
|
+
"_completer", "_qDocument", "_keyContext", "_followTag1", "_followTag2",
|
94
|
+
"_timerDoc", "_wCounterDoc", "_timerSel", "_wCounterSel",
|
95
|
+
)
|
96
|
+
|
86
97
|
MOVE_KEYS = (
|
87
98
|
Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down,
|
88
99
|
Qt.Key.Key_PageUp, Qt.Key.Key_PageDown
|
89
100
|
)
|
90
101
|
|
91
102
|
# Custom Signals
|
92
|
-
|
93
|
-
docCountsChanged = pyqtSignal(str, int, int, int)
|
103
|
+
closeEditorRequest = pyqtSignal()
|
94
104
|
docTextChanged = pyqtSignal(str, float)
|
95
105
|
editedStatusChanged = pyqtSignal(bool)
|
106
|
+
itemHandleChanged = pyqtSignal(str)
|
96
107
|
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
97
|
-
novelStructureChanged = pyqtSignal()
|
98
108
|
novelItemMetaChanged = pyqtSignal(str)
|
99
|
-
|
100
|
-
|
101
|
-
toggleFocusModeRequest = pyqtSignal()
|
102
|
-
requestProjectItemSelected = pyqtSignal(str, bool)
|
103
|
-
requestProjectItemRenamed = pyqtSignal(str, str)
|
109
|
+
novelStructureChanged = pyqtSignal()
|
110
|
+
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
104
111
|
requestNewNoteCreation = pyqtSignal(str, nwItemClass)
|
105
112
|
requestNextDocument = pyqtSignal(str, bool)
|
113
|
+
requestProjectItemRenamed = pyqtSignal(str, str)
|
114
|
+
requestProjectItemSelected = pyqtSignal(str, bool)
|
115
|
+
spellCheckStateChanged = pyqtSignal(bool)
|
116
|
+
toggleFocusModeRequest = pyqtSignal()
|
117
|
+
updateStatusMessage = pyqtSignal(str)
|
106
118
|
|
107
119
|
def __init__(self, parent: QWidget) -> None:
|
108
120
|
super().__init__(parent=parent)
|
@@ -123,18 +135,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
123
135
|
self._lastFind = None # Position of the last found search word
|
124
136
|
self._doReplace = False # Switch to temporarily disable auto-replace
|
125
137
|
|
126
|
-
#
|
127
|
-
self.
|
128
|
-
self._typDQuoteO = '"'
|
129
|
-
self._typDQuoteC = '"'
|
130
|
-
self._typSQuoteO = "'"
|
131
|
-
self._typSQuoteC = "'"
|
132
|
-
self._typRepDQuote = False
|
133
|
-
self._typRepSQuote = False
|
134
|
-
self._typRepDash = False
|
135
|
-
self._typRepDots = False
|
136
|
-
self._typPadBefore = ""
|
137
|
-
self._typPadAfter = ""
|
138
|
+
# Auto-Replace
|
139
|
+
self._autoReplace = TextAutoReplace()
|
138
140
|
|
139
141
|
# Completer
|
140
142
|
self._completer = MetaCompleter(self)
|
@@ -169,40 +171,41 @@ class GuiDocEditor(QPlainTextEdit):
|
|
169
171
|
self.setMinimumWidth(CONFIG.pxInt(300))
|
170
172
|
self.setAutoFillBackground(True)
|
171
173
|
self.setFrameStyle(QFrame.Shape.NoFrame)
|
174
|
+
self.setAcceptDrops(True)
|
172
175
|
|
173
176
|
# Custom Shortcuts
|
174
|
-
self.
|
175
|
-
self.
|
176
|
-
self.
|
177
|
-
self.
|
177
|
+
self._keyContext = QShortcut(self)
|
178
|
+
self._keyContext.setKey("Ctrl+.")
|
179
|
+
self._keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
|
180
|
+
self._keyContext.activated.connect(self._openContextFromCursor)
|
178
181
|
|
179
|
-
self.
|
180
|
-
self.
|
181
|
-
self.
|
182
|
-
self.
|
182
|
+
self._followTag1 = QShortcut(self)
|
183
|
+
self._followTag1.setKey("Ctrl+Return")
|
184
|
+
self._followTag1.setContext(Qt.ShortcutContext.WidgetShortcut)
|
185
|
+
self._followTag1.activated.connect(self._processTag)
|
183
186
|
|
184
|
-
self.
|
185
|
-
self.
|
186
|
-
self.
|
187
|
-
self.
|
187
|
+
self._followTag2 = QShortcut(self)
|
188
|
+
self._followTag2.setKey("Ctrl+Enter")
|
189
|
+
self._followTag2.setContext(Qt.ShortcutContext.WidgetShortcut)
|
190
|
+
self._followTag2.activated.connect(self._processTag)
|
188
191
|
|
189
192
|
# Set Up Document Word Counter
|
190
|
-
self.
|
191
|
-
self.
|
192
|
-
self.
|
193
|
+
self._timerDoc = QTimer(self)
|
194
|
+
self._timerDoc.timeout.connect(self._runDocumentTasks)
|
195
|
+
self._timerDoc.setInterval(5000)
|
193
196
|
|
194
|
-
self.
|
195
|
-
self.
|
196
|
-
self.
|
197
|
+
self._wCounterDoc = BackgroundWordCounter(self)
|
198
|
+
self._wCounterDoc.setAutoDelete(False)
|
199
|
+
self._wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
|
197
200
|
|
198
201
|
# Set Up Selection Word Counter
|
199
|
-
self.
|
200
|
-
self.
|
201
|
-
self.
|
202
|
+
self._timerSel = QTimer(self)
|
203
|
+
self._timerSel.timeout.connect(self._runSelCounter)
|
204
|
+
self._timerSel.setInterval(500)
|
202
205
|
|
203
|
-
self.
|
204
|
-
self.
|
205
|
-
self.
|
206
|
+
self._wCounterSel = BackgroundWordCounter(self, forSelection=True)
|
207
|
+
self._wCounterSel.setAutoDelete(False)
|
208
|
+
self._wCounterSel.signals.countsReady.connect(self._updateSelCounts)
|
206
209
|
|
207
210
|
# Install Event Filter for Mouse Wheel
|
208
211
|
self.wheelEventFilter = WheelEventFilter(self)
|
@@ -256,8 +259,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
256
259
|
self._nwDocument = None
|
257
260
|
self.setReadOnly(True)
|
258
261
|
self.clear()
|
259
|
-
self.
|
260
|
-
self.
|
262
|
+
self._timerDoc.stop()
|
263
|
+
self._timerSel.stop()
|
261
264
|
|
262
265
|
self._docHandle = None
|
263
266
|
self._lastEdit = 0.0
|
@@ -270,6 +273,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
270
273
|
self.docFooter.setHandle(self._docHandle)
|
271
274
|
self.docToolBar.setVisible(False)
|
272
275
|
|
276
|
+
self.itemHandleChanged.emit("")
|
277
|
+
|
273
278
|
return
|
274
279
|
|
275
280
|
def updateTheme(self) -> None:
|
@@ -303,28 +308,16 @@ class GuiDocEditor(QPlainTextEdit):
|
|
303
308
|
settings. This function is both called when the editor is
|
304
309
|
created, and when the user changes the main editor preferences.
|
305
310
|
"""
|
306
|
-
#
|
307
|
-
|
308
|
-
self._typPadChar = nwUnicode.U_THNBSP
|
309
|
-
else:
|
310
|
-
self._typPadChar = nwUnicode.U_NBSP
|
311
|
-
|
312
|
-
self._typSQuoteO = CONFIG.fmtSQuoteOpen
|
313
|
-
self._typSQuoteC = CONFIG.fmtSQuoteClose
|
314
|
-
self._typDQuoteO = CONFIG.fmtDQuoteOpen
|
315
|
-
self._typDQuoteC = CONFIG.fmtDQuoteClose
|
316
|
-
self._typRepDQuote = CONFIG.doReplaceDQuote
|
317
|
-
self._typRepSQuote = CONFIG.doReplaceSQuote
|
318
|
-
self._typRepDash = CONFIG.doReplaceDash
|
319
|
-
self._typRepDots = CONFIG.doReplaceDots
|
320
|
-
self._typPadBefore = CONFIG.fmtPadBefore
|
321
|
-
self._typPadAfter = CONFIG.fmtPadAfter
|
311
|
+
# Auto-Replace
|
312
|
+
self._autoReplace.initSettings()
|
322
313
|
|
323
314
|
# Reload spell check and dictionaries
|
324
315
|
SHARED.updateSpellCheckLanguage()
|
325
316
|
|
326
317
|
# Set the font. See issues #1862 and #1875.
|
327
|
-
|
318
|
+
font = fontMatcher(CONFIG.textFont)
|
319
|
+
self.setFont(font)
|
320
|
+
self._qDocument.setDefaultFont(font)
|
328
321
|
self.docHeader.updateFont()
|
329
322
|
self.docFooter.updateFont()
|
330
323
|
self.docSearch.updateFont()
|
@@ -354,14 +347,14 @@ class GuiDocEditor(QPlainTextEdit):
|
|
354
347
|
# Scrolling
|
355
348
|
self.setCenterOnScroll(CONFIG.scrollPastEnd)
|
356
349
|
if CONFIG.hideVScroll:
|
357
|
-
self.setVerticalScrollBarPolicy(
|
350
|
+
self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
|
358
351
|
else:
|
359
|
-
self.setVerticalScrollBarPolicy(
|
352
|
+
self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
|
360
353
|
|
361
354
|
if CONFIG.hideHScroll:
|
362
|
-
self.setHorizontalScrollBarPolicy(
|
355
|
+
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
|
363
356
|
else:
|
364
|
-
self.setHorizontalScrollBarPolicy(
|
357
|
+
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
365
358
|
|
366
359
|
# Refresh the tab stops
|
367
360
|
self.setTabStopDistance(CONFIG.getTabWidth())
|
@@ -388,9 +381,12 @@ class GuiDocEditor(QPlainTextEdit):
|
|
388
381
|
"""
|
389
382
|
self._nwDocument = SHARED.project.storage.getDocument(tHandle)
|
390
383
|
self._nwItem = self._nwDocument.nwItem
|
384
|
+
if not ((nwItem := self._nwItem) and nwItem.itemType == nwItemType.FILE):
|
385
|
+
logger.debug("Requested item '%s' is not a document", tHandle)
|
386
|
+
self.clearEditor()
|
387
|
+
return False
|
391
388
|
|
392
|
-
docText
|
393
|
-
if docText is None:
|
389
|
+
if (docText := self._nwDocument.readDocument()) is None:
|
394
390
|
# There was an I/O error
|
395
391
|
self.clearEditor()
|
396
392
|
return False
|
@@ -406,15 +402,15 @@ class GuiDocEditor(QPlainTextEdit):
|
|
406
402
|
self._lastEdit = time()
|
407
403
|
self._lastActive = time()
|
408
404
|
self._runDocumentTasks()
|
409
|
-
self.
|
405
|
+
self._timerDoc.start()
|
410
406
|
|
411
407
|
self.setReadOnly(False)
|
412
408
|
self.updateDocMargins()
|
413
409
|
|
414
|
-
if tLine
|
415
|
-
self.setCursorPosition(self._nwItem.cursorPos)
|
416
|
-
elif isinstance(tLine, int):
|
410
|
+
if isinstance(tLine, int):
|
417
411
|
self.setCursorLine(tLine)
|
412
|
+
else:
|
413
|
+
self.setCursorPosition(nwItem.cursorPos)
|
418
414
|
|
419
415
|
self.docHeader.setHandle(tHandle)
|
420
416
|
self.docFooter.setHandle(tHandle)
|
@@ -430,11 +426,15 @@ class GuiDocEditor(QPlainTextEdit):
|
|
430
426
|
self._qDocument.clearUndoRedoStacks()
|
431
427
|
self.docToolBar.setVisible(CONFIG.showEditToolBar)
|
432
428
|
|
433
|
-
|
429
|
+
# Process State Changes
|
430
|
+
SHARED.project.data.setLastHandle(tHandle, "editor")
|
431
|
+
self.itemHandleChanged.emit(tHandle)
|
434
432
|
|
435
|
-
#
|
436
|
-
|
437
|
-
|
433
|
+
# Finalise
|
434
|
+
QApplication.restoreOverrideCursor()
|
435
|
+
self.updateStatusMessage.emit(
|
436
|
+
self.tr("Opened Document: {0}").format(nwItem.itemName)
|
437
|
+
)
|
438
438
|
|
439
439
|
return True
|
440
440
|
|
@@ -505,7 +505,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|
505
505
|
self.docFooter.updateInfo()
|
506
506
|
|
507
507
|
# Update the status bar
|
508
|
-
self.
|
508
|
+
self.updateStatusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName))
|
509
509
|
|
510
510
|
return True
|
511
511
|
|
@@ -700,7 +700,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|
700
700
|
self._qDocument.syntaxHighlighter.rehighlight()
|
701
701
|
QApplication.restoreOverrideCursor()
|
702
702
|
logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
|
703
|
-
self.
|
703
|
+
self.updateStatusMessage.emit(self.tr("Spell check complete"))
|
704
704
|
return
|
705
705
|
|
706
706
|
##
|
@@ -742,9 +742,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|
742
742
|
elif action == nwDocAction.MD_STRIKE:
|
743
743
|
self._toggleFormat(2, "~")
|
744
744
|
elif action == nwDocAction.S_QUOTE:
|
745
|
-
self._wrapSelection(
|
745
|
+
self._wrapSelection(CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
|
746
746
|
elif action == nwDocAction.D_QUOTE:
|
747
|
-
self._wrapSelection(
|
747
|
+
self._wrapSelection(CONFIG.fmtDQuoteOpen, CONFIG.fmtDQuoteClose)
|
748
748
|
elif action == nwDocAction.SEL_ALL:
|
749
749
|
self._makeSelection(QTextCursor.SelectionType.Document)
|
750
750
|
elif action == nwDocAction.SEL_PARA:
|
@@ -770,9 +770,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|
770
770
|
elif action == nwDocAction.BLOCK_HSC:
|
771
771
|
self._formatBlock(nwDocAction.BLOCK_HSC)
|
772
772
|
elif action == nwDocAction.REPL_SNG:
|
773
|
-
self._replaceQuotes("'",
|
773
|
+
self._replaceQuotes("'", CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
|
774
774
|
elif action == nwDocAction.REPL_DBL:
|
775
|
-
self._replaceQuotes("\"",
|
775
|
+
self._replaceQuotes("\"", CONFIG.fmtDQuoteOpen, CONFIG.fmtDQuoteClose)
|
776
776
|
elif action == nwDocAction.RM_BREAKS:
|
777
777
|
self._removeInParLineBreaks()
|
778
778
|
elif action == nwDocAction.ALIGN_L:
|
@@ -844,13 +844,13 @@ class GuiDocEditor(QPlainTextEdit):
|
|
844
844
|
text = insert
|
845
845
|
elif isinstance(insert, nwDocInsert):
|
846
846
|
if insert == nwDocInsert.QUOTE_LS:
|
847
|
-
text =
|
847
|
+
text = CONFIG.fmtSQuoteOpen
|
848
848
|
elif insert == nwDocInsert.QUOTE_RS:
|
849
|
-
text =
|
849
|
+
text = CONFIG.fmtSQuoteClose
|
850
850
|
elif insert == nwDocInsert.QUOTE_LD:
|
851
|
-
text =
|
851
|
+
text = CONFIG.fmtDQuoteOpen
|
852
852
|
elif insert == nwDocInsert.QUOTE_RD:
|
853
|
-
text =
|
853
|
+
text = CONFIG.fmtDQuoteClose
|
854
854
|
elif insert == nwDocInsert.SYNOPSIS:
|
855
855
|
text = "%Synopsis: "
|
856
856
|
block = True
|
@@ -877,6 +877,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
877
877
|
after = False
|
878
878
|
elif insert == nwDocInsert.FOOTNOTE:
|
879
879
|
self._insertCommentStructure(nwComment.FOOTNOTE)
|
880
|
+
elif insert == nwDocInsert.LINE_BRK:
|
881
|
+
text = nwShortcode.BREAK
|
880
882
|
|
881
883
|
if text:
|
882
884
|
if block:
|
@@ -965,6 +967,32 @@ class GuiDocEditor(QPlainTextEdit):
|
|
965
967
|
|
966
968
|
return
|
967
969
|
|
970
|
+
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
|
971
|
+
"""Overload drag enter event to handle dragged items."""
|
972
|
+
if event.mimeData().hasFormat(nwConst.MIME_HANDLE):
|
973
|
+
event.acceptProposedAction()
|
974
|
+
else:
|
975
|
+
super().dragEnterEvent(event)
|
976
|
+
return
|
977
|
+
|
978
|
+
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
|
979
|
+
"""Overload drag move event to handle dragged items."""
|
980
|
+
if event.mimeData().hasFormat(nwConst.MIME_HANDLE):
|
981
|
+
event.acceptProposedAction()
|
982
|
+
else:
|
983
|
+
super().dragMoveEvent(event)
|
984
|
+
return
|
985
|
+
|
986
|
+
def dropEvent(self, event: QDropEvent) -> None:
|
987
|
+
"""Overload drop event to handle dragged items."""
|
988
|
+
if event.mimeData().hasFormat(nwConst.MIME_HANDLE):
|
989
|
+
if handles := decodeMimeHandles(event.mimeData()):
|
990
|
+
if SHARED.project.tree.checkType(handles[0], nwItemType.FILE):
|
991
|
+
self.openDocumentRequest.emit(handles[0], nwDocMode.EDIT, "", True)
|
992
|
+
else:
|
993
|
+
super().dropEvent(event)
|
994
|
+
return
|
995
|
+
|
968
996
|
def focusNextPrevChild(self, next: bool) -> bool:
|
969
997
|
"""Capture the focus request from the tab key on the text
|
970
998
|
editor. If the editor has focus, we do not change focus and
|
@@ -982,8 +1010,13 @@ class GuiDocEditor(QPlainTextEdit):
|
|
982
1010
|
pressed, check if we're clicking on a tag, and trigger the
|
983
1011
|
follow tag function.
|
984
1012
|
"""
|
985
|
-
if
|
986
|
-
self.
|
1013
|
+
if event.modifiers() & QtModCtrl == QtModCtrl:
|
1014
|
+
cursor = self.cursorForPosition(event.pos())
|
1015
|
+
mData, mType = self._qDocument.metaDataAtPos(cursor.position())
|
1016
|
+
if mData and mType == "url":
|
1017
|
+
SHARED.openWebsite(mData)
|
1018
|
+
else:
|
1019
|
+
self._processTag(cursor)
|
987
1020
|
super().mouseReleaseEvent(event)
|
988
1021
|
return
|
989
1022
|
|
@@ -999,12 +1032,12 @@ class GuiDocEditor(QPlainTextEdit):
|
|
999
1032
|
# Public Slots
|
1000
1033
|
##
|
1001
1034
|
|
1002
|
-
@pyqtSlot(str)
|
1003
|
-
def
|
1035
|
+
@pyqtSlot(str, Enum)
|
1036
|
+
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
|
1004
1037
|
"""Called when an item label is changed to check if the document
|
1005
1038
|
title bar needs updating,
|
1006
1039
|
"""
|
1007
|
-
if tHandle and
|
1040
|
+
if tHandle == self._docHandle and change == nwChange.UPDATE:
|
1008
1041
|
self.docHeader.setHandle(tHandle)
|
1009
1042
|
self.docFooter.updateInfo()
|
1010
1043
|
self.updateDocMargins()
|
@@ -1053,8 +1086,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1053
1086
|
if not self._docChanged:
|
1054
1087
|
self.setDocumentChanged(removed != 0 or added != 0)
|
1055
1088
|
|
1056
|
-
if not self.
|
1057
|
-
self.
|
1089
|
+
if not self._timerDoc.isActive():
|
1090
|
+
self._timerDoc.start()
|
1058
1091
|
|
1059
1092
|
if (block := self._qDocument.findBlock(pos)).isValid():
|
1060
1093
|
text = block.text()
|
@@ -1071,8 +1104,10 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1071
1104
|
else:
|
1072
1105
|
self._completer.setVisible(False)
|
1073
1106
|
|
1074
|
-
|
1075
|
-
|
1107
|
+
if self._doReplace and added == 1:
|
1108
|
+
cursor = self.textCursor()
|
1109
|
+
if self._autoReplace.process(text, cursor):
|
1110
|
+
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
1076
1111
|
|
1077
1112
|
return
|
1078
1113
|
|
@@ -1095,6 +1130,12 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1095
1130
|
self._completer.hide()
|
1096
1131
|
return
|
1097
1132
|
|
1133
|
+
@pyqtSlot()
|
1134
|
+
def _openContextFromCursor(self) -> None:
|
1135
|
+
"""Open the spell check context menu at the cursor."""
|
1136
|
+
self._openContextMenu(self.cursorRect().center())
|
1137
|
+
return
|
1138
|
+
|
1098
1139
|
@pyqtSlot("QPoint")
|
1099
1140
|
def _openContextMenu(self, pos: QPoint) -> None:
|
1100
1141
|
"""Open the editor context menu at a given coordinate."""
|
@@ -1106,41 +1147,48 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1106
1147
|
ctxMenu.setObjectName("ContextMenu")
|
1107
1148
|
if pBlock.userState() == BLOCK_TITLE:
|
1108
1149
|
action = ctxMenu.addAction(self.tr("Set as Document Name"))
|
1109
|
-
action.triggered.connect(
|
1150
|
+
action.triggered.connect(qtLambda(self._emitRenameItem, pBlock))
|
1151
|
+
|
1152
|
+
# URL
|
1153
|
+
(mData, mType) = self._qDocument.metaDataAtPos(pCursor.position())
|
1154
|
+
if mData and mType == "url":
|
1155
|
+
action = ctxMenu.addAction(self.tr("Open URL"))
|
1156
|
+
action.triggered.connect(qtLambda(SHARED.openWebsite, mData))
|
1157
|
+
ctxMenu.addSeparator()
|
1110
1158
|
|
1111
1159
|
# Follow
|
1112
1160
|
status = self._processTag(cursor=pCursor, follow=False)
|
1113
1161
|
if status == nwTrinary.POSITIVE:
|
1114
1162
|
action = ctxMenu.addAction(self.tr("Follow Tag"))
|
1115
|
-
action.triggered.connect(
|
1163
|
+
action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, follow=True))
|
1116
1164
|
ctxMenu.addSeparator()
|
1117
1165
|
elif status == nwTrinary.NEGATIVE:
|
1118
1166
|
action = ctxMenu.addAction(self.tr("Create Note for Tag"))
|
1119
|
-
action.triggered.connect(
|
1167
|
+
action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, create=True))
|
1120
1168
|
ctxMenu.addSeparator()
|
1121
1169
|
|
1122
1170
|
# Cut, Copy and Paste
|
1123
1171
|
if uCursor.hasSelection():
|
1124
1172
|
action = ctxMenu.addAction(self.tr("Cut"))
|
1125
|
-
action.triggered.connect(
|
1173
|
+
action.triggered.connect(qtLambda(self.docAction, nwDocAction.CUT))
|
1126
1174
|
action = ctxMenu.addAction(self.tr("Copy"))
|
1127
|
-
action.triggered.connect(
|
1175
|
+
action.triggered.connect(qtLambda(self.docAction, nwDocAction.COPY))
|
1128
1176
|
|
1129
1177
|
action = ctxMenu.addAction(self.tr("Paste"))
|
1130
|
-
action.triggered.connect(
|
1178
|
+
action.triggered.connect(qtLambda(self.docAction, nwDocAction.PASTE))
|
1131
1179
|
ctxMenu.addSeparator()
|
1132
1180
|
|
1133
1181
|
# Selections
|
1134
1182
|
action = ctxMenu.addAction(self.tr("Select All"))
|
1135
|
-
action.triggered.connect(
|
1183
|
+
action.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL))
|
1136
1184
|
action = ctxMenu.addAction(self.tr("Select Word"))
|
1137
|
-
action.triggered.connect(
|
1138
|
-
|
1139
|
-
)
|
1185
|
+
action.triggered.connect(qtLambda(
|
1186
|
+
self._makePosSelection, QTextCursor.SelectionType.WordUnderCursor, pos,
|
1187
|
+
))
|
1140
1188
|
action = ctxMenu.addAction(self.tr("Select Paragraph"))
|
1141
|
-
action.triggered.connect(
|
1142
|
-
QTextCursor.SelectionType.BlockUnderCursor, pos
|
1143
|
-
)
|
1189
|
+
action.triggered.connect(qtLambda(
|
1190
|
+
self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, pos
|
1191
|
+
))
|
1144
1192
|
|
1145
1193
|
# Spell Checking
|
1146
1194
|
if SHARED.project.data.spellCheck:
|
@@ -1156,47 +1204,23 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1156
1204
|
ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
|
1157
1205
|
for option in suggest[:15]:
|
1158
1206
|
action = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
|
1159
|
-
action.triggered.connect(
|
1160
|
-
lambda _, option=option: self._correctWord(sCursor, option)
|
1161
|
-
)
|
1207
|
+
action.triggered.connect(qtLambda(self._correctWord, sCursor, option))
|
1162
1208
|
else:
|
1163
1209
|
trNone = self.tr("No Suggestions")
|
1164
1210
|
ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {trNone}")
|
1165
1211
|
|
1166
1212
|
ctxMenu.addSeparator()
|
1213
|
+
action = ctxMenu.addAction(self.tr("Ignore Word"))
|
1214
|
+
action.triggered.connect(qtLambda(self._addWord, word, block, False))
|
1167
1215
|
action = ctxMenu.addAction(self.tr("Add Word to Dictionary"))
|
1168
|
-
action.triggered.connect(
|
1216
|
+
action.triggered.connect(qtLambda(self._addWord, word, block, True))
|
1169
1217
|
|
1170
1218
|
# Execute the context menu
|
1171
1219
|
ctxMenu.exec(self.viewport().mapToGlobal(pos))
|
1172
|
-
ctxMenu.
|
1220
|
+
ctxMenu.setParent(None)
|
1173
1221
|
|
1174
1222
|
return
|
1175
1223
|
|
1176
|
-
@pyqtSlot("QTextCursor", str)
|
1177
|
-
def _correctWord(self, cursor: QTextCursor, word: str) -> None:
|
1178
|
-
"""Slot for the spell check context menu triggering the
|
1179
|
-
replacement of a word with the word from the dictionary.
|
1180
|
-
"""
|
1181
|
-
pos = cursor.selectionStart()
|
1182
|
-
cursor.beginEditBlock()
|
1183
|
-
cursor.removeSelectedText()
|
1184
|
-
cursor.insertText(word)
|
1185
|
-
cursor.endEditBlock()
|
1186
|
-
cursor.setPosition(pos)
|
1187
|
-
self.setTextCursor(cursor)
|
1188
|
-
return
|
1189
|
-
|
1190
|
-
@pyqtSlot(str, "QTextBlock")
|
1191
|
-
def _addWord(self, word: str, block: QTextBlock) -> None:
|
1192
|
-
"""Slot for the spell check context menu triggered when the user
|
1193
|
-
wants to add a word to the project dictionary.
|
1194
|
-
"""
|
1195
|
-
logger.debug("Added '%s' to project dictionary", word)
|
1196
|
-
SHARED.spelling.addWord(word)
|
1197
|
-
self._qDocument.syntaxHighlighter.rehighlightBlock(block)
|
1198
|
-
return
|
1199
|
-
|
1200
1224
|
@pyqtSlot()
|
1201
1225
|
def _runDocumentTasks(self) -> None:
|
1202
1226
|
"""Run timer document tasks."""
|
@@ -1205,8 +1229,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1205
1229
|
|
1206
1230
|
if time() - self._lastEdit < 25.0:
|
1207
1231
|
logger.debug("Running document tasks")
|
1208
|
-
if not self.
|
1209
|
-
SHARED.runInThreadPool(self.
|
1232
|
+
if not self._wCounterDoc.isRunning():
|
1233
|
+
SHARED.runInThreadPool(self._wCounterDoc)
|
1210
1234
|
|
1211
1235
|
self.docHeader.setOutline({
|
1212
1236
|
block.blockNumber(): block.text()
|
@@ -1223,11 +1247,15 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1223
1247
|
"""Process the word counter's finished signal."""
|
1224
1248
|
if self._docHandle and self._nwItem:
|
1225
1249
|
logger.debug("Updating word count")
|
1250
|
+
needsRefresh = wCount != self._nwItem.wordCount
|
1226
1251
|
self._nwItem.setCharCount(cCount)
|
1227
1252
|
self._nwItem.setWordCount(wCount)
|
1228
1253
|
self._nwItem.setParaCount(pCount)
|
1229
|
-
|
1230
|
-
|
1254
|
+
if needsRefresh:
|
1255
|
+
self._nwItem.notifyToRefresh()
|
1256
|
+
if not self.textCursor().hasSelection():
|
1257
|
+
# Selection counter should take precedence (#2155)
|
1258
|
+
self.docFooter.updateWordCount(wCount, False)
|
1231
1259
|
return
|
1232
1260
|
|
1233
1261
|
@pyqtSlot()
|
@@ -1236,10 +1264,10 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1236
1264
|
information to the footer, and start the selection word counter.
|
1237
1265
|
"""
|
1238
1266
|
if self.textCursor().hasSelection():
|
1239
|
-
if not self.
|
1240
|
-
self.
|
1267
|
+
if not self._timerSel.isActive():
|
1268
|
+
self._timerSel.start()
|
1241
1269
|
else:
|
1242
|
-
self.
|
1270
|
+
self._timerSel.stop()
|
1243
1271
|
self.docFooter.updateWordCount(0, False)
|
1244
1272
|
return
|
1245
1273
|
|
@@ -1249,11 +1277,11 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1249
1277
|
if self._docHandle is None:
|
1250
1278
|
return
|
1251
1279
|
|
1252
|
-
if self.
|
1280
|
+
if self._wCounterSel.isRunning():
|
1253
1281
|
logger.debug("Selection word counter is busy")
|
1254
1282
|
return
|
1255
1283
|
|
1256
|
-
SHARED.runInThreadPool(self.
|
1284
|
+
SHARED.runInThreadPool(self._wCounterSel)
|
1257
1285
|
|
1258
1286
|
return
|
1259
1287
|
|
@@ -1263,13 +1291,13 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1263
1291
|
if self._docHandle and self._nwItem:
|
1264
1292
|
logger.debug("User selected %d words", wCount)
|
1265
1293
|
self.docFooter.updateWordCount(wCount, True)
|
1266
|
-
self.
|
1294
|
+
self._timerSel.stop()
|
1267
1295
|
return
|
1268
1296
|
|
1269
1297
|
@pyqtSlot()
|
1270
1298
|
def _closeCurrentDocument(self) -> None:
|
1271
1299
|
"""Close the document. Forwarded to the main Gui."""
|
1272
|
-
self.
|
1300
|
+
self.closeEditorRequest.emit()
|
1273
1301
|
self.docToolBar.setVisible(False)
|
1274
1302
|
return
|
1275
1303
|
|
@@ -1875,6 +1903,28 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1875
1903
|
# Internal Functions
|
1876
1904
|
##
|
1877
1905
|
|
1906
|
+
def _correctWord(self, cursor: QTextCursor, word: str) -> None:
|
1907
|
+
"""Slot for the spell check context menu triggering the
|
1908
|
+
replacement of a word with the word from the dictionary.
|
1909
|
+
"""
|
1910
|
+
pos = cursor.selectionStart()
|
1911
|
+
cursor.beginEditBlock()
|
1912
|
+
cursor.removeSelectedText()
|
1913
|
+
cursor.insertText(word)
|
1914
|
+
cursor.endEditBlock()
|
1915
|
+
cursor.setPosition(pos)
|
1916
|
+
self.setTextCursor(cursor)
|
1917
|
+
return
|
1918
|
+
|
1919
|
+
def _addWord(self, word: str, block: QTextBlock, save: bool) -> None:
|
1920
|
+
"""Slot for the spell check context menu triggered when the user
|
1921
|
+
wants to add a word to the project dictionary.
|
1922
|
+
"""
|
1923
|
+
logger.debug("Added '%s' to project dictionary, %s", word, "saved" if save else "unsaved")
|
1924
|
+
SHARED.spelling.addWord(word, save=save)
|
1925
|
+
self._qDocument.syntaxHighlighter.rehighlightBlock(block)
|
1926
|
+
return
|
1927
|
+
|
1878
1928
|
def _processTag(self, cursor: QTextCursor | None = None,
|
1879
1929
|
follow: bool = True, create: bool = False) -> nwTrinary:
|
1880
1930
|
"""Activated by Ctrl+Enter. Checks that we're in a block
|
@@ -1938,119 +1988,6 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1938
1988
|
self.requestProjectItemRenamed.emit(self._docHandle, text)
|
1939
1989
|
return
|
1940
1990
|
|
1941
|
-
def _openContextFromCursor(self) -> None:
|
1942
|
-
"""Open the spell check context menu at the cursor."""
|
1943
|
-
self._openContextMenu(self.cursorRect().center())
|
1944
|
-
return
|
1945
|
-
|
1946
|
-
def _docAutoReplace(self, text: str) -> None:
|
1947
|
-
"""Auto-replace text elements based on main configuration."""
|
1948
|
-
cursor = self.textCursor()
|
1949
|
-
tPos = cursor.positionInBlock()
|
1950
|
-
tLen = len(text)
|
1951
|
-
|
1952
|
-
if tLen < 1 or tPos-1 > tLen:
|
1953
|
-
return
|
1954
|
-
|
1955
|
-
tOne = text[tPos-1:tPos]
|
1956
|
-
tTwo = text[tPos-2:tPos]
|
1957
|
-
tThree = text[tPos-3:tPos]
|
1958
|
-
|
1959
|
-
if not tOne:
|
1960
|
-
return
|
1961
|
-
|
1962
|
-
nDelete = 0
|
1963
|
-
tInsert = tOne
|
1964
|
-
|
1965
|
-
if self._typRepDQuote and tTwo[:1].isspace() and tTwo.endswith('"'):
|
1966
|
-
nDelete = 1
|
1967
|
-
tInsert = self._typDQuoteO
|
1968
|
-
|
1969
|
-
elif self._typRepDQuote and tOne == '"':
|
1970
|
-
nDelete = 1
|
1971
|
-
if tPos == 1:
|
1972
|
-
tInsert = self._typDQuoteO
|
1973
|
-
elif tPos == 2 and tTwo == '>"':
|
1974
|
-
tInsert = self._typDQuoteO
|
1975
|
-
elif tPos == 3 and tThree == '>>"':
|
1976
|
-
tInsert = self._typDQuoteO
|
1977
|
-
else:
|
1978
|
-
tInsert = self._typDQuoteC
|
1979
|
-
|
1980
|
-
elif self._typRepSQuote and tTwo[:1].isspace() and tTwo.endswith("'"):
|
1981
|
-
nDelete = 1
|
1982
|
-
tInsert = self._typSQuoteO
|
1983
|
-
|
1984
|
-
elif self._typRepSQuote and tOne == "'":
|
1985
|
-
nDelete = 1
|
1986
|
-
if tPos == 1:
|
1987
|
-
tInsert = self._typSQuoteO
|
1988
|
-
elif tPos == 2 and tTwo == ">'":
|
1989
|
-
tInsert = self._typSQuoteO
|
1990
|
-
elif tPos == 3 and tThree == ">>'":
|
1991
|
-
tInsert = self._typSQuoteO
|
1992
|
-
else:
|
1993
|
-
tInsert = self._typSQuoteC
|
1994
|
-
|
1995
|
-
elif self._typRepDash and tThree == "---":
|
1996
|
-
nDelete = 3
|
1997
|
-
tInsert = nwUnicode.U_EMDASH
|
1998
|
-
|
1999
|
-
elif self._typRepDash and tTwo == "--":
|
2000
|
-
nDelete = 2
|
2001
|
-
tInsert = nwUnicode.U_ENDASH
|
2002
|
-
|
2003
|
-
elif self._typRepDash and tTwo == nwUnicode.U_ENDASH + "-":
|
2004
|
-
nDelete = 2
|
2005
|
-
tInsert = nwUnicode.U_EMDASH
|
2006
|
-
|
2007
|
-
elif self._typRepDots and tThree == "...":
|
2008
|
-
nDelete = 3
|
2009
|
-
tInsert = nwUnicode.U_HELLIP
|
2010
|
-
|
2011
|
-
elif tOne == nwUnicode.U_LSEP:
|
2012
|
-
# This resolves issue #1150
|
2013
|
-
nDelete = 1
|
2014
|
-
tInsert = nwUnicode.U_PSEP
|
2015
|
-
|
2016
|
-
tCheck = tInsert
|
2017
|
-
if self._typPadBefore and tCheck in self._typPadBefore:
|
2018
|
-
if self._allowSpaceBeforeColon(text, tCheck):
|
2019
|
-
nDelete = max(nDelete, 1)
|
2020
|
-
chkPos = tPos - nDelete - 1
|
2021
|
-
if chkPos >= 0 and text[chkPos].isspace():
|
2022
|
-
# Strip existing space before inserting a new (#1061)
|
2023
|
-
nDelete += 1
|
2024
|
-
tInsert = self._typPadChar + tInsert
|
2025
|
-
|
2026
|
-
if self._typPadAfter and tCheck in self._typPadAfter:
|
2027
|
-
if self._allowSpaceBeforeColon(text, tCheck):
|
2028
|
-
nDelete = max(nDelete, 1)
|
2029
|
-
tInsert = tInsert + self._typPadChar
|
2030
|
-
|
2031
|
-
if nDelete > 0:
|
2032
|
-
cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete)
|
2033
|
-
cursor.insertText(tInsert)
|
2034
|
-
|
2035
|
-
# Re-highlight, since the auto-replace sometimes interferes with it
|
2036
|
-
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
2037
|
-
|
2038
|
-
return
|
2039
|
-
|
2040
|
-
@staticmethod
|
2041
|
-
def _allowSpaceBeforeColon(text: str, char: str) -> bool:
|
2042
|
-
"""Special checker function only used by the insert space
|
2043
|
-
feature for French, Spanish, etc, so it doesn't insert a
|
2044
|
-
space before colons in meta data lines. See issue #1090.
|
2045
|
-
"""
|
2046
|
-
if char == ":" and len(text) > 1:
|
2047
|
-
if text[0] == "@":
|
2048
|
-
return False
|
2049
|
-
if text[0] == "%":
|
2050
|
-
if text[1:].lstrip()[:9].lower() == "synopsis:":
|
2051
|
-
return False
|
2052
|
-
return True
|
2053
|
-
|
2054
1991
|
def _autoSelect(self) -> QTextCursor:
|
2055
1992
|
"""Return a cursor which may or may not have a selection based
|
2056
1993
|
on user settings and document action. The selection will be the
|
@@ -2173,7 +2110,7 @@ class MetaCompleter(QMenu):
|
|
2173
2110
|
suffix = ""
|
2174
2111
|
options = list(filter(
|
2175
2112
|
lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
|
2176
|
-
nwKeyWords.KEY_CLASS.get(kw.strip()
|
2113
|
+
nwKeyWords.KEY_CLASS.get(kw.strip())
|
2177
2114
|
)
|
2178
2115
|
))[:15]
|
2179
2116
|
|
@@ -2183,7 +2120,7 @@ class MetaCompleter(QMenu):
|
|
2183
2120
|
for value in sorted(options):
|
2184
2121
|
rep = value + suffix
|
2185
2122
|
action = self.addAction(value)
|
2186
|
-
action.triggered.connect(
|
2123
|
+
action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
|
2187
2124
|
|
2188
2125
|
return True
|
2189
2126
|
|
@@ -2259,6 +2196,131 @@ class BackgroundWordCounterSignals(QObject):
|
|
2259
2196
|
countsReady = pyqtSignal(int, int, int)
|
2260
2197
|
|
2261
2198
|
|
2199
|
+
class TextAutoReplace:
|
2200
|
+
|
2201
|
+
__slots__ = (
|
2202
|
+
"_quoteSO", "_quoteSC", "_quoteDO", "_quoteDC",
|
2203
|
+
"_replaceSQuote", "_replaceDQuote", "_replaceDash", "_replaceDots",
|
2204
|
+
"_padChar", "_padBefore", "_padAfter", "_doPadBefore", "_doPadAfter",
|
2205
|
+
)
|
2206
|
+
|
2207
|
+
def __init__(self) -> None:
|
2208
|
+
self.initSettings()
|
2209
|
+
return
|
2210
|
+
|
2211
|
+
def initSettings(self) -> None:
|
2212
|
+
"""Initialise the auto-replace settings from config."""
|
2213
|
+
self._quoteSO = CONFIG.fmtSQuoteOpen
|
2214
|
+
self._quoteSC = CONFIG.fmtSQuoteClose
|
2215
|
+
self._quoteDO = CONFIG.fmtDQuoteOpen
|
2216
|
+
self._quoteDC = CONFIG.fmtDQuoteClose
|
2217
|
+
|
2218
|
+
self._replaceSQuote = CONFIG.doReplaceSQuote
|
2219
|
+
self._replaceDQuote = CONFIG.doReplaceDQuote
|
2220
|
+
self._replaceDash = CONFIG.doReplaceDash
|
2221
|
+
self._replaceDots = CONFIG.doReplaceDots
|
2222
|
+
|
2223
|
+
self._padChar = nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP
|
2224
|
+
self._padBefore = CONFIG.fmtPadBefore
|
2225
|
+
self._padAfter = CONFIG.fmtPadAfter
|
2226
|
+
self._doPadBefore = bool(CONFIG.fmtPadBefore)
|
2227
|
+
self._doPadAfter = bool(CONFIG.fmtPadAfter)
|
2228
|
+
return
|
2229
|
+
|
2230
|
+
def process(self, text: str, cursor: QTextCursor) -> bool:
|
2231
|
+
"""Auto-replace text elements based on main configuration.
|
2232
|
+
Returns True if anything was changed.
|
2233
|
+
"""
|
2234
|
+
pos = cursor.positionInBlock()
|
2235
|
+
length = len(text)
|
2236
|
+
if length < 1 or pos-1 > length:
|
2237
|
+
return False
|
2238
|
+
|
2239
|
+
delete, insert = self._determine(text, pos)
|
2240
|
+
if insert == "":
|
2241
|
+
return False
|
2242
|
+
|
2243
|
+
check = insert
|
2244
|
+
if self._doPadBefore and check in self._padBefore:
|
2245
|
+
if not (check == ":" and length > 1 and text[0] == "@"):
|
2246
|
+
delete = max(delete, 1)
|
2247
|
+
chkPos = pos - delete - 1
|
2248
|
+
if chkPos >= 0 and text[chkPos].isspace():
|
2249
|
+
# Strip existing space before inserting a new (#1061)
|
2250
|
+
delete += 1
|
2251
|
+
insert = self._padChar + insert
|
2252
|
+
|
2253
|
+
if self._doPadAfter and check in self._padAfter:
|
2254
|
+
if not (check == ":" and length > 1 and text[0] == "@"):
|
2255
|
+
delete = max(delete, 1)
|
2256
|
+
insert = insert + self._padChar
|
2257
|
+
|
2258
|
+
if delete > 0:
|
2259
|
+
cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
|
2260
|
+
cursor.insertText(insert)
|
2261
|
+
return True
|
2262
|
+
|
2263
|
+
return False
|
2264
|
+
|
2265
|
+
def _determine(self, text: str, pos: int) -> tuple[int, str]:
|
2266
|
+
"""Determine what to replace, if anything."""
|
2267
|
+
t1 = text[pos-1:pos]
|
2268
|
+
t2 = text[pos-2:pos]
|
2269
|
+
t3 = text[pos-3:pos]
|
2270
|
+
t4 = text[pos-4:pos]
|
2271
|
+
if t1 == "":
|
2272
|
+
# Return early if there is nothing to check
|
2273
|
+
return 0, ""
|
2274
|
+
|
2275
|
+
leading = t2[:1].isspace()
|
2276
|
+
if self._replaceDQuote:
|
2277
|
+
if leading and t2.endswith('"'):
|
2278
|
+
return 1, self._quoteDO
|
2279
|
+
elif t1 == '"':
|
2280
|
+
if pos == 1:
|
2281
|
+
return 1, self._quoteDO
|
2282
|
+
elif pos == 2 and t2 == '>"':
|
2283
|
+
return 1, self._quoteDO
|
2284
|
+
elif pos == 3 and t3 == '>>"':
|
2285
|
+
return 1, self._quoteDO
|
2286
|
+
else:
|
2287
|
+
return 1, self._quoteDC
|
2288
|
+
|
2289
|
+
if self._replaceSQuote:
|
2290
|
+
if leading and t2.endswith("'"):
|
2291
|
+
return 1, self._quoteSO
|
2292
|
+
elif t1 == "'":
|
2293
|
+
if pos == 1:
|
2294
|
+
return 1, self._quoteSO
|
2295
|
+
elif pos == 2 and t2 == ">'":
|
2296
|
+
return 1, self._quoteSO
|
2297
|
+
elif pos == 3 and t3 == ">>'":
|
2298
|
+
return 1, self._quoteSO
|
2299
|
+
else:
|
2300
|
+
return 1, self._quoteSC
|
2301
|
+
|
2302
|
+
if self._replaceDash:
|
2303
|
+
if t4 == "----":
|
2304
|
+
return 4, "\u2015" # Horizontal bar
|
2305
|
+
elif t3 == "---":
|
2306
|
+
return 3, "\u2014" # Long dash
|
2307
|
+
elif t2 == "--":
|
2308
|
+
return 2, "\u2013" # Short dash
|
2309
|
+
elif t2 == "\u2013-":
|
2310
|
+
return 2, "\u2014" # Long dash
|
2311
|
+
elif t2 == "\u2014-":
|
2312
|
+
return 2, "\u2015" # Horizontal bar
|
2313
|
+
|
2314
|
+
if self._replaceDots and t3 == "...":
|
2315
|
+
return 3, "\u2026" # Ellipsis
|
2316
|
+
|
2317
|
+
if t1 == "\u2028": # Line separator
|
2318
|
+
# This resolves issue #1150
|
2319
|
+
return 1, "\u2029" # Paragraph separator
|
2320
|
+
|
2321
|
+
return 0, t1
|
2322
|
+
|
2323
|
+
|
2262
2324
|
class GuiDocToolBar(QWidget):
|
2263
2325
|
"""The Formatting and Options Fold Out Menu
|
2264
2326
|
|
@@ -2283,61 +2345,61 @@ class GuiDocToolBar(QWidget):
|
|
2283
2345
|
self.tbBoldMD = NIconToolButton(self, iSz)
|
2284
2346
|
self.tbBoldMD.setToolTip(self.tr("Markdown Bold"))
|
2285
2347
|
self.tbBoldMD.clicked.connect(
|
2286
|
-
|
2348
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.MD_BOLD)
|
2287
2349
|
)
|
2288
2350
|
|
2289
2351
|
self.tbItalicMD = NIconToolButton(self, iSz)
|
2290
2352
|
self.tbItalicMD.setToolTip(self.tr("Markdown Italic"))
|
2291
2353
|
self.tbItalicMD.clicked.connect(
|
2292
|
-
|
2354
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.MD_ITALIC)
|
2293
2355
|
)
|
2294
2356
|
|
2295
2357
|
self.tbStrikeMD = NIconToolButton(self, iSz)
|
2296
2358
|
self.tbStrikeMD.setToolTip(self.tr("Markdown Strikethrough"))
|
2297
2359
|
self.tbStrikeMD.clicked.connect(
|
2298
|
-
|
2360
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE)
|
2299
2361
|
)
|
2300
2362
|
|
2301
2363
|
self.tbBold = NIconToolButton(self, iSz)
|
2302
2364
|
self.tbBold.setToolTip(self.tr("Shortcode Bold"))
|
2303
2365
|
self.tbBold.clicked.connect(
|
2304
|
-
|
2366
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_BOLD)
|
2305
2367
|
)
|
2306
2368
|
|
2307
2369
|
self.tbItalic = NIconToolButton(self, iSz)
|
2308
2370
|
self.tbItalic.setToolTip(self.tr("Shortcode Italic"))
|
2309
2371
|
self.tbItalic.clicked.connect(
|
2310
|
-
|
2372
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_ITALIC)
|
2311
2373
|
)
|
2312
2374
|
|
2313
2375
|
self.tbStrike = NIconToolButton(self, iSz)
|
2314
2376
|
self.tbStrike.setToolTip(self.tr("Shortcode Strikethrough"))
|
2315
2377
|
self.tbStrike.clicked.connect(
|
2316
|
-
|
2378
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_STRIKE)
|
2317
2379
|
)
|
2318
2380
|
|
2319
2381
|
self.tbUnderline = NIconToolButton(self, iSz)
|
2320
2382
|
self.tbUnderline.setToolTip(self.tr("Shortcode Underline"))
|
2321
2383
|
self.tbUnderline.clicked.connect(
|
2322
|
-
|
2384
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_ULINE)
|
2323
2385
|
)
|
2324
2386
|
|
2325
2387
|
self.tbMark = NIconToolButton(self, iSz)
|
2326
2388
|
self.tbMark.setToolTip(self.tr("Shortcode Highlight"))
|
2327
2389
|
self.tbMark.clicked.connect(
|
2328
|
-
|
2390
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_MARK)
|
2329
2391
|
)
|
2330
2392
|
|
2331
2393
|
self.tbSuperscript = NIconToolButton(self, iSz)
|
2332
2394
|
self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript"))
|
2333
2395
|
self.tbSuperscript.clicked.connect(
|
2334
|
-
|
2396
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUP)
|
2335
2397
|
)
|
2336
2398
|
|
2337
2399
|
self.tbSubscript = NIconToolButton(self, iSz)
|
2338
2400
|
self.tbSubscript.setToolTip(self.tr("Shortcode Subscript"))
|
2339
2401
|
self.tbSubscript.clicked.connect(
|
2340
|
-
|
2402
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUB)
|
2341
2403
|
)
|
2342
2404
|
|
2343
2405
|
# Assemble
|
@@ -2801,7 +2863,7 @@ class GuiDocEditHeader(QWidget):
|
|
2801
2863
|
self.tbButton = NIconToolButton(self, iSz)
|
2802
2864
|
self.tbButton.setVisible(False)
|
2803
2865
|
self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
|
2804
|
-
self.tbButton.clicked.connect(
|
2866
|
+
self.tbButton.clicked.connect(qtLambda(self.toggleToolBarRequest.emit))
|
2805
2867
|
|
2806
2868
|
self.outlineButton = NIconToolButton(self, iSz)
|
2807
2869
|
self.outlineButton.setVisible(False)
|
@@ -2816,7 +2878,7 @@ class GuiDocEditHeader(QWidget):
|
|
2816
2878
|
self.minmaxButton = NIconToolButton(self, iSz)
|
2817
2879
|
self.minmaxButton.setVisible(False)
|
2818
2880
|
self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
|
2819
|
-
self.minmaxButton.clicked.connect(
|
2881
|
+
self.minmaxButton.clicked.connect(qtLambda(self.docEditor.toggleFocusModeRequest.emit))
|
2820
2882
|
|
2821
2883
|
self.closeButton = NIconToolButton(self, iSz)
|
2822
2884
|
self.closeButton.setVisible(False)
|
@@ -2879,9 +2941,7 @@ class GuiDocEditHeader(QWidget):
|
|
2879
2941
|
self.outlineMenu.clear()
|
2880
2942
|
for number, text in data.items():
|
2881
2943
|
action = self.outlineMenu.addAction(text)
|
2882
|
-
action.triggered.connect(
|
2883
|
-
lambda _, number=number: self._gotoBlock(number)
|
2884
|
-
)
|
2944
|
+
action.triggered.connect(qtLambda(self._gotoBlock, number))
|
2885
2945
|
self._docOutline = data
|
2886
2946
|
logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
|
2887
2947
|
return
|
@@ -2894,7 +2954,7 @@ class GuiDocEditHeader(QWidget):
|
|
2894
2954
|
|
2895
2955
|
def updateTheme(self) -> None:
|
2896
2956
|
"""Update theme elements."""
|
2897
|
-
self.tbButton.setThemeIcon("
|
2957
|
+
self.tbButton.setThemeIcon("toolbar")
|
2898
2958
|
self.outlineButton.setThemeIcon("list")
|
2899
2959
|
self.searchButton.setThemeIcon("search")
|
2900
2960
|
self.minmaxButton.setThemeIcon("maximise")
|
@@ -2938,7 +2998,7 @@ class GuiDocEditHeader(QWidget):
|
|
2938
2998
|
|
2939
2999
|
if CONFIG.showFullPath:
|
2940
3000
|
self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
|
2941
|
-
[name for name in SHARED.project.tree.
|
3001
|
+
[name for name in SHARED.project.tree.itemPath(tHandle, asName=True)]
|
2942
3002
|
)))
|
2943
3003
|
else:
|
2944
3004
|
self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
|