novelWriter 2.5.3__py3-none-any.whl → 2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/METADATA +2 -2
  2. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/RECORD +123 -103
  3. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +50 -11
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  13. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
  16. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  17. novelwriter/assets/i18n/project_de_DE.json +2 -0
  18. novelwriter/assets/i18n/project_en_GB.json +1 -0
  19. novelwriter/assets/i18n/project_en_US.json +2 -0
  20. novelwriter/assets/i18n/project_it_IT.json +2 -0
  21. novelwriter/assets/i18n/project_ja_JP.json +2 -0
  22. novelwriter/assets/i18n/project_nb_NO.json +2 -0
  23. novelwriter/assets/i18n/project_nl_NL.json +2 -0
  24. novelwriter/assets/i18n/project_pl_PL.json +2 -0
  25. novelwriter/assets/i18n/project_pt_BR.json +2 -0
  26. novelwriter/assets/i18n/project_zh_CN.json +2 -0
  27. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  28. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  29. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  30. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  31. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  32. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  33. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  34. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  35. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  36. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  37. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  38. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  39. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  40. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  41. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  42. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  43. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  44. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  45. novelwriter/assets/manual.pdf +0 -0
  46. novelwriter/assets/sample.zip +0 -0
  47. novelwriter/common.py +101 -3
  48. novelwriter/config.py +30 -17
  49. novelwriter/constants.py +189 -81
  50. novelwriter/core/buildsettings.py +74 -40
  51. novelwriter/core/coretools.py +146 -148
  52. novelwriter/core/docbuild.py +133 -171
  53. novelwriter/core/document.py +1 -1
  54. novelwriter/core/index.py +39 -38
  55. novelwriter/core/item.py +42 -9
  56. novelwriter/core/itemmodel.py +518 -0
  57. novelwriter/core/options.py +5 -2
  58. novelwriter/core/project.py +68 -90
  59. novelwriter/core/projectdata.py +8 -2
  60. novelwriter/core/projectxml.py +1 -1
  61. novelwriter/core/sessions.py +1 -1
  62. novelwriter/core/spellcheck.py +10 -15
  63. novelwriter/core/status.py +24 -8
  64. novelwriter/core/storage.py +1 -1
  65. novelwriter/core/tree.py +269 -288
  66. novelwriter/dialogs/about.py +1 -1
  67. novelwriter/dialogs/docmerge.py +8 -18
  68. novelwriter/dialogs/docsplit.py +1 -1
  69. novelwriter/dialogs/editlabel.py +1 -1
  70. novelwriter/dialogs/preferences.py +47 -34
  71. novelwriter/dialogs/projectsettings.py +149 -99
  72. novelwriter/dialogs/quotes.py +1 -1
  73. novelwriter/dialogs/wordlist.py +11 -10
  74. novelwriter/enum.py +37 -24
  75. novelwriter/error.py +2 -2
  76. novelwriter/extensions/configlayout.py +28 -13
  77. novelwriter/extensions/eventfilters.py +1 -1
  78. novelwriter/extensions/modified.py +30 -6
  79. novelwriter/extensions/novelselector.py +4 -3
  80. novelwriter/extensions/pagedsidebar.py +9 -9
  81. novelwriter/extensions/progressbars.py +4 -4
  82. novelwriter/extensions/statusled.py +3 -3
  83. novelwriter/extensions/switch.py +3 -3
  84. novelwriter/extensions/switchbox.py +1 -1
  85. novelwriter/extensions/versioninfo.py +1 -1
  86. novelwriter/formats/shared.py +156 -0
  87. novelwriter/formats/todocx.py +1191 -0
  88. novelwriter/formats/tohtml.py +454 -0
  89. novelwriter/{core → formats}/tokenizer.py +497 -495
  90. novelwriter/formats/tomarkdown.py +218 -0
  91. novelwriter/{core → formats}/toodt.py +312 -433
  92. novelwriter/formats/toqdoc.py +486 -0
  93. novelwriter/formats/toraw.py +91 -0
  94. novelwriter/gui/doceditor.py +347 -287
  95. novelwriter/gui/dochighlight.py +97 -85
  96. novelwriter/gui/docviewer.py +90 -33
  97. novelwriter/gui/docviewerpanel.py +18 -26
  98. novelwriter/gui/editordocument.py +18 -3
  99. novelwriter/gui/itemdetails.py +27 -29
  100. novelwriter/gui/mainmenu.py +130 -64
  101. novelwriter/gui/noveltree.py +46 -48
  102. novelwriter/gui/outline.py +202 -256
  103. novelwriter/gui/projtree.py +590 -1242
  104. novelwriter/gui/search.py +11 -19
  105. novelwriter/gui/sidebar.py +8 -7
  106. novelwriter/gui/statusbar.py +20 -3
  107. novelwriter/gui/theme.py +11 -6
  108. novelwriter/guimain.py +101 -201
  109. novelwriter/shared.py +67 -28
  110. novelwriter/text/counting.py +3 -1
  111. novelwriter/text/patterns.py +169 -61
  112. novelwriter/tools/dictionaries.py +3 -3
  113. novelwriter/tools/lipsum.py +1 -1
  114. novelwriter/tools/manusbuild.py +15 -13
  115. novelwriter/tools/manuscript.py +121 -79
  116. novelwriter/tools/manussettings.py +424 -291
  117. novelwriter/tools/noveldetails.py +1 -1
  118. novelwriter/tools/welcome.py +6 -6
  119. novelwriter/tools/writingstats.py +4 -4
  120. novelwriter/types.py +25 -9
  121. novelwriter/core/tohtml.py +0 -530
  122. novelwriter/core/tomarkdown.py +0 -252
  123. novelwriter/core/toqdoc.py +0 -419
  124. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/LICENSE.md +0 -0
  125. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/entry_points.txt +0 -0
  126. {novelWriter-2.5.3.dist-info → novelWriter-2.6.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@ Created: 2023-11-06 [2.2b1] MetaCompleter
14
14
  Created: 2023-11-07 [2.2b1] GuiDocToolBar
15
15
 
16
16
  This file is a part of novelWriter
17
- Copyright 2018–2024, Veronica Berglyd Olsen
17
+ Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
18
18
 
19
19
  This program is free software: you can redistribute it and/or modify
20
20
  it under the terms of the GNU General Public License as published by
@@ -42,8 +42,9 @@ from PyQt5.QtCore import (
42
42
  pyqtSlot
43
43
  )
44
44
  from PyQt5.QtGui import (
45
- QColor, QCursor, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap,
46
- QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption
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 nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
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
- statusMessage = pyqtSignal(str)
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
- spellCheckStateChanged = pyqtSignal(bool)
100
- closeDocumentRequest = pyqtSignal()
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
- # Typography Cache
127
- self._typPadChar = " "
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.keyContext = QShortcut(self)
175
- self.keyContext.setKey("Ctrl+.")
176
- self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
177
- 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)
178
181
 
179
- self.followTag1 = QShortcut(self)
180
- self.followTag1.setKey(Qt.Key.Key_Return | QtModCtrl)
181
- self.followTag1.setContext(Qt.ShortcutContext.WidgetShortcut)
182
- 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)
183
186
 
184
- self.followTag2 = QShortcut(self)
185
- self.followTag2.setKey(Qt.Key.Key_Enter | QtModCtrl)
186
- self.followTag2.setContext(Qt.ShortcutContext.WidgetShortcut)
187
- 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)
188
191
 
189
192
  # Set Up Document Word Counter
190
- self.timerDoc = QTimer(self)
191
- self.timerDoc.timeout.connect(self._runDocumentTasks)
192
- self.timerDoc.setInterval(5000)
193
+ self._timerDoc = QTimer(self)
194
+ self._timerDoc.timeout.connect(self._runDocumentTasks)
195
+ self._timerDoc.setInterval(5000)
193
196
 
194
- self.wCounterDoc = BackgroundWordCounter(self)
195
- self.wCounterDoc.setAutoDelete(False)
196
- 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)
197
200
 
198
201
  # Set Up Selection Word Counter
199
- self.timerSel = QTimer(self)
200
- self.timerSel.timeout.connect(self._runSelCounter)
201
- self.timerSel.setInterval(500)
202
+ self._timerSel = QTimer(self)
203
+ self._timerSel.timeout.connect(self._runSelCounter)
204
+ self._timerSel.setInterval(500)
202
205
 
203
- self.wCounterSel = BackgroundWordCounter(self, forSelection=True)
204
- self.wCounterSel.setAutoDelete(False)
205
- 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)
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.timerDoc.stop()
260
- self.timerSel.stop()
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
- # Typography
307
- if CONFIG.fmtPadThin:
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
- self.setFont(CONFIG.textFont)
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(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
350
+ self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
358
351
  else:
359
- self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
352
+ self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
360
353
 
361
354
  if CONFIG.hideHScroll:
362
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
355
+ self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
363
356
  else:
364
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
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 = self._nwDocument.readDocument()
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.timerDoc.start()
405
+ self._timerDoc.start()
410
406
 
411
407
  self.setReadOnly(False)
412
408
  self.updateDocMargins()
413
409
 
414
- if tLine is None and self._nwItem is not None:
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
- QApplication.restoreOverrideCursor()
429
+ # Process State Changes
430
+ SHARED.project.data.setLastHandle(tHandle, "editor")
431
+ self.itemHandleChanged.emit(tHandle)
434
432
 
435
- # Update the status bar
436
- if self._nwItem is not None:
437
- self.statusMessage.emit(self.tr("Opened Document: {0}").format(self._nwItem.itemName))
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.statusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName))
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.statusMessage.emit(self.tr("Spell check complete"))
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(self._typSQuoteO, self._typSQuoteC)
745
+ self._wrapSelection(CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
746
746
  elif action == nwDocAction.D_QUOTE:
747
- self._wrapSelection(self._typDQuoteO, self._typDQuoteC)
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("'", self._typSQuoteO, self._typSQuoteC)
773
+ self._replaceQuotes("'", CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose)
774
774
  elif action == nwDocAction.REPL_DBL:
775
- self._replaceQuotes("\"", self._typDQuoteO, self._typDQuoteC)
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 = self._typSQuoteO
847
+ text = CONFIG.fmtSQuoteOpen
848
848
  elif insert == nwDocInsert.QUOTE_RS:
849
- text = self._typSQuoteC
849
+ text = CONFIG.fmtSQuoteClose
850
850
  elif insert == nwDocInsert.QUOTE_LD:
851
- text = self._typDQuoteO
851
+ text = CONFIG.fmtDQuoteOpen
852
852
  elif insert == nwDocInsert.QUOTE_RD:
853
- text = self._typDQuoteC
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 QApplication.keyboardModifiers() == QtModCtrl:
986
- self._processTag(self.cursorForPosition(event.pos()))
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 updateDocInfo(self, tHandle: str) -> None:
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 tHandle == self._docHandle:
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.timerDoc.isActive():
1057
- self.timerDoc.start()
1089
+ if not self._timerDoc.isActive():
1090
+ self._timerDoc.start()
1058
1091
 
1059
1092
  if (block := self._qDocument.findBlock(pos)).isValid():
1060
1093
  text = block.text()
@@ -1071,8 +1104,10 @@ class GuiDocEditor(QPlainTextEdit):
1071
1104
  else:
1072
1105
  self._completer.setVisible(False)
1073
1106
 
1074
- if self._doReplace and added == 1:
1075
- self._docAutoReplace(text)
1107
+ if self._doReplace and added == 1:
1108
+ cursor = self.textCursor()
1109
+ if self._autoReplace.process(text, cursor):
1110
+ self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
1076
1111
 
1077
1112
  return
1078
1113
 
@@ -1095,6 +1130,12 @@ class GuiDocEditor(QPlainTextEdit):
1095
1130
  self._completer.hide()
1096
1131
  return
1097
1132
 
1133
+ @pyqtSlot()
1134
+ def _openContextFromCursor(self) -> None:
1135
+ """Open the spell check context menu at the cursor."""
1136
+ self._openContextMenu(self.cursorRect().center())
1137
+ return
1138
+
1098
1139
  @pyqtSlot("QPoint")
1099
1140
  def _openContextMenu(self, pos: QPoint) -> None:
1100
1141
  """Open the editor context menu at a given coordinate."""
@@ -1106,41 +1147,48 @@ class GuiDocEditor(QPlainTextEdit):
1106
1147
  ctxMenu.setObjectName("ContextMenu")
1107
1148
  if pBlock.userState() == BLOCK_TITLE:
1108
1149
  action = ctxMenu.addAction(self.tr("Set as Document Name"))
1109
- action.triggered.connect(lambda: self._emitRenameItem(pBlock))
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(lambda: self._processTag(cursor=pCursor, follow=True))
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(lambda: self._processTag(cursor=pCursor, create=True))
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(lambda: self.docAction(nwDocAction.CUT))
1173
+ action.triggered.connect(qtLambda(self.docAction, nwDocAction.CUT))
1126
1174
  action = ctxMenu.addAction(self.tr("Copy"))
1127
- action.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
1175
+ action.triggered.connect(qtLambda(self.docAction, nwDocAction.COPY))
1128
1176
 
1129
1177
  action = ctxMenu.addAction(self.tr("Paste"))
1130
- action.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
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(lambda: self.docAction(nwDocAction.SEL_ALL))
1183
+ action.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL))
1136
1184
  action = ctxMenu.addAction(self.tr("Select Word"))
1137
- action.triggered.connect(
1138
- lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, pos)
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(lambda: self._makePosSelection(
1142
- QTextCursor.SelectionType.BlockUnderCursor, pos)
1143
- )
1189
+ action.triggered.connect(qtLambda(
1190
+ self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, pos
1191
+ ))
1144
1192
 
1145
1193
  # Spell Checking
1146
1194
  if SHARED.project.data.spellCheck:
@@ -1156,47 +1204,23 @@ class GuiDocEditor(QPlainTextEdit):
1156
1204
  ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
1157
1205
  for option in suggest[:15]:
1158
1206
  action = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
1159
- action.triggered.connect(
1160
- lambda _, option=option: self._correctWord(sCursor, option)
1161
- )
1207
+ action.triggered.connect(qtLambda(self._correctWord, sCursor, option))
1162
1208
  else:
1163
1209
  trNone = self.tr("No Suggestions")
1164
1210
  ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {trNone}")
1165
1211
 
1166
1212
  ctxMenu.addSeparator()
1213
+ action = ctxMenu.addAction(self.tr("Ignore Word"))
1214
+ action.triggered.connect(qtLambda(self._addWord, word, block, False))
1167
1215
  action = ctxMenu.addAction(self.tr("Add Word to Dictionary"))
1168
- action.triggered.connect(lambda: self._addWord(word, block))
1216
+ action.triggered.connect(qtLambda(self._addWord, word, block, True))
1169
1217
 
1170
1218
  # Execute the context menu
1171
1219
  ctxMenu.exec(self.viewport().mapToGlobal(pos))
1172
- ctxMenu.deleteLater()
1220
+ ctxMenu.setParent(None)
1173
1221
 
1174
1222
  return
1175
1223
 
1176
- @pyqtSlot("QTextCursor", str)
1177
- def _correctWord(self, cursor: QTextCursor, word: str) -> None:
1178
- """Slot for the spell check context menu triggering the
1179
- replacement of a word with the word from the dictionary.
1180
- """
1181
- pos = cursor.selectionStart()
1182
- cursor.beginEditBlock()
1183
- cursor.removeSelectedText()
1184
- cursor.insertText(word)
1185
- cursor.endEditBlock()
1186
- cursor.setPosition(pos)
1187
- self.setTextCursor(cursor)
1188
- return
1189
-
1190
- @pyqtSlot(str, "QTextBlock")
1191
- def _addWord(self, word: str, block: QTextBlock) -> None:
1192
- """Slot for the spell check context menu triggered when the user
1193
- wants to add a word to the project dictionary.
1194
- """
1195
- logger.debug("Added '%s' to project dictionary", word)
1196
- SHARED.spelling.addWord(word)
1197
- self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1198
- return
1199
-
1200
1224
  @pyqtSlot()
1201
1225
  def _runDocumentTasks(self) -> None:
1202
1226
  """Run timer document tasks."""
@@ -1205,8 +1229,8 @@ class GuiDocEditor(QPlainTextEdit):
1205
1229
 
1206
1230
  if time() - self._lastEdit < 25.0:
1207
1231
  logger.debug("Running document tasks")
1208
- if not self.wCounterDoc.isRunning():
1209
- SHARED.runInThreadPool(self.wCounterDoc)
1232
+ if not self._wCounterDoc.isRunning():
1233
+ SHARED.runInThreadPool(self._wCounterDoc)
1210
1234
 
1211
1235
  self.docHeader.setOutline({
1212
1236
  block.blockNumber(): block.text()
@@ -1223,11 +1247,15 @@ class GuiDocEditor(QPlainTextEdit):
1223
1247
  """Process the word counter's finished signal."""
1224
1248
  if self._docHandle and self._nwItem:
1225
1249
  logger.debug("Updating word count")
1250
+ needsRefresh = wCount != self._nwItem.wordCount
1226
1251
  self._nwItem.setCharCount(cCount)
1227
1252
  self._nwItem.setWordCount(wCount)
1228
1253
  self._nwItem.setParaCount(pCount)
1229
- self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
1230
- self.docFooter.updateWordCount(wCount, False)
1254
+ if needsRefresh:
1255
+ self._nwItem.notifyToRefresh()
1256
+ if not self.textCursor().hasSelection():
1257
+ # Selection counter should take precedence (#2155)
1258
+ self.docFooter.updateWordCount(wCount, False)
1231
1259
  return
1232
1260
 
1233
1261
  @pyqtSlot()
@@ -1236,10 +1264,10 @@ class GuiDocEditor(QPlainTextEdit):
1236
1264
  information to the footer, and start the selection word counter.
1237
1265
  """
1238
1266
  if self.textCursor().hasSelection():
1239
- if not self.timerSel.isActive():
1240
- self.timerSel.start()
1267
+ if not self._timerSel.isActive():
1268
+ self._timerSel.start()
1241
1269
  else:
1242
- self.timerSel.stop()
1270
+ self._timerSel.stop()
1243
1271
  self.docFooter.updateWordCount(0, False)
1244
1272
  return
1245
1273
 
@@ -1249,11 +1277,11 @@ class GuiDocEditor(QPlainTextEdit):
1249
1277
  if self._docHandle is None:
1250
1278
  return
1251
1279
 
1252
- if self.wCounterSel.isRunning():
1280
+ if self._wCounterSel.isRunning():
1253
1281
  logger.debug("Selection word counter is busy")
1254
1282
  return
1255
1283
 
1256
- SHARED.runInThreadPool(self.wCounterSel)
1284
+ SHARED.runInThreadPool(self._wCounterSel)
1257
1285
 
1258
1286
  return
1259
1287
 
@@ -1263,13 +1291,13 @@ class GuiDocEditor(QPlainTextEdit):
1263
1291
  if self._docHandle and self._nwItem:
1264
1292
  logger.debug("User selected %d words", wCount)
1265
1293
  self.docFooter.updateWordCount(wCount, True)
1266
- self.timerSel.stop()
1294
+ self._timerSel.stop()
1267
1295
  return
1268
1296
 
1269
1297
  @pyqtSlot()
1270
1298
  def _closeCurrentDocument(self) -> None:
1271
1299
  """Close the document. Forwarded to the main Gui."""
1272
- self.closeDocumentRequest.emit()
1300
+ self.closeEditorRequest.emit()
1273
1301
  self.docToolBar.setVisible(False)
1274
1302
  return
1275
1303
 
@@ -1875,6 +1903,28 @@ class GuiDocEditor(QPlainTextEdit):
1875
1903
  # Internal Functions
1876
1904
  ##
1877
1905
 
1906
+ def _correctWord(self, cursor: QTextCursor, word: str) -> None:
1907
+ """Slot for the spell check context menu triggering the
1908
+ replacement of a word with the word from the dictionary.
1909
+ """
1910
+ pos = cursor.selectionStart()
1911
+ cursor.beginEditBlock()
1912
+ cursor.removeSelectedText()
1913
+ cursor.insertText(word)
1914
+ cursor.endEditBlock()
1915
+ cursor.setPosition(pos)
1916
+ self.setTextCursor(cursor)
1917
+ return
1918
+
1919
+ def _addWord(self, word: str, block: QTextBlock, save: bool) -> None:
1920
+ """Slot for the spell check context menu triggered when the user
1921
+ wants to add a word to the project dictionary.
1922
+ """
1923
+ logger.debug("Added '%s' to project dictionary, %s", word, "saved" if save else "unsaved")
1924
+ SHARED.spelling.addWord(word, save=save)
1925
+ self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1926
+ return
1927
+
1878
1928
  def _processTag(self, cursor: QTextCursor | None = None,
1879
1929
  follow: bool = True, create: bool = False) -> nwTrinary:
1880
1930
  """Activated by Ctrl+Enter. Checks that we're in a block
@@ -1938,119 +1988,6 @@ class GuiDocEditor(QPlainTextEdit):
1938
1988
  self.requestProjectItemRenamed.emit(self._docHandle, text)
1939
1989
  return
1940
1990
 
1941
- def _openContextFromCursor(self) -> None:
1942
- """Open the spell check context menu at the cursor."""
1943
- self._openContextMenu(self.cursorRect().center())
1944
- return
1945
-
1946
- def _docAutoReplace(self, text: str) -> None:
1947
- """Auto-replace text elements based on main configuration."""
1948
- cursor = self.textCursor()
1949
- tPos = cursor.positionInBlock()
1950
- tLen = len(text)
1951
-
1952
- if tLen < 1 or tPos-1 > tLen:
1953
- return
1954
-
1955
- tOne = text[tPos-1:tPos]
1956
- tTwo = text[tPos-2:tPos]
1957
- tThree = text[tPos-3:tPos]
1958
-
1959
- if not tOne:
1960
- return
1961
-
1962
- nDelete = 0
1963
- tInsert = tOne
1964
-
1965
- if self._typRepDQuote and tTwo[:1].isspace() and tTwo.endswith('"'):
1966
- nDelete = 1
1967
- tInsert = self._typDQuoteO
1968
-
1969
- elif self._typRepDQuote and tOne == '"':
1970
- nDelete = 1
1971
- if tPos == 1:
1972
- tInsert = self._typDQuoteO
1973
- elif tPos == 2 and tTwo == '>"':
1974
- tInsert = self._typDQuoteO
1975
- elif tPos == 3 and tThree == '>>"':
1976
- tInsert = self._typDQuoteO
1977
- else:
1978
- tInsert = self._typDQuoteC
1979
-
1980
- elif self._typRepSQuote and tTwo[:1].isspace() and tTwo.endswith("'"):
1981
- nDelete = 1
1982
- tInsert = self._typSQuoteO
1983
-
1984
- elif self._typRepSQuote and tOne == "'":
1985
- nDelete = 1
1986
- if tPos == 1:
1987
- tInsert = self._typSQuoteO
1988
- elif tPos == 2 and tTwo == ">'":
1989
- tInsert = self._typSQuoteO
1990
- elif tPos == 3 and tThree == ">>'":
1991
- tInsert = self._typSQuoteO
1992
- else:
1993
- tInsert = self._typSQuoteC
1994
-
1995
- elif self._typRepDash and tThree == "---":
1996
- nDelete = 3
1997
- tInsert = nwUnicode.U_EMDASH
1998
-
1999
- elif self._typRepDash and tTwo == "--":
2000
- nDelete = 2
2001
- tInsert = nwUnicode.U_ENDASH
2002
-
2003
- elif self._typRepDash and tTwo == nwUnicode.U_ENDASH + "-":
2004
- nDelete = 2
2005
- tInsert = nwUnicode.U_EMDASH
2006
-
2007
- elif self._typRepDots and tThree == "...":
2008
- nDelete = 3
2009
- tInsert = nwUnicode.U_HELLIP
2010
-
2011
- elif tOne == nwUnicode.U_LSEP:
2012
- # This resolves issue #1150
2013
- nDelete = 1
2014
- tInsert = nwUnicode.U_PSEP
2015
-
2016
- tCheck = tInsert
2017
- if self._typPadBefore and tCheck in self._typPadBefore:
2018
- if self._allowSpaceBeforeColon(text, tCheck):
2019
- nDelete = max(nDelete, 1)
2020
- chkPos = tPos - nDelete - 1
2021
- if chkPos >= 0 and text[chkPos].isspace():
2022
- # Strip existing space before inserting a new (#1061)
2023
- nDelete += 1
2024
- tInsert = self._typPadChar + tInsert
2025
-
2026
- if self._typPadAfter and tCheck in self._typPadAfter:
2027
- if self._allowSpaceBeforeColon(text, tCheck):
2028
- nDelete = max(nDelete, 1)
2029
- tInsert = tInsert + self._typPadChar
2030
-
2031
- if nDelete > 0:
2032
- cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete)
2033
- cursor.insertText(tInsert)
2034
-
2035
- # Re-highlight, since the auto-replace sometimes interferes with it
2036
- self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
2037
-
2038
- return
2039
-
2040
- @staticmethod
2041
- def _allowSpaceBeforeColon(text: str, char: str) -> bool:
2042
- """Special checker function only used by the insert space
2043
- feature for French, Spanish, etc, so it doesn't insert a
2044
- space before colons in meta data lines. See issue #1090.
2045
- """
2046
- if char == ":" and len(text) > 1:
2047
- if text[0] == "@":
2048
- return False
2049
- if text[0] == "%":
2050
- if text[1:].lstrip()[:9].lower() == "synopsis:":
2051
- return False
2052
- return True
2053
-
2054
1991
  def _autoSelect(self) -> QTextCursor:
2055
1992
  """Return a cursor which may or may not have a selection based
2056
1993
  on user settings and document action. The selection will be the
@@ -2173,7 +2110,7 @@ class MetaCompleter(QMenu):
2173
2110
  suffix = ""
2174
2111
  options = list(filter(
2175
2112
  lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
2176
- nwKeyWords.KEY_CLASS.get(kw.strip(), nwItemClass.NO_CLASS)
2113
+ nwKeyWords.KEY_CLASS.get(kw.strip())
2177
2114
  )
2178
2115
  ))[:15]
2179
2116
 
@@ -2183,7 +2120,7 @@ class MetaCompleter(QMenu):
2183
2120
  for value in sorted(options):
2184
2121
  rep = value + suffix
2185
2122
  action = self.addAction(value)
2186
- action.triggered.connect(lambda _, r=rep: self._emitComplete(offset, length, r))
2123
+ action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
2187
2124
 
2188
2125
  return True
2189
2126
 
@@ -2259,6 +2196,131 @@ class BackgroundWordCounterSignals(QObject):
2259
2196
  countsReady = pyqtSignal(int, int, int)
2260
2197
 
2261
2198
 
2199
+ class TextAutoReplace:
2200
+
2201
+ __slots__ = (
2202
+ "_quoteSO", "_quoteSC", "_quoteDO", "_quoteDC",
2203
+ "_replaceSQuote", "_replaceDQuote", "_replaceDash", "_replaceDots",
2204
+ "_padChar", "_padBefore", "_padAfter", "_doPadBefore", "_doPadAfter",
2205
+ )
2206
+
2207
+ def __init__(self) -> None:
2208
+ self.initSettings()
2209
+ return
2210
+
2211
+ def initSettings(self) -> None:
2212
+ """Initialise the auto-replace settings from config."""
2213
+ self._quoteSO = CONFIG.fmtSQuoteOpen
2214
+ self._quoteSC = CONFIG.fmtSQuoteClose
2215
+ self._quoteDO = CONFIG.fmtDQuoteOpen
2216
+ self._quoteDC = CONFIG.fmtDQuoteClose
2217
+
2218
+ self._replaceSQuote = CONFIG.doReplaceSQuote
2219
+ self._replaceDQuote = CONFIG.doReplaceDQuote
2220
+ self._replaceDash = CONFIG.doReplaceDash
2221
+ self._replaceDots = CONFIG.doReplaceDots
2222
+
2223
+ self._padChar = nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP
2224
+ self._padBefore = CONFIG.fmtPadBefore
2225
+ self._padAfter = CONFIG.fmtPadAfter
2226
+ self._doPadBefore = bool(CONFIG.fmtPadBefore)
2227
+ self._doPadAfter = bool(CONFIG.fmtPadAfter)
2228
+ return
2229
+
2230
+ def process(self, text: str, cursor: QTextCursor) -> bool:
2231
+ """Auto-replace text elements based on main configuration.
2232
+ Returns True if anything was changed.
2233
+ """
2234
+ pos = cursor.positionInBlock()
2235
+ length = len(text)
2236
+ if length < 1 or pos-1 > length:
2237
+ return False
2238
+
2239
+ delete, insert = self._determine(text, pos)
2240
+ if insert == "":
2241
+ return False
2242
+
2243
+ check = insert
2244
+ if self._doPadBefore and check in self._padBefore:
2245
+ if not (check == ":" and length > 1 and text[0] == "@"):
2246
+ delete = max(delete, 1)
2247
+ chkPos = pos - delete - 1
2248
+ if chkPos >= 0 and text[chkPos].isspace():
2249
+ # Strip existing space before inserting a new (#1061)
2250
+ delete += 1
2251
+ insert = self._padChar + insert
2252
+
2253
+ if self._doPadAfter and check in self._padAfter:
2254
+ if not (check == ":" and length > 1 and text[0] == "@"):
2255
+ delete = max(delete, 1)
2256
+ insert = insert + self._padChar
2257
+
2258
+ if delete > 0:
2259
+ cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
2260
+ cursor.insertText(insert)
2261
+ return True
2262
+
2263
+ return False
2264
+
2265
+ def _determine(self, text: str, pos: int) -> tuple[int, str]:
2266
+ """Determine what to replace, if anything."""
2267
+ t1 = text[pos-1:pos]
2268
+ t2 = text[pos-2:pos]
2269
+ t3 = text[pos-3:pos]
2270
+ t4 = text[pos-4:pos]
2271
+ if t1 == "":
2272
+ # Return early if there is nothing to check
2273
+ return 0, ""
2274
+
2275
+ leading = t2[:1].isspace()
2276
+ if self._replaceDQuote:
2277
+ if leading and t2.endswith('"'):
2278
+ return 1, self._quoteDO
2279
+ elif t1 == '"':
2280
+ if pos == 1:
2281
+ return 1, self._quoteDO
2282
+ elif pos == 2 and t2 == '>"':
2283
+ return 1, self._quoteDO
2284
+ elif pos == 3 and t3 == '>>"':
2285
+ return 1, self._quoteDO
2286
+ else:
2287
+ return 1, self._quoteDC
2288
+
2289
+ if self._replaceSQuote:
2290
+ if leading and t2.endswith("'"):
2291
+ return 1, self._quoteSO
2292
+ elif t1 == "'":
2293
+ if pos == 1:
2294
+ return 1, self._quoteSO
2295
+ elif pos == 2 and t2 == ">'":
2296
+ return 1, self._quoteSO
2297
+ elif pos == 3 and t3 == ">>'":
2298
+ return 1, self._quoteSO
2299
+ else:
2300
+ return 1, self._quoteSC
2301
+
2302
+ if self._replaceDash:
2303
+ if t4 == "----":
2304
+ return 4, "\u2015" # Horizontal bar
2305
+ elif t3 == "---":
2306
+ return 3, "\u2014" # Long dash
2307
+ elif t2 == "--":
2308
+ return 2, "\u2013" # Short dash
2309
+ elif t2 == "\u2013-":
2310
+ return 2, "\u2014" # Long dash
2311
+ elif t2 == "\u2014-":
2312
+ return 2, "\u2015" # Horizontal bar
2313
+
2314
+ if self._replaceDots and t3 == "...":
2315
+ return 3, "\u2026" # Ellipsis
2316
+
2317
+ if t1 == "\u2028": # Line separator
2318
+ # This resolves issue #1150
2319
+ return 1, "\u2029" # Paragraph separator
2320
+
2321
+ return 0, t1
2322
+
2323
+
2262
2324
  class GuiDocToolBar(QWidget):
2263
2325
  """The Formatting and Options Fold Out Menu
2264
2326
 
@@ -2283,61 +2345,61 @@ class GuiDocToolBar(QWidget):
2283
2345
  self.tbBoldMD = NIconToolButton(self, iSz)
2284
2346
  self.tbBoldMD.setToolTip(self.tr("Markdown Bold"))
2285
2347
  self.tbBoldMD.clicked.connect(
2286
- lambda: self.requestDocAction.emit(nwDocAction.MD_BOLD)
2348
+ qtLambda(self.requestDocAction.emit, nwDocAction.MD_BOLD)
2287
2349
  )
2288
2350
 
2289
2351
  self.tbItalicMD = NIconToolButton(self, iSz)
2290
2352
  self.tbItalicMD.setToolTip(self.tr("Markdown Italic"))
2291
2353
  self.tbItalicMD.clicked.connect(
2292
- lambda: self.requestDocAction.emit(nwDocAction.MD_ITALIC)
2354
+ qtLambda(self.requestDocAction.emit, nwDocAction.MD_ITALIC)
2293
2355
  )
2294
2356
 
2295
2357
  self.tbStrikeMD = NIconToolButton(self, iSz)
2296
2358
  self.tbStrikeMD.setToolTip(self.tr("Markdown Strikethrough"))
2297
2359
  self.tbStrikeMD.clicked.connect(
2298
- lambda: self.requestDocAction.emit(nwDocAction.MD_STRIKE)
2360
+ qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE)
2299
2361
  )
2300
2362
 
2301
2363
  self.tbBold = NIconToolButton(self, iSz)
2302
2364
  self.tbBold.setToolTip(self.tr("Shortcode Bold"))
2303
2365
  self.tbBold.clicked.connect(
2304
- lambda: self.requestDocAction.emit(nwDocAction.SC_BOLD)
2366
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_BOLD)
2305
2367
  )
2306
2368
 
2307
2369
  self.tbItalic = NIconToolButton(self, iSz)
2308
2370
  self.tbItalic.setToolTip(self.tr("Shortcode Italic"))
2309
2371
  self.tbItalic.clicked.connect(
2310
- lambda: self.requestDocAction.emit(nwDocAction.SC_ITALIC)
2372
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_ITALIC)
2311
2373
  )
2312
2374
 
2313
2375
  self.tbStrike = NIconToolButton(self, iSz)
2314
2376
  self.tbStrike.setToolTip(self.tr("Shortcode Strikethrough"))
2315
2377
  self.tbStrike.clicked.connect(
2316
- lambda: self.requestDocAction.emit(nwDocAction.SC_STRIKE)
2378
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_STRIKE)
2317
2379
  )
2318
2380
 
2319
2381
  self.tbUnderline = NIconToolButton(self, iSz)
2320
2382
  self.tbUnderline.setToolTip(self.tr("Shortcode Underline"))
2321
2383
  self.tbUnderline.clicked.connect(
2322
- lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE)
2384
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_ULINE)
2323
2385
  )
2324
2386
 
2325
2387
  self.tbMark = NIconToolButton(self, iSz)
2326
2388
  self.tbMark.setToolTip(self.tr("Shortcode Highlight"))
2327
2389
  self.tbMark.clicked.connect(
2328
- lambda: self.requestDocAction.emit(nwDocAction.SC_MARK)
2390
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_MARK)
2329
2391
  )
2330
2392
 
2331
2393
  self.tbSuperscript = NIconToolButton(self, iSz)
2332
2394
  self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript"))
2333
2395
  self.tbSuperscript.clicked.connect(
2334
- lambda: self.requestDocAction.emit(nwDocAction.SC_SUP)
2396
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUP)
2335
2397
  )
2336
2398
 
2337
2399
  self.tbSubscript = NIconToolButton(self, iSz)
2338
2400
  self.tbSubscript.setToolTip(self.tr("Shortcode Subscript"))
2339
2401
  self.tbSubscript.clicked.connect(
2340
- lambda: self.requestDocAction.emit(nwDocAction.SC_SUB)
2402
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUB)
2341
2403
  )
2342
2404
 
2343
2405
  # Assemble
@@ -2801,7 +2863,7 @@ class GuiDocEditHeader(QWidget):
2801
2863
  self.tbButton = NIconToolButton(self, iSz)
2802
2864
  self.tbButton.setVisible(False)
2803
2865
  self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
2804
- self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
2866
+ self.tbButton.clicked.connect(qtLambda(self.toggleToolBarRequest.emit))
2805
2867
 
2806
2868
  self.outlineButton = NIconToolButton(self, iSz)
2807
2869
  self.outlineButton.setVisible(False)
@@ -2816,7 +2878,7 @@ class GuiDocEditHeader(QWidget):
2816
2878
  self.minmaxButton = NIconToolButton(self, iSz)
2817
2879
  self.minmaxButton.setVisible(False)
2818
2880
  self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
2819
- self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit())
2881
+ self.minmaxButton.clicked.connect(qtLambda(self.docEditor.toggleFocusModeRequest.emit))
2820
2882
 
2821
2883
  self.closeButton = NIconToolButton(self, iSz)
2822
2884
  self.closeButton.setVisible(False)
@@ -2879,9 +2941,7 @@ class GuiDocEditHeader(QWidget):
2879
2941
  self.outlineMenu.clear()
2880
2942
  for number, text in data.items():
2881
2943
  action = self.outlineMenu.addAction(text)
2882
- action.triggered.connect(
2883
- lambda _, number=number: self._gotoBlock(number)
2884
- )
2944
+ action.triggered.connect(qtLambda(self._gotoBlock, number))
2885
2945
  self._docOutline = data
2886
2946
  logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
2887
2947
  return
@@ -2894,7 +2954,7 @@ class GuiDocEditHeader(QWidget):
2894
2954
 
2895
2955
  def updateTheme(self) -> None:
2896
2956
  """Update theme elements."""
2897
- self.tbButton.setThemeIcon("menu")
2957
+ self.tbButton.setThemeIcon("toolbar")
2898
2958
  self.outlineButton.setThemeIcon("list")
2899
2959
  self.searchButton.setThemeIcon("search")
2900
2960
  self.minmaxButton.setThemeIcon("maximise")
@@ -2938,7 +2998,7 @@ class GuiDocEditHeader(QWidget):
2938
2998
 
2939
2999
  if CONFIG.showFullPath:
2940
3000
  self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
2941
- [name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)]
3001
+ [name for name in SHARED.project.tree.itemPath(tHandle, asName=True)]
2942
3002
  )))
2943
3003
  else:
2944
3004
  self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")