novelWriter 2.5.1__py3-none-any.whl → 2.6b1__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 (64) hide show
  1. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/METADATA +2 -1
  2. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/RECORD +61 -56
  3. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +3 -3
  5. novelwriter/assets/i18n/project_en_GB.json +1 -0
  6. novelwriter/assets/icons/typicons_dark/icons.conf +1 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  8. novelwriter/assets/icons/typicons_light/icons.conf +1 -0
  9. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  10. novelwriter/assets/manual.pdf +0 -0
  11. novelwriter/assets/sample.zip +0 -0
  12. novelwriter/assets/themes/default_light.conf +2 -2
  13. novelwriter/common.py +63 -0
  14. novelwriter/config.py +10 -3
  15. novelwriter/constants.py +153 -60
  16. novelwriter/core/buildsettings.py +66 -39
  17. novelwriter/core/coretools.py +34 -22
  18. novelwriter/core/docbuild.py +130 -169
  19. novelwriter/core/index.py +29 -18
  20. novelwriter/core/item.py +2 -2
  21. novelwriter/core/options.py +4 -1
  22. novelwriter/core/spellcheck.py +9 -14
  23. novelwriter/dialogs/preferences.py +45 -32
  24. novelwriter/dialogs/projectsettings.py +3 -3
  25. novelwriter/enum.py +29 -23
  26. novelwriter/extensions/configlayout.py +24 -11
  27. novelwriter/extensions/modified.py +13 -1
  28. novelwriter/extensions/pagedsidebar.py +5 -5
  29. novelwriter/formats/shared.py +155 -0
  30. novelwriter/formats/todocx.py +1195 -0
  31. novelwriter/formats/tohtml.py +452 -0
  32. novelwriter/{core → formats}/tokenizer.py +483 -485
  33. novelwriter/formats/tomarkdown.py +217 -0
  34. novelwriter/{core → formats}/toodt.py +270 -320
  35. novelwriter/formats/toqdoc.py +436 -0
  36. novelwriter/formats/toraw.py +91 -0
  37. novelwriter/gui/doceditor.py +240 -193
  38. novelwriter/gui/dochighlight.py +96 -84
  39. novelwriter/gui/docviewer.py +56 -30
  40. novelwriter/gui/docviewerpanel.py +3 -3
  41. novelwriter/gui/editordocument.py +17 -2
  42. novelwriter/gui/itemdetails.py +8 -4
  43. novelwriter/gui/mainmenu.py +121 -60
  44. novelwriter/gui/noveltree.py +35 -37
  45. novelwriter/gui/outline.py +186 -238
  46. novelwriter/gui/projtree.py +142 -131
  47. novelwriter/gui/sidebar.py +7 -6
  48. novelwriter/gui/theme.py +5 -4
  49. novelwriter/guimain.py +43 -155
  50. novelwriter/shared.py +14 -4
  51. novelwriter/text/counting.py +2 -0
  52. novelwriter/text/patterns.py +155 -59
  53. novelwriter/tools/manusbuild.py +1 -1
  54. novelwriter/tools/manuscript.py +121 -78
  55. novelwriter/tools/manussettings.py +403 -260
  56. novelwriter/tools/welcome.py +4 -4
  57. novelwriter/tools/writingstats.py +3 -3
  58. novelwriter/types.py +16 -6
  59. novelwriter/core/tohtml.py +0 -530
  60. novelwriter/core/tomarkdown.py +0 -252
  61. novelwriter/core/toqdoc.py +0 -419
  62. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/LICENSE.md +0 -0
  63. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/entry_points.txt +0 -0
  64. {novelWriter-2.5.1.dist-info → novelWriter-2.6b1.dist-info}/top_level.txt +0 -0
@@ -36,6 +36,7 @@ import logging
36
36
 
37
37
  from enum import Enum
38
38
  from time import time
39
+ from typing import NamedTuple
39
40
 
40
41
  from PyQt5.QtCore import (
41
42
  QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
@@ -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 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
+ nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwItemType,
60
+ 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__)
@@ -80,6 +84,21 @@ class _SelectAction(Enum):
80
84
  MOVE_AFTER = 3
81
85
 
82
86
 
87
+ class AutoReplaceConfig(NamedTuple):
88
+
89
+ typPadChar: str
90
+ typSQuoteO: str
91
+ typSQuoteC: str
92
+ typDQuoteO: str
93
+ typDQuoteC: str
94
+ typRepDQuote: bool
95
+ typRepSQuote: bool
96
+ typRepDash: bool
97
+ typRepDots: bool
98
+ typPadBefore: str
99
+ typPadAfter: str
100
+
101
+
83
102
  class GuiDocEditor(QPlainTextEdit):
84
103
  """Gui Widget: Main Document Editor"""
85
104
 
@@ -89,20 +108,21 @@ class GuiDocEditor(QPlainTextEdit):
89
108
  )
90
109
 
91
110
  # Custom Signals
92
- statusMessage = pyqtSignal(str)
111
+ closeEditorRequest = pyqtSignal()
93
112
  docCountsChanged = pyqtSignal(str, int, int, int)
94
113
  docTextChanged = pyqtSignal(str, float)
95
114
  editedStatusChanged = pyqtSignal(bool)
115
+ itemHandleChanged = pyqtSignal(str)
96
116
  loadDocumentTagRequest = pyqtSignal(str, Enum)
97
- novelStructureChanged = pyqtSignal()
98
117
  novelItemMetaChanged = pyqtSignal(str)
99
- spellCheckStateChanged = pyqtSignal(bool)
100
- closeDocumentRequest = pyqtSignal()
101
- toggleFocusModeRequest = pyqtSignal()
102
- requestProjectItemSelected = pyqtSignal(str, bool)
103
- requestProjectItemRenamed = pyqtSignal(str, str)
118
+ novelStructureChanged = pyqtSignal()
104
119
  requestNewNoteCreation = pyqtSignal(str, nwItemClass)
105
120
  requestNextDocument = pyqtSignal(str, bool)
121
+ requestProjectItemRenamed = pyqtSignal(str, str)
122
+ requestProjectItemSelected = pyqtSignal(str, bool)
123
+ spellCheckStateChanged = pyqtSignal(bool)
124
+ toggleFocusModeRequest = pyqtSignal()
125
+ updateStatusMessage = pyqtSignal(str)
106
126
 
107
127
  def __init__(self, parent: QWidget) -> None:
108
128
  super().__init__(parent=parent)
@@ -124,17 +144,19 @@ class GuiDocEditor(QPlainTextEdit):
124
144
  self._doReplace = False # Switch to temporarily disable auto-replace
125
145
 
126
146
  # 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 = ""
147
+ self._typConf = AutoReplaceConfig(
148
+ typPadChar=" ",
149
+ typSQuoteO="'",
150
+ typSQuoteC="'",
151
+ typDQuoteO='"',
152
+ typDQuoteC='"',
153
+ typRepSQuote=False,
154
+ typRepDQuote=False,
155
+ typRepDash=False,
156
+ typRepDots=False,
157
+ typPadBefore="",
158
+ typPadAfter="",
159
+ )
138
160
 
139
161
  # Completer
140
162
  self._completer = MetaCompleter(self)
@@ -177,12 +199,12 @@ class GuiDocEditor(QPlainTextEdit):
177
199
  self.keyContext.activated.connect(self._openContextFromCursor)
178
200
 
179
201
  self.followTag1 = QShortcut(self)
180
- self.followTag1.setKey(Qt.Key.Key_Return | QtModCtrl)
202
+ self.followTag1.setKey("Ctrl+Return")
181
203
  self.followTag1.setContext(Qt.ShortcutContext.WidgetShortcut)
182
204
  self.followTag1.activated.connect(self._processTag)
183
205
 
184
206
  self.followTag2 = QShortcut(self)
185
- self.followTag2.setKey(Qt.Key.Key_Enter | QtModCtrl)
207
+ self.followTag2.setKey("Ctrl+Enter")
186
208
  self.followTag2.setContext(Qt.ShortcutContext.WidgetShortcut)
187
209
  self.followTag2.activated.connect(self._processTag)
188
210
 
@@ -270,6 +292,8 @@ class GuiDocEditor(QPlainTextEdit):
270
292
  self.docFooter.setHandle(self._docHandle)
271
293
  self.docToolBar.setVisible(False)
272
294
 
295
+ self.itemHandleChanged.emit("")
296
+
273
297
  return
274
298
 
275
299
  def updateTheme(self) -> None:
@@ -304,21 +328,19 @@ class GuiDocEditor(QPlainTextEdit):
304
328
  created, and when the user changes the main editor preferences.
305
329
  """
306
330
  # 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
331
+ self._typConf = AutoReplaceConfig(
332
+ typPadChar=nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP,
333
+ typSQuoteO=CONFIG.fmtSQuoteOpen,
334
+ typSQuoteC=CONFIG.fmtSQuoteClose,
335
+ typDQuoteO=CONFIG.fmtDQuoteOpen,
336
+ typDQuoteC=CONFIG.fmtDQuoteClose,
337
+ typRepSQuote=CONFIG.doReplaceSQuote,
338
+ typRepDQuote=CONFIG.doReplaceDQuote,
339
+ typRepDash=CONFIG.doReplaceDash,
340
+ typRepDots=CONFIG.doReplaceDots,
341
+ typPadBefore=CONFIG.fmtPadBefore,
342
+ typPadAfter=CONFIG.fmtPadAfter,
343
+ )
322
344
 
323
345
  # Reload spell check and dictionaries
324
346
  SHARED.updateSpellCheckLanguage()
@@ -354,14 +376,14 @@ class GuiDocEditor(QPlainTextEdit):
354
376
  # Scrolling
355
377
  self.setCenterOnScroll(CONFIG.scrollPastEnd)
356
378
  if CONFIG.hideVScroll:
357
- self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
379
+ self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
358
380
  else:
359
- self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
381
+ self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
360
382
 
361
383
  if CONFIG.hideHScroll:
362
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
384
+ self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
363
385
  else:
364
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
386
+ self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
365
387
 
366
388
  # Refresh the tab stops
367
389
  self.setTabStopDistance(CONFIG.getTabWidth())
@@ -388,9 +410,12 @@ class GuiDocEditor(QPlainTextEdit):
388
410
  """
389
411
  self._nwDocument = SHARED.project.storage.getDocument(tHandle)
390
412
  self._nwItem = self._nwDocument.nwItem
413
+ if not ((nwItem := self._nwItem) and nwItem.itemType == nwItemType.FILE):
414
+ logger.debug("Requested item '%s' is not a document", tHandle)
415
+ self.clearEditor()
416
+ return False
391
417
 
392
- docText = self._nwDocument.readDocument()
393
- if docText is None:
418
+ if (docText := self._nwDocument.readDocument()) is None:
394
419
  # There was an I/O error
395
420
  self.clearEditor()
396
421
  return False
@@ -411,10 +436,10 @@ class GuiDocEditor(QPlainTextEdit):
411
436
  self.setReadOnly(False)
412
437
  self.updateDocMargins()
413
438
 
414
- if tLine is None and self._nwItem is not None:
415
- self.setCursorPosition(self._nwItem.cursorPos)
416
- elif isinstance(tLine, int):
439
+ if isinstance(tLine, int):
417
440
  self.setCursorLine(tLine)
441
+ else:
442
+ self.setCursorPosition(nwItem.cursorPos)
418
443
 
419
444
  self.docHeader.setHandle(tHandle)
420
445
  self.docFooter.setHandle(tHandle)
@@ -430,11 +455,15 @@ class GuiDocEditor(QPlainTextEdit):
430
455
  self._qDocument.clearUndoRedoStacks()
431
456
  self.docToolBar.setVisible(CONFIG.showEditToolBar)
432
457
 
433
- QApplication.restoreOverrideCursor()
458
+ # Process State Changes
459
+ SHARED.project.data.setLastHandle(tHandle, "editor")
460
+ self.itemHandleChanged.emit(tHandle)
434
461
 
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))
462
+ # Finalise
463
+ QApplication.restoreOverrideCursor()
464
+ self.updateStatusMessage.emit(
465
+ self.tr("Opened Document: {0}").format(nwItem.itemName)
466
+ )
438
467
 
439
468
  return True
440
469
 
@@ -505,7 +534,7 @@ class GuiDocEditor(QPlainTextEdit):
505
534
  self.docFooter.updateInfo()
506
535
 
507
536
  # Update the status bar
508
- self.statusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName))
537
+ self.updateStatusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName))
509
538
 
510
539
  return True
511
540
 
@@ -700,7 +729,7 @@ class GuiDocEditor(QPlainTextEdit):
700
729
  self._qDocument.syntaxHighlighter.rehighlight()
701
730
  QApplication.restoreOverrideCursor()
702
731
  logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
703
- self.statusMessage.emit(self.tr("Spell check complete"))
732
+ self.updateStatusMessage.emit(self.tr("Spell check complete"))
704
733
  return
705
734
 
706
735
  ##
@@ -724,6 +753,7 @@ class GuiDocEditor(QPlainTextEdit):
724
753
 
725
754
  logger.debug("Requesting action: %s", action.name)
726
755
 
756
+ tConf = self._typConf
727
757
  self._allowAutoReplace(False)
728
758
  if action == nwDocAction.UNDO:
729
759
  self.undo()
@@ -742,9 +772,9 @@ class GuiDocEditor(QPlainTextEdit):
742
772
  elif action == nwDocAction.MD_STRIKE:
743
773
  self._toggleFormat(2, "~")
744
774
  elif action == nwDocAction.S_QUOTE:
745
- self._wrapSelection(self._typSQuoteO, self._typSQuoteC)
775
+ self._wrapSelection(tConf.typSQuoteO, tConf.typSQuoteC)
746
776
  elif action == nwDocAction.D_QUOTE:
747
- self._wrapSelection(self._typDQuoteO, self._typDQuoteC)
777
+ self._wrapSelection(tConf.typDQuoteO, tConf.typDQuoteC)
748
778
  elif action == nwDocAction.SEL_ALL:
749
779
  self._makeSelection(QTextCursor.SelectionType.Document)
750
780
  elif action == nwDocAction.SEL_PARA:
@@ -770,9 +800,9 @@ class GuiDocEditor(QPlainTextEdit):
770
800
  elif action == nwDocAction.BLOCK_HSC:
771
801
  self._formatBlock(nwDocAction.BLOCK_HSC)
772
802
  elif action == nwDocAction.REPL_SNG:
773
- self._replaceQuotes("'", self._typSQuoteO, self._typSQuoteC)
803
+ self._replaceQuotes("'", tConf.typSQuoteO, tConf.typSQuoteC)
774
804
  elif action == nwDocAction.REPL_DBL:
775
- self._replaceQuotes("\"", self._typDQuoteO, self._typDQuoteC)
805
+ self._replaceQuotes("\"", tConf.typDQuoteO, tConf.typDQuoteC)
776
806
  elif action == nwDocAction.RM_BREAKS:
777
807
  self._removeInParLineBreaks()
778
808
  elif action == nwDocAction.ALIGN_L:
@@ -844,13 +874,13 @@ class GuiDocEditor(QPlainTextEdit):
844
874
  text = insert
845
875
  elif isinstance(insert, nwDocInsert):
846
876
  if insert == nwDocInsert.QUOTE_LS:
847
- text = self._typSQuoteO
877
+ text = self._typConf.typSQuoteO
848
878
  elif insert == nwDocInsert.QUOTE_RS:
849
- text = self._typSQuoteC
879
+ text = self._typConf.typSQuoteC
850
880
  elif insert == nwDocInsert.QUOTE_LD:
851
- text = self._typDQuoteO
881
+ text = self._typConf.typDQuoteO
852
882
  elif insert == nwDocInsert.QUOTE_RD:
853
- text = self._typDQuoteC
883
+ text = self._typConf.typDQuoteC
854
884
  elif insert == nwDocInsert.SYNOPSIS:
855
885
  text = "%Synopsis: "
856
886
  block = True
@@ -877,6 +907,8 @@ class GuiDocEditor(QPlainTextEdit):
877
907
  after = False
878
908
  elif insert == nwDocInsert.FOOTNOTE:
879
909
  self._insertCommentStructure(nwComment.FOOTNOTE)
910
+ elif insert == nwDocInsert.LINE_BRK:
911
+ text = nwShortcode.BREAK
880
912
 
881
913
  if text:
882
914
  if block:
@@ -982,8 +1014,13 @@ class GuiDocEditor(QPlainTextEdit):
982
1014
  pressed, check if we're clicking on a tag, and trigger the
983
1015
  follow tag function.
984
1016
  """
985
- if QApplication.keyboardModifiers() == QtModCtrl:
986
- self._processTag(self.cursorForPosition(event.pos()))
1017
+ if event.modifiers() & QtModCtrl == QtModCtrl:
1018
+ cursor = self.cursorForPosition(event.pos())
1019
+ mData, mType = self._qDocument.metaDataAtPos(cursor.position())
1020
+ if mData and mType == "url":
1021
+ SHARED.openWebsite(mData)
1022
+ else:
1023
+ self._processTag(cursor)
987
1024
  super().mouseReleaseEvent(event)
988
1025
  return
989
1026
 
@@ -1095,6 +1132,12 @@ class GuiDocEditor(QPlainTextEdit):
1095
1132
  self._completer.hide()
1096
1133
  return
1097
1134
 
1135
+ @pyqtSlot()
1136
+ def _openContextFromCursor(self) -> None:
1137
+ """Open the spell check context menu at the cursor."""
1138
+ self._openContextMenu(self.cursorRect().center())
1139
+ return
1140
+
1098
1141
  @pyqtSlot("QPoint")
1099
1142
  def _openContextMenu(self, pos: QPoint) -> None:
1100
1143
  """Open the editor context menu at a given coordinate."""
@@ -1106,41 +1149,48 @@ class GuiDocEditor(QPlainTextEdit):
1106
1149
  ctxMenu.setObjectName("ContextMenu")
1107
1150
  if pBlock.userState() == BLOCK_TITLE:
1108
1151
  action = ctxMenu.addAction(self.tr("Set as Document Name"))
1109
- action.triggered.connect(lambda: self._emitRenameItem(pBlock))
1152
+ action.triggered.connect(qtLambda(self._emitRenameItem, pBlock))
1153
+
1154
+ # URL
1155
+ (mData, mType) = self._qDocument.metaDataAtPos(pCursor.position())
1156
+ if mData and mType == "url":
1157
+ action = ctxMenu.addAction(self.tr("Open URL"))
1158
+ action.triggered.connect(qtLambda(SHARED.openWebsite, mData))
1159
+ ctxMenu.addSeparator()
1110
1160
 
1111
1161
  # Follow
1112
1162
  status = self._processTag(cursor=pCursor, follow=False)
1113
1163
  if status == nwTrinary.POSITIVE:
1114
1164
  action = ctxMenu.addAction(self.tr("Follow Tag"))
1115
- action.triggered.connect(lambda: self._processTag(cursor=pCursor, follow=True))
1165
+ action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, follow=True))
1116
1166
  ctxMenu.addSeparator()
1117
1167
  elif status == nwTrinary.NEGATIVE:
1118
1168
  action = ctxMenu.addAction(self.tr("Create Note for Tag"))
1119
- action.triggered.connect(lambda: self._processTag(cursor=pCursor, create=True))
1169
+ action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, create=True))
1120
1170
  ctxMenu.addSeparator()
1121
1171
 
1122
1172
  # Cut, Copy and Paste
1123
1173
  if uCursor.hasSelection():
1124
1174
  action = ctxMenu.addAction(self.tr("Cut"))
1125
- action.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
1175
+ action.triggered.connect(qtLambda(self.docAction, nwDocAction.CUT))
1126
1176
  action = ctxMenu.addAction(self.tr("Copy"))
1127
- action.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
1177
+ action.triggered.connect(qtLambda(self.docAction, nwDocAction.COPY))
1128
1178
 
1129
1179
  action = ctxMenu.addAction(self.tr("Paste"))
1130
- action.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
1180
+ action.triggered.connect(qtLambda(self.docAction, nwDocAction.PASTE))
1131
1181
  ctxMenu.addSeparator()
1132
1182
 
1133
1183
  # Selections
1134
1184
  action = ctxMenu.addAction(self.tr("Select All"))
1135
- action.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
1185
+ action.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL))
1136
1186
  action = ctxMenu.addAction(self.tr("Select Word"))
1137
- action.triggered.connect(
1138
- lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, pos)
1139
- )
1187
+ action.triggered.connect(qtLambda(
1188
+ self._makePosSelection, QTextCursor.SelectionType.WordUnderCursor, pos,
1189
+ ))
1140
1190
  action = ctxMenu.addAction(self.tr("Select Paragraph"))
1141
- action.triggered.connect(lambda: self._makePosSelection(
1142
- QTextCursor.SelectionType.BlockUnderCursor, pos)
1143
- )
1191
+ action.triggered.connect(qtLambda(
1192
+ self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, pos
1193
+ ))
1144
1194
 
1145
1195
  # Spell Checking
1146
1196
  if SHARED.project.data.spellCheck:
@@ -1156,16 +1206,16 @@ class GuiDocEditor(QPlainTextEdit):
1156
1206
  ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
1157
1207
  for option in suggest[:15]:
1158
1208
  action = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
1159
- action.triggered.connect(
1160
- lambda _, option=option: self._correctWord(sCursor, option)
1161
- )
1209
+ action.triggered.connect(qtLambda(self._correctWord, sCursor, option))
1162
1210
  else:
1163
1211
  trNone = self.tr("No Suggestions")
1164
1212
  ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {trNone}")
1165
1213
 
1166
1214
  ctxMenu.addSeparator()
1215
+ action = ctxMenu.addAction(self.tr("Ignore Word"))
1216
+ action.triggered.connect(qtLambda(self._addWord, word, block, False))
1167
1217
  action = ctxMenu.addAction(self.tr("Add Word to Dictionary"))
1168
- action.triggered.connect(lambda: self._addWord(word, block))
1218
+ action.triggered.connect(qtLambda(self._addWord, word, block, True))
1169
1219
 
1170
1220
  # Execute the context menu
1171
1221
  ctxMenu.exec(self.viewport().mapToGlobal(pos))
@@ -1173,30 +1223,6 @@ class GuiDocEditor(QPlainTextEdit):
1173
1223
 
1174
1224
  return
1175
1225
 
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
1226
  @pyqtSlot()
1201
1227
  def _runDocumentTasks(self) -> None:
1202
1228
  """Run timer document tasks."""
@@ -1269,7 +1295,7 @@ class GuiDocEditor(QPlainTextEdit):
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,11 +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
1989
  def _docAutoReplace(self, text: str) -> None:
1947
1990
  """Auto-replace text elements based on main configuration."""
1948
1991
  cursor = self.textCursor()
@@ -1952,88 +1995,98 @@ class GuiDocEditor(QPlainTextEdit):
1952
1995
  if tLen < 1 or tPos-1 > tLen:
1953
1996
  return
1954
1997
 
1955
- tOne = text[tPos-1:tPos]
1956
- tTwo = text[tPos-2:tPos]
1957
- tThree = text[tPos-3:tPos]
1998
+ t1 = text[tPos-1:tPos]
1999
+ t2 = text[tPos-2:tPos]
2000
+ t3 = text[tPos-3:tPos]
2001
+ t4 = text[tPos-4:tPos]
1958
2002
 
1959
- if not tOne:
2003
+ if not t1:
1960
2004
  return
1961
2005
 
1962
- nDelete = 0
1963
- tInsert = tOne
2006
+ delete = 0
2007
+ insert = t1
2008
+ tConf = self._typConf
1964
2009
 
1965
- if self._typRepDQuote and tTwo[:1].isspace() and tTwo.endswith('"'):
1966
- nDelete = 1
1967
- tInsert = self._typDQuoteO
2010
+ if tConf.typRepDQuote and t2[:1].isspace() and t2.endswith('"'):
2011
+ delete = 1
2012
+ insert = tConf.typDQuoteO
1968
2013
 
1969
- elif self._typRepDQuote and tOne == '"':
1970
- nDelete = 1
2014
+ elif tConf.typRepDQuote and t1 == '"':
2015
+ delete = 1
1971
2016
  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
2017
+ insert = tConf.typDQuoteO
2018
+ elif tPos == 2 and t2 == '>"':
2019
+ insert = tConf.typDQuoteO
2020
+ elif tPos == 3 and t3 == '>>"':
2021
+ insert = tConf.typDQuoteO
1977
2022
  else:
1978
- tInsert = self._typDQuoteC
2023
+ insert = tConf.typDQuoteC
1979
2024
 
1980
- elif self._typRepSQuote and tTwo[:1].isspace() and tTwo.endswith("'"):
1981
- nDelete = 1
1982
- tInsert = self._typSQuoteO
2025
+ elif tConf.typRepSQuote and t2[:1].isspace() and t2.endswith("'"):
2026
+ delete = 1
2027
+ insert = tConf.typSQuoteO
1983
2028
 
1984
- elif self._typRepSQuote and tOne == "'":
1985
- nDelete = 1
2029
+ elif tConf.typRepSQuote and t1 == "'":
2030
+ delete = 1
1986
2031
  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
2032
+ insert = tConf.typSQuoteO
2033
+ elif tPos == 2 and t2 == ">'":
2034
+ insert = tConf.typSQuoteO
2035
+ elif tPos == 3 and t3 == ">>'":
2036
+ insert = tConf.typSQuoteO
1992
2037
  else:
1993
- tInsert = self._typSQuoteC
2038
+ insert = tConf.typSQuoteC
1994
2039
 
1995
- elif self._typRepDash and tThree == "---":
1996
- nDelete = 3
1997
- tInsert = nwUnicode.U_EMDASH
2040
+ elif tConf.typRepDash and t4 == "----":
2041
+ delete = 4
2042
+ insert = nwUnicode.U_HBAR
1998
2043
 
1999
- elif self._typRepDash and tTwo == "--":
2000
- nDelete = 2
2001
- tInsert = nwUnicode.U_ENDASH
2044
+ elif tConf.typRepDash and t3 == "---":
2045
+ delete = 3
2046
+ insert = nwUnicode.U_EMDASH
2002
2047
 
2003
- elif self._typRepDash and tTwo == nwUnicode.U_ENDASH + "-":
2004
- nDelete = 2
2005
- tInsert = nwUnicode.U_EMDASH
2048
+ elif tConf.typRepDash and t2 == "--":
2049
+ delete = 2
2050
+ insert = nwUnicode.U_ENDASH
2006
2051
 
2007
- elif self._typRepDots and tThree == "...":
2008
- nDelete = 3
2009
- tInsert = nwUnicode.U_HELLIP
2052
+ elif tConf.typRepDash and t2 == nwUnicode.U_ENDASH + "-":
2053
+ delete = 2
2054
+ insert = nwUnicode.U_EMDASH
2010
2055
 
2011
- elif tOne == nwUnicode.U_LSEP:
2056
+ elif tConf.typRepDash and t2 == nwUnicode.U_EMDASH + "-":
2057
+ delete = 2
2058
+ insert = nwUnicode.U_HBAR
2059
+
2060
+ elif tConf.typRepDots and t3 == "...":
2061
+ delete = 3
2062
+ insert = nwUnicode.U_HELLIP
2063
+
2064
+ elif t1 == nwUnicode.U_LSEP:
2012
2065
  # 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
2066
+ delete = 1
2067
+ insert = nwUnicode.U_PSEP
2068
+
2069
+ check = insert
2070
+ if tConf.typPadBefore and check in tConf.typPadBefore:
2071
+ if self._allowSpaceBeforeColon(text, check):
2072
+ delete = max(delete, 1)
2073
+ chkPos = tPos - delete - 1
2021
2074
  if chkPos >= 0 and text[chkPos].isspace():
2022
2075
  # Strip existing space before inserting a new (#1061)
2023
- nDelete += 1
2024
- tInsert = self._typPadChar + tInsert
2076
+ delete += 1
2077
+ insert = tConf.typPadChar + insert
2025
2078
 
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
2079
+ if tConf.typPadAfter and check in tConf.typPadAfter:
2080
+ if self._allowSpaceBeforeColon(text, check):
2081
+ delete = max(delete, 1)
2082
+ insert = insert + tConf.typPadChar
2030
2083
 
2031
- if nDelete > 0:
2032
- cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete)
2033
- cursor.insertText(tInsert)
2084
+ if delete > 0:
2085
+ cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete)
2086
+ cursor.insertText(insert)
2034
2087
 
2035
- # Re-highlight, since the auto-replace sometimes interferes with it
2036
- self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
2088
+ # Re-highlight, since the auto-replace sometimes interferes with it
2089
+ self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
2037
2090
 
2038
2091
  return
2039
2092
 
@@ -2043,12 +2096,8 @@ class GuiDocEditor(QPlainTextEdit):
2043
2096
  feature for French, Spanish, etc, so it doesn't insert a
2044
2097
  space before colons in meta data lines. See issue #1090.
2045
2098
  """
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
2099
+ if char == ":" and len(text) > 1 and text[0] == "@":
2100
+ return False
2052
2101
  return True
2053
2102
 
2054
2103
  def _autoSelect(self) -> QTextCursor:
@@ -2173,7 +2222,7 @@ class MetaCompleter(QMenu):
2173
2222
  suffix = ""
2174
2223
  options = list(filter(
2175
2224
  lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
2176
- nwKeyWords.KEY_CLASS.get(kw.strip(), nwItemClass.NO_CLASS)
2225
+ nwKeyWords.KEY_CLASS.get(kw.strip())
2177
2226
  )
2178
2227
  ))[:15]
2179
2228
 
@@ -2183,7 +2232,7 @@ class MetaCompleter(QMenu):
2183
2232
  for value in sorted(options):
2184
2233
  rep = value + suffix
2185
2234
  action = self.addAction(value)
2186
- action.triggered.connect(lambda _, r=rep: self._emitComplete(offset, length, r))
2235
+ action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
2187
2236
 
2188
2237
  return True
2189
2238
 
@@ -2283,61 +2332,61 @@ class GuiDocToolBar(QWidget):
2283
2332
  self.tbBoldMD = NIconToolButton(self, iSz)
2284
2333
  self.tbBoldMD.setToolTip(self.tr("Markdown Bold"))
2285
2334
  self.tbBoldMD.clicked.connect(
2286
- lambda: self.requestDocAction.emit(nwDocAction.MD_BOLD)
2335
+ qtLambda(self.requestDocAction.emit, nwDocAction.MD_BOLD)
2287
2336
  )
2288
2337
 
2289
2338
  self.tbItalicMD = NIconToolButton(self, iSz)
2290
2339
  self.tbItalicMD.setToolTip(self.tr("Markdown Italic"))
2291
2340
  self.tbItalicMD.clicked.connect(
2292
- lambda: self.requestDocAction.emit(nwDocAction.MD_ITALIC)
2341
+ qtLambda(self.requestDocAction.emit, nwDocAction.MD_ITALIC)
2293
2342
  )
2294
2343
 
2295
2344
  self.tbStrikeMD = NIconToolButton(self, iSz)
2296
2345
  self.tbStrikeMD.setToolTip(self.tr("Markdown Strikethrough"))
2297
2346
  self.tbStrikeMD.clicked.connect(
2298
- lambda: self.requestDocAction.emit(nwDocAction.MD_STRIKE)
2347
+ qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE)
2299
2348
  )
2300
2349
 
2301
2350
  self.tbBold = NIconToolButton(self, iSz)
2302
2351
  self.tbBold.setToolTip(self.tr("Shortcode Bold"))
2303
2352
  self.tbBold.clicked.connect(
2304
- lambda: self.requestDocAction.emit(nwDocAction.SC_BOLD)
2353
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_BOLD)
2305
2354
  )
2306
2355
 
2307
2356
  self.tbItalic = NIconToolButton(self, iSz)
2308
2357
  self.tbItalic.setToolTip(self.tr("Shortcode Italic"))
2309
2358
  self.tbItalic.clicked.connect(
2310
- lambda: self.requestDocAction.emit(nwDocAction.SC_ITALIC)
2359
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_ITALIC)
2311
2360
  )
2312
2361
 
2313
2362
  self.tbStrike = NIconToolButton(self, iSz)
2314
2363
  self.tbStrike.setToolTip(self.tr("Shortcode Strikethrough"))
2315
2364
  self.tbStrike.clicked.connect(
2316
- lambda: self.requestDocAction.emit(nwDocAction.SC_STRIKE)
2365
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_STRIKE)
2317
2366
  )
2318
2367
 
2319
2368
  self.tbUnderline = NIconToolButton(self, iSz)
2320
2369
  self.tbUnderline.setToolTip(self.tr("Shortcode Underline"))
2321
2370
  self.tbUnderline.clicked.connect(
2322
- lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE)
2371
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_ULINE)
2323
2372
  )
2324
2373
 
2325
2374
  self.tbMark = NIconToolButton(self, iSz)
2326
2375
  self.tbMark.setToolTip(self.tr("Shortcode Highlight"))
2327
2376
  self.tbMark.clicked.connect(
2328
- lambda: self.requestDocAction.emit(nwDocAction.SC_MARK)
2377
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_MARK)
2329
2378
  )
2330
2379
 
2331
2380
  self.tbSuperscript = NIconToolButton(self, iSz)
2332
2381
  self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript"))
2333
2382
  self.tbSuperscript.clicked.connect(
2334
- lambda: self.requestDocAction.emit(nwDocAction.SC_SUP)
2383
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUP)
2335
2384
  )
2336
2385
 
2337
2386
  self.tbSubscript = NIconToolButton(self, iSz)
2338
2387
  self.tbSubscript.setToolTip(self.tr("Shortcode Subscript"))
2339
2388
  self.tbSubscript.clicked.connect(
2340
- lambda: self.requestDocAction.emit(nwDocAction.SC_SUB)
2389
+ qtLambda(self.requestDocAction.emit, nwDocAction.SC_SUB)
2341
2390
  )
2342
2391
 
2343
2392
  # Assemble
@@ -2801,7 +2850,7 @@ class GuiDocEditHeader(QWidget):
2801
2850
  self.tbButton = NIconToolButton(self, iSz)
2802
2851
  self.tbButton.setVisible(False)
2803
2852
  self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
2804
- self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
2853
+ self.tbButton.clicked.connect(qtLambda(self.toggleToolBarRequest.emit))
2805
2854
 
2806
2855
  self.outlineButton = NIconToolButton(self, iSz)
2807
2856
  self.outlineButton.setVisible(False)
@@ -2816,7 +2865,7 @@ class GuiDocEditHeader(QWidget):
2816
2865
  self.minmaxButton = NIconToolButton(self, iSz)
2817
2866
  self.minmaxButton.setVisible(False)
2818
2867
  self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
2819
- self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit())
2868
+ self.minmaxButton.clicked.connect(qtLambda(self.docEditor.toggleFocusModeRequest.emit))
2820
2869
 
2821
2870
  self.closeButton = NIconToolButton(self, iSz)
2822
2871
  self.closeButton.setVisible(False)
@@ -2879,9 +2928,7 @@ class GuiDocEditHeader(QWidget):
2879
2928
  self.outlineMenu.clear()
2880
2929
  for number, text in data.items():
2881
2930
  action = self.outlineMenu.addAction(text)
2882
- action.triggered.connect(
2883
- lambda _, number=number: self._gotoBlock(number)
2884
- )
2931
+ action.triggered.connect(qtLambda(self._gotoBlock, number))
2885
2932
  self._docOutline = data
2886
2933
  logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart))
2887
2934
  return