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
@@ -29,7 +29,6 @@ from __future__ import annotations
29
29
  import logging
30
30
 
31
31
  from enum import Enum
32
- from time import time
33
32
 
34
33
  from PyQt5.QtCore import QPoint, Qt, QTimer, pyqtSignal, pyqtSlot
35
34
  from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette
@@ -39,8 +38,8 @@ from PyQt5.QtWidgets import (
39
38
  )
40
39
 
41
40
  from novelwriter import CONFIG, SHARED
42
- from novelwriter.common import minmax
43
- from novelwriter.constants import nwHeaders, nwLabels, nwUnicode, trConst
41
+ from novelwriter.common import minmax, qtLambda
42
+ from novelwriter.constants import nwLabels, nwStyles, nwUnicode, trConst
44
43
  from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
45
44
  from novelwriter.core.item import NWItem
46
45
  from novelwriter.dialogs.docmerge import GuiDocMerge
@@ -51,8 +50,8 @@ from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType
51
50
  from novelwriter.extensions.modified import NIconToolButton
52
51
  from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
53
52
  from novelwriter.types import (
54
- QtAlignLeft, QtAlignRight, QtMouseLeft, QtMouseMiddle, QtSizeExpanding,
55
- QtUserRole
53
+ QtAlignLeft, QtAlignRight, QtMouseLeft, QtMouseMiddle, QtScrollAlwaysOff,
54
+ QtScrollAsNeeded, QtSizeExpanding, QtUserRole
56
55
  )
57
56
 
58
57
  logger = logging.getLogger(__name__)
@@ -97,37 +96,37 @@ class GuiProjectView(QWidget):
97
96
  self.keyMoveUp = QShortcut(self.projTree)
98
97
  self.keyMoveUp.setKey("Ctrl+Up")
99
98
  self.keyMoveUp.setContext(Qt.ShortcutContext.WidgetShortcut)
100
- self.keyMoveUp.activated.connect(lambda: self.projTree.moveTreeItem(-1))
99
+ self.keyMoveUp.activated.connect(qtLambda(self.projTree.moveTreeItem, -1))
101
100
 
102
101
  self.keyMoveDn = QShortcut(self.projTree)
103
102
  self.keyMoveDn.setKey("Ctrl+Down")
104
103
  self.keyMoveDn.setContext(Qt.ShortcutContext.WidgetShortcut)
105
- self.keyMoveDn.activated.connect(lambda: self.projTree.moveTreeItem(1))
104
+ self.keyMoveDn.activated.connect(qtLambda(self.projTree.moveTreeItem, 1))
106
105
 
107
106
  self.keyGoPrev = QShortcut(self.projTree)
108
107
  self.keyGoPrev.setKey("Alt+Up")
109
108
  self.keyGoPrev.setContext(Qt.ShortcutContext.WidgetShortcut)
110
- self.keyGoPrev.activated.connect(lambda: self.projTree.moveToNextItem(-1))
109
+ self.keyGoPrev.activated.connect(qtLambda(self.projTree.moveToNextItem, -1))
111
110
 
112
111
  self.keyGoNext = QShortcut(self.projTree)
113
112
  self.keyGoNext.setKey("Alt+Down")
114
113
  self.keyGoNext.setContext(Qt.ShortcutContext.WidgetShortcut)
115
- self.keyGoNext.activated.connect(lambda: self.projTree.moveToNextItem(1))
114
+ self.keyGoNext.activated.connect(qtLambda(self.projTree.moveToNextItem, 1))
116
115
 
117
116
  self.keyGoUp = QShortcut(self.projTree)
118
117
  self.keyGoUp.setKey("Alt+Left")
119
118
  self.keyGoUp.setContext(Qt.ShortcutContext.WidgetShortcut)
120
- self.keyGoUp.activated.connect(lambda: self.projTree.moveToLevel(-1))
119
+ self.keyGoUp.activated.connect(qtLambda(self.projTree.moveToLevel, -1))
121
120
 
122
121
  self.keyGoDown = QShortcut(self.projTree)
123
122
  self.keyGoDown.setKey("Alt+Right")
124
123
  self.keyGoDown.setContext(Qt.ShortcutContext.WidgetShortcut)
125
- self.keyGoDown.activated.connect(lambda: self.projTree.moveToLevel(1))
124
+ self.keyGoDown.activated.connect(qtLambda(self.projTree.moveToLevel, 1))
126
125
 
127
126
  self.keyContext = QShortcut(self.projTree)
128
127
  self.keyContext.setKey("Ctrl+.")
129
128
  self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
130
- self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected())
129
+ self.keyContext.activated.connect(self.projTree.openContextMenu)
131
130
 
132
131
  # Signals
133
132
  self.selectedItemChanged.connect(self.projBar.treeSelectionChanged)
@@ -138,7 +137,6 @@ class GuiProjectView(QWidget):
138
137
  self.emptyTrash = self.projTree.emptyTrash
139
138
  self.requestDeleteItem = self.projTree.requestDeleteItem
140
139
  self.getSelectedHandle = self.projTree.getSelectedHandle
141
- self.changedSince = self.projTree.changedSince
142
140
 
143
141
  return
144
142
 
@@ -211,6 +209,12 @@ class GuiProjectView(QWidget):
211
209
  self.projTree.setSelectedHandle(tHandle, doScroll=doScroll)
212
210
  return
213
211
 
212
+ @pyqtSlot(str)
213
+ def setActiveHandle(self, tHandle: str | None) -> None:
214
+ """Highlight the active handle."""
215
+ self.projTree.setActiveHandle(tHandle)
216
+ return
217
+
214
218
  @pyqtSlot(str)
215
219
  def updateItemValues(self, tHandle: str) -> None:
216
220
  """Update tree item."""
@@ -286,38 +290,38 @@ class GuiProjectToolBar(QWidget):
286
290
  # Move Buttons
287
291
  self.tbMoveU = NIconToolButton(self, iSz)
288
292
  self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
289
- self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1))
293
+ self.tbMoveU.clicked.connect(qtLambda(self.projTree.moveTreeItem, -1))
290
294
 
291
295
  self.tbMoveD = NIconToolButton(self, iSz)
292
296
  self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
293
- self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1))
297
+ self.tbMoveD.clicked.connect(qtLambda(self.projTree.moveTreeItem, 1))
294
298
 
295
299
  # Add Item Menu
296
300
  self.mAdd = QMenu(self)
297
301
 
298
302
  self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"]))
299
303
  self.aAddEmpty.triggered.connect(
300
- lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False)
304
+ qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=0, isNote=False)
301
305
  )
302
306
 
303
307
  self.aAddChap = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"]))
304
308
  self.aAddChap.triggered.connect(
305
- lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False)
309
+ qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=2, isNote=False)
306
310
  )
307
311
 
308
312
  self.aAddScene = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"]))
309
313
  self.aAddScene.triggered.connect(
310
- lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False)
314
+ qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=3, isNote=False)
311
315
  )
312
316
 
313
317
  self.aAddNote = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["note"]))
314
318
  self.aAddNote.triggered.connect(
315
- lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
319
+ qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=1, isNote=True)
316
320
  )
317
321
 
318
322
  self.aAddFolder = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["folder"]))
319
323
  self.aAddFolder.triggered.connect(
320
- lambda: self.projTree.newTreeItem(nwItemType.FOLDER)
324
+ qtLambda(self.projTree.newTreeItem, nwItemType.FOLDER)
321
325
  )
322
326
 
323
327
  self.mTemplates = _UpdatableMenu(self.mAdd)
@@ -337,13 +341,17 @@ class GuiProjectToolBar(QWidget):
337
341
  self.mMore = QMenu(self)
338
342
 
339
343
  self.aExpand = self.mMore.addAction(self.tr("Expand All"))
340
- self.aExpand.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, True))
344
+ self.aExpand.triggered.connect(
345
+ qtLambda(self.projTree.setExpandedFromHandle, None, True)
346
+ )
341
347
 
342
348
  self.aCollapse = self.mMore.addAction(self.tr("Collapse All"))
343
- self.aCollapse.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, False))
349
+ self.aCollapse.triggered.connect(
350
+ qtLambda(self.projTree.setExpandedFromHandle, None, False)
351
+ )
344
352
 
345
353
  self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
346
- self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
354
+ self.aEmptyTrash.triggered.connect(qtLambda(self.projTree.emptyTrash))
347
355
 
348
356
  self.tbMore = NIconToolButton(self, iSz)
349
357
  self.tbMore.setToolTip(self.tr("More Options"))
@@ -416,7 +424,7 @@ class GuiProjectToolBar(QWidget):
416
424
  action.setData(tHandle)
417
425
  action.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]))
418
426
  action.triggered.connect(
419
- lambda _, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True)
427
+ qtLambda(self.projView.setSelectedHandle, tHandle, doScroll=True)
420
428
  )
421
429
  return
422
430
 
@@ -455,7 +463,9 @@ class GuiProjectToolBar(QWidget):
455
463
  def addClass(itemClass: nwItemClass) -> None:
456
464
  aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
457
465
  aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]))
458
- aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass))
466
+ aNew.triggered.connect(
467
+ qtLambda(self.projTree.newTreeItem, nwItemType.ROOT, itemClass)
468
+ )
459
469
  self.mAddRoot.addAction(aNew)
460
470
  return
461
471
 
@@ -498,8 +508,8 @@ class GuiProjectTree(QTreeWidget):
498
508
 
499
509
  # Internal Variables
500
510
  self._treeMap: dict[str, QTreeWidgetItem] = {}
501
- self._timeChanged = 0.0
502
511
  self._popAlert = None
512
+ self._actHandle = None
503
513
 
504
514
  # Cached Translations
505
515
  self.trActive = self.tr("Active")
@@ -511,7 +521,7 @@ class GuiProjectTree(QTreeWidget):
511
521
 
512
522
  # Context Menu
513
523
  self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
514
- self.customContextMenuRequested.connect(self._openContextMenu)
524
+ self.customContextMenuRequested.connect(self.openContextMenu)
515
525
 
516
526
  # Tree Settings
517
527
  iPx = SHARED.theme.baseIconHeight
@@ -578,13 +588,13 @@ class GuiProjectTree(QTreeWidget):
578
588
  """Set or update tree widget settings."""
579
589
  # Scroll bars
580
590
  if CONFIG.hideVScroll:
581
- self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
591
+ self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
582
592
  else:
583
- self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
593
+ self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
584
594
  if CONFIG.hideHScroll:
585
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
595
+ self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
586
596
  else:
587
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
597
+ self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
588
598
  return
589
599
 
590
600
  ##
@@ -595,7 +605,6 @@ class GuiProjectTree(QTreeWidget):
595
605
  """Clear the GUI content and the related map."""
596
606
  self.clear()
597
607
  self._treeMap = {}
598
- self._timeChanged = 0.0
599
608
  return
600
609
 
601
610
  def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
@@ -642,7 +651,7 @@ class GuiProjectTree(QTreeWidget):
642
651
 
643
652
  # Collect some information about the selected item
644
653
  qItem = self._getTreeItem(sHandle)
645
- sLevel = nwHeaders.H_LEVEL.get(pItem.mainHeading, 0)
654
+ sLevel = nwStyles.H_LEVEL.get(pItem.mainHeading, 0)
646
655
  sIsParent = False if qItem is None else qItem.childCount() > 0
647
656
 
648
657
  if SHARED.project.tree.isTrash(sHandle):
@@ -1114,12 +1123,10 @@ class GuiProjectTree(QTreeWidget):
1114
1123
  """
1115
1124
  logger.debug("Building the project tree ...")
1116
1125
  self.clearTree()
1117
- count = 0
1118
1126
  for nwItem in SHARED.project.iterProjectItems():
1119
- count += 1
1120
1127
  self._addTreeItem(nwItem)
1121
- if count > 0:
1122
- logger.info("%d item(s) added to the project tree", count)
1128
+ self.setActiveHandle(self._actHandle)
1129
+ logger.info("%d item(s) added to the project tree", len(self._treeMap))
1123
1130
  return
1124
1131
 
1125
1132
  def getSelectedHandle(self) -> str | None:
@@ -1130,38 +1137,78 @@ class GuiProjectTree(QTreeWidget):
1130
1137
  return items[0].data(self.C_DATA, self.D_HANDLE)
1131
1138
  return None
1132
1139
 
1133
- def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
1140
+ def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
1134
1141
  """Set a specific handle as the selected item."""
1135
- tItem = self._getTreeItem(tHandle)
1136
- if tItem is None:
1137
- return False
1138
-
1139
1142
  if tHandle in self._treeMap:
1140
1143
  self.setCurrentItem(self._treeMap[tHandle])
1144
+ if (indexes := self.selectedIndexes()) and doScroll:
1145
+ self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1146
+ return
1141
1147
 
1142
- if (indexes := self.selectedIndexes()) and doScroll:
1143
- self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1144
-
1145
- return True
1148
+ def setActiveHandle(self, tHandle: str | None) -> None:
1149
+ """Highlight the rows associated with a given handle."""
1150
+ brushOn = self.palette().alternateBase()
1151
+ brushOff = self.palette().base()
1152
+ if (pHandle := self._actHandle) and (item := self._treeMap.get(pHandle)):
1153
+ for i in range(self.columnCount()):
1154
+ item.setBackground(i, brushOff)
1155
+ if tHandle and (item := self._treeMap.get(tHandle)):
1156
+ for i in range(self.columnCount()):
1157
+ item.setBackground(i, brushOn)
1158
+ self._actHandle = tHandle or None
1159
+ return
1146
1160
 
1147
1161
  def setExpandedFromHandle(self, tHandle: str | None, isExpanded: bool) -> None:
1148
1162
  """Iterate through items below tHandle and change expanded
1149
1163
  status for all child items. If tHandle is None, it affects the
1150
1164
  entire tree.
1151
1165
  """
1152
- trItem = self._getTreeItem(tHandle) or self.invisibleRootItem()
1153
- self._recursiveSetExpanded(trItem, isExpanded)
1166
+ item = self._getTreeItem(tHandle) or self.invisibleRootItem()
1167
+ self._recursiveSetExpanded(item, isExpanded)
1154
1168
  return
1155
1169
 
1156
- def openContextOnSelected(self) -> bool:
1157
- """Open the context menu on the current selected item."""
1158
- if items := self.selectedItems():
1159
- return self._openContextMenu(self.visualItemRect(items[0]).center())
1160
- return False
1170
+ ##
1171
+ # Public Slots
1172
+ ##
1173
+
1174
+ @pyqtSlot()
1175
+ @pyqtSlot("QPoint")
1176
+ def openContextMenu(self, clickPos: QPoint | None = None) -> None:
1177
+ """The user right clicked an element in the project tree, so we
1178
+ open a context menu in-place.
1179
+ """
1180
+ if clickPos is None and (items := self.selectedItems()):
1181
+ clickPos = self.visualItemRect(items[0]).center()
1182
+
1183
+ if clickPos is not None:
1184
+ tItem = None
1185
+ tHandle = None
1186
+ hasChild = False
1187
+ sItem = self.itemAt(clickPos)
1188
+ sItems = self.selectedItems()
1189
+ if isinstance(sItem, QTreeWidgetItem):
1190
+ tHandle = sItem.data(self.C_DATA, self.D_HANDLE)
1191
+ tItem = SHARED.project.tree[tHandle]
1192
+ hasChild = sItem.childCount() > 0
1193
+
1194
+ if tItem is None or tHandle is None:
1195
+ logger.debug("No item found")
1196
+ return
1197
+
1198
+ ctxMenu = _TreeContextMenu(self, tItem)
1199
+ trashHandle = SHARED.project.tree.trashRoot
1200
+ if trashHandle and tHandle == trashHandle:
1201
+ ctxMenu.buildTrashMenu()
1202
+ elif len(sItems) > 1:
1203
+ handles = [str(x.data(self.C_DATA, self.D_HANDLE)) for x in sItems]
1204
+ ctxMenu.buildMultiSelectMenu(handles)
1205
+ else:
1206
+ ctxMenu.buildSingleSelectMenu(hasChild)
1161
1207
 
1162
- def changedSince(self, checkTime: float) -> bool:
1163
- """Check if the tree has changed since a given time."""
1164
- return self._timeChanged > checkTime
1208
+ ctxMenu.exec(self.viewport().mapToGlobal(clickPos))
1209
+ ctxMenu.deleteLater()
1210
+
1211
+ return
1165
1212
 
1166
1213
  ##
1167
1214
  # Private Slots
@@ -1204,40 +1251,6 @@ class GuiProjectTree(QTreeWidget):
1204
1251
 
1205
1252
  return
1206
1253
 
1207
- @pyqtSlot("QPoint")
1208
- def _openContextMenu(self, clickPos: QPoint) -> bool:
1209
- """The user right clicked an element in the project tree, so we
1210
- open a context menu in-place.
1211
- """
1212
- tItem = None
1213
- tHandle = None
1214
- hasChild = False
1215
- sItem = self.itemAt(clickPos)
1216
- sItems = self.selectedItems()
1217
- if isinstance(sItem, QTreeWidgetItem):
1218
- tHandle = sItem.data(self.C_DATA, self.D_HANDLE)
1219
- tItem = SHARED.project.tree[tHandle]
1220
- hasChild = sItem.childCount() > 0
1221
-
1222
- if tItem is None or tHandle is None:
1223
- logger.debug("No item found")
1224
- return False
1225
-
1226
- ctxMenu = _TreeContextMenu(self, tItem)
1227
- trashHandle = SHARED.project.tree.trashRoot
1228
- if trashHandle and tHandle == trashHandle:
1229
- ctxMenu.buildTrashMenu()
1230
- elif len(sItems) > 1:
1231
- handles = [str(x.data(self.C_DATA, self.D_HANDLE)) for x in sItems]
1232
- ctxMenu.buildMultiSelectMenu(handles)
1233
- else:
1234
- ctxMenu.buildSingleSelectMenu(hasChild)
1235
-
1236
- ctxMenu.exec(self.viewport().mapToGlobal(clickPos))
1237
- ctxMenu.deleteLater()
1238
-
1239
- return True
1240
-
1241
1254
  @pyqtSlot()
1242
1255
  def _doAutoScroll(self) -> None:
1243
1256
  """Scroll one item up or down based on direction value."""
@@ -1377,11 +1390,6 @@ class GuiProjectTree(QTreeWidget):
1377
1390
 
1378
1391
  return
1379
1392
 
1380
- def _getItemWordCount(self, tHandle: str) -> int:
1381
- """Return the word count of a given item handle."""
1382
- tItem = self._getTreeItem(tHandle)
1383
- return int(tItem.data(self.C_DATA, self.D_WORDS)) if tItem else 0
1384
-
1385
1393
  def _getTreeItem(self, tHandle: str | None) -> QTreeWidgetItem | None:
1386
1394
  """Return the QTreeWidgetItem of a given item handle."""
1387
1395
  return self._treeMap.get(tHandle, None) if tHandle else None
@@ -1638,7 +1646,6 @@ class GuiProjectTree(QTreeWidget):
1638
1646
  signals. A flush is only needed if an item is moved, created or
1639
1647
  deleted.
1640
1648
  """
1641
- self._timeChanged = time()
1642
1649
  SHARED.project.setProjectChanged(True)
1643
1650
  if flush:
1644
1651
  self.saveTreeOrder()
@@ -1765,7 +1772,7 @@ class _TreeContextMenu(QMenu):
1765
1772
 
1766
1773
  # Edit Item Settings
1767
1774
  action = self.addAction(self.tr("Rename"))
1768
- action.triggered.connect(lambda: self.projTree.renameTreeItem(self._handle))
1775
+ action.triggered.connect(qtLambda(self.projTree.renameTreeItem, self._handle))
1769
1776
  if isFile:
1770
1777
  self._itemHeader()
1771
1778
  self._itemActive(False)
@@ -1801,13 +1808,15 @@ class _TreeContextMenu(QMenu):
1801
1808
  def _docActions(self) -> None:
1802
1809
  """Add document actions."""
1803
1810
  action = self.addAction(self.tr("Open Document"))
1804
- action.triggered.connect(
1805
- lambda: self.projView.openDocumentRequest.emit(self._handle, nwDocMode.EDIT, "", True)
1806
- )
1811
+ action.triggered.connect(qtLambda(
1812
+ self.projView.openDocumentRequest.emit,
1813
+ self._handle, nwDocMode.EDIT, "", True
1814
+ ))
1807
1815
  action = self.addAction(self.tr("View Document"))
1808
- action.triggered.connect(
1809
- lambda: self.projView.openDocumentRequest.emit(self._handle, nwDocMode.VIEW, "", False)
1810
- )
1816
+ action.triggered.connect(qtLambda(
1817
+ self.projView.openDocumentRequest.emit,
1818
+ self._handle, nwDocMode.VIEW, "", False
1819
+ ))
1811
1820
  return
1812
1821
 
1813
1822
  def _itemCreation(self) -> None:
@@ -1826,7 +1835,7 @@ class _TreeContextMenu(QMenu):
1826
1835
  if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"):
1827
1836
  action = self.addAction(self.tr("Rename to Heading"))
1828
1837
  action.triggered.connect(
1829
- lambda: self.projTree.renameTreeItem(self._handle, hItem.title)
1838
+ qtLambda(self.projTree.renameTreeItem, self._handle, hItem.title)
1830
1839
  )
1831
1840
  return
1832
1841
 
@@ -1835,9 +1844,9 @@ class _TreeContextMenu(QMenu):
1835
1844
  if multi:
1836
1845
  mSub = self.addMenu(self.tr("Set Active to ..."))
1837
1846
  aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive)
1838
- aOne.triggered.connect(lambda: self._iterItemActive(True))
1847
+ aOne.triggered.connect(qtLambda(self._iterItemActive, True))
1839
1848
  aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive)
1840
- aTwo.triggered.connect(lambda: self._iterItemActive(False))
1849
+ aTwo.triggered.connect(qtLambda(self._iterItemActive, False))
1841
1850
  else:
1842
1851
  action = self.addAction(self.tr("Toggle Active"))
1843
1852
  action.triggered.connect(self._toggleItemActive)
@@ -1848,37 +1857,39 @@ class _TreeContextMenu(QMenu):
1848
1857
  if self._item.isNovelLike():
1849
1858
  menu = self.addMenu(self.tr("Set Status to ..."))
1850
1859
  current = self._item.itemStatus
1851
- for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.iterItems()):
1860
+ for key, entry in SHARED.project.data.itemStatus.iterItems():
1852
1861
  name = entry.name
1853
1862
  if not multi and current == key:
1854
1863
  name += f" ({nwUnicode.U_CHECK})"
1855
1864
  action = menu.addAction(entry.icon, name)
1856
1865
  if multi:
1857
- action.triggered.connect(lambda n, key=key: self._iterSetItemStatus(key))
1866
+ action.triggered.connect(qtLambda(self._iterSetItemStatus, key))
1858
1867
  else:
1859
- action.triggered.connect(lambda n, key=key: self._changeItemStatus(key))
1868
+ action.triggered.connect(qtLambda(self._changeItemStatus, key))
1860
1869
  menu.addSeparator()
1861
1870
  action = menu.addAction(self.tr("Manage Labels ..."))
1862
- action.triggered.connect(
1863
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS)
1864
- )
1871
+ action.triggered.connect(qtLambda(
1872
+ self.projView.projectSettingsRequest.emit,
1873
+ GuiProjectSettings.PAGE_STATUS
1874
+ ))
1865
1875
  else:
1866
1876
  menu = self.addMenu(self.tr("Set Importance to ..."))
1867
1877
  current = self._item.itemImport
1868
- for n, (key, entry) in enumerate(SHARED.project.data.itemImport.iterItems()):
1878
+ for key, entry in SHARED.project.data.itemImport.iterItems():
1869
1879
  name = entry.name
1870
1880
  if not multi and current == key:
1871
1881
  name += f" ({nwUnicode.U_CHECK})"
1872
1882
  action = menu.addAction(entry.icon, name)
1873
1883
  if multi:
1874
- action.triggered.connect(lambda n, key=key: self._iterSetItemImport(key))
1884
+ action.triggered.connect(qtLambda(self._iterSetItemImport, key))
1875
1885
  else:
1876
- action.triggered.connect(lambda n, key=key: self._changeItemImport(key))
1886
+ action.triggered.connect(qtLambda(self._changeItemImport, key))
1877
1887
  menu.addSeparator()
1878
1888
  action = menu.addAction(self.tr("Manage Labels ..."))
1879
- action.triggered.connect(
1880
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT)
1881
- )
1889
+ action.triggered.connect(qtLambda(
1890
+ self.projView.projectSettingsRequest.emit,
1891
+ GuiProjectSettings.PAGE_IMPORT
1892
+ ))
1882
1893
  return
1883
1894
 
1884
1895
  def _itemTransform(self, isFile: bool, isFolder: bool, hasChild: bool) -> None:
@@ -1897,33 +1908,33 @@ class _TreeContextMenu(QMenu):
1897
1908
 
1898
1909
  if isNoteFile and self._item.documentAllowed():
1899
1910
  action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
1900
- action.triggered.connect(lambda: self._changeItemLayout(loDoc))
1911
+ action.triggered.connect(qtLambda(self._changeItemLayout, loDoc))
1901
1912
 
1902
1913
  if isDocFile:
1903
1914
  action = menu.addAction(self.tr("Convert to {0}").format(trNote))
1904
- action.triggered.connect(lambda: self._changeItemLayout(loNote))
1915
+ action.triggered.connect(qtLambda(self._changeItemLayout, loNote))
1905
1916
 
1906
1917
  if isFolder and self._item.documentAllowed():
1907
1918
  action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
1908
- action.triggered.connect(lambda: self._covertFolderToFile(loDoc))
1919
+ action.triggered.connect(qtLambda(self._covertFolderToFile, loDoc))
1909
1920
 
1910
1921
  if isFolder:
1911
1922
  action = menu.addAction(self.tr("Convert to {0}").format(trNote))
1912
- action.triggered.connect(lambda: self._covertFolderToFile(loNote))
1923
+ action.triggered.connect(qtLambda(self._covertFolderToFile, loNote))
1913
1924
 
1914
1925
  if hasChild and isFile:
1915
1926
  action = menu.addAction(self.tr("Merge Child Items into Self"))
1916
- action.triggered.connect(lambda: tree._mergeDocuments(tHandle, False))
1927
+ action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, False))
1917
1928
  action = menu.addAction(self.tr("Merge Child Items into New"))
1918
- action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True))
1929
+ action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True))
1919
1930
 
1920
1931
  if hasChild and isFolder:
1921
1932
  action = menu.addAction(self.tr("Merge Documents in Folder"))
1922
- action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True))
1933
+ action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True))
1923
1934
 
1924
1935
  if isFile:
1925
1936
  action = menu.addAction(self.tr("Split Document by Headings"))
1926
- action.triggered.connect(lambda: tree._splitDocument(tHandle))
1937
+ action.triggered.connect(qtLambda(tree._splitDocument, tHandle))
1927
1938
 
1928
1939
  return
1929
1940
 
@@ -1933,19 +1944,19 @@ class _TreeContextMenu(QMenu):
1933
1944
  tHandle = self._handle
1934
1945
  if hasChild:
1935
1946
  action = self.addAction(self.tr("Expand All"))
1936
- action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, True))
1947
+ action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, True))
1937
1948
  action = self.addAction(self.tr("Collapse All"))
1938
- action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, False))
1949
+ action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, False))
1939
1950
 
1940
1951
  action = self.addAction(self.tr("Duplicate"))
1941
- action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
1952
+ action.triggered.connect(qtLambda(tree._duplicateFromHandle, tHandle))
1942
1953
 
1943
1954
  if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
1944
1955
  action = self.addAction(self.tr("Delete Permanently"))
1945
- action.triggered.connect(lambda: tree.permDeleteItem(tHandle))
1956
+ action.triggered.connect(qtLambda(tree.permDeleteItem, tHandle))
1946
1957
  else:
1947
1958
  action = self.addAction(self.tr("Move to Trash"))
1948
- action.triggered.connect(lambda: tree.moveItemToTrash(tHandle))
1959
+ action.triggered.connect(qtLambda(tree.moveItemToTrash, tHandle))
1949
1960
 
1950
1961
  return
1951
1962
 
@@ -32,6 +32,7 @@ from PyQt5.QtGui import QPalette
32
32
  from PyQt5.QtWidgets import QMenu, QVBoxLayout, QWidget
33
33
 
34
34
  from novelwriter import CONFIG, SHARED
35
+ from novelwriter.common import qtLambda
35
36
  from novelwriter.enum import nwView
36
37
  from novelwriter.extensions.eventfilters import StatusTipFilter
37
38
  from novelwriter.extensions.modified import NIconToolButton
@@ -54,7 +55,7 @@ class GuiSideBar(QWidget):
54
55
 
55
56
  self.mainGui = mainGui
56
57
 
57
- iPx = int(1.2*SHARED.theme.baseButtonHeight)
58
+ iPx = int(1.25*SHARED.theme.baseButtonHeight)
58
59
  iSz = QSize(iPx, iPx)
59
60
 
60
61
  self.setContentsMargins(0, 0, 0, 0)
@@ -63,19 +64,19 @@ class GuiSideBar(QWidget):
63
64
  # Buttons
64
65
  self.tbProject = NIconToolButton(self, iSz)
65
66
  self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
66
- self.tbProject.clicked.connect(lambda: self.requestViewChange.emit(nwView.PROJECT))
67
+ self.tbProject.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.PROJECT))
67
68
 
68
69
  self.tbNovel = NIconToolButton(self, iSz)
69
70
  self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
70
- self.tbNovel.clicked.connect(lambda: self.requestViewChange.emit(nwView.NOVEL))
71
+ self.tbNovel.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.NOVEL))
71
72
 
72
73
  self.tbSearch = NIconToolButton(self, iSz)
73
74
  self.tbSearch.setToolTip("{0} [Ctrl+Shift+F]".format(self.tr("Project Search")))
74
- self.tbSearch.clicked.connect(lambda: self.requestViewChange.emit(nwView.SEARCH))
75
+ self.tbSearch.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.SEARCH))
75
76
 
76
77
  self.tbOutline = NIconToolButton(self, iSz)
77
78
  self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
78
- self.tbOutline.clicked.connect(lambda: self.requestViewChange.emit(nwView.OUTLINE))
79
+ self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE))
79
80
 
80
81
  self.tbBuild = NIconToolButton(self, iSz)
81
82
  self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
@@ -113,7 +114,7 @@ class GuiSideBar(QWidget):
113
114
  self.outerBox.addWidget(self.tbStats)
114
115
  self.outerBox.addWidget(self.tbSettings)
115
116
  self.outerBox.setContentsMargins(0, 0, 0, 0)
116
- self.outerBox.setSpacing(CONFIG.pxInt(4))
117
+ self.outerBox.setSpacing(CONFIG.pxInt(6))
117
118
 
118
119
  self.setLayout(self.outerBox)
119
120
  self.updateTheme()
novelwriter/gui/theme.py CHANGED
@@ -503,10 +503,11 @@ class GuiIcons:
503
503
  "fmt_strike-md", "fmt_subscript", "fmt_superscript", "fmt_underline",
504
504
 
505
505
  # General Button Icons
506
- "add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross",
507
- "document", "down", "edit", "export", "font", "forward", "import", "list", "maximise",
508
- "menu", "minimise", "more", "noncheckable", "open", "panel", "quote", "refresh", "remove",
509
- "revert", "search_replace", "search", "settings", "star", "unchecked", "up", "view",
506
+ "add", "add_document", "backward", "bookmark", "browse", "checked", "close", "copy",
507
+ "cross", "document", "down", "edit", "export", "font", "forward", "import", "list",
508
+ "maximise", "menu", "minimise", "more", "noncheckable", "open", "panel", "quote",
509
+ "refresh", "remove", "revert", "search_replace", "search", "settings", "star", "unchecked",
510
+ "up", "view",
510
511
 
511
512
  # Switches
512
513
  "sticky-on", "sticky-off",