novelWriter 2.3rc1__py3-none-any.whl → 2.4b1__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 (100) hide show
  1. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/RECORD +99 -85
  3. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/WHEEL +1 -1
  4. novelWriter-2.4b1.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +5 -5
  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/project_nl_NL.json +11 -0
  15. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  16. novelwriter/assets/icons/typicons_dark/icons.conf +4 -0
  17. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +7 -0
  18. novelwriter/assets/icons/typicons_dark/typ_arrow-down.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/typ_arrow-right.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +1 -1
  21. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +1 -1
  22. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +4 -0
  23. novelwriter/assets/icons/typicons_dark/typ_times.svg +1 -1
  24. novelwriter/assets/icons/typicons_light/icons.conf +4 -0
  25. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +7 -0
  26. novelwriter/assets/icons/typicons_light/typ_arrow-down.svg +4 -0
  27. novelwriter/assets/icons/typicons_light/typ_arrow-right.svg +4 -0
  28. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +1 -1
  29. novelwriter/assets/icons/typicons_light/typ_refresh.svg +1 -1
  30. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +4 -0
  31. novelwriter/assets/icons/typicons_light/typ_times.svg +1 -1
  32. novelwriter/assets/manual.pdf +0 -0
  33. novelwriter/assets/sample.zip +0 -0
  34. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  35. novelwriter/assets/syntax/default_dark.conf +1 -0
  36. novelwriter/assets/syntax/default_light.conf +1 -0
  37. novelwriter/assets/syntax/grey_dark.conf +1 -0
  38. novelwriter/assets/syntax/grey_light.conf +1 -0
  39. novelwriter/assets/syntax/light_owl.conf +1 -0
  40. novelwriter/assets/syntax/night_owl.conf +1 -0
  41. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  42. novelwriter/assets/syntax/solarized_light.conf +1 -0
  43. novelwriter/assets/syntax/tango.conf +23 -0
  44. novelwriter/assets/syntax/tomorrow.conf +1 -0
  45. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  46. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  47. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  48. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  49. novelwriter/assets/text/credits_en.htm +25 -23
  50. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  51. novelwriter/common.py +1 -1
  52. novelwriter/config.py +35 -12
  53. novelwriter/constants.py +5 -6
  54. novelwriter/core/buildsettings.py +60 -40
  55. novelwriter/core/coretools.py +98 -13
  56. novelwriter/core/docbuild.py +74 -7
  57. novelwriter/core/document.py +24 -3
  58. novelwriter/core/index.py +31 -112
  59. novelwriter/core/project.py +11 -15
  60. novelwriter/core/projectxml.py +2 -1
  61. novelwriter/core/sessions.py +2 -2
  62. novelwriter/core/status.py +4 -4
  63. novelwriter/core/storage.py +16 -6
  64. novelwriter/core/tohtml.py +22 -25
  65. novelwriter/core/tokenizer.py +416 -236
  66. novelwriter/core/tomd.py +17 -8
  67. novelwriter/core/toodt.py +65 -7
  68. novelwriter/core/tree.py +8 -8
  69. novelwriter/dialogs/about.py +2 -2
  70. novelwriter/dialogs/docsplit.py +7 -8
  71. novelwriter/dialogs/preferences.py +3 -6
  72. novelwriter/dialogs/wordlist.py +1 -1
  73. novelwriter/enum.py +17 -14
  74. novelwriter/extensions/configlayout.py +22 -0
  75. novelwriter/extensions/modified.py +20 -2
  76. novelwriter/extensions/versioninfo.py +1 -1
  77. novelwriter/gui/doceditor.py +257 -279
  78. novelwriter/gui/dochighlight.py +29 -25
  79. novelwriter/gui/docviewer.py +139 -148
  80. novelwriter/gui/docviewerpanel.py +4 -24
  81. novelwriter/gui/editordocument.py +12 -1
  82. novelwriter/gui/itemdetails.py +6 -6
  83. novelwriter/gui/mainmenu.py +37 -17
  84. novelwriter/gui/noveltree.py +11 -19
  85. novelwriter/gui/outline.py +43 -20
  86. novelwriter/gui/projtree.py +88 -88
  87. novelwriter/gui/search.py +316 -0
  88. novelwriter/gui/sidebar.py +25 -30
  89. novelwriter/gui/theme.py +68 -8
  90. novelwriter/guimain.py +183 -178
  91. novelwriter/shared.py +26 -1
  92. novelwriter/text/__init__.py +3 -0
  93. novelwriter/text/counting.py +137 -0
  94. novelwriter/tools/manuscript.py +344 -55
  95. novelwriter/tools/manussettings.py +214 -71
  96. novelwriter/tools/noveldetails.py +1 -1
  97. novelwriter/tools/welcome.py +8 -9
  98. novelWriter-2.3rc1.dist-info/entry_points.txt +0 -5
  99. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/LICENSE.md +0 -0
  100. {novelWriter-2.3rc1.dist-info → novelWriter-2.4b1.dist-info}/top_level.txt +0 -0
@@ -38,20 +38,22 @@ from PyQt5.QtGui import (
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
55
57
 
56
58
  if TYPE_CHECKING: # pragma: no cover
57
59
  from novelwriter.guimain import GuiMain
@@ -142,7 +144,6 @@ class GuiProjectView(QWidget):
142
144
  self.requestDeleteItem = self.projTree.requestDeleteItem
143
145
  self.getSelectedHandle = self.projTree.getSelectedHandle
144
146
  self.changedSince = self.projTree.changedSince
145
- self.createNewNote = self.projTree.createNewNote
146
147
 
147
148
  return
148
149
 
@@ -240,6 +241,12 @@ class GuiProjectView(QWidget):
240
241
  self.projBar.buildQuickLinksMenu()
241
242
  return
242
243
 
244
+ @pyqtSlot(str, nwItemClass)
245
+ def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
246
+ """Process new not request."""
247
+ self.projTree.createNewNote(tag, itemClass)
248
+ return
249
+
243
250
  # END Class GuiProjectView
244
251
 
245
252
 
@@ -263,29 +270,26 @@ class GuiProjectToolBar(QWidget):
263
270
  self.setAutoFillBackground(True)
264
271
 
265
272
  # Widget Label
266
- self.viewLabel = QLabel("<b>%s</b>" % self.tr("Project Content"))
273
+ self.viewLabel = QLabel(self.tr("Project Content"))
274
+ self.viewLabel.setFont(SHARED.theme.guiFontB)
267
275
  self.viewLabel.setContentsMargins(0, 0, 0, 0)
268
276
  self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
269
277
 
270
278
  # Quick Links
271
279
  self.mQuick = QMenu(self)
272
280
 
273
- self.tbQuick = QToolButton(self)
281
+ self.tbQuick = NIconToolButton(self, iPx)
274
282
  self.tbQuick.setToolTip("%s [Ctrl+L]" % self.tr("Quick Links"))
275
283
  self.tbQuick.setShortcut("Ctrl+L")
276
- self.tbQuick.setIconSize(QSize(iPx, iPx))
277
284
  self.tbQuick.setMenu(self.mQuick)
278
- self.tbQuick.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
279
285
 
280
286
  # Move Buttons
281
- self.tbMoveU = QToolButton(self)
287
+ self.tbMoveU = NIconToolButton(self, iPx)
282
288
  self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
283
- self.tbMoveU.setIconSize(QSize(iPx, iPx))
284
289
  self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1))
285
290
 
286
- self.tbMoveD = QToolButton(self)
291
+ self.tbMoveD = NIconToolButton(self, iPx)
287
292
  self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
288
- self.tbMoveD.setIconSize(QSize(iPx, iPx))
289
293
  self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1))
290
294
 
291
295
  # Add Item Menu
@@ -324,12 +328,10 @@ class GuiProjectToolBar(QWidget):
324
328
  self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
325
329
  self._buildRootMenu()
326
330
 
327
- self.tbAdd = QToolButton(self)
331
+ self.tbAdd = NIconToolButton(self, iPx)
328
332
  self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item"))
329
333
  self.tbAdd.setShortcut("Ctrl+N")
330
- self.tbAdd.setIconSize(QSize(iPx, iPx))
331
334
  self.tbAdd.setMenu(self.mAdd)
332
- self.tbAdd.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
333
335
 
334
336
  # More Options Menu
335
337
  self.mMore = QMenu(self)
@@ -343,11 +345,9 @@ class GuiProjectToolBar(QWidget):
343
345
  self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
344
346
  self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
345
347
 
346
- self.tbMore = QToolButton(self)
348
+ self.tbMore = NIconToolButton(self, iPx)
347
349
  self.tbMore.setToolTip(self.tr("More Options"))
348
- self.tbMore.setIconSize(QSize(iPx, iPx))
349
350
  self.tbMore.setMenu(self.mMore)
350
- self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
351
351
 
352
352
  # Assemble
353
353
  self.outerBox = QHBoxLayout()
@@ -377,18 +377,12 @@ class GuiProjectToolBar(QWidget):
377
377
  qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
378
378
  self.setPalette(qPalette)
379
379
 
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)
380
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
381
+ self.tbQuick.setStyleSheet(buttonStyle)
388
382
  self.tbMoveU.setStyleSheet(buttonStyle)
389
383
  self.tbMoveD.setStyleSheet(buttonStyle)
390
- self.tbAdd.setStyleSheet(buttonStyleMenu)
391
- self.tbMore.setStyleSheet(buttonStyleMenu)
384
+ self.tbAdd.setStyleSheet(buttonStyle)
385
+ self.tbMore.setStyleSheet(buttonStyle)
392
386
 
393
387
  self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark"))
394
388
  self.tbMoveU.setIcon(SHARED.theme.getIcon("up"))
@@ -505,10 +499,15 @@ class GuiProjectTree(QTreeWidget):
505
499
  self.mainGui = projView.mainGui
506
500
 
507
501
  # Internal Variables
508
- self._treeMap = {}
502
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
509
503
  self._timeChanged = 0.0
510
504
  self._popAlert = None
511
505
 
506
+ # Cached Translations
507
+ self.trActive = self.tr("Active")
508
+ self.trInactive = self.tr("Inactive")
509
+ self.trPermDelete = self.tr("Permanently delete {0} file(s) from Trash?")
510
+
512
511
  # Build GUI
513
512
  # =========
514
513
 
@@ -555,10 +554,6 @@ class GuiProjectTree(QTreeWidget):
555
554
  trRoot = self.invisibleRootItem()
556
555
  trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
557
556
 
558
- # Cached values
559
- self._lblActive = self.tr("Active")
560
- self._lblInactive = self.tr("Inactive")
561
-
562
557
  # Set selection options
563
558
  self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
564
559
  self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
@@ -605,18 +600,18 @@ class GuiProjectTree(QTreeWidget):
605
600
  self._timeChanged = 0.0
606
601
  return
607
602
 
608
- def createNewNote(self, tag: str, itemClass: nwItemClass | None) -> bool:
603
+ def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
609
604
  """Create a new note. This function is used by the document
610
605
  editor to create note files for unknown tags.
611
606
  """
612
- rHandle = SHARED.project.tree.findRoot(itemClass)
613
- if rHandle:
614
- tHandle = SHARED.project.newFile(tag, rHandle)
615
- if tHandle:
607
+ if itemClass != nwItemClass.NO_CLASS:
608
+ if not (rHandle := SHARED.project.tree.findRoot(itemClass)):
609
+ self.newTreeItem(nwItemType.ROOT, itemClass)
610
+ rHandle = SHARED.project.tree.findRoot(itemClass)
611
+ if rHandle and (tHandle := SHARED.project.newFile(tag, rHandle)):
616
612
  SHARED.project.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
617
613
  self.revealNewTreeItem(tHandle, wordCount=True)
618
- return True
619
- return False
614
+ return
620
615
 
621
616
  def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
622
617
  hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
@@ -902,10 +897,7 @@ class GuiProjectTree(QTreeWidget):
902
897
  SHARED.info(self.tr("The Trash folder is already empty."))
903
898
  return False
904
899
 
905
- msgYes = SHARED.question(
906
- self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
907
- )
908
- if not msgYes:
900
+ if not SHARED.question(self.trPermDelete.format(nTrash)):
909
901
  logger.info("Action cancelled by user")
910
902
  return False
911
903
 
@@ -1052,7 +1044,7 @@ class GuiProjectTree(QTreeWidget):
1052
1044
 
1053
1045
  if nwItem.isFileType():
1054
1046
  iconName = "checked" if nwItem.isActive else "unchecked"
1055
- toolTip = self._lblActive if nwItem.isActive else self._lblInactive
1047
+ toolTip = self.trActive if nwItem.isActive else self.trInactive
1056
1048
  trItem.setToolTip(self.C_ACTIVE, toolTip)
1057
1049
  else:
1058
1050
  iconName = "noncheckable"
@@ -1129,9 +1121,8 @@ class GuiProjectTree(QTreeWidget):
1129
1121
  """Get the currently selected handle. If multiple items are
1130
1122
  selected, return the first.
1131
1123
  """
1132
- selItem = self.selectedItems()
1133
- if selItem:
1134
- return selItem[0].data(self.C_DATA, self.D_HANDLE)
1124
+ if items := self.selectedItems():
1125
+ return items[0].data(self.C_DATA, self.D_HANDLE)
1135
1126
  return None
1136
1127
 
1137
1128
  def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
@@ -1143,9 +1134,8 @@ class GuiProjectTree(QTreeWidget):
1143
1134
  if tHandle in self._treeMap:
1144
1135
  self.setCurrentItem(self._treeMap[tHandle])
1145
1136
 
1146
- selIndex = self.selectedIndexes()
1147
- if selIndex and doScroll:
1148
- self.scrollTo(selIndex[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1137
+ if (indexes := self.selectedIndexes()) and doScroll:
1138
+ self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1149
1139
 
1150
1140
  return True
1151
1141
 
@@ -1160,10 +1150,8 @@ class GuiProjectTree(QTreeWidget):
1160
1150
 
1161
1151
  def openContextOnSelected(self) -> bool:
1162
1152
  """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)
1153
+ if items := self.selectedItems():
1154
+ return self._openContextMenu(self.visualItemRect(items[0]).center())
1167
1155
  return False
1168
1156
 
1169
1157
  def changedSince(self, checkTime: float) -> bool:
@@ -1746,7 +1734,7 @@ class _TreeContextMenu(QMenu):
1746
1734
 
1747
1735
  self._item = nwItem
1748
1736
  self._handle = nwItem.itemHandle
1749
- self._items: list[str] = []
1737
+ self._items: list[NWItem] = []
1750
1738
 
1751
1739
  logger.debug("Ready: _TreeContextMenu")
1752
1740
 
@@ -1799,13 +1787,17 @@ class _TreeContextMenu(QMenu):
1799
1787
 
1800
1788
  return
1801
1789
 
1802
- def buildMultiSelectMenu(self, items: list[str]) -> None:
1790
+ def buildMultiSelectMenu(self, handles: list[str]) -> None:
1803
1791
  """Build the multi-select menu."""
1804
- self._items = items
1792
+ self._items = []
1793
+ for tHandle in handles:
1794
+ if (tItem := SHARED.project.tree[tHandle]):
1795
+ self._items.append(tItem)
1796
+
1805
1797
  self._itemActive(True)
1806
1798
  self._itemStatusImport(True)
1807
1799
  self.addSeparator()
1808
- self._moveToTrash(True)
1800
+ self._multiMoveToTrash()
1809
1801
  return
1810
1802
 
1811
1803
  ##
@@ -1836,7 +1828,7 @@ class _TreeContextMenu(QMenu):
1836
1828
 
1837
1829
  def _itemHeader(self) -> None:
1838
1830
  """Check if there is a header that can be used for rename."""
1839
- if hItem := SHARED.project.index.getItemHeader(self._handle, "T0001"):
1831
+ if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"):
1840
1832
  action = self.addAction(self.tr("Rename to Heading"))
1841
1833
  action.triggered.connect(
1842
1834
  lambda: self.projTree.renameTreeItem(self._handle, hItem.title)
@@ -1847,9 +1839,9 @@ class _TreeContextMenu(QMenu):
1847
1839
  """Add Active/Inactive actions."""
1848
1840
  if multi:
1849
1841
  mSub = self.addMenu(self.tr("Set Active to ..."))
1850
- aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.tr("Active"))
1842
+ aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive)
1851
1843
  aOne.triggered.connect(lambda: self._iterItemActive(True))
1852
- aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.tr("Inactive"))
1844
+ aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive)
1853
1845
  aTwo.triggered.connect(lambda: self._iterItemActive(False))
1854
1846
  else:
1855
1847
  action = self.addAction(self.tr("Toggle Active"))
@@ -1935,7 +1927,7 @@ class _TreeContextMenu(QMenu):
1935
1927
  action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True))
1936
1928
 
1937
1929
  if isFile:
1938
- action = menu.addAction(self.tr("Split Document by Headers"))
1930
+ action = menu.addAction(self.tr("Split Document by Headings"))
1939
1931
  action.triggered.connect(lambda: tree._splitDocument(tHandle))
1940
1932
 
1941
1933
  return
@@ -1949,11 +1941,9 @@ class _TreeContextMenu(QMenu):
1949
1941
  action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, True))
1950
1942
  action = self.addAction(self.tr("Collapse All"))
1951
1943
  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))
1944
+
1945
+ action = self.addAction(self.tr("Duplicate"))
1946
+ action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
1957
1947
 
1958
1948
  if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
1959
1949
  action = self.addAction(self.tr("Delete Permanently"))
@@ -1964,10 +1954,14 @@ class _TreeContextMenu(QMenu):
1964
1954
 
1965
1955
  return
1966
1956
 
1967
- def _moveToTrash(self, multi: bool) -> None:
1957
+ def _multiMoveToTrash(self) -> None:
1968
1958
  """Add move to Trash action."""
1969
- action = self.addAction(self.tr("Move to Trash"))
1970
- if multi:
1959
+ areTrash = [i.itemClass == nwItemClass.TRASH for i in self._items]
1960
+ if all(areTrash):
1961
+ action = self.addAction(self.tr("Delete Permanently"))
1962
+ action.triggered.connect(self._iterPermDelete)
1963
+ elif not any(areTrash):
1964
+ action = self.addAction(self.tr("Move to Trash"))
1971
1965
  action.triggered.connect(self._iterMoveToTrash)
1972
1966
  return
1973
1967
 
@@ -1979,10 +1973,19 @@ class _TreeContextMenu(QMenu):
1979
1973
  def _iterMoveToTrash(self) -> None:
1980
1974
  """Iterate through files and move them to Trash."""
1981
1975
  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)
1976
+ for tItem in self._items:
1977
+ if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH:
1978
+ self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False)
1979
+ self.projTree.saveTreeOrder()
1980
+ return
1981
+
1982
+ @pyqtSlot()
1983
+ def _iterPermDelete(self) -> None:
1984
+ """Iterate through files and delete them."""
1985
+ if SHARED.question(self.projTree.trPermDelete.format(len(self._items))):
1986
+ for tItem in self._items:
1987
+ if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH:
1988
+ self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False)
1986
1989
  self.projTree.saveTreeOrder()
1987
1990
  return
1988
1991
 
@@ -2000,12 +2003,11 @@ class _TreeContextMenu(QMenu):
2000
2003
 
2001
2004
  def _iterItemActive(self, isActive: bool) -> None:
2002
2005
  """Set the active status of multiple items."""
2003
- for tHandle in self._items:
2004
- tItem = SHARED.project.tree[tHandle]
2006
+ for tItem in self._items:
2005
2007
  if tItem and tItem.isFileType():
2006
2008
  tItem.setActive(isActive)
2007
- self.projTree.setTreeItemValues(tHandle)
2008
- self.projTree._alertTreeChange(tHandle, flush=False)
2009
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2010
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
2009
2011
  return
2010
2012
 
2011
2013
  def _changeItemStatus(self, key: str) -> None:
@@ -2017,12 +2019,11 @@ class _TreeContextMenu(QMenu):
2017
2019
 
2018
2020
  def _iterSetItemStatus(self, key: str) -> None:
2019
2021
  """Change the status value for multiple items."""
2020
- for tHandle in self._items:
2021
- tItem = SHARED.project.tree[tHandle]
2022
+ for tItem in self._items:
2022
2023
  if tItem and tItem.isNovelLike():
2023
2024
  tItem.setStatus(key)
2024
- self.projTree.setTreeItemValues(tHandle)
2025
- self.projTree._alertTreeChange(tHandle, flush=False)
2025
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2026
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
2026
2027
  return
2027
2028
 
2028
2029
  def _changeItemImport(self, key: str) -> None:
@@ -2034,12 +2035,11 @@ class _TreeContextMenu(QMenu):
2034
2035
 
2035
2036
  def _iterSetItemImport(self, key: str) -> None:
2036
2037
  """Change the status value for multiple items."""
2037
- for tHandle in self._items:
2038
- tItem = SHARED.project.tree[tHandle]
2038
+ for tItem in self._items:
2039
2039
  if tItem and not tItem.isNovelLike():
2040
2040
  tItem.setImport(key)
2041
- self.projTree.setTreeItemValues(tHandle)
2042
- self.projTree._alertTreeChange(tHandle, flush=False)
2041
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2042
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
2043
2043
  return
2044
2044
 
2045
2045
  def _changeItemLayout(self, itemLayout: nwItemLayout) -> None: