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.
Files changed (68) hide show
  1. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/METADATA +3 -3
  2. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/RECORD +68 -52
  3. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +49 -10
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  7. novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
  8. novelwriter/assets/i18n/project_de_DE.json +2 -2
  9. novelwriter/assets/i18n/project_ru_RU.json +11 -0
  10. novelwriter/assets/icons/typicons_dark/icons.conf +7 -0
  11. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  12. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  13. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  14. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  15. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  16. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  17. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  18. novelwriter/assets/icons/typicons_light/icons.conf +7 -0
  19. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  20. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  21. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  22. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  23. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  24. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  25. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  26. novelwriter/assets/manual.pdf +0 -0
  27. novelwriter/assets/sample.zip +0 -0
  28. novelwriter/assets/text/credits_en.htm +1 -0
  29. novelwriter/common.py +37 -2
  30. novelwriter/config.py +15 -12
  31. novelwriter/constants.py +24 -9
  32. novelwriter/core/coretools.py +111 -125
  33. novelwriter/core/docbuild.py +3 -2
  34. novelwriter/core/index.py +9 -19
  35. novelwriter/core/item.py +39 -6
  36. novelwriter/core/itemmodel.py +518 -0
  37. novelwriter/core/project.py +67 -89
  38. novelwriter/core/status.py +7 -5
  39. novelwriter/core/tree.py +268 -287
  40. novelwriter/dialogs/docmerge.py +7 -17
  41. novelwriter/dialogs/preferences.py +3 -3
  42. novelwriter/dialogs/projectsettings.py +2 -2
  43. novelwriter/enum.py +7 -0
  44. novelwriter/extensions/configlayout.py +6 -4
  45. novelwriter/formats/todocx.py +34 -38
  46. novelwriter/formats/tohtml.py +14 -15
  47. novelwriter/formats/tokenizer.py +21 -17
  48. novelwriter/formats/toodt.py +53 -124
  49. novelwriter/formats/toqdoc.py +92 -44
  50. novelwriter/gui/doceditor.py +230 -219
  51. novelwriter/gui/docviewer.py +38 -9
  52. novelwriter/gui/docviewerpanel.py +14 -22
  53. novelwriter/gui/itemdetails.py +17 -24
  54. novelwriter/gui/mainmenu.py +13 -8
  55. novelwriter/gui/noveltree.py +12 -12
  56. novelwriter/gui/outline.py +10 -11
  57. novelwriter/gui/projtree.py +548 -1202
  58. novelwriter/gui/search.py +9 -10
  59. novelwriter/gui/theme.py +7 -3
  60. novelwriter/guimain.py +59 -43
  61. novelwriter/shared.py +52 -23
  62. novelwriter/text/patterns.py +17 -5
  63. novelwriter/tools/manusbuild.py +13 -11
  64. novelwriter/tools/manussettings.py +42 -52
  65. novelwriter/types.py +7 -1
  66. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
  67. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
  68. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
@@ -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, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap,
47
- QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption
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, nwItemType,
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
- # Typography Cache
147
- self._typConf = AutoReplaceConfig(
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.keyContext = QShortcut(self)
197
- self.keyContext.setKey("Ctrl+.")
198
- self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
199
- self.keyContext.activated.connect(self._openContextFromCursor)
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.followTag1 = QShortcut(self)
202
- self.followTag1.setKey("Ctrl+Return")
203
- self.followTag1.setContext(Qt.ShortcutContext.WidgetShortcut)
204
- self.followTag1.activated.connect(self._processTag)
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.followTag2 = QShortcut(self)
207
- self.followTag2.setKey("Ctrl+Enter")
208
- self.followTag2.setContext(Qt.ShortcutContext.WidgetShortcut)
209
- self.followTag2.activated.connect(self._processTag)
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.timerDoc = QTimer(self)
213
- self.timerDoc.timeout.connect(self._runDocumentTasks)
214
- self.timerDoc.setInterval(5000)
193
+ self._timerDoc = QTimer(self)
194
+ self._timerDoc.timeout.connect(self._runDocumentTasks)
195
+ self._timerDoc.setInterval(5000)
215
196
 
216
- self.wCounterDoc = BackgroundWordCounter(self)
217
- self.wCounterDoc.setAutoDelete(False)
218
- self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
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.timerSel = QTimer(self)
222
- self.timerSel.timeout.connect(self._runSelCounter)
223
- self.timerSel.setInterval(500)
202
+ self._timerSel = QTimer(self)
203
+ self._timerSel.timeout.connect(self._runSelCounter)
204
+ self._timerSel.setInterval(500)
224
205
 
225
- self.wCounterSel = BackgroundWordCounter(self, forSelection=True)
226
- self.wCounterSel.setAutoDelete(False)
227
- self.wCounterSel.signals.countsReady.connect(self._updateSelCounts)
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.timerDoc.stop()
282
- self.timerSel.stop()
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
- # Typography
331
- self._typConf = AutoReplaceConfig(
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
- self.setFont(CONFIG.textFont)
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.timerDoc.start()
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(tConf.typSQuoteO, tConf.typSQuoteC)
745
+ self._wrapSelection(CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
776
746
  elif action == nwDocAction.D_QUOTE:
777
- self._wrapSelection(tConf.typDQuoteO, tConf.typDQuoteC)
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("'", tConf.typSQuoteO, tConf.typSQuoteC)
773
+ self._replaceQuotes("'", CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
804
774
  elif action == nwDocAction.REPL_DBL:
805
- self._replaceQuotes("\"", tConf.typDQuoteO, tConf.typDQuoteC)
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 = self._typConf.typSQuoteO
847
+ text = CONFIG.fmtSQuoteOpen
878
848
  elif insert == nwDocInsert.QUOTE_RS:
879
- text = self._typConf.typSQuoteC
849
+ text = CONFIG.fmtSQuoteClose
880
850
  elif insert == nwDocInsert.QUOTE_LD:
881
- text = self._typConf.typDQuoteO
851
+ text = CONFIG.fmtDQuoteOpen
882
852
  elif insert == nwDocInsert.QUOTE_RD:
883
- text = self._typConf.typDQuoteC
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 updateDocInfo(self, tHandle: str) -> None:
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 tHandle == self._docHandle:
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.timerDoc.isActive():
1094
- self.timerDoc.start()
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._docAutoReplace(text)
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.wCounterDoc.isRunning():
1235
- SHARED.runInThreadPool(self.wCounterDoc)
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
- self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
1256
- self.docFooter.updateWordCount(wCount, False)
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.timerSel.isActive():
1266
- self.timerSel.start()
1265
+ if not self._timerSel.isActive():
1266
+ self._timerSel.start()
1267
1267
  else:
1268
- self.timerSel.stop()
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.wCounterSel.isRunning():
1278
+ if self._wCounterSel.isRunning():
1279
1279
  logger.debug("Selection word counter is busy")
1280
1280
  return
1281
1281
 
1282
- SHARED.runInThreadPool(self.wCounterSel)
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.timerSel.stop()
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("menu")
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.getItemPath(tHandle, asName=True)]
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 "")