novelWriter 2.6b1__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.6b1.dist-info → novelWriter-2.6b2.dist-info}/METADATA +3 -3
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/RECORD +68 -52
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +49 -10
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
- novelwriter/assets/i18n/project_de_DE.json +2 -2
- novelwriter/assets/i18n/project_ru_RU.json +11 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +7 -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 +7 -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/assets/text/credits_en.htm +1 -0
- novelwriter/common.py +37 -2
- novelwriter/config.py +15 -12
- novelwriter/constants.py +24 -9
- novelwriter/core/coretools.py +111 -125
- novelwriter/core/docbuild.py +3 -2
- novelwriter/core/index.py +9 -19
- novelwriter/core/item.py +39 -6
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/project.py +67 -89
- novelwriter/core/status.py +7 -5
- novelwriter/core/tree.py +268 -287
- novelwriter/dialogs/docmerge.py +7 -17
- novelwriter/dialogs/preferences.py +3 -3
- novelwriter/dialogs/projectsettings.py +2 -2
- novelwriter/enum.py +7 -0
- novelwriter/extensions/configlayout.py +6 -4
- novelwriter/formats/todocx.py +34 -38
- novelwriter/formats/tohtml.py +14 -15
- novelwriter/formats/tokenizer.py +21 -17
- novelwriter/formats/toodt.py +53 -124
- novelwriter/formats/toqdoc.py +92 -44
- novelwriter/gui/doceditor.py +230 -219
- novelwriter/gui/docviewer.py +38 -9
- novelwriter/gui/docviewerpanel.py +14 -22
- novelwriter/gui/itemdetails.py +17 -24
- novelwriter/gui/mainmenu.py +13 -8
- novelwriter/gui/noveltree.py +12 -12
- novelwriter/gui/outline.py +10 -11
- novelwriter/gui/projtree.py +548 -1202
- novelwriter/gui/search.py +9 -10
- novelwriter/gui/theme.py +7 -3
- novelwriter/guimain.py +59 -43
- novelwriter/shared.py +52 -23
- novelwriter/text/patterns.py +17 -5
- novelwriter/tools/manusbuild.py +13 -11
- novelwriter/tools/manussettings.py +42 -52
- novelwriter/types.py +7 -1
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
novelwriter/gui/doceditor.py
CHANGED
@@ -36,15 +36,15 @@ import logging
|
|
36
36
|
|
37
37
|
from enum import Enum
|
38
38
|
from time import time
|
39
|
-
from typing import NamedTuple
|
40
39
|
|
41
40
|
from PyQt5.QtCore import (
|
42
41
|
QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
|
43
42
|
pyqtSlot
|
44
43
|
)
|
45
44
|
from PyQt5.QtGui import (
|
46
|
-
QColor, QCursor,
|
47
|
-
|
45
|
+
QColor, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent,
|
46
|
+
QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, QTextBlock,
|
47
|
+
QTextCursor, QTextDocument, QTextOption
|
48
48
|
)
|
49
49
|
from PyQt5.QtWidgets import (
|
50
50
|
QAction, QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit,
|
@@ -52,12 +52,12 @@ from PyQt5.QtWidgets import (
|
|
52
52
|
)
|
53
53
|
|
54
54
|
from novelwriter import CONFIG, SHARED
|
55
|
-
from novelwriter.common import minmax, qtLambda, transferCase
|
55
|
+
from novelwriter.common import decodeMimeHandles, fontMatcher, minmax, qtLambda, transferCase
|
56
56
|
from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
|
57
57
|
from novelwriter.core.document import NWDocument
|
58
58
|
from novelwriter.enum import (
|
59
|
-
nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass,
|
60
|
-
nwTrinary
|
59
|
+
nwChange, nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass,
|
60
|
+
nwItemType, nwTrinary
|
61
61
|
)
|
62
62
|
from novelwriter.extensions.configlayout import NColourLabel
|
63
63
|
from novelwriter.extensions.eventfilters import WheelEventFilter
|
@@ -84,24 +84,16 @@ class _SelectAction(Enum):
|
|
84
84
|
MOVE_AFTER = 3
|
85
85
|
|
86
86
|
|
87
|
-
class AutoReplaceConfig(NamedTuple):
|
88
|
-
|
89
|
-
typPadChar: str
|
90
|
-
typSQuoteO: str
|
91
|
-
typSQuoteC: str
|
92
|
-
typDQuoteO: str
|
93
|
-
typDQuoteC: str
|
94
|
-
typRepDQuote: bool
|
95
|
-
typRepSQuote: bool
|
96
|
-
typRepDash: bool
|
97
|
-
typRepDots: bool
|
98
|
-
typPadBefore: str
|
99
|
-
typPadAfter: str
|
100
|
-
|
101
|
-
|
102
87
|
class GuiDocEditor(QPlainTextEdit):
|
103
88
|
"""Gui Widget: Main Document Editor"""
|
104
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
|
+
|
105
97
|
MOVE_KEYS = (
|
106
98
|
Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down,
|
107
99
|
Qt.Key.Key_PageUp, Qt.Key.Key_PageDown
|
@@ -109,13 +101,13 @@ class GuiDocEditor(QPlainTextEdit):
|
|
109
101
|
|
110
102
|
# Custom Signals
|
111
103
|
closeEditorRequest = pyqtSignal()
|
112
|
-
docCountsChanged = pyqtSignal(str, int, int, int)
|
113
104
|
docTextChanged = pyqtSignal(str, float)
|
114
105
|
editedStatusChanged = pyqtSignal(bool)
|
115
106
|
itemHandleChanged = pyqtSignal(str)
|
116
107
|
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
117
108
|
novelItemMetaChanged = pyqtSignal(str)
|
118
109
|
novelStructureChanged = pyqtSignal()
|
110
|
+
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
119
111
|
requestNewNoteCreation = pyqtSignal(str, nwItemClass)
|
120
112
|
requestNextDocument = pyqtSignal(str, bool)
|
121
113
|
requestProjectItemRenamed = pyqtSignal(str, str)
|
@@ -143,20 +135,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
143
135
|
self._lastFind = None # Position of the last found search word
|
144
136
|
self._doReplace = False # Switch to temporarily disable auto-replace
|
145
137
|
|
146
|
-
#
|
147
|
-
self.
|
148
|
-
typPadChar=" ",
|
149
|
-
typSQuoteO="'",
|
150
|
-
typSQuoteC="'",
|
151
|
-
typDQuoteO='"',
|
152
|
-
typDQuoteC='"',
|
153
|
-
typRepSQuote=False,
|
154
|
-
typRepDQuote=False,
|
155
|
-
typRepDash=False,
|
156
|
-
typRepDots=False,
|
157
|
-
typPadBefore="",
|
158
|
-
typPadAfter="",
|
159
|
-
)
|
138
|
+
# Auto-Replace
|
139
|
+
self._autoReplace = TextAutoReplace()
|
160
140
|
|
161
141
|
# Completer
|
162
142
|
self._completer = MetaCompleter(self)
|
@@ -191,40 +171,41 @@ class GuiDocEditor(QPlainTextEdit):
|
|
191
171
|
self.setMinimumWidth(CONFIG.pxInt(300))
|
192
172
|
self.setAutoFillBackground(True)
|
193
173
|
self.setFrameStyle(QFrame.Shape.NoFrame)
|
174
|
+
self.setAcceptDrops(True)
|
194
175
|
|
195
176
|
# Custom Shortcuts
|
196
|
-
self.
|
197
|
-
self.
|
198
|
-
self.
|
199
|
-
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)
|
200
181
|
|
201
|
-
self.
|
202
|
-
self.
|
203
|
-
self.
|
204
|
-
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)
|
205
186
|
|
206
|
-
self.
|
207
|
-
self.
|
208
|
-
self.
|
209
|
-
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)
|
210
191
|
|
211
192
|
# Set Up Document Word Counter
|
212
|
-
self.
|
213
|
-
self.
|
214
|
-
self.
|
193
|
+
self._timerDoc = QTimer(self)
|
194
|
+
self._timerDoc.timeout.connect(self._runDocumentTasks)
|
195
|
+
self._timerDoc.setInterval(5000)
|
215
196
|
|
216
|
-
self.
|
217
|
-
self.
|
218
|
-
self.
|
197
|
+
self._wCounterDoc = BackgroundWordCounter(self)
|
198
|
+
self._wCounterDoc.setAutoDelete(False)
|
199
|
+
self._wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
|
219
200
|
|
220
201
|
# Set Up Selection Word Counter
|
221
|
-
self.
|
222
|
-
self.
|
223
|
-
self.
|
202
|
+
self._timerSel = QTimer(self)
|
203
|
+
self._timerSel.timeout.connect(self._runSelCounter)
|
204
|
+
self._timerSel.setInterval(500)
|
224
205
|
|
225
|
-
self.
|
226
|
-
self.
|
227
|
-
self.
|
206
|
+
self._wCounterSel = BackgroundWordCounter(self, forSelection=True)
|
207
|
+
self._wCounterSel.setAutoDelete(False)
|
208
|
+
self._wCounterSel.signals.countsReady.connect(self._updateSelCounts)
|
228
209
|
|
229
210
|
# Install Event Filter for Mouse Wheel
|
230
211
|
self.wheelEventFilter = WheelEventFilter(self)
|
@@ -278,8 +259,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
278
259
|
self._nwDocument = None
|
279
260
|
self.setReadOnly(True)
|
280
261
|
self.clear()
|
281
|
-
self.
|
282
|
-
self.
|
262
|
+
self._timerDoc.stop()
|
263
|
+
self._timerSel.stop()
|
283
264
|
|
284
265
|
self._docHandle = None
|
285
266
|
self._lastEdit = 0.0
|
@@ -327,26 +308,16 @@ class GuiDocEditor(QPlainTextEdit):
|
|
327
308
|
settings. This function is both called when the editor is
|
328
309
|
created, and when the user changes the main editor preferences.
|
329
310
|
"""
|
330
|
-
#
|
331
|
-
self.
|
332
|
-
typPadChar=nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP,
|
333
|
-
typSQuoteO=CONFIG.fmtSQuoteOpen,
|
334
|
-
typSQuoteC=CONFIG.fmtSQuoteClose,
|
335
|
-
typDQuoteO=CONFIG.fmtDQuoteOpen,
|
336
|
-
typDQuoteC=CONFIG.fmtDQuoteClose,
|
337
|
-
typRepSQuote=CONFIG.doReplaceSQuote,
|
338
|
-
typRepDQuote=CONFIG.doReplaceDQuote,
|
339
|
-
typRepDash=CONFIG.doReplaceDash,
|
340
|
-
typRepDots=CONFIG.doReplaceDots,
|
341
|
-
typPadBefore=CONFIG.fmtPadBefore,
|
342
|
-
typPadAfter=CONFIG.fmtPadAfter,
|
343
|
-
)
|
311
|
+
# Auto-Replace
|
312
|
+
self._autoReplace.initSettings()
|
344
313
|
|
345
314
|
# Reload spell check and dictionaries
|
346
315
|
SHARED.updateSpellCheckLanguage()
|
347
316
|
|
348
317
|
# Set the font. See issues #1862 and #1875.
|
349
|
-
|
318
|
+
font = fontMatcher(CONFIG.textFont)
|
319
|
+
self.setFont(font)
|
320
|
+
self._qDocument.setDefaultFont(font)
|
350
321
|
self.docHeader.updateFont()
|
351
322
|
self.docFooter.updateFont()
|
352
323
|
self.docSearch.updateFont()
|
@@ -431,7 +402,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|
431
402
|
self._lastEdit = time()
|
432
403
|
self._lastActive = time()
|
433
404
|
self._runDocumentTasks()
|
434
|
-
self.
|
405
|
+
self._timerDoc.start()
|
435
406
|
|
436
407
|
self.setReadOnly(False)
|
437
408
|
self.updateDocMargins()
|
@@ -753,7 +724,6 @@ class GuiDocEditor(QPlainTextEdit):
|
|
753
724
|
|
754
725
|
logger.debug("Requesting action: %s", action.name)
|
755
726
|
|
756
|
-
tConf = self._typConf
|
757
727
|
self._allowAutoReplace(False)
|
758
728
|
if action == nwDocAction.UNDO:
|
759
729
|
self.undo()
|
@@ -772,9 +742,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|
772
742
|
elif action == nwDocAction.MD_STRIKE:
|
773
743
|
self._toggleFormat(2, "~")
|
774
744
|
elif action == nwDocAction.S_QUOTE:
|
775
|
-
self._wrapSelection(
|
745
|
+
self._wrapSelection(CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
|
776
746
|
elif action == nwDocAction.D_QUOTE:
|
777
|
-
self._wrapSelection(
|
747
|
+
self._wrapSelection(CONFIG.fmtDQuoteOpen, CONFIG.fmtDQuoteClose)
|
778
748
|
elif action == nwDocAction.SEL_ALL:
|
779
749
|
self._makeSelection(QTextCursor.SelectionType.Document)
|
780
750
|
elif action == nwDocAction.SEL_PARA:
|
@@ -800,9 +770,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|
800
770
|
elif action == nwDocAction.BLOCK_HSC:
|
801
771
|
self._formatBlock(nwDocAction.BLOCK_HSC)
|
802
772
|
elif action == nwDocAction.REPL_SNG:
|
803
|
-
self._replaceQuotes("'",
|
773
|
+
self._replaceQuotes("'", CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
|
804
774
|
elif action == nwDocAction.REPL_DBL:
|
805
|
-
self._replaceQuotes("\"",
|
775
|
+
self._replaceQuotes("\"", CONFIG.fmtDQuoteOpen, CONFIG.fmtDQuoteClose)
|
806
776
|
elif action == nwDocAction.RM_BREAKS:
|
807
777
|
self._removeInParLineBreaks()
|
808
778
|
elif action == nwDocAction.ALIGN_L:
|
@@ -874,13 +844,13 @@ class GuiDocEditor(QPlainTextEdit):
|
|
874
844
|
text = insert
|
875
845
|
elif isinstance(insert, nwDocInsert):
|
876
846
|
if insert == nwDocInsert.QUOTE_LS:
|
877
|
-
text =
|
847
|
+
text = CONFIG.fmtSQuoteOpen
|
878
848
|
elif insert == nwDocInsert.QUOTE_RS:
|
879
|
-
text =
|
849
|
+
text = CONFIG.fmtSQuoteClose
|
880
850
|
elif insert == nwDocInsert.QUOTE_LD:
|
881
|
-
text =
|
851
|
+
text = CONFIG.fmtDQuoteOpen
|
882
852
|
elif insert == nwDocInsert.QUOTE_RD:
|
883
|
-
text =
|
853
|
+
text = CONFIG.fmtDQuoteClose
|
884
854
|
elif insert == nwDocInsert.SYNOPSIS:
|
885
855
|
text = "%Synopsis: "
|
886
856
|
block = True
|
@@ -997,6 +967,32 @@ class GuiDocEditor(QPlainTextEdit):
|
|
997
967
|
|
998
968
|
return
|
999
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
|
+
|
1000
996
|
def focusNextPrevChild(self, next: bool) -> bool:
|
1001
997
|
"""Capture the focus request from the tab key on the text
|
1002
998
|
editor. If the editor has focus, we do not change focus and
|
@@ -1036,12 +1032,12 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1036
1032
|
# Public Slots
|
1037
1033
|
##
|
1038
1034
|
|
1039
|
-
@pyqtSlot(str)
|
1040
|
-
def
|
1035
|
+
@pyqtSlot(str, Enum)
|
1036
|
+
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
|
1041
1037
|
"""Called when an item label is changed to check if the document
|
1042
1038
|
title bar needs updating,
|
1043
1039
|
"""
|
1044
|
-
if tHandle and
|
1040
|
+
if tHandle == self._docHandle and change == nwChange.UPDATE:
|
1045
1041
|
self.docHeader.setHandle(tHandle)
|
1046
1042
|
self.docFooter.updateInfo()
|
1047
1043
|
self.updateDocMargins()
|
@@ -1090,8 +1086,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1090
1086
|
if not self._docChanged:
|
1091
1087
|
self.setDocumentChanged(removed != 0 or added != 0)
|
1092
1088
|
|
1093
|
-
if not self.
|
1094
|
-
self.
|
1089
|
+
if not self._timerDoc.isActive():
|
1090
|
+
self._timerDoc.start()
|
1095
1091
|
|
1096
1092
|
if (block := self._qDocument.findBlock(pos)).isValid():
|
1097
1093
|
text = block.text()
|
@@ -1109,7 +1105,9 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1109
1105
|
self._completer.setVisible(False)
|
1110
1106
|
|
1111
1107
|
if self._doReplace and added == 1:
|
1112
|
-
self.
|
1108
|
+
cursor = self.textCursor()
|
1109
|
+
if self._autoReplace.process(text, cursor):
|
1110
|
+
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
1113
1111
|
|
1114
1112
|
return
|
1115
1113
|
|
@@ -1231,8 +1229,8 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1231
1229
|
|
1232
1230
|
if time() - self._lastEdit < 25.0:
|
1233
1231
|
logger.debug("Running document tasks")
|
1234
|
-
if not self.
|
1235
|
-
SHARED.runInThreadPool(self.
|
1232
|
+
if not self._wCounterDoc.isRunning():
|
1233
|
+
SHARED.runInThreadPool(self._wCounterDoc)
|
1236
1234
|
|
1237
1235
|
self.docHeader.setOutline({
|
1238
1236
|
block.blockNumber(): block.text()
|
@@ -1249,11 +1247,13 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1249
1247
|
"""Process the word counter's finished signal."""
|
1250
1248
|
if self._docHandle and self._nwItem:
|
1251
1249
|
logger.debug("Updating word count")
|
1250
|
+
needsRefresh = wCount != self._nwItem.wordCount
|
1252
1251
|
self._nwItem.setCharCount(cCount)
|
1253
1252
|
self._nwItem.setWordCount(wCount)
|
1254
1253
|
self._nwItem.setParaCount(pCount)
|
1255
|
-
|
1256
|
-
|
1254
|
+
if needsRefresh:
|
1255
|
+
self._nwItem.notifyToRefresh()
|
1256
|
+
self.docFooter.updateWordCount(wCount, False)
|
1257
1257
|
return
|
1258
1258
|
|
1259
1259
|
@pyqtSlot()
|
@@ -1262,10 +1262,10 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1262
1262
|
information to the footer, and start the selection word counter.
|
1263
1263
|
"""
|
1264
1264
|
if self.textCursor().hasSelection():
|
1265
|
-
if not self.
|
1266
|
-
self.
|
1265
|
+
if not self._timerSel.isActive():
|
1266
|
+
self._timerSel.start()
|
1267
1267
|
else:
|
1268
|
-
self.
|
1268
|
+
self._timerSel.stop()
|
1269
1269
|
self.docFooter.updateWordCount(0, False)
|
1270
1270
|
return
|
1271
1271
|
|
@@ -1275,11 +1275,11 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1275
1275
|
if self._docHandle is None:
|
1276
1276
|
return
|
1277
1277
|
|
1278
|
-
if self.
|
1278
|
+
if self._wCounterSel.isRunning():
|
1279
1279
|
logger.debug("Selection word counter is busy")
|
1280
1280
|
return
|
1281
1281
|
|
1282
|
-
SHARED.runInThreadPool(self.
|
1282
|
+
SHARED.runInThreadPool(self._wCounterSel)
|
1283
1283
|
|
1284
1284
|
return
|
1285
1285
|
|
@@ -1289,7 +1289,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1289
1289
|
if self._docHandle and self._nwItem:
|
1290
1290
|
logger.debug("User selected %d words", wCount)
|
1291
1291
|
self.docFooter.updateWordCount(wCount, True)
|
1292
|
-
self.
|
1292
|
+
self._timerSel.stop()
|
1293
1293
|
return
|
1294
1294
|
|
1295
1295
|
@pyqtSlot()
|
@@ -1986,120 +1986,6 @@ class GuiDocEditor(QPlainTextEdit):
|
|
1986
1986
|
self.requestProjectItemRenamed.emit(self._docHandle, text)
|
1987
1987
|
return
|
1988
1988
|
|
1989
|
-
def _docAutoReplace(self, text: str) -> None:
|
1990
|
-
"""Auto-replace text elements based on main configuration."""
|
1991
|
-
cursor = self.textCursor()
|
1992
|
-
tPos = cursor.positionInBlock()
|
1993
|
-
tLen = len(text)
|
1994
|
-
|
1995
|
-
if tLen < 1 or tPos-1 > tLen:
|
1996
|
-
return
|
1997
|
-
|
1998
|
-
t1 = text[tPos-1:tPos]
|
1999
|
-
t2 = text[tPos-2:tPos]
|
2000
|
-
t3 = text[tPos-3:tPos]
|
2001
|
-
t4 = text[tPos-4:tPos]
|
2002
|
-
|
2003
|
-
if not t1:
|
2004
|
-
return
|
2005
|
-
|
2006
|
-
delete = 0
|
2007
|
-
insert = t1
|
2008
|
-
tConf = self._typConf
|
2009
|
-
|
2010
|
-
if tConf.typRepDQuote and t2[:1].isspace() and t2.endswith('"'):
|
2011
|
-
delete = 1
|
2012
|
-
insert = tConf.typDQuoteO
|
2013
|
-
|
2014
|
-
elif tConf.typRepDQuote and t1 == '"':
|
2015
|
-
delete = 1
|
2016
|
-
if tPos == 1:
|
2017
|
-
insert = tConf.typDQuoteO
|
2018
|
-
elif tPos == 2 and t2 == '>"':
|
2019
|
-
insert = tConf.typDQuoteO
|
2020
|
-
elif tPos == 3 and t3 == '>>"':
|
2021
|
-
insert = tConf.typDQuoteO
|
2022
|
-
else:
|
2023
|
-
insert = tConf.typDQuoteC
|
2024
|
-
|
2025
|
-
elif tConf.typRepSQuote and t2[:1].isspace() and t2.endswith("'"):
|
2026
|
-
delete = 1
|
2027
|
-
insert = tConf.typSQuoteO
|
2028
|
-
|
2029
|
-
elif tConf.typRepSQuote and t1 == "'":
|
2030
|
-
delete = 1
|
2031
|
-
if tPos == 1:
|
2032
|
-
insert = tConf.typSQuoteO
|
2033
|
-
elif tPos == 2 and t2 == ">'":
|
2034
|
-
insert = tConf.typSQuoteO
|
2035
|
-
elif tPos == 3 and t3 == ">>'":
|
2036
|
-
insert = tConf.typSQuoteO
|
2037
|
-
else:
|
2038
|
-
insert = tConf.typSQuoteC
|
2039
|
-
|
2040
|
-
elif tConf.typRepDash and t4 == "----":
|
2041
|
-
delete = 4
|
2042
|
-
insert = nwUnicode.U_HBAR
|
2043
|
-
|
2044
|
-
elif tConf.typRepDash and t3 == "---":
|
2045
|
-
delete = 3
|
2046
|
-
insert = nwUnicode.U_EMDASH
|
2047
|
-
|
2048
|
-
elif tConf.typRepDash and t2 == "--":
|
2049
|
-
delete = 2
|
2050
|
-
insert = nwUnicode.U_ENDASH
|
2051
|
-
|
2052
|
-
elif tConf.typRepDash and t2 == nwUnicode.U_ENDASH + "-":
|
2053
|
-
delete = 2
|
2054
|
-
insert = nwUnicode.U_EMDASH
|
2055
|
-
|
2056
|
-
elif tConf.typRepDash and t2 == nwUnicode.U_EMDASH + "-":
|
2057
|
-
delete = 2
|
2058
|
-
insert = nwUnicode.U_HBAR
|
2059
|
-
|
2060
|
-
elif tConf.typRepDots and t3 == "...":
|
2061
|
-
delete = 3
|
2062
|
-
insert = nwUnicode.U_HELLIP
|
2063
|
-
|
2064
|
-
elif t1 == nwUnicode.U_LSEP:
|
2065
|
-
# This resolves issue #1150
|
2066
|
-
delete = 1
|
2067
|
-
insert = nwUnicode.U_PSEP
|
2068
|
-
|
2069
|
-
check = insert
|
2070
|
-
if tConf.typPadBefore and check in tConf.typPadBefore:
|
2071
|
-
if self._allowSpaceBeforeColon(text, check):
|
2072
|
-
delete = max(delete, 1)
|
2073
|
-
chkPos = tPos - delete - 1
|
2074
|
-
if chkPos >= 0 and text[chkPos].isspace():
|
2075
|
-
# Strip existing space before inserting a new (#1061)
|
2076
|
-
delete += 1
|
2077
|
-
insert = tConf.typPadChar + insert
|
2078
|
-
|
2079
|
-
if tConf.typPadAfter and check in tConf.typPadAfter:
|
2080
|
-
if self._allowSpaceBeforeColon(text, check):
|
2081
|
-
delete = max(delete, 1)
|
2082
|
-
insert = insert + tConf.typPadChar
|
2083
|
-
|
2084
|
-
if delete > 0:
|
2085
|
-
cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
|
2086
|
-
cursor.insertText(insert)
|
2087
|
-
|
2088
|
-
# Re-highlight, since the auto-replace sometimes interferes with it
|
2089
|
-
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
|
2090
|
-
|
2091
|
-
return
|
2092
|
-
|
2093
|
-
@staticmethod
|
2094
|
-
def _allowSpaceBeforeColon(text: str, char: str) -> bool:
|
2095
|
-
"""Special checker function only used by the insert space
|
2096
|
-
feature for French, Spanish, etc, so it doesn't insert a
|
2097
|
-
space before colons in meta data lines. See issue #1090.
|
2098
|
-
"""
|
2099
|
-
if char == ":" and len(text) > 1 and text[0] == "@":
|
2100
|
-
return False
|
2101
|
-
return True
|
2102
|
-
|
2103
1989
|
def _autoSelect(self) -> QTextCursor:
|
2104
1990
|
"""Return a cursor which may or may not have a selection based
|
2105
1991
|
on user settings and document action. The selection will be the
|
@@ -2308,6 +2194,131 @@ class BackgroundWordCounterSignals(QObject):
|
|
2308
2194
|
countsReady = pyqtSignal(int, int, int)
|
2309
2195
|
|
2310
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
|
+
|
2311
2322
|
class GuiDocToolBar(QWidget):
|
2312
2323
|
"""The Formatting and Options Fold Out Menu
|
2313
2324
|
|
@@ -2941,7 +2952,7 @@ class GuiDocEditHeader(QWidget):
|
|
2941
2952
|
|
2942
2953
|
def updateTheme(self) -> None:
|
2943
2954
|
"""Update theme elements."""
|
2944
|
-
self.tbButton.setThemeIcon("
|
2955
|
+
self.tbButton.setThemeIcon("toolbar")
|
2945
2956
|
self.outlineButton.setThemeIcon("list")
|
2946
2957
|
self.searchButton.setThemeIcon("search")
|
2947
2958
|
self.minmaxButton.setThemeIcon("maximise")
|
@@ -2985,7 +2996,7 @@ class GuiDocEditHeader(QWidget):
|
|
2985
2996
|
|
2986
2997
|
if CONFIG.showFullPath:
|
2987
2998
|
self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
|
2988
|
-
[name for name in SHARED.project.tree.
|
2999
|
+
[name for name in SHARED.project.tree.itemPath(tHandle, asName=True)]
|
2989
3000
|
)))
|
2990
3001
|
else:
|
2991
3002
|
self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
|