novelWriter 2.3rc1__py3-none-any.whl → 2.4__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 (125) hide show
  1. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/METADATA +5 -6
  2. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/RECORD +119 -109
  3. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/WHEEL +1 -1
  4. novelWriter-2.4.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +17 -10
  6. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  7. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  8. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  9. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  10. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  11. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  12. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  13. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  16. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  17. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  18. novelwriter/assets/icons/none.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  20. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  21. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  22. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  23. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  24. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  25. novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +4 -0
  27. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  28. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  29. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  30. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  31. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  32. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  33. novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +4 -0
  34. novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +4 -0
  35. novelwriter/assets/manual.pdf +0 -0
  36. novelwriter/assets/sample.zip +0 -0
  37. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  38. novelwriter/assets/syntax/default_dark.conf +1 -0
  39. novelwriter/assets/syntax/default_light.conf +1 -0
  40. novelwriter/assets/syntax/grey_dark.conf +1 -0
  41. novelwriter/assets/syntax/grey_light.conf +1 -0
  42. novelwriter/assets/syntax/light_owl.conf +1 -0
  43. novelwriter/assets/syntax/night_owl.conf +1 -0
  44. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  45. novelwriter/assets/syntax/solarized_light.conf +1 -0
  46. novelwriter/assets/syntax/tango.conf +23 -0
  47. novelwriter/assets/syntax/tomorrow.conf +1 -0
  48. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  49. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  50. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  51. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  52. novelwriter/assets/text/credits_en.htm +25 -23
  53. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  54. novelwriter/common.py +12 -4
  55. novelwriter/config.py +47 -16
  56. novelwriter/constants.py +5 -6
  57. novelwriter/core/buildsettings.py +64 -44
  58. novelwriter/core/coretools.py +97 -13
  59. novelwriter/core/docbuild.py +74 -7
  60. novelwriter/core/document.py +24 -3
  61. novelwriter/core/index.py +31 -112
  62. novelwriter/core/project.py +11 -15
  63. novelwriter/core/projectxml.py +3 -2
  64. novelwriter/core/sessions.py +2 -2
  65. novelwriter/core/spellcheck.py +3 -3
  66. novelwriter/core/status.py +6 -5
  67. novelwriter/core/storage.py +16 -6
  68. novelwriter/core/tohtml.py +22 -25
  69. novelwriter/core/tokenizer.py +417 -237
  70. novelwriter/core/tomd.py +17 -8
  71. novelwriter/core/toodt.py +386 -351
  72. novelwriter/core/tree.py +8 -8
  73. novelwriter/dialogs/about.py +10 -12
  74. novelwriter/dialogs/docmerge.py +17 -14
  75. novelwriter/dialogs/docsplit.py +20 -19
  76. novelwriter/dialogs/editlabel.py +5 -4
  77. novelwriter/dialogs/preferences.py +32 -40
  78. novelwriter/dialogs/projectsettings.py +31 -28
  79. novelwriter/dialogs/quotes.py +10 -9
  80. novelwriter/dialogs/wordlist.py +18 -15
  81. novelwriter/enum.py +17 -14
  82. novelwriter/error.py +14 -12
  83. novelwriter/extensions/circularprogress.py +12 -8
  84. novelwriter/extensions/configlayout.py +23 -3
  85. novelwriter/extensions/modified.py +51 -2
  86. novelwriter/extensions/pagedsidebar.py +16 -14
  87. novelwriter/extensions/simpleprogress.py +3 -1
  88. novelwriter/extensions/statusled.py +3 -1
  89. novelwriter/extensions/switch.py +10 -9
  90. novelwriter/extensions/switchbox.py +14 -13
  91. novelwriter/extensions/versioninfo.py +1 -1
  92. novelwriter/gui/doceditor.py +433 -496
  93. novelwriter/gui/dochighlight.py +54 -33
  94. novelwriter/gui/docviewer.py +162 -175
  95. novelwriter/gui/docviewerpanel.py +20 -37
  96. novelwriter/gui/editordocument.py +15 -4
  97. novelwriter/gui/itemdetails.py +51 -54
  98. novelwriter/gui/mainmenu.py +37 -17
  99. novelwriter/gui/noveltree.py +31 -37
  100. novelwriter/gui/outline.py +120 -98
  101. novelwriter/gui/projtree.py +114 -112
  102. novelwriter/gui/search.py +362 -0
  103. novelwriter/gui/sidebar.py +36 -45
  104. novelwriter/gui/statusbar.py +14 -14
  105. novelwriter/gui/theme.py +116 -34
  106. novelwriter/guimain.py +216 -207
  107. novelwriter/shared.py +31 -6
  108. novelwriter/text/counting.py +138 -0
  109. novelwriter/tools/dictionaries.py +15 -14
  110. novelwriter/tools/lipsum.py +20 -17
  111. novelwriter/tools/manusbuild.py +43 -35
  112. novelwriter/tools/manuscript.py +381 -104
  113. novelwriter/tools/manussettings.py +263 -125
  114. novelwriter/tools/noveldetails.py +21 -19
  115. novelwriter/tools/welcome.py +59 -57
  116. novelwriter/tools/writingstats.py +61 -55
  117. novelwriter/types.py +90 -0
  118. novelWriter-2.3rc1.dist-info/entry_points.txt +0 -5
  119. novelwriter/core/__init__.py +0 -3
  120. novelwriter/dialogs/__init__.py +0 -3
  121. novelwriter/extensions/__init__.py +0 -3
  122. novelwriter/gui/__init__.py +0 -3
  123. novelwriter/tools/__init__.py +0 -3
  124. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/LICENSE.md +0 -0
  125. {novelWriter-2.3rc1.dist-info → novelWriter-2.4.dist-info}/top_level.txt +0 -0
@@ -32,26 +32,29 @@ from enum import Enum
32
32
  from time import time
33
33
  from typing import TYPE_CHECKING
34
34
 
35
+ from PyQt5.QtCore import QPoint, QTimer, Qt, pyqtSignal, pyqtSlot
35
36
  from PyQt5.QtGui import (
36
37
  QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette
37
38
  )
38
- from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
39
39
  from PyQt5.QtWidgets import (
40
40
  QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
41
- QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget,
42
- QTreeWidgetItem, QVBoxLayout, QWidget
41
+ QLabel, QMenu, QShortcut, QSizePolicy, QTreeWidget, QTreeWidgetItem,
42
+ QVBoxLayout, QWidget
43
43
  )
44
44
 
45
45
  from novelwriter import CONFIG, SHARED
46
- from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
47
46
  from novelwriter.common import minmax
48
47
  from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels
49
- from novelwriter.core.item import NWItem
50
48
  from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
49
+ from novelwriter.core.item import NWItem
51
50
  from novelwriter.dialogs.docmerge import GuiDocMerge
52
51
  from novelwriter.dialogs.docsplit import GuiDocSplit
53
52
  from novelwriter.dialogs.editlabel import GuiEditLabel
54
53
  from novelwriter.dialogs.projectsettings import GuiProjectSettings
54
+ from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
55
+ from novelwriter.extensions.modified import NIconToolButton
56
+ from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
57
+ from novelwriter.types import QtAlignLeft, QtAlignRight, QtMouseLeft, QtMouseMiddle, QtUserRole
55
58
 
56
59
  if TYPE_CHECKING: # pragma: no cover
57
60
  from novelwriter.guimain import GuiMain
@@ -142,7 +145,6 @@ class GuiProjectView(QWidget):
142
145
  self.requestDeleteItem = self.projTree.requestDeleteItem
143
146
  self.getSelectedHandle = self.projTree.getSelectedHandle
144
147
  self.changedSince = self.projTree.changedSince
145
- self.createNewNote = self.projTree.createNewNote
146
148
 
147
149
  return
148
150
 
@@ -240,6 +242,12 @@ class GuiProjectView(QWidget):
240
242
  self.projBar.buildQuickLinksMenu()
241
243
  return
242
244
 
245
+ @pyqtSlot(str, nwItemClass)
246
+ def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
247
+ """Process new not request."""
248
+ self.projTree.createNewNote(tag, itemClass)
249
+ return
250
+
243
251
  # END Class GuiProjectView
244
252
 
245
253
 
@@ -256,36 +264,33 @@ class GuiProjectToolBar(QWidget):
256
264
  self.projTree = projView.projTree
257
265
  self.mainGui = projView.mainGui
258
266
 
259
- iPx = SHARED.theme.baseIconSize
267
+ iSz = SHARED.theme.baseIconSize
260
268
  mPx = CONFIG.pxInt(2)
261
269
 
262
270
  self.setContentsMargins(0, 0, 0, 0)
263
271
  self.setAutoFillBackground(True)
264
272
 
265
273
  # Widget Label
266
- self.viewLabel = QLabel("<b>%s</b>" % self.tr("Project Content"))
274
+ self.viewLabel = QLabel(self.tr("Project Content"), self)
275
+ self.viewLabel.setFont(SHARED.theme.guiFontB)
267
276
  self.viewLabel.setContentsMargins(0, 0, 0, 0)
268
277
  self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
269
278
 
270
279
  # Quick Links
271
280
  self.mQuick = QMenu(self)
272
281
 
273
- self.tbQuick = QToolButton(self)
282
+ self.tbQuick = NIconToolButton(self, iSz)
274
283
  self.tbQuick.setToolTip("%s [Ctrl+L]" % self.tr("Quick Links"))
275
284
  self.tbQuick.setShortcut("Ctrl+L")
276
- self.tbQuick.setIconSize(QSize(iPx, iPx))
277
285
  self.tbQuick.setMenu(self.mQuick)
278
- self.tbQuick.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
279
286
 
280
287
  # Move Buttons
281
- self.tbMoveU = QToolButton(self)
288
+ self.tbMoveU = NIconToolButton(self, iSz)
282
289
  self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
283
- self.tbMoveU.setIconSize(QSize(iPx, iPx))
284
290
  self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1))
285
291
 
286
- self.tbMoveD = QToolButton(self)
292
+ self.tbMoveD = NIconToolButton(self, iSz)
287
293
  self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
288
- self.tbMoveD.setIconSize(QSize(iPx, iPx))
289
294
  self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1))
290
295
 
291
296
  # Add Item Menu
@@ -324,12 +329,10 @@ class GuiProjectToolBar(QWidget):
324
329
  self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
325
330
  self._buildRootMenu()
326
331
 
327
- self.tbAdd = QToolButton(self)
332
+ self.tbAdd = NIconToolButton(self, iSz)
328
333
  self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item"))
329
334
  self.tbAdd.setShortcut("Ctrl+N")
330
- self.tbAdd.setIconSize(QSize(iPx, iPx))
331
335
  self.tbAdd.setMenu(self.mAdd)
332
- self.tbAdd.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
333
336
 
334
337
  # More Options Menu
335
338
  self.mMore = QMenu(self)
@@ -343,11 +346,9 @@ class GuiProjectToolBar(QWidget):
343
346
  self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
344
347
  self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
345
348
 
346
- self.tbMore = QToolButton(self)
349
+ self.tbMore = NIconToolButton(self, iSz)
347
350
  self.tbMore.setToolTip(self.tr("More Options"))
348
- self.tbMore.setIconSize(QSize(iPx, iPx))
349
351
  self.tbMore.setMenu(self.mMore)
350
- self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
351
352
 
352
353
  # Assemble
353
354
  self.outerBox = QHBoxLayout()
@@ -377,29 +378,24 @@ class GuiProjectToolBar(QWidget):
377
378
  qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
378
379
  self.setPalette(qPalette)
379
380
 
380
- fadeCol = qPalette.text().color()
381
- buttonStyle = (
382
- "QToolButton {{padding: {0}px; border: none; background: transparent;}} "
383
- "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
384
- ).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
385
- buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
386
-
387
- self.tbQuick.setStyleSheet(buttonStyleMenu)
381
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
382
+ self.tbQuick.setStyleSheet(buttonStyle)
388
383
  self.tbMoveU.setStyleSheet(buttonStyle)
389
384
  self.tbMoveD.setStyleSheet(buttonStyle)
390
- self.tbAdd.setStyleSheet(buttonStyleMenu)
391
- self.tbMore.setStyleSheet(buttonStyleMenu)
385
+ self.tbAdd.setStyleSheet(buttonStyle)
386
+ self.tbMore.setStyleSheet(buttonStyle)
387
+
388
+ self.tbQuick.setThemeIcon("bookmark")
389
+ self.tbMoveU.setThemeIcon("up")
390
+ self.tbMoveD.setThemeIcon("down")
391
+ self.tbAdd.setThemeIcon("add")
392
+ self.tbMore.setThemeIcon("menu")
392
393
 
393
- self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark"))
394
- self.tbMoveU.setIcon(SHARED.theme.getIcon("up"))
395
- self.tbMoveD.setIcon(SHARED.theme.getIcon("down"))
396
394
  self.aAddEmpty.setIcon(SHARED.theme.getIcon("proj_document"))
397
395
  self.aAddChap.setIcon(SHARED.theme.getIcon("proj_chapter"))
398
396
  self.aAddScene.setIcon(SHARED.theme.getIcon("proj_scene"))
399
397
  self.aAddNote.setIcon(SHARED.theme.getIcon("proj_note"))
400
398
  self.aAddFolder.setIcon(SHARED.theme.getIcon("proj_folder"))
401
- self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
402
- self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
403
399
 
404
400
  self.buildQuickLinksMenu()
405
401
  self._buildRootMenu()
@@ -457,7 +453,7 @@ class GuiProjectToolBar(QWidget):
457
453
 
458
454
  def _buildRootMenu(self) -> None:
459
455
  """Build the rood folder menu."""
460
- def addClass(itemClass):
456
+ def addClass(itemClass: nwItemClass) -> None:
461
457
  aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
462
458
  aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]))
463
459
  aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass))
@@ -491,8 +487,8 @@ class GuiProjectTree(QTreeWidget):
491
487
  C_ACTIVE = 2
492
488
  C_STATUS = 3
493
489
 
494
- D_HANDLE = Qt.ItemDataRole.UserRole
495
- D_WORDS = Qt.ItemDataRole.UserRole + 1
490
+ D_HANDLE = QtUserRole
491
+ D_WORDS = QtUserRole + 1
496
492
 
497
493
  itemRefreshed = pyqtSignal(str, NWItem, QIcon)
498
494
 
@@ -505,10 +501,15 @@ class GuiProjectTree(QTreeWidget):
505
501
  self.mainGui = projView.mainGui
506
502
 
507
503
  # Internal Variables
508
- self._treeMap = {}
504
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
509
505
  self._timeChanged = 0.0
510
506
  self._popAlert = None
511
507
 
508
+ # Cached Translations
509
+ self.trActive = self.tr("Active")
510
+ self.trInactive = self.tr("Inactive")
511
+ self.trPermDelete = self.tr("Permanently delete {0} file(s) from Trash?")
512
+
512
513
  # Build GUI
513
514
  # =========
514
515
 
@@ -517,10 +518,10 @@ class GuiProjectTree(QTreeWidget):
517
518
  self.customContextMenuRequested.connect(self._openContextMenu)
518
519
 
519
520
  # Tree Settings
520
- iPx = SHARED.theme.baseIconSize
521
+ iPx = SHARED.theme.baseIconHeight
521
522
  cMg = CONFIG.pxInt(6)
522
523
 
523
- self.setIconSize(QSize(iPx, iPx))
524
+ self.setIconSize(SHARED.theme.baseIconSize)
524
525
  self.setFrameStyle(QFrame.Shape.NoFrame)
525
526
  self.setUniformRowHeights(True)
526
527
  self.setAllColumnsShowFocus(True)
@@ -555,10 +556,6 @@ class GuiProjectTree(QTreeWidget):
555
556
  trRoot = self.invisibleRootItem()
556
557
  trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
557
558
 
558
- # Cached values
559
- self._lblActive = self.tr("Active")
560
- self._lblInactive = self.tr("Inactive")
561
-
562
559
  # Set selection options
563
560
  self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
564
561
  self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
@@ -568,9 +565,9 @@ class GuiProjectTree(QTreeWidget):
568
565
  self.itemSelectionChanged.connect(self._treeSelectionChange)
569
566
 
570
567
  # Auto Scroll
571
- self._scrollMargin = SHARED.theme.baseIconSize
568
+ self._scrollMargin = SHARED.theme.baseIconHeight
572
569
  self._scrollDirection = 0
573
- self._scrollTimer = QTimer()
570
+ self._scrollTimer = QTimer(self)
574
571
  self._scrollTimer.timeout.connect(self._doAutoScroll)
575
572
  self._scrollTimer.setInterval(250)
576
573
 
@@ -605,18 +602,18 @@ class GuiProjectTree(QTreeWidget):
605
602
  self._timeChanged = 0.0
606
603
  return
607
604
 
608
- def createNewNote(self, tag: str, itemClass: nwItemClass | None) -> bool:
605
+ def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
609
606
  """Create a new note. This function is used by the document
610
607
  editor to create note files for unknown tags.
611
608
  """
612
- rHandle = SHARED.project.tree.findRoot(itemClass)
613
- if rHandle:
614
- tHandle = SHARED.project.newFile(tag, rHandle)
615
- if tHandle:
609
+ if itemClass != nwItemClass.NO_CLASS:
610
+ if not (rHandle := SHARED.project.tree.findRoot(itemClass)):
611
+ self.newTreeItem(nwItemType.ROOT, itemClass)
612
+ rHandle = SHARED.project.tree.findRoot(itemClass)
613
+ if rHandle and (tHandle := SHARED.project.newFile(tag, rHandle)):
616
614
  SHARED.project.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
617
615
  self.revealNewTreeItem(tHandle, wordCount=True)
618
- return True
619
- return False
616
+ return
620
617
 
621
618
  def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
622
619
  hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
@@ -902,10 +899,7 @@ class GuiProjectTree(QTreeWidget):
902
899
  SHARED.info(self.tr("The Trash folder is already empty."))
903
900
  return False
904
901
 
905
- msgYes = SHARED.question(
906
- self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
907
- )
908
- if not msgYes:
902
+ if not SHARED.question(self.trPermDelete.format(nTrash)):
909
903
  logger.info("Action cancelled by user")
910
904
  return False
911
905
 
@@ -1052,7 +1046,7 @@ class GuiProjectTree(QTreeWidget):
1052
1046
 
1053
1047
  if nwItem.isFileType():
1054
1048
  iconName = "checked" if nwItem.isActive else "unchecked"
1055
- toolTip = self._lblActive if nwItem.isActive else self._lblInactive
1049
+ toolTip = self.trActive if nwItem.isActive else self.trInactive
1056
1050
  trItem.setToolTip(self.C_ACTIVE, toolTip)
1057
1051
  else:
1058
1052
  iconName = "noncheckable"
@@ -1129,9 +1123,8 @@ class GuiProjectTree(QTreeWidget):
1129
1123
  """Get the currently selected handle. If multiple items are
1130
1124
  selected, return the first.
1131
1125
  """
1132
- selItem = self.selectedItems()
1133
- if selItem:
1134
- return selItem[0].data(self.C_DATA, self.D_HANDLE)
1126
+ if items := self.selectedItems():
1127
+ return items[0].data(self.C_DATA, self.D_HANDLE)
1135
1128
  return None
1136
1129
 
1137
1130
  def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
@@ -1143,9 +1136,8 @@ class GuiProjectTree(QTreeWidget):
1143
1136
  if tHandle in self._treeMap:
1144
1137
  self.setCurrentItem(self._treeMap[tHandle])
1145
1138
 
1146
- selIndex = self.selectedIndexes()
1147
- if selIndex and doScroll:
1148
- self.scrollTo(selIndex[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1139
+ if (indexes := self.selectedIndexes()) and doScroll:
1140
+ self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1149
1141
 
1150
1142
  return True
1151
1143
 
@@ -1160,10 +1152,8 @@ class GuiProjectTree(QTreeWidget):
1160
1152
 
1161
1153
  def openContextOnSelected(self) -> bool:
1162
1154
  """Open the context menu on the current selected item."""
1163
- selItem = self.selectedItems()
1164
- if selItem:
1165
- pos = self.visualItemRect(selItem[0]).center()
1166
- return self._openContextMenu(pos)
1155
+ if items := self.selectedItems():
1156
+ return self._openContextMenu(self.visualItemRect(items[0]).center())
1167
1157
  return False
1168
1158
 
1169
1159
  def changedSince(self, checkTime: float) -> bool:
@@ -1240,7 +1230,7 @@ class GuiProjectTree(QTreeWidget):
1240
1230
  else:
1241
1231
  ctxMenu.buildSingleSelectMenu(hasChild)
1242
1232
 
1243
- ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
1233
+ ctxMenu.exec(self.viewport().mapToGlobal(clickPos))
1244
1234
  ctxMenu.deleteLater()
1245
1235
 
1246
1236
  return True
@@ -1266,11 +1256,11 @@ class GuiProjectTree(QTreeWidget):
1266
1256
  for viewing if the user middle-clicked.
1267
1257
  """
1268
1258
  super().mousePressEvent(event)
1269
- if event.button() == Qt.MouseButton.LeftButton:
1259
+ if event.button() == QtMouseLeft:
1270
1260
  selItem = self.indexAt(event.pos())
1271
1261
  if not selItem.isValid():
1272
1262
  self.clearSelection()
1273
- elif event.button() == Qt.MouseButton.MiddleButton:
1263
+ elif event.button() == QtMouseMiddle:
1274
1264
  selItem = self.itemAt(event.pos())
1275
1265
  if selItem:
1276
1266
  tHandle = selItem.data(self.C_DATA, self.D_HANDLE)
@@ -1278,7 +1268,7 @@ class GuiProjectTree(QTreeWidget):
1278
1268
  self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
1279
1269
  return
1280
1270
 
1281
- def startDrag(self, dropAction: Qt.DropActions) -> None:
1271
+ def startDrag(self, dropAction: Qt.DropAction) -> None:
1282
1272
  """Capture the drag and drop handling to pop alerts."""
1283
1273
  super().startDrag(dropAction)
1284
1274
  if self._popAlert:
@@ -1420,7 +1410,7 @@ class GuiProjectTree(QTreeWidget):
1420
1410
  itemList.remove(tHandle)
1421
1411
 
1422
1412
  dlgMerge = GuiDocMerge(self.mainGui, tHandle, itemList)
1423
- dlgMerge.exec_()
1413
+ dlgMerge.exec()
1424
1414
 
1425
1415
  if dlgMerge.result() == QDialog.DialogCode.Accepted:
1426
1416
 
@@ -1490,7 +1480,7 @@ class GuiProjectTree(QTreeWidget):
1490
1480
  return False
1491
1481
 
1492
1482
  dlgSplit = GuiDocSplit(self.mainGui, tHandle)
1493
- dlgSplit.exec_()
1483
+ dlgSplit.exec()
1494
1484
 
1495
1485
  if dlgSplit.result() == QDialog.DialogCode.Accepted:
1496
1486
 
@@ -1597,10 +1587,10 @@ class GuiProjectTree(QTreeWidget):
1597
1587
  newItem.setText(self.C_ACTIVE, "")
1598
1588
  newItem.setText(self.C_STATUS, "")
1599
1589
 
1600
- newItem.setTextAlignment(self.C_NAME, Qt.AlignmentFlag.AlignLeft)
1601
- newItem.setTextAlignment(self.C_COUNT, Qt.AlignmentFlag.AlignRight)
1602
- newItem.setTextAlignment(self.C_ACTIVE, Qt.AlignmentFlag.AlignLeft)
1603
- newItem.setTextAlignment(self.C_STATUS, Qt.AlignmentFlag.AlignLeft)
1590
+ newItem.setTextAlignment(self.C_NAME, QtAlignLeft)
1591
+ newItem.setTextAlignment(self.C_COUNT, QtAlignRight)
1592
+ newItem.setTextAlignment(self.C_ACTIVE, QtAlignLeft)
1593
+ newItem.setTextAlignment(self.C_STATUS, QtAlignLeft)
1604
1594
 
1605
1595
  newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
1606
1596
  newItem.setData(self.C_DATA, self.D_WORDS, 0)
@@ -1746,7 +1736,7 @@ class _TreeContextMenu(QMenu):
1746
1736
 
1747
1737
  self._item = nwItem
1748
1738
  self._handle = nwItem.itemHandle
1749
- self._items: list[str] = []
1739
+ self._items: list[NWItem] = []
1750
1740
 
1751
1741
  logger.debug("Ready: _TreeContextMenu")
1752
1742
 
@@ -1799,13 +1789,17 @@ class _TreeContextMenu(QMenu):
1799
1789
 
1800
1790
  return
1801
1791
 
1802
- def buildMultiSelectMenu(self, items: list[str]) -> None:
1792
+ def buildMultiSelectMenu(self, handles: list[str]) -> None:
1803
1793
  """Build the multi-select menu."""
1804
- self._items = items
1794
+ self._items = []
1795
+ for tHandle in handles:
1796
+ if (tItem := SHARED.project.tree[tHandle]):
1797
+ self._items.append(tItem)
1798
+
1805
1799
  self._itemActive(True)
1806
1800
  self._itemStatusImport(True)
1807
1801
  self.addSeparator()
1808
- self._moveToTrash(True)
1802
+ self._multiMoveToTrash()
1809
1803
  return
1810
1804
 
1811
1805
  ##
@@ -1836,7 +1830,7 @@ class _TreeContextMenu(QMenu):
1836
1830
 
1837
1831
  def _itemHeader(self) -> None:
1838
1832
  """Check if there is a header that can be used for rename."""
1839
- if hItem := SHARED.project.index.getItemHeader(self._handle, "T0001"):
1833
+ if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"):
1840
1834
  action = self.addAction(self.tr("Rename to Heading"))
1841
1835
  action.triggered.connect(
1842
1836
  lambda: self.projTree.renameTreeItem(self._handle, hItem.title)
@@ -1847,9 +1841,9 @@ class _TreeContextMenu(QMenu):
1847
1841
  """Add Active/Inactive actions."""
1848
1842
  if multi:
1849
1843
  mSub = self.addMenu(self.tr("Set Active to ..."))
1850
- aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.tr("Active"))
1844
+ aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive)
1851
1845
  aOne.triggered.connect(lambda: self._iterItemActive(True))
1852
- aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.tr("Inactive"))
1846
+ aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive)
1853
1847
  aTwo.triggered.connect(lambda: self._iterItemActive(False))
1854
1848
  else:
1855
1849
  action = self.addAction(self.tr("Toggle Active"))
@@ -1935,7 +1929,7 @@ class _TreeContextMenu(QMenu):
1935
1929
  action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True))
1936
1930
 
1937
1931
  if isFile:
1938
- action = menu.addAction(self.tr("Split Document by Headers"))
1932
+ action = menu.addAction(self.tr("Split Document by Headings"))
1939
1933
  action.triggered.connect(lambda: tree._splitDocument(tHandle))
1940
1934
 
1941
1935
  return
@@ -1949,11 +1943,9 @@ class _TreeContextMenu(QMenu):
1949
1943
  action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, True))
1950
1944
  action = self.addAction(self.tr("Collapse All"))
1951
1945
  action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, False))
1952
- action = self.addAction(self.tr("Duplicate from Here"))
1953
- action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
1954
- elif isFile:
1955
- action = self.addAction(self.tr("Duplicate Document"))
1956
- action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
1946
+
1947
+ action = self.addAction(self.tr("Duplicate"))
1948
+ action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
1957
1949
 
1958
1950
  if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
1959
1951
  action = self.addAction(self.tr("Delete Permanently"))
@@ -1964,10 +1956,14 @@ class _TreeContextMenu(QMenu):
1964
1956
 
1965
1957
  return
1966
1958
 
1967
- def _moveToTrash(self, multi: bool) -> None:
1959
+ def _multiMoveToTrash(self) -> None:
1968
1960
  """Add move to Trash action."""
1969
- action = self.addAction(self.tr("Move to Trash"))
1970
- if multi:
1961
+ areTrash = [i.itemClass == nwItemClass.TRASH for i in self._items]
1962
+ if all(areTrash):
1963
+ action = self.addAction(self.tr("Delete Permanently"))
1964
+ action.triggered.connect(self._iterPermDelete)
1965
+ elif not any(areTrash):
1966
+ action = self.addAction(self.tr("Move to Trash"))
1971
1967
  action.triggered.connect(self._iterMoveToTrash)
1972
1968
  return
1973
1969
 
@@ -1979,10 +1975,19 @@ class _TreeContextMenu(QMenu):
1979
1975
  def _iterMoveToTrash(self) -> None:
1980
1976
  """Iterate through files and move them to Trash."""
1981
1977
  if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))):
1982
- for tHandle in self._items:
1983
- tItem = SHARED.project.tree[tHandle]
1984
- if tItem and tItem.isFileType():
1985
- self.projTree.moveItemToTrash(tHandle, askFirst=False, flush=False)
1978
+ for tItem in self._items:
1979
+ if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH:
1980
+ self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False)
1981
+ self.projTree.saveTreeOrder()
1982
+ return
1983
+
1984
+ @pyqtSlot()
1985
+ def _iterPermDelete(self) -> None:
1986
+ """Iterate through files and delete them."""
1987
+ if SHARED.question(self.projTree.trPermDelete.format(len(self._items))):
1988
+ for tItem in self._items:
1989
+ if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH:
1990
+ self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False)
1986
1991
  self.projTree.saveTreeOrder()
1987
1992
  return
1988
1993
 
@@ -2000,12 +2005,11 @@ class _TreeContextMenu(QMenu):
2000
2005
 
2001
2006
  def _iterItemActive(self, isActive: bool) -> None:
2002
2007
  """Set the active status of multiple items."""
2003
- for tHandle in self._items:
2004
- tItem = SHARED.project.tree[tHandle]
2008
+ for tItem in self._items:
2005
2009
  if tItem and tItem.isFileType():
2006
2010
  tItem.setActive(isActive)
2007
- self.projTree.setTreeItemValues(tHandle)
2008
- self.projTree._alertTreeChange(tHandle, flush=False)
2011
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2012
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
2009
2013
  return
2010
2014
 
2011
2015
  def _changeItemStatus(self, key: str) -> None:
@@ -2017,12 +2021,11 @@ class _TreeContextMenu(QMenu):
2017
2021
 
2018
2022
  def _iterSetItemStatus(self, key: str) -> None:
2019
2023
  """Change the status value for multiple items."""
2020
- for tHandle in self._items:
2021
- tItem = SHARED.project.tree[tHandle]
2024
+ for tItem in self._items:
2022
2025
  if tItem and tItem.isNovelLike():
2023
2026
  tItem.setStatus(key)
2024
- self.projTree.setTreeItemValues(tHandle)
2025
- self.projTree._alertTreeChange(tHandle, flush=False)
2027
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2028
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
2026
2029
  return
2027
2030
 
2028
2031
  def _changeItemImport(self, key: str) -> None:
@@ -2034,12 +2037,11 @@ class _TreeContextMenu(QMenu):
2034
2037
 
2035
2038
  def _iterSetItemImport(self, key: str) -> None:
2036
2039
  """Change the status value for multiple items."""
2037
- for tHandle in self._items:
2038
- tItem = SHARED.project.tree[tHandle]
2040
+ for tItem in self._items:
2039
2041
  if tItem and not tItem.isNovelLike():
2040
2042
  tItem.setImport(key)
2041
- self.projTree.setTreeItemValues(tHandle)
2042
- self.projTree._alertTreeChange(tHandle, flush=False)
2043
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2044
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
2043
2045
  return
2044
2046
 
2045
2047
  def _changeItemLayout(self, itemLayout: nwItemLayout) -> None: