novelWriter 2.5.3__py3-none-any.whl → 2.6b2__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.6b2.dist-info}/METADATA +1 -1
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
- novelwriter/__init__.py +49 -10
- novelwriter/assets/i18n/project_en_GB.json +1 -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 +100 -2
- novelwriter/config.py +25 -15
- novelwriter/constants.py +168 -60
- novelwriter/core/buildsettings.py +66 -39
- novelwriter/core/coretools.py +145 -147
- novelwriter/core/docbuild.py +132 -170
- novelwriter/core/index.py +38 -37
- novelwriter/core/item.py +41 -8
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/options.py +4 -1
- novelwriter/core/project.py +67 -89
- novelwriter/core/spellcheck.py +9 -14
- novelwriter/core/status.py +7 -5
- novelwriter/core/tree.py +268 -287
- novelwriter/dialogs/docmerge.py +7 -17
- novelwriter/dialogs/preferences.py +46 -33
- novelwriter/dialogs/projectsettings.py +5 -5
- novelwriter/enum.py +36 -23
- novelwriter/extensions/configlayout.py +27 -12
- novelwriter/extensions/modified.py +13 -1
- novelwriter/extensions/pagedsidebar.py +5 -5
- novelwriter/formats/shared.py +155 -0
- novelwriter/formats/todocx.py +1191 -0
- novelwriter/formats/tohtml.py +451 -0
- novelwriter/{core → formats}/tokenizer.py +487 -491
- novelwriter/formats/tomarkdown.py +217 -0
- novelwriter/{core → formats}/toodt.py +311 -432
- novelwriter/formats/toqdoc.py +484 -0
- novelwriter/formats/toraw.py +91 -0
- novelwriter/gui/doceditor.py +342 -284
- novelwriter/gui/dochighlight.py +96 -84
- novelwriter/gui/docviewer.py +88 -31
- novelwriter/gui/docviewerpanel.py +17 -25
- novelwriter/gui/editordocument.py +17 -2
- novelwriter/gui/itemdetails.py +25 -28
- novelwriter/gui/mainmenu.py +129 -63
- novelwriter/gui/noveltree.py +45 -47
- novelwriter/gui/outline.py +196 -249
- novelwriter/gui/projtree.py +594 -1241
- novelwriter/gui/search.py +9 -10
- novelwriter/gui/sidebar.py +7 -6
- novelwriter/gui/theme.py +10 -5
- novelwriter/guimain.py +100 -196
- novelwriter/shared.py +66 -27
- novelwriter/text/counting.py +2 -0
- novelwriter/text/patterns.py +168 -60
- novelwriter/tools/manusbuild.py +14 -12
- novelwriter/tools/manuscript.py +120 -78
- novelwriter/tools/manussettings.py +424 -291
- novelwriter/tools/welcome.py +4 -4
- novelwriter/tools/writingstats.py +3 -3
- novelwriter/types.py +23 -7
- 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.6b2.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
novelwriter/gui/doceditor.py
CHANGED
@@ -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()
|
@@ -1072,7 +1105,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1072
1105
|
self._completer.setVisible(False)
|
1073
1106
|
|
1074
1107
|
if self._doReplace and added == 1:
|
1075
|
-
self.
|
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,16 +1204,16 @@ 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))
|
@@ -1173,30 +1221,6 @@ class GuiDocEditor(QPlainTextEdit):
|
|
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,13 @@ 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
|
+
self.docFooter.updateWordCount(wCount, False)
|
1231
1257
|
return
|
1232
1258
|
|
1233
1259
|
@pyqtSlot()
|
@@ -1236,10 +1262,10 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1236
1262
|
information to the footer, and start the selection word counter.
|
1237
1263
|
"""
|
1238
1264
|
if self.textCursor().hasSelection():
|
1239
|
-
if not self.
|
1240
|
-
self.
|
1265
|
+
if not self._timerSel.isActive():
|
1266
|
+
self._timerSel.start()
|
1241
1267
|
else:
|
1242
|
-
self.
|
1268
|
+
self._timerSel.stop()
|
1243
1269
|
self.docFooter.updateWordCount(0, False)
|
1244
1270
|
return
|
1245
1271
|
|
@@ -1249,11 +1275,11 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1249
1275
|
if self._docHandle is None:
|
1250
1276
|
return
|
1251
1277
|
|
1252
|
-
if self.
|
1278
|
+
if self._wCounterSel.isRunning():
|
1253
1279
|
logger.debug("Selection word counter is busy")
|
1254
1280
|
return
|
1255
1281
|
|
1256
|
-
SHARED.runInThreadPool(self.
|
1282
|
+
SHARED.runInThreadPool(self._wCounterSel)
|
1257
1283
|
|
1258
1284
|
return
|
1259
1285
|
|
@@ -1263,13 +1289,13 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1263
1289
|
if self._docHandle and self._nwItem:
|
1264
1290
|
logger.debug("User selected %d words", wCount)
|
1265
1291
|
self.docFooter.updateWordCount(wCount, True)
|
1266
|
-
self.
|
1292
|
+
self._timerSel.stop()
|
1267
1293
|
return
|
1268
1294
|
|
1269
1295
|
@pyqtSlot()
|
1270
1296
|
def _closeCurrentDocument(self) -> None:
|
1271
1297
|
"""Close the document. Forwarded to the main Gui."""
|
1272
|
-
self.
|
1298
|
+
self.closeEditorRequest.emit()
|
1273
1299
|
self.docToolBar.setVisible(False)
|
1274
1300
|
return
|
1275
1301
|
|
@@ -1875,6 +1901,28 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1875
1901
|
# Internal Functions
|
1876
1902
|
##
|
1877
1903
|
|
1904
|
+
def _correctWord(self, cursor: QTextCursor, word: str) -> None:
|
1905
|
+
"""Slot for the spell check context menu triggering the
|
1906
|
+
replacement of a word with the word from the dictionary.
|
1907
|
+
"""
|
1908
|
+
pos = cursor.selectionStart()
|
1909
|
+
cursor.beginEditBlock()
|
1910
|
+
cursor.removeSelectedText()
|
1911
|
+
cursor.insertText(word)
|
1912
|
+
cursor.endEditBlock()
|
1913
|
+
cursor.setPosition(pos)
|
1914
|
+
self.setTextCursor(cursor)
|
1915
|
+
return
|
1916
|
+
|
1917
|
+
def _addWord(self, word: str, block: QTextBlock, save: bool) -> None:
|
1918
|
+
"""Slot for the spell check context menu triggered when the user
|
1919
|
+
wants to add a word to the project dictionary.
|
1920
|
+
"""
|
1921
|
+
logger.debug("Added '%s' to project dictionary, %s", word, "saved" if save else "unsaved")
|
1922
|
+
SHARED.spelling.addWord(word, save=save)
|
1923
|
+
self._qDocument.syntaxHighlighter.rehighlightBlock(block)
|
1924
|
+
return
|
1925
|
+
|
1878
1926
|
def _processTag(self, cursor: QTextCursor | None = None,
|
1879
1927
|
follow: bool = True, create: bool = False) -> nwTrinary:
|
1880
1928
|
"""Activated by Ctrl+Enter. Checks that we're in a block
|
@@ -1938,119 +1986,6 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1938
1986
|
self.requestProjectItemRenamed.emit(self._docHandle, text)
|
1939
1987
|
return
|
1940
1988
|
|
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
1989
|
def _autoSelect(self) -> QTextCursor:
|
2055
1990
|
"""Return a cursor which may or may not have a selection based
|
2056
1991
|
on user settings and document action. The selection will be the
|
@@ -2173,7 +2108,7 @@ class MetaCompleter(QMenu):
|
|
2173
2108
|
suffix = ""
|
2174
2109
|
options = list(filter(
|
2175
2110
|
lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
|
2176
|
-
nwKeyWords.KEY_CLASS.get(kw.strip()
|
2111
|
+
nwKeyWords.KEY_CLASS.get(kw.strip())
|
2177
2112
|
)
|
2178
2113
|
))[:15]
|
2179
2114
|
|
@@ -2183,7 +2118,7 @@ class MetaCompleter(QMenu):
|
|
2183
2118
|
for value in sorted(options):
|
2184
2119
|
rep = value + suffix
|
2185
2120
|
action = self.addAction(value)
|
2186
|
-
action.triggered.connect(
|
2121
|
+
action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
|
2187
2122
|
|
2188
2123
|
return True
|
2189
2124
|
|
@@ -2259,6 +2194,131 @@ class BackgroundWordCounterSignals(QObject):
|
|
2259
2194
|
countsReady = pyqtSignal(int, int, int)
|
2260
2195
|
|
2261
2196
|
|
2197
|
+
class TextAutoReplace:
|
2198
|
+
|
2199
|
+
__slots__ = (
|
2200
|
+
"_quoteSO", "_quoteSC", "_quoteDO", "_quoteDC",
|
2201
|
+
"_replaceSQuote", "_replaceDQuote", "_replaceDash", "_replaceDots",
|
2202
|
+
"_padChar", "_padBefore", "_padAfter", "_doPadBefore", "_doPadAfter",
|
2203
|
+
)
|
2204
|
+
|
2205
|
+
def __init__(self) -> None:
|
2206
|
+
self.initSettings()
|
2207
|
+
return
|
2208
|
+
|
2209
|
+
def initSettings(self) -> None:
|
2210
|
+
"""Initialise the auto-replace settings from config."""
|
2211
|
+
self._quoteSO = CONFIG.fmtSQuoteOpen
|
2212
|
+
self._quoteSC = CONFIG.fmtSQuoteClose
|
2213
|
+
self._quoteDO = CONFIG.fmtDQuoteOpen
|
2214
|
+
self._quoteDC = CONFIG.fmtDQuoteClose
|
2215
|
+
|
2216
|
+
self._replaceSQuote = CONFIG.doReplaceSQuote
|
2217
|
+
self._replaceDQuote = CONFIG.doReplaceDQuote
|
2218
|
+
self._replaceDash = CONFIG.doReplaceDash
|
2219
|
+
self._replaceDots = CONFIG.doReplaceDots
|
2220
|
+
|
2221
|
+
self._padChar = nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP
|
2222
|
+
self._padBefore = CONFIG.fmtPadBefore
|
2223
|
+
self._padAfter = CONFIG.fmtPadAfter
|
2224
|
+
self._doPadBefore = bool(CONFIG.fmtPadBefore)
|
2225
|
+
self._doPadAfter = bool(CONFIG.fmtPadAfter)
|
2226
|
+
return
|
2227
|
+
|
2228
|
+
def process(self, text: str, cursor: QTextCursor) -> bool:
|
2229
|
+
"""Auto-replace text elements based on main configuration.
|
2230
|
+
Returns True if anything was changed.
|
2231
|
+
"""
|
2232
|
+
pos = cursor.positionInBlock()
|
2233
|
+
length = len(text)
|
2234
|
+
if length < 1 or pos-1 > length:
|
2235
|
+
return False
|
2236
|
+
|
2237
|
+
delete, insert = self._determine(text, pos)
|
2238
|
+
if insert == "":
|
2239
|
+
return False
|
2240
|
+
|
2241
|
+
check = insert
|
2242
|
+
if self._doPadBefore and check in self._padBefore:
|
2243
|
+
if not (check == ":" and length > 1 and text[0] == "@"):
|
2244
|
+
delete = max(delete, 1)
|
2245
|
+
chkPos = pos - delete - 1
|
2246
|
+
if chkPos >= 0 and text[chkPos].isspace():
|
2247
|
+
# Strip existing space before inserting a new (#1061)
|
2248
|
+
delete += 1
|
2249
|
+
insert = self._padChar + insert
|
2250
|
+
|
2251
|
+
if self._doPadAfter and check in self._padAfter:
|
2252
|
+
if not (check == ":" and length > 1 and text[0] == "@"):
|
2253
|
+
delete = max(delete, 1)
|
2254
|
+
insert = insert + self._padChar
|
2255
|
+
|
2256
|
+
if delete > 0:
|
2257
|
+
cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
|
2258
|
+
cursor.insertText(insert)
|
2259
|
+
return True
|
2260
|
+
|
2261
|
+
return False
|
2262
|
+
|
2263
|
+
def _determine(self, text: str, pos: int) -> tuple[int, str]:
|
2264
|
+
"""Determine what to replace, if anything."""
|
2265
|
+
t1 = text[pos-1:pos]
|
2266
|
+
t2 = text[pos-2:pos]
|
2267
|
+
t3 = text[pos-3:pos]
|
2268
|
+
t4 = text[pos-4:pos]
|
2269
|
+
if t1 == "":
|
2270
|
+
# Return early if there is nothing to check
|
2271
|
+
return 0, ""
|
2272
|
+
|
2273
|
+
leading = t2[:1].isspace()
|
2274
|
+
if self._replaceDQuote:
|
2275
|
+
if leading and t2.endswith('"'):
|
2276
|
+
return 1, self._quoteDO
|
2277
|
+
elif t1 == '"':
|
2278
|
+
if pos == 1:
|
2279
|
+
return 1, self._quoteDO
|
2280
|
+
elif pos == 2 and t2 == '>"':
|
2281
|
+
return 1, self._quoteDO
|
2282
|
+
elif pos == 3 and t3 == '>>"':
|
2283
|
+
return 1, self._quoteDO
|
2284
|
+
else:
|
2285
|
+
return 1, self._quoteDC
|
2286
|
+
|
2287
|
+
if self._replaceSQuote:
|
2288
|
+
if leading and t2.endswith("'"):
|
2289
|
+
return 1, self._quoteSO
|
2290
|
+
elif t1 == "'":
|
2291
|
+
if pos == 1:
|
2292
|
+
return 1, self._quoteSO
|
2293
|
+
elif pos == 2 and t2 == ">'":
|
2294
|
+
return 1, self._quoteSO
|
2295
|
+
elif pos == 3 and t3 == ">>'":
|
2296
|
+
return 1, self._quoteSO
|
2297
|
+
else:
|
2298
|
+
return 1, self._quoteSC
|
2299
|
+
|
2300
|
+
if self._replaceDash:
|
2301
|
+
if t4 == "----":
|
2302
|
+
return 4, "\u2015" # Horizontal bar
|
2303
|
+
elif t3 == "---":
|
2304
|
+
return 3, "\u2014" # Long dash
|
2305
|
+
elif t2 == "--":
|
2306
|
+
return 2, "\u2013" # Short dash
|
2307
|
+
elif t2 == "\u2013-":
|
2308
|
+
return 2, "\u2014" # Long dash
|
2309
|
+
elif t2 == "\u2014-":
|
2310
|
+
return 2, "\u2015" # Horizontal bar
|
2311
|
+
|
2312
|
+
if self._replaceDots and t3 == "...":
|
2313
|
+
return 3, "\u2026" # Ellipsis
|
2314
|
+
|
2315
|
+
if t1 == "\u2028": # Line separator
|
2316
|
+
# This resolves issue #1150
|
2317
|
+
return 1, "\u2029" # Paragraph separator
|
2318
|
+
|
2319
|
+
return 0, t1
|
2320
|
+
|
2321
|
+
|
2262
2322
|
class GuiDocToolBar(QWidget):
|
2263
2323
|
"""The Formatting and Options Fold Out Menu
|
2264
2324
|
|
@@ -2283,61 +2343,61 @@ class GuiDocToolBar(QWidget):
|
|
2283
2343
|
self.tbBoldMD = NIconToolButton(self, iSz)
|
2284
2344
|
self.tbBoldMD.setToolTip(self.tr("Markdown Bold"))
|
2285
2345
|
self.tbBoldMD.clicked.connect(
|
2286
|
-
|
2346
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.MD_BOLD)
|
2287
2347
|
)
|
2288
2348
|
|
2289
2349
|
self.tbItalicMD = NIconToolButton(self, iSz)
|
2290
2350
|
self.tbItalicMD.setToolTip(self.tr("Markdown Italic"))
|
2291
2351
|
self.tbItalicMD.clicked.connect(
|
2292
|
-
|
2352
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.MD_ITALIC)
|
2293
2353
|
)
|
2294
2354
|
|
2295
2355
|
self.tbStrikeMD = NIconToolButton(self, iSz)
|
2296
2356
|
self.tbStrikeMD.setToolTip(self.tr("Markdown Strikethrough"))
|
2297
2357
|
self.tbStrikeMD.clicked.connect(
|
2298
|
-
|
2358
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE)
|
2299
2359
|
)
|
2300
2360
|
|
2301
2361
|
self.tbBold = NIconToolButton(self, iSz)
|
2302
2362
|
self.tbBold.setToolTip(self.tr("Shortcode Bold"))
|
2303
2363
|
self.tbBold.clicked.connect(
|
2304
|
-
|
2364
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_BOLD)
|
2305
2365
|
)
|
2306
2366
|
|
2307
2367
|
self.tbItalic = NIconToolButton(self, iSz)
|
2308
2368
|
self.tbItalic.setToolTip(self.tr("Shortcode Italic"))
|
2309
2369
|
self.tbItalic.clicked.connect(
|
2310
|
-
|
2370
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_ITALIC)
|
2311
2371
|
)
|
2312
2372
|
|
2313
2373
|
self.tbStrike = NIconToolButton(self, iSz)
|
2314
2374
|
self.tbStrike.setToolTip(self.tr("Shortcode Strikethrough"))
|
2315
2375
|
self.tbStrike.clicked.connect(
|
2316
|
-
|
2376
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_STRIKE)
|
2317
2377
|
)
|
2318
2378
|
|
2319
2379
|
self.tbUnderline = NIconToolButton(self, iSz)
|
2320
2380
|
self.tbUnderline.setToolTip(self.tr("Shortcode Underline"))
|
2321
2381
|
self.tbUnderline.clicked.connect(
|
2322
|
-
|
2382
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_ULINE)
|
2323
2383
|
)
|
2324
2384
|
|
2325
2385
|
self.tbMark = NIconToolButton(self, iSz)
|
2326
2386
|
self.tbMark.setToolTip(self.tr("Shortcode Highlight"))
|
2327
2387
|
self.tbMark.clicked.connect(
|
2328
|
-
|
2388
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_MARK)
|
2329
2389
|
)
|
2330
2390
|
|
2331
2391
|
self.tbSuperscript = NIconToolButton(self, iSz)
|
2332
2392
|
self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript"))
|
2333
2393
|
self.tbSuperscript.clicked.connect(
|
2334
|
-
|
2394
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUP)
|
2335
2395
|
)
|
2336
2396
|
|
2337
2397
|
self.tbSubscript = NIconToolButton(self, iSz)
|
2338
2398
|
self.tbSubscript.setToolTip(self.tr("Shortcode Subscript"))
|
2339
2399
|
self.tbSubscript.clicked.connect(
|
2340
|
-
|
2400
|
+
qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUB)
|
2341
2401
|
)
|
2342
2402
|
|
2343
2403
|
# Assemble
|
@@ -2801,7 +2861,7 @@ class GuiDocEditHeader(QWidget):
|
|
2801
2861
|
self.tbButton = NIconToolButton(self, iSz)
|
2802
2862
|
self.tbButton.setVisible(False)
|
2803
2863
|
self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
|
2804
|
-
self.tbButton.clicked.connect(
|
2864
|
+
self.tbButton.clicked.connect(qtLambda(self.toggleToolBarRequest.emit))
|
2805
2865
|
|
2806
2866
|
self.outlineButton = NIconToolButton(self, iSz)
|
2807
2867
|
self.outlineButton.setVisible(False)
|
@@ -2816,7 +2876,7 @@ class GuiDocEditHeader(QWidget):
|
|
2816
2876
|
self.minmaxButton = NIconToolButton(self, iSz)
|
2817
2877
|
self.minmaxButton.setVisible(False)
|
2818
2878
|
self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
|
2819
|
-
self.minmaxButton.clicked.connect(
|
2879
|
+
self.minmaxButton.clicked.connect(qtLambda(self.docEditor.toggleFocusModeRequest.emit))
|
2820
2880
|
|
2821
2881
|
self.closeButton = NIconToolButton(self, iSz)
|
2822
2882
|
self.closeButton.setVisible(False)
|
@@ -2879,9 +2939,7 @@ class GuiDocEditHeader(QWidget):
|
|
2879
2939
|
self.outlineMenu.clear()
|
2880
2940
|
for number, text in data.items():
|
2881
2941
|
action = self.outlineMenu.addAction(text)
|
2882
|
-
action.triggered.connect(
|
2883
|
-
lambda _, number=number: self._gotoBlock(number)
|
2884
|
-
)
|
2942
|
+
action.triggered.connect(qtLambda(self._gotoBlock, number))
|
2885
2943
|
self._docOutline = data
|
2886
2944
|
logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
|
2887
2945
|
return
|
@@ -2894,7 +2952,7 @@ class GuiDocEditHeader(QWidget):
|
|
2894
2952
|
|
2895
2953
|
def updateTheme(self) -> None:
|
2896
2954
|
"""Update theme elements."""
|
2897
|
-
self.tbButton.setThemeIcon("
|
2955
|
+
self.tbButton.setThemeIcon("toolbar")
|
2898
2956
|
self.outlineButton.setThemeIcon("list")
|
2899
2957
|
self.searchButton.setThemeIcon("search")
|
2900
2958
|
self.minmaxButton.setThemeIcon("maximise")
|
@@ -2938,7 +2996,7 @@ class GuiDocEditHeader(QWidget):
|
|
2938
2996
|
|
2939
2997
|
if CONFIG.showFullPath:
|
2940
2998
|
self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
|
2941
|
-
[name for name in SHARED.project.tree.
|
2999
|
+
[name for name in SHARED.project.tree.itemPath(tHandle, asName=True)]
|
2942
3000
|
)))
|
2943
3001
|
else:
|
2944
3002
|
self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
|