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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
  2. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
  3. novelwriter/__init__.py +49 -10
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  6. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  8. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  9. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  10. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  11. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  12. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  13. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  14. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  15. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  17. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  18. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  19. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  20. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  21. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  22. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  23. novelwriter/assets/manual.pdf +0 -0
  24. novelwriter/assets/sample.zip +0 -0
  25. novelwriter/common.py +100 -2
  26. novelwriter/config.py +25 -15
  27. novelwriter/constants.py +168 -60
  28. novelwriter/core/buildsettings.py +66 -39
  29. novelwriter/core/coretools.py +145 -147
  30. novelwriter/core/docbuild.py +132 -170
  31. novelwriter/core/index.py +38 -37
  32. novelwriter/core/item.py +41 -8
  33. novelwriter/core/itemmodel.py +518 -0
  34. novelwriter/core/options.py +4 -1
  35. novelwriter/core/project.py +67 -89
  36. novelwriter/core/spellcheck.py +9 -14
  37. novelwriter/core/status.py +7 -5
  38. novelwriter/core/tree.py +268 -287
  39. novelwriter/dialogs/docmerge.py +7 -17
  40. novelwriter/dialogs/preferences.py +46 -33
  41. novelwriter/dialogs/projectsettings.py +5 -5
  42. novelwriter/enum.py +36 -23
  43. novelwriter/extensions/configlayout.py +27 -12
  44. novelwriter/extensions/modified.py +13 -1
  45. novelwriter/extensions/pagedsidebar.py +5 -5
  46. novelwriter/formats/shared.py +155 -0
  47. novelwriter/formats/todocx.py +1191 -0
  48. novelwriter/formats/tohtml.py +451 -0
  49. novelwriter/{core → formats}/tokenizer.py +487 -491
  50. novelwriter/formats/tomarkdown.py +217 -0
  51. novelwriter/{core → formats}/toodt.py +311 -432
  52. novelwriter/formats/toqdoc.py +484 -0
  53. novelwriter/formats/toraw.py +91 -0
  54. novelwriter/gui/doceditor.py +342 -284
  55. novelwriter/gui/dochighlight.py +96 -84
  56. novelwriter/gui/docviewer.py +88 -31
  57. novelwriter/gui/docviewerpanel.py +17 -25
  58. novelwriter/gui/editordocument.py +17 -2
  59. novelwriter/gui/itemdetails.py +25 -28
  60. novelwriter/gui/mainmenu.py +129 -63
  61. novelwriter/gui/noveltree.py +45 -47
  62. novelwriter/gui/outline.py +196 -249
  63. novelwriter/gui/projtree.py +594 -1241
  64. novelwriter/gui/search.py +9 -10
  65. novelwriter/gui/sidebar.py +7 -6
  66. novelwriter/gui/theme.py +10 -5
  67. novelwriter/guimain.py +100 -196
  68. novelwriter/shared.py +66 -27
  69. novelwriter/text/counting.py +2 -0
  70. novelwriter/text/patterns.py +168 -60
  71. novelwriter/tools/manusbuild.py +14 -12
  72. novelwriter/tools/manuscript.py +120 -78
  73. novelwriter/tools/manussettings.py +424 -291
  74. novelwriter/tools/welcome.py +4 -4
  75. novelwriter/tools/writingstats.py +3 -3
  76. novelwriter/types.py +23 -7
  77. novelwriter/core/tohtml.py +0 -530
  78. novelwriter/core/tomarkdown.py +0 -252
  79. novelwriter/core/toqdoc.py +0 -419
  80. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
  81. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
  82. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
  83. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
@@ -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()
@@ -1072,7 +1105,9 @@ class GuiDocEditor(QPlainTextEdit):
1072
1105
  self._completer.setVisible(False)
1073
1106
 
1074
1107
  if self._doReplace and added == 1:
1075
- self._docAutoReplace(text)
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,16 +1204,16 @@ class GuiDocEditor(QPlainTextEdit):
1156
1204
  ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
1157
1205
  for option in suggest[:15]:
1158
1206
  action = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
1159
- action.triggered.connect(
1160
- lambda _, option=option: self._correctWord(sCursor, option)
1161
- )
1207
+ action.triggered.connect(qtLambda(self._correctWord, sCursor, option))
1162
1208
  else:
1163
1209
  trNone = self.tr("No Suggestions")
1164
1210
  ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {trNone}")
1165
1211
 
1166
1212
  ctxMenu.addSeparator()
1213
+ action = ctxMenu.addAction(self.tr("Ignore Word"))
1214
+ action.triggered.connect(qtLambda(self._addWord, word, block, False))
1167
1215
  action = ctxMenu.addAction(self.tr("Add Word to Dictionary"))
1168
- action.triggered.connect(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))
@@ -1173,30 +1221,6 @@ class GuiDocEditor(QPlainTextEdit):
1173
1221
 
1174
1222
  return
1175
1223
 
1176
- @pyqtSlot("QTextCursor", str)
1177
- def _correctWord(self, cursor: QTextCursor, word: str) -> None:
1178
- """Slot for the spell check context menu triggering the
1179
- replacement of a word with the word from the dictionary.
1180
- """
1181
- pos = cursor.selectionStart()
1182
- cursor.beginEditBlock()
1183
- cursor.removeSelectedText()
1184
- cursor.insertText(word)
1185
- cursor.endEditBlock()
1186
- cursor.setPosition(pos)
1187
- self.setTextCursor(cursor)
1188
- return
1189
-
1190
- @pyqtSlot(str, "QTextBlock")
1191
- def _addWord(self, word: str, block: QTextBlock) -> None:
1192
- """Slot for the spell check context menu triggered when the user
1193
- wants to add a word to the project dictionary.
1194
- """
1195
- logger.debug("Added '%s' to project dictionary", word)
1196
- SHARED.spelling.addWord(word)
1197
- self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1198
- return
1199
-
1200
1224
  @pyqtSlot()
1201
1225
  def _runDocumentTasks(self) -> None:
1202
1226
  """Run timer document tasks."""
@@ -1205,8 +1229,8 @@ class GuiDocEditor(QPlainTextEdit):
1205
1229
 
1206
1230
  if time() - self._lastEdit < 25.0:
1207
1231
  logger.debug("Running document tasks")
1208
- if not self.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,13 @@ class GuiDocEditor(QPlainTextEdit):
1223
1247
  """Process the word counter's finished signal."""
1224
1248
  if self._docHandle and self._nwItem:
1225
1249
  logger.debug("Updating word count")
1250
+ needsRefresh = wCount != self._nwItem.wordCount
1226
1251
  self._nwItem.setCharCount(cCount)
1227
1252
  self._nwItem.setWordCount(wCount)
1228
1253
  self._nwItem.setParaCount(pCount)
1229
- self.docCountsChanged.emit(self._docHandle, cCount, wCount, pCount)
1230
- self.docFooter.updateWordCount(wCount, False)
1254
+ if needsRefresh:
1255
+ self._nwItem.notifyToRefresh()
1256
+ self.docFooter.updateWordCount(wCount, False)
1231
1257
  return
1232
1258
 
1233
1259
  @pyqtSlot()
@@ -1236,10 +1262,10 @@ class GuiDocEditor(QPlainTextEdit):
1236
1262
  information to the footer, and start the selection word counter.
1237
1263
  """
1238
1264
  if self.textCursor().hasSelection():
1239
- if not self.timerSel.isActive():
1240
- self.timerSel.start()
1265
+ if not self._timerSel.isActive():
1266
+ self._timerSel.start()
1241
1267
  else:
1242
- self.timerSel.stop()
1268
+ self._timerSel.stop()
1243
1269
  self.docFooter.updateWordCount(0, False)
1244
1270
  return
1245
1271
 
@@ -1249,11 +1275,11 @@ class GuiDocEditor(QPlainTextEdit):
1249
1275
  if self._docHandle is None:
1250
1276
  return
1251
1277
 
1252
- if self.wCounterSel.isRunning():
1278
+ if self._wCounterSel.isRunning():
1253
1279
  logger.debug("Selection word counter is busy")
1254
1280
  return
1255
1281
 
1256
- SHARED.runInThreadPool(self.wCounterSel)
1282
+ SHARED.runInThreadPool(self._wCounterSel)
1257
1283
 
1258
1284
  return
1259
1285
 
@@ -1263,13 +1289,13 @@ class GuiDocEditor(QPlainTextEdit):
1263
1289
  if self._docHandle and self._nwItem:
1264
1290
  logger.debug("User selected %d words", wCount)
1265
1291
  self.docFooter.updateWordCount(wCount, True)
1266
- self.timerSel.stop()
1292
+ self._timerSel.stop()
1267
1293
  return
1268
1294
 
1269
1295
  @pyqtSlot()
1270
1296
  def _closeCurrentDocument(self) -> None:
1271
1297
  """Close the document. Forwarded to the main Gui."""
1272
- self.closeDocumentRequest.emit()
1298
+ self.closeEditorRequest.emit()
1273
1299
  self.docToolBar.setVisible(False)
1274
1300
  return
1275
1301
 
@@ -1875,6 +1901,28 @@ class GuiDocEditor(QPlainTextEdit):
1875
1901
  # Internal Functions
1876
1902
  ##
1877
1903
 
1904
+ def _correctWord(self, cursor: QTextCursor, word: str) -> None:
1905
+ """Slot for the spell check context menu triggering the
1906
+ replacement of a word with the word from the dictionary.
1907
+ """
1908
+ pos = cursor.selectionStart()
1909
+ cursor.beginEditBlock()
1910
+ cursor.removeSelectedText()
1911
+ cursor.insertText(word)
1912
+ cursor.endEditBlock()
1913
+ cursor.setPosition(pos)
1914
+ self.setTextCursor(cursor)
1915
+ return
1916
+
1917
+ def _addWord(self, word: str, block: QTextBlock, save: bool) -> None:
1918
+ """Slot for the spell check context menu triggered when the user
1919
+ wants to add a word to the project dictionary.
1920
+ """
1921
+ logger.debug("Added '%s' to project dictionary, %s", word, "saved" if save else "unsaved")
1922
+ SHARED.spelling.addWord(word, save=save)
1923
+ self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1924
+ return
1925
+
1878
1926
  def _processTag(self, cursor: QTextCursor | None = None,
1879
1927
  follow: bool = True, create: bool = False) -> nwTrinary:
1880
1928
  """Activated by Ctrl+Enter. Checks that we're in a block
@@ -1938,119 +1986,6 @@ class GuiDocEditor(QPlainTextEdit):
1938
1986
  self.requestProjectItemRenamed.emit(self._docHandle, text)
1939
1987
  return
1940
1988
 
1941
- def _openContextFromCursor(self) -> None:
1942
- """Open the spell check context menu at the cursor."""
1943
- self._openContextMenu(self.cursorRect().center())
1944
- return
1945
-
1946
- def _docAutoReplace(self, text: str) -> None:
1947
- """Auto-replace text elements based on main configuration."""
1948
- cursor = self.textCursor()
1949
- tPos = cursor.positionInBlock()
1950
- tLen = len(text)
1951
-
1952
- if tLen < 1 or tPos-1 > tLen:
1953
- return
1954
-
1955
- tOne = text[tPos-1:tPos]
1956
- tTwo = text[tPos-2:tPos]
1957
- tThree = text[tPos-3:tPos]
1958
-
1959
- if not tOne:
1960
- return
1961
-
1962
- nDelete = 0
1963
- tInsert = tOne
1964
-
1965
- if self._typRepDQuote and tTwo[:1].isspace() and tTwo.endswith('"'):
1966
- nDelete = 1
1967
- tInsert = self._typDQuoteO
1968
-
1969
- elif self._typRepDQuote and tOne == '"':
1970
- nDelete = 1
1971
- if tPos == 1:
1972
- tInsert = self._typDQuoteO
1973
- elif tPos == 2 and tTwo == '>"':
1974
- tInsert = self._typDQuoteO
1975
- elif tPos == 3 and tThree == '>>"':
1976
- tInsert = self._typDQuoteO
1977
- else:
1978
- tInsert = self._typDQuoteC
1979
-
1980
- elif self._typRepSQuote and tTwo[:1].isspace() and tTwo.endswith("'"):
1981
- nDelete = 1
1982
- tInsert = self._typSQuoteO
1983
-
1984
- elif self._typRepSQuote and tOne == "'":
1985
- nDelete = 1
1986
- if tPos == 1:
1987
- tInsert = self._typSQuoteO
1988
- elif tPos == 2 and tTwo == ">'":
1989
- tInsert = self._typSQuoteO
1990
- elif tPos == 3 and tThree == ">>'":
1991
- tInsert = self._typSQuoteO
1992
- else:
1993
- tInsert = self._typSQuoteC
1994
-
1995
- elif self._typRepDash and tThree == "---":
1996
- nDelete = 3
1997
- tInsert = nwUnicode.U_EMDASH
1998
-
1999
- elif self._typRepDash and tTwo == "--":
2000
- nDelete = 2
2001
- tInsert = nwUnicode.U_ENDASH
2002
-
2003
- elif self._typRepDash and tTwo == nwUnicode.U_ENDASH + "-":
2004
- nDelete = 2
2005
- tInsert = nwUnicode.U_EMDASH
2006
-
2007
- elif self._typRepDots and tThree == "...":
2008
- nDelete = 3
2009
- tInsert = nwUnicode.U_HELLIP
2010
-
2011
- elif tOne == nwUnicode.U_LSEP:
2012
- # This resolves issue #1150
2013
- nDelete = 1
2014
- tInsert = nwUnicode.U_PSEP
2015
-
2016
- tCheck = tInsert
2017
- if self._typPadBefore and tCheck in self._typPadBefore:
2018
- if self._allowSpaceBeforeColon(text, tCheck):
2019
- nDelete = max(nDelete, 1)
2020
- chkPos = tPos - nDelete - 1
2021
- if chkPos >= 0 and text[chkPos].isspace():
2022
- # Strip existing space before inserting a new (#1061)
2023
- nDelete += 1
2024
- tInsert = self._typPadChar + tInsert
2025
-
2026
- if self._typPadAfter and tCheck in self._typPadAfter:
2027
- if self._allowSpaceBeforeColon(text, tCheck):
2028
- nDelete = max(nDelete, 1)
2029
- tInsert = tInsert + self._typPadChar
2030
-
2031
- if nDelete > 0:
2032
- cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete)
2033
- cursor.insertText(tInsert)
2034
-
2035
- # Re-highlight, since the auto-replace sometimes interferes with it
2036
- self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
2037
-
2038
- return
2039
-
2040
- @staticmethod
2041
- def _allowSpaceBeforeColon(text: str, char: str) -> bool:
2042
- """Special checker function only used by the insert space
2043
- feature for French, Spanish, etc, so it doesn't insert a
2044
- space before colons in meta data lines. See issue #1090.
2045
- """
2046
- if char == ":" and len(text) > 1:
2047
- if text[0] == "@":
2048
- return False
2049
- if text[0] == "%":
2050
- if text[1:].lstrip()[:9].lower() == "synopsis:":
2051
- return False
2052
- return True
2053
-
2054
1989
  def _autoSelect(self) -> QTextCursor:
2055
1990
  """Return a cursor which may or may not have a selection based
2056
1991
  on user settings and document action. The selection will be the
@@ -2173,7 +2108,7 @@ class MetaCompleter(QMenu):
2173
2108
  suffix = ""
2174
2109
  options = list(filter(
2175
2110
  lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
2176
- nwKeyWords.KEY_CLASS.get(kw.strip(), nwItemClass.NO_CLASS)
2111
+ nwKeyWords.KEY_CLASS.get(kw.strip())
2177
2112
  )
2178
2113
  ))[:15]
2179
2114
 
@@ -2183,7 +2118,7 @@ class MetaCompleter(QMenu):
2183
2118
  for value in sorted(options):
2184
2119
  rep = value + suffix
2185
2120
  action = self.addAction(value)
2186
- action.triggered.connect(lambda _, r=rep: self._emitComplete(offset, length, r))
2121
+ action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
2187
2122
 
2188
2123
  return True
2189
2124
 
@@ -2259,6 +2194,131 @@ class BackgroundWordCounterSignals(QObject):
2259
2194
  countsReady = pyqtSignal(int, int, int)
2260
2195
 
2261
2196
 
2197
+ class TextAutoReplace:
2198
+
2199
+ __slots__ = (
2200
+ "_quoteSO", "_quoteSC", "_quoteDO", "_quoteDC",
2201
+ "_replaceSQuote", "_replaceDQuote", "_replaceDash", "_replaceDots",
2202
+ "_padChar", "_padBefore", "_padAfter", "_doPadBefore", "_doPadAfter",
2203
+ )
2204
+
2205
+ def __init__(self) -> None:
2206
+ self.initSettings()
2207
+ return
2208
+
2209
+ def initSettings(self) -> None:
2210
+ """Initialise the auto-replace settings from config."""
2211
+ self._quoteSO = CONFIG.fmtSQuoteOpen
2212
+ self._quoteSC = CONFIG.fmtSQuoteClose
2213
+ self._quoteDO = CONFIG.fmtDQuoteOpen
2214
+ self._quoteDC = CONFIG.fmtDQuoteClose
2215
+
2216
+ self._replaceSQuote = CONFIG.doReplaceSQuote
2217
+ self._replaceDQuote = CONFIG.doReplaceDQuote
2218
+ self._replaceDash = CONFIG.doReplaceDash
2219
+ self._replaceDots = CONFIG.doReplaceDots
2220
+
2221
+ self._padChar = nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP
2222
+ self._padBefore = CONFIG.fmtPadBefore
2223
+ self._padAfter = CONFIG.fmtPadAfter
2224
+ self._doPadBefore = bool(CONFIG.fmtPadBefore)
2225
+ self._doPadAfter = bool(CONFIG.fmtPadAfter)
2226
+ return
2227
+
2228
+ def process(self, text: str, cursor: QTextCursor) -> bool:
2229
+ """Auto-replace text elements based on main configuration.
2230
+ Returns True if anything was changed.
2231
+ """
2232
+ pos = cursor.positionInBlock()
2233
+ length = len(text)
2234
+ if length < 1 or pos-1 > length:
2235
+ return False
2236
+
2237
+ delete, insert = self._determine(text, pos)
2238
+ if insert == "":
2239
+ return False
2240
+
2241
+ check = insert
2242
+ if self._doPadBefore and check in self._padBefore:
2243
+ if not (check == ":" and length > 1 and text[0] == "@"):
2244
+ delete = max(delete, 1)
2245
+ chkPos = pos - delete - 1
2246
+ if chkPos >= 0 and text[chkPos].isspace():
2247
+ # Strip existing space before inserting a new (#1061)
2248
+ delete += 1
2249
+ insert = self._padChar + insert
2250
+
2251
+ if self._doPadAfter and check in self._padAfter:
2252
+ if not (check == ":" and length > 1 and text[0] == "@"):
2253
+ delete = max(delete, 1)
2254
+ insert = insert + self._padChar
2255
+
2256
+ if delete > 0:
2257
+ cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
2258
+ cursor.insertText(insert)
2259
+ return True
2260
+
2261
+ return False
2262
+
2263
+ def _determine(self, text: str, pos: int) -> tuple[int, str]:
2264
+ """Determine what to replace, if anything."""
2265
+ t1 = text[pos-1:pos]
2266
+ t2 = text[pos-2:pos]
2267
+ t3 = text[pos-3:pos]
2268
+ t4 = text[pos-4:pos]
2269
+ if t1 == "":
2270
+ # Return early if there is nothing to check
2271
+ return 0, ""
2272
+
2273
+ leading = t2[:1].isspace()
2274
+ if self._replaceDQuote:
2275
+ if leading and t2.endswith('"'):
2276
+ return 1, self._quoteDO
2277
+ elif t1 == '"':
2278
+ if pos == 1:
2279
+ return 1, self._quoteDO
2280
+ elif pos == 2 and t2 == '>"':
2281
+ return 1, self._quoteDO
2282
+ elif pos == 3 and t3 == '>>"':
2283
+ return 1, self._quoteDO
2284
+ else:
2285
+ return 1, self._quoteDC
2286
+
2287
+ if self._replaceSQuote:
2288
+ if leading and t2.endswith("'"):
2289
+ return 1, self._quoteSO
2290
+ elif t1 == "'":
2291
+ if pos == 1:
2292
+ return 1, self._quoteSO
2293
+ elif pos == 2 and t2 == ">'":
2294
+ return 1, self._quoteSO
2295
+ elif pos == 3 and t3 == ">>'":
2296
+ return 1, self._quoteSO
2297
+ else:
2298
+ return 1, self._quoteSC
2299
+
2300
+ if self._replaceDash:
2301
+ if t4 == "----":
2302
+ return 4, "\u2015" # Horizontal bar
2303
+ elif t3 == "---":
2304
+ return 3, "\u2014" # Long dash
2305
+ elif t2 == "--":
2306
+ return 2, "\u2013" # Short dash
2307
+ elif t2 == "\u2013-":
2308
+ return 2, "\u2014" # Long dash
2309
+ elif t2 == "\u2014-":
2310
+ return 2, "\u2015" # Horizontal bar
2311
+
2312
+ if self._replaceDots and t3 == "...":
2313
+ return 3, "\u2026" # Ellipsis
2314
+
2315
+ if t1 == "\u2028": # Line separator
2316
+ # This resolves issue #1150
2317
+ return 1, "\u2029" # Paragraph separator
2318
+
2319
+ return 0, t1
2320
+
2321
+
2262
2322
  class GuiDocToolBar(QWidget):
2263
2323
  """The Formatting and Options Fold Out Menu
2264
2324
 
@@ -2283,61 +2343,61 @@ class GuiDocToolBar(QWidget):
2283
2343
  self.tbBoldMD = NIconToolButton(self, iSz)
2284
2344
  self.tbBoldMD.setToolTip(self.tr("Markdown Bold"))
2285
2345
  self.tbBoldMD.clicked.connect(
2286
- lambda: self.requestDocAction.emit(nwDocAction.MD_BOLD)
2346
+ qtLambda(self.requestDocAction.emit, nwDocAction.MD_BOLD)
2287
2347
  )
2288
2348
 
2289
2349
  self.tbItalicMD = NIconToolButton(self, iSz)
2290
2350
  self.tbItalicMD.setToolTip(self.tr("Markdown Italic"))
2291
2351
  self.tbItalicMD.clicked.connect(
2292
- lambda: self.requestDocAction.emit(nwDocAction.MD_ITALIC)
2352
+ qtLambda(self.requestDocAction.emit, nwDocAction.MD_ITALIC)
2293
2353
  )
2294
2354
 
2295
2355
  self.tbStrikeMD = NIconToolButton(self, iSz)
2296
2356
  self.tbStrikeMD.setToolTip(self.tr("Markdown Strikethrough"))
2297
2357
  self.tbStrikeMD.clicked.connect(
2298
- lambda: self.requestDocAction.emit(nwDocAction.MD_STRIKE)
2358
+ qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE)
2299
2359
  )
2300
2360
 
2301
2361
  self.tbBold = NIconToolButton(self, iSz)
2302
2362
  self.tbBold.setToolTip(self.tr("Shortcode Bold"))
2303
2363
  self.tbBold.clicked.connect(
2304
- lambda: self.requestDocAction.emit(nwDocAction.SC_BOLD)
2364
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_BOLD)
2305
2365
  )
2306
2366
 
2307
2367
  self.tbItalic = NIconToolButton(self, iSz)
2308
2368
  self.tbItalic.setToolTip(self.tr("Shortcode Italic"))
2309
2369
  self.tbItalic.clicked.connect(
2310
- lambda: self.requestDocAction.emit(nwDocAction.SC_ITALIC)
2370
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_ITALIC)
2311
2371
  )
2312
2372
 
2313
2373
  self.tbStrike = NIconToolButton(self, iSz)
2314
2374
  self.tbStrike.setToolTip(self.tr("Shortcode Strikethrough"))
2315
2375
  self.tbStrike.clicked.connect(
2316
- lambda: self.requestDocAction.emit(nwDocAction.SC_STRIKE)
2376
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_STRIKE)
2317
2377
  )
2318
2378
 
2319
2379
  self.tbUnderline = NIconToolButton(self, iSz)
2320
2380
  self.tbUnderline.setToolTip(self.tr("Shortcode Underline"))
2321
2381
  self.tbUnderline.clicked.connect(
2322
- lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE)
2382
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_ULINE)
2323
2383
  )
2324
2384
 
2325
2385
  self.tbMark = NIconToolButton(self, iSz)
2326
2386
  self.tbMark.setToolTip(self.tr("Shortcode Highlight"))
2327
2387
  self.tbMark.clicked.connect(
2328
- lambda: self.requestDocAction.emit(nwDocAction.SC_MARK)
2388
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_MARK)
2329
2389
  )
2330
2390
 
2331
2391
  self.tbSuperscript = NIconToolButton(self, iSz)
2332
2392
  self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript"))
2333
2393
  self.tbSuperscript.clicked.connect(
2334
- lambda: self.requestDocAction.emit(nwDocAction.SC_SUP)
2394
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUP)
2335
2395
  )
2336
2396
 
2337
2397
  self.tbSubscript = NIconToolButton(self, iSz)
2338
2398
  self.tbSubscript.setToolTip(self.tr("Shortcode Subscript"))
2339
2399
  self.tbSubscript.clicked.connect(
2340
- lambda: self.requestDocAction.emit(nwDocAction.SC_SUB)
2400
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUB)
2341
2401
  )
2342
2402
 
2343
2403
  # Assemble
@@ -2801,7 +2861,7 @@ class GuiDocEditHeader(QWidget):
2801
2861
  self.tbButton = NIconToolButton(self, iSz)
2802
2862
  self.tbButton.setVisible(False)
2803
2863
  self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
2804
- self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
2864
+ self.tbButton.clicked.connect(qtLambda(self.toggleToolBarRequest.emit))
2805
2865
 
2806
2866
  self.outlineButton = NIconToolButton(self, iSz)
2807
2867
  self.outlineButton.setVisible(False)
@@ -2816,7 +2876,7 @@ class GuiDocEditHeader(QWidget):
2816
2876
  self.minmaxButton = NIconToolButton(self, iSz)
2817
2877
  self.minmaxButton.setVisible(False)
2818
2878
  self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
2819
- self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit())
2879
+ self.minmaxButton.clicked.connect(qtLambda(self.docEditor.toggleFocusModeRequest.emit))
2820
2880
 
2821
2881
  self.closeButton = NIconToolButton(self, iSz)
2822
2882
  self.closeButton.setVisible(False)
@@ -2879,9 +2939,7 @@ class GuiDocEditHeader(QWidget):
2879
2939
  self.outlineMenu.clear()
2880
2940
  for number, text in data.items():
2881
2941
  action = self.outlineMenu.addAction(text)
2882
- action.triggered.connect(
2883
- lambda _, number=number: self._gotoBlock(number)
2884
- )
2942
+ action.triggered.connect(qtLambda(self._gotoBlock, number))
2885
2943
  self._docOutline = data
2886
2944
  logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
2887
2945
  return
@@ -2894,7 +2952,7 @@ class GuiDocEditHeader(QWidget):
2894
2952
 
2895
2953
  def updateTheme(self) -> None:
2896
2954
  """Update theme elements."""
2897
- self.tbButton.setThemeIcon("menu")
2955
+ self.tbButton.setThemeIcon("toolbar")
2898
2956
  self.outlineButton.setThemeIcon("list")
2899
2957
  self.searchButton.setThemeIcon("search")
2900
2958
  self.minmaxButton.setThemeIcon("maximise")
@@ -2938,7 +2996,7 @@ class GuiDocEditHeader(QWidget):
2938
2996
 
2939
2997
  if CONFIG.showFullPath:
2940
2998
  self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
2941
- [name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)]
2999
+ [name for name in SHARED.project.tree.itemPath(tHandle, asName=True)]
2942
3000
  )))
2943
3001
  else:
2944
3002
  self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")