novelWriter 2.2.1__py3-none-any.whl → 2.3__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.2.1.dist-info → novelWriter-2.3.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/RECORD +116 -101
  3. novelWriter-2.3.dist-info/entry_points.txt +2 -0
  4. novelwriter/__init__.py +4 -4
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  9. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  10. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  11. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  12. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  13. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  14. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  15. novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +6 -0
  16. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  17. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  18. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/typ_th-list.svg +9 -0
  21. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  22. novelwriter/assets/icons/typicons_light/mixed_document-new.svg +6 -0
  23. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  24. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  25. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  26. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  27. novelwriter/assets/icons/typicons_light/typ_th-list.svg +9 -0
  28. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  29. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  30. novelwriter/assets/images/welcome-dark.jpg +0 -0
  31. novelwriter/assets/images/welcome-light.jpg +0 -0
  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 +4 -2
  50. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  51. novelwriter/assets/themes/default_dark.conf +2 -2
  52. novelwriter/assets/themes/default_light.conf +2 -2
  53. novelwriter/common.py +48 -37
  54. novelwriter/config.py +36 -41
  55. novelwriter/constants.py +38 -16
  56. novelwriter/core/buildsettings.py +7 -7
  57. novelwriter/core/coretools.py +196 -156
  58. novelwriter/core/docbuild.py +6 -3
  59. novelwriter/core/document.py +6 -6
  60. novelwriter/core/index.py +89 -56
  61. novelwriter/core/item.py +21 -3
  62. novelwriter/core/options.py +8 -7
  63. novelwriter/core/project.py +70 -44
  64. novelwriter/core/projectdata.py +1 -14
  65. novelwriter/core/projectxml.py +13 -41
  66. novelwriter/core/sessions.py +2 -1
  67. novelwriter/core/spellcheck.py +2 -1
  68. novelwriter/core/status.py +2 -1
  69. novelwriter/core/storage.py +182 -140
  70. novelwriter/core/tohtml.py +4 -2
  71. novelwriter/core/tokenizer.py +109 -82
  72. novelwriter/core/toodt.py +40 -30
  73. novelwriter/core/tree.py +3 -2
  74. novelwriter/dialogs/about.py +70 -160
  75. novelwriter/dialogs/docmerge.py +6 -5
  76. novelwriter/dialogs/docsplit.py +6 -6
  77. novelwriter/dialogs/editlabel.py +1 -1
  78. novelwriter/dialogs/preferences.py +553 -703
  79. novelwriter/dialogs/{projsettings.py → projectsettings.py} +288 -262
  80. novelwriter/dialogs/quotes.py +27 -23
  81. novelwriter/dialogs/wordlist.py +96 -40
  82. novelwriter/enum.py +20 -18
  83. novelwriter/error.py +1 -1
  84. novelwriter/extensions/circularprogress.py +11 -11
  85. novelwriter/extensions/configlayout.py +185 -134
  86. novelwriter/extensions/modified.py +81 -0
  87. novelwriter/extensions/novelselector.py +26 -12
  88. novelwriter/extensions/pagedsidebar.py +14 -16
  89. novelwriter/extensions/simpleprogress.py +5 -5
  90. novelwriter/extensions/statusled.py +8 -8
  91. novelwriter/extensions/switch.py +31 -63
  92. novelwriter/extensions/switchbox.py +1 -1
  93. novelwriter/extensions/versioninfo.py +153 -0
  94. novelwriter/gui/doceditor.py +178 -150
  95. novelwriter/gui/dochighlight.py +63 -92
  96. novelwriter/gui/docviewer.py +49 -51
  97. novelwriter/gui/docviewerpanel.py +72 -24
  98. novelwriter/gui/itemdetails.py +7 -7
  99. novelwriter/gui/mainmenu.py +14 -19
  100. novelwriter/gui/noveltree.py +9 -8
  101. novelwriter/gui/outline.py +98 -75
  102. novelwriter/gui/projtree.py +241 -106
  103. novelwriter/gui/sidebar.py +3 -4
  104. novelwriter/gui/statusbar.py +3 -4
  105. novelwriter/gui/theme.py +69 -70
  106. novelwriter/guimain.py +51 -156
  107. novelwriter/shared.py +15 -1
  108. novelwriter/tools/dictionaries.py +5 -6
  109. novelwriter/tools/manuscript.py +6 -6
  110. novelwriter/tools/manussettings.py +192 -221
  111. novelwriter/tools/noveldetails.py +525 -0
  112. novelwriter/tools/welcome.py +819 -0
  113. novelwriter/tools/writingstats.py +9 -9
  114. novelWriter-2.2.1.dist-info/entry_points.txt +0 -5
  115. novelwriter/assets/images/wizard-back.jpg +0 -0
  116. novelwriter/assets/text/gplv3_en.htm +0 -641
  117. novelwriter/assets/text/release_notes.htm +0 -60
  118. novelwriter/dialogs/projdetails.py +0 -518
  119. novelwriter/dialogs/projload.py +0 -294
  120. novelwriter/dialogs/updates.py +0 -172
  121. novelwriter/extensions/pageddialog.py +0 -130
  122. novelwriter/tools/projwizard.py +0 -478
  123. {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/LICENSE.md +0 -0
  124. {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/WHEEL +0 -0
  125. {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/top_level.txt +0 -0
@@ -32,12 +32,14 @@ from enum import Enum
32
32
  from time import time
33
33
  from typing import TYPE_CHECKING
34
34
 
35
- from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent, QPalette
35
+ from PyQt5.QtGui import (
36
+ QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette
37
+ )
36
38
  from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
37
39
  from PyQt5.QtWidgets import (
38
- QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
39
- QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
40
- QVBoxLayout, QWidget
40
+ QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
41
+ QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget,
42
+ QTreeWidgetItem, QVBoxLayout, QWidget
41
43
  )
42
44
 
43
45
  from novelwriter import CONFIG, SHARED
@@ -49,7 +51,7 @@ from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
49
51
  from novelwriter.dialogs.docmerge import GuiDocMerge
50
52
  from novelwriter.dialogs.docsplit import GuiDocSplit
51
53
  from novelwriter.dialogs.editlabel import GuiEditLabel
52
- from novelwriter.dialogs.projsettings import GuiProjectSettings
54
+ from novelwriter.dialogs.projectsettings import GuiProjectSettings
53
55
 
54
56
  if TYPE_CHECKING: # pragma: no cover
55
57
  from novelwriter.guimain import GuiMain
@@ -131,7 +133,9 @@ class GuiProjectView(QWidget):
131
133
  self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected())
132
134
 
133
135
  # Signals
134
- self.selectedItemChanged.connect(self.projBar._treeSelectionChanged)
136
+ self.selectedItemChanged.connect(self.projBar.treeSelectionChanged)
137
+ self.projTree.itemRefreshed.connect(self.projBar.treeItemRefreshed)
138
+ self.projBar.newDocumentFromTemplate.connect(self.createFileFromTemplate)
135
139
 
136
140
  # Function Mappings
137
141
  self.emptyTrash = self.projTree.emptyTrash
@@ -157,7 +161,7 @@ class GuiProjectView(QWidget):
157
161
  self.projTree.initSettings()
158
162
  return
159
163
 
160
- def clearProjectView(self) -> None:
164
+ def closeProjectTasks(self) -> None:
161
165
  """Clear project-related GUI content."""
162
166
  self.projBar.clearContent()
163
167
  self.projBar.setEnabled(False)
@@ -166,7 +170,7 @@ class GuiProjectView(QWidget):
166
170
 
167
171
  def openProjectTasks(self) -> None:
168
172
  """Run open project tasks."""
169
- self.projBar.buildQuickLinkMenu()
173
+ self.projBar.buildQuickLinksMenu()
170
174
  self.projBar.setEnabled(True)
171
175
  return
172
176
 
@@ -212,10 +216,17 @@ class GuiProjectView(QWidget):
212
216
 
213
217
  @pyqtSlot(str)
214
218
  def updateItemValues(self, tHandle: str) -> None:
215
- """Update tree item"""
219
+ """Update tree item."""
216
220
  self.projTree.setTreeItemValues(tHandle)
217
221
  return
218
222
 
223
+ @pyqtSlot(str)
224
+ def createFileFromTemplate(self, tHandle: str) -> None:
225
+ """Create a new document from a template."""
226
+ logger.debug("Template selected: '%s'", tHandle)
227
+ self.projTree.newTreeItem(nwItemType.FILE, copyDoc=tHandle)
228
+ return
229
+
219
230
  @pyqtSlot(str, int, int, int)
220
231
  def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None:
221
232
  """Slot for updating the word count of a specific item."""
@@ -225,8 +236,8 @@ class GuiProjectView(QWidget):
225
236
 
226
237
  @pyqtSlot(str)
227
238
  def updateRootItem(self, tHandle: str) -> None:
228
- """If any root item changes, rebuild the quick link menu."""
229
- self.projBar.buildQuickLinkMenu()
239
+ """Process root item changes."""
240
+ self.projBar.buildQuickLinksMenu()
230
241
  return
231
242
 
232
243
  # END Class GuiProjectView
@@ -234,6 +245,8 @@ class GuiProjectView(QWidget):
234
245
 
235
246
  class GuiProjectToolBar(QWidget):
236
247
 
248
+ newDocumentFromTemplate = pyqtSignal(str)
249
+
237
250
  def __init__(self, projView: GuiProjectView) -> None:
238
251
  super().__init__(parent=projView)
239
252
 
@@ -303,6 +316,11 @@ class GuiProjectToolBar(QWidget):
303
316
  lambda: self.projTree.newTreeItem(nwItemType.FOLDER)
304
317
  )
305
318
 
319
+ self.mTemplates = _UpdatableMenu(self.mAdd)
320
+ self.mTemplates.setActionsVisible(False)
321
+ self.mTemplates.menuItemTriggered.connect(lambda h: self.newDocumentFromTemplate.emit(h))
322
+ self.mAdd.addMenu(self.mTemplates)
323
+
306
324
  self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
307
325
  self._buildRootMenu()
308
326
 
@@ -383,7 +401,7 @@ class GuiProjectToolBar(QWidget):
383
401
  self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
384
402
  self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
385
403
 
386
- self.buildQuickLinkMenu()
404
+ self.buildQuickLinksMenu()
387
405
  self._buildRootMenu()
388
406
 
389
407
  return
@@ -391,21 +409,48 @@ class GuiProjectToolBar(QWidget):
391
409
  def clearContent(self) -> None:
392
410
  """Clear dynamic content on the tool bar."""
393
411
  self.mQuick.clear()
412
+ self.mTemplates.clearMenu()
394
413
  return
395
414
 
396
- def buildQuickLinkMenu(self) -> None:
415
+ def buildQuickLinksMenu(self) -> None:
397
416
  """Build the quick link menu."""
398
417
  logger.debug("Rebuilding quick links menu")
399
418
  self.mQuick.clear()
400
- for n, (tHandle, nwItem) in enumerate(SHARED.project.tree.iterRoots(None)):
401
- aRoot = self.mQuick.addAction(nwItem.itemName)
402
- aRoot.setData(tHandle)
403
- aRoot.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]))
404
- aRoot.triggered.connect(
405
- lambda n, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True)
419
+ for tHandle, nwItem in SHARED.project.tree.iterRoots(None):
420
+ action = self.mQuick.addAction(nwItem.itemName)
421
+ action.setData(tHandle)
422
+ action.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]))
423
+ action.triggered.connect(
424
+ lambda _, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True)
406
425
  )
407
426
  return
408
427
 
428
+ ##
429
+ # Public Slots
430
+ ##
431
+
432
+ @pyqtSlot(str, NWItem, QIcon)
433
+ def treeItemRefreshed(self, tHandle: str, nwItem: NWItem, icon: QIcon) -> None:
434
+ """Process change in tree items to update menu content."""
435
+ if nwItem.isTemplateFile() and nwItem.isActive:
436
+ self.mTemplates.addUpdate(tHandle, nwItem.itemName, icon)
437
+ elif tHandle in self.mTemplates:
438
+ self.mTemplates.remove(tHandle)
439
+ return
440
+
441
+ @pyqtSlot(str)
442
+ def treeSelectionChanged(self, tHandle: str) -> None:
443
+ """Toggle the visibility of the new item entries for novel
444
+ documents. They should only be visible if novel documents can
445
+ actually be added.
446
+ """
447
+ nwItem = SHARED.project.tree[tHandle]
448
+ allowDoc = isinstance(nwItem, NWItem) and nwItem.documentAllowed()
449
+ self.aAddEmpty.setVisible(allowDoc)
450
+ self.aAddChap.setVisible(allowDoc)
451
+ self.aAddScene.setVisible(allowDoc)
452
+ return
453
+
409
454
  ##
410
455
  # Internal Functions
411
456
  ##
@@ -421,7 +466,6 @@ class GuiProjectToolBar(QWidget):
421
466
 
422
467
  self.mAddRoot.clear()
423
468
  addClass(nwItemClass.NOVEL)
424
- addClass(nwItemClass.ARCHIVE)
425
469
  self.mAddRoot.addSeparator()
426
470
  addClass(nwItemClass.PLOT)
427
471
  addClass(nwItemClass.CHARACTER)
@@ -430,26 +474,12 @@ class GuiProjectToolBar(QWidget):
430
474
  addClass(nwItemClass.OBJECT)
431
475
  addClass(nwItemClass.ENTITY)
432
476
  addClass(nwItemClass.CUSTOM)
477
+ self.mAddRoot.addSeparator()
478
+ addClass(nwItemClass.ARCHIVE)
479
+ addClass(nwItemClass.TEMPLATE)
433
480
 
434
481
  return
435
482
 
436
- ##
437
- # Private Slots
438
- ##
439
-
440
- @pyqtSlot(str)
441
- def _treeSelectionChanged(self, tHandle: str) -> None:
442
- """Toggle the visibility of the new item entries for novel
443
- documents. They should only be visible if novel documents can
444
- actually be added.
445
- """
446
- nwItem = SHARED.project.tree[tHandle]
447
- allowDoc = isinstance(nwItem, NWItem) and nwItem.documentAllowed()
448
- self.aAddEmpty.setVisible(allowDoc)
449
- self.aAddChap.setVisible(allowDoc)
450
- self.aAddScene.setVisible(allowDoc)
451
- return
452
-
453
483
  # END Class GuiProjectToolBar
454
484
 
455
485
 
@@ -464,6 +494,8 @@ class GuiProjectTree(QTreeWidget):
464
494
  D_HANDLE = Qt.ItemDataRole.UserRole
465
495
  D_WORDS = Qt.ItemDataRole.UserRole + 1
466
496
 
497
+ itemRefreshed = pyqtSignal(str, NWItem, QIcon)
498
+
467
499
  def __init__(self, projView: GuiProjectView) -> None:
468
500
  super().__init__(parent=projView)
469
501
 
@@ -473,10 +505,15 @@ class GuiProjectTree(QTreeWidget):
473
505
  self.mainGui = projView.mainGui
474
506
 
475
507
  # Internal Variables
476
- self._treeMap = {}
508
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
477
509
  self._timeChanged = 0.0
478
510
  self._popAlert = None
479
511
 
512
+ # Cached Translations
513
+ self.trActive = self.tr("Active")
514
+ self.trInactive = self.tr("Inactive")
515
+ self.trPermDelete = self.tr("Permanently delete {0} file(s) from Trash?")
516
+
480
517
  # Build GUI
481
518
  # =========
482
519
 
@@ -523,10 +560,6 @@ class GuiProjectTree(QTreeWidget):
523
560
  trRoot = self.invisibleRootItem()
524
561
  trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
525
562
 
526
- # Cached values
527
- self._lblActive = self.tr("Active")
528
- self._lblInactive = self.tr("Inactive")
529
-
530
563
  # Set selection options
531
564
  self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
532
565
  self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
@@ -587,7 +620,7 @@ class GuiProjectTree(QTreeWidget):
587
620
  return False
588
621
 
589
622
  def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
590
- hLevel: int = 1, isNote: bool = False) -> bool:
623
+ hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
591
624
  """Add new item to the tree, with a given itemType (and
592
625
  itemClass if Root), and attach it to the selected handle. Also
593
626
  make sure the item is added in a place it can be added, and that
@@ -627,7 +660,10 @@ class GuiProjectTree(QTreeWidget):
627
660
  # Set default label and determine if new item is to be added
628
661
  # as child or sibling to the selected item
629
662
  if itemType == nwItemType.FILE:
630
- if isNote:
663
+ if copyDoc and (cItem := SHARED.project.tree[copyDoc]):
664
+ newLabel = cItem.itemName
665
+ asChild = sIsParent and pItem.isDocumentLayout()
666
+ elif isNote:
631
667
  newLabel = self.tr("New Note")
632
668
  asChild = sIsParent
633
669
  elif hLevel == 2:
@@ -675,7 +711,9 @@ class GuiProjectTree(QTreeWidget):
675
711
  return True
676
712
 
677
713
  # Handle new file creation
678
- if itemType == nwItemType.FILE and hLevel > 0:
714
+ if itemType == nwItemType.FILE and copyDoc:
715
+ SHARED.project.copyFileContent(tHandle, copyDoc)
716
+ elif itemType == nwItemType.FILE and hLevel > 0:
679
717
  SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
680
718
 
681
719
  # Add the new item to the project tree
@@ -770,8 +808,7 @@ class GuiProjectTree(QTreeWidget):
770
808
 
771
809
  def renameTreeItem(self, tHandle: str, name: str = "") -> None:
772
810
  """Open a dialog to edit the label of an item."""
773
- tItem = SHARED.project.tree[tHandle]
774
- if tItem:
811
+ if tItem := SHARED.project.tree[tHandle]:
775
812
  newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or tItem.itemName)
776
813
  if dlgOk:
777
814
  tItem.setName(newLabel)
@@ -785,24 +822,24 @@ class GuiProjectTree(QTreeWidget):
785
822
  project structure, and must be called before any code that
786
823
  depends on this order to be up to date.
787
824
  """
788
- theList = []
825
+ items = []
789
826
  for i in range(self.topLevelItemCount()):
790
827
  item = self.topLevelItem(i)
791
828
  if isinstance(item, QTreeWidgetItem):
792
- theList = self._scanChildren(theList, item, i)
829
+ items = self._scanChildren(items, item, i)
793
830
  logger.debug("Saving project tree item order")
794
- SHARED.project.setTreeOrder(theList)
831
+ SHARED.project.setTreeOrder(items)
795
832
  return
796
833
 
797
834
  def getTreeFromHandle(self, tHandle: str) -> list[str]:
798
835
  """Recursively return all the child items starting from a given
799
836
  item handle.
800
837
  """
801
- theList = []
802
- theItem = self._getTreeItem(tHandle)
803
- if theItem is not None:
804
- theList = self._scanChildren(theList, theItem, 0)
805
- return theList
838
+ result = []
839
+ tIten = self._getTreeItem(tHandle)
840
+ if tIten is not None:
841
+ result = self._scanChildren(result, tIten, 0)
842
+ return result
806
843
 
807
844
  def requestDeleteItem(self, tHandle: str | None = None) -> bool:
808
845
  """Request an item deleted from the project tree. This function
@@ -857,19 +894,16 @@ class GuiProjectTree(QTreeWidget):
857
894
  SHARED.info(self.tr("There is currently no Trash folder in this project."))
858
895
  return False
859
896
 
860
- theTrash = self.getTreeFromHandle(trashHandle)
861
- if trashHandle in theTrash:
862
- theTrash.remove(trashHandle)
897
+ trashItems = self.getTreeFromHandle(trashHandle)
898
+ if trashHandle in trashItems:
899
+ trashItems.remove(trashHandle)
863
900
 
864
- nTrash = len(theTrash)
901
+ nTrash = len(trashItems)
865
902
  if nTrash == 0:
866
903
  SHARED.info(self.tr("The Trash folder is already empty."))
867
904
  return False
868
905
 
869
- msgYes = SHARED.question(
870
- self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
871
- )
872
- if not msgYes:
906
+ if not SHARED.question(self.trPermDelete.format(nTrash)):
873
907
  logger.info("Action cancelled by user")
874
908
  return False
875
909
 
@@ -1016,7 +1050,7 @@ class GuiProjectTree(QTreeWidget):
1016
1050
 
1017
1051
  if nwItem.isFileType():
1018
1052
  iconName = "checked" if nwItem.isActive else "unchecked"
1019
- toolTip = self._lblActive if nwItem.isActive else self._lblInactive
1053
+ toolTip = self.trActive if nwItem.isActive else self.trInactive
1020
1054
  trItem.setToolTip(self.C_ACTIVE, toolTip)
1021
1055
  else:
1022
1056
  iconName = "noncheckable"
@@ -1029,6 +1063,9 @@ class GuiProjectTree(QTreeWidget):
1029
1063
  trFont.setUnderline(hLevel == "H1")
1030
1064
  trItem.setFont(self.C_NAME, trFont)
1031
1065
 
1066
+ # Emit Refresh Signal
1067
+ self.itemRefreshed.emit(tHandle, nwItem, itemIcon)
1068
+
1032
1069
  return
1033
1070
 
1034
1071
  def propagateCount(self, tHandle: str, newCount: int, countChildren: bool = False) -> None:
@@ -1090,9 +1127,8 @@ class GuiProjectTree(QTreeWidget):
1090
1127
  """Get the currently selected handle. If multiple items are
1091
1128
  selected, return the first.
1092
1129
  """
1093
- selItem = self.selectedItems()
1094
- if selItem:
1095
- return selItem[0].data(self.C_DATA, self.D_HANDLE)
1130
+ if items := self.selectedItems():
1131
+ return items[0].data(self.C_DATA, self.D_HANDLE)
1096
1132
  return None
1097
1133
 
1098
1134
  def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
@@ -1104,9 +1140,8 @@ class GuiProjectTree(QTreeWidget):
1104
1140
  if tHandle in self._treeMap:
1105
1141
  self.setCurrentItem(self._treeMap[tHandle])
1106
1142
 
1107
- selIndex = self.selectedIndexes()
1108
- if selIndex and doScroll:
1109
- self.scrollTo(selIndex[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1143
+ if (indexes := self.selectedIndexes()) and doScroll:
1144
+ self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1110
1145
 
1111
1146
  return True
1112
1147
 
@@ -1121,10 +1156,8 @@ class GuiProjectTree(QTreeWidget):
1121
1156
 
1122
1157
  def openContextOnSelected(self) -> bool:
1123
1158
  """Open the context menu on the current selected item."""
1124
- selItem = self.selectedItems()
1125
- if selItem:
1126
- pos = self.visualItemRect(selItem[0]).center()
1127
- return self._openContextMenu(pos)
1159
+ if items := self.selectedItems():
1160
+ return self._openContextMenu(self.visualItemRect(items[0]).center())
1128
1161
  return False
1129
1162
 
1130
1163
  def changedSince(self, checkTime: float) -> bool:
@@ -1358,8 +1391,7 @@ class GuiProjectTree(QTreeWidget):
1358
1391
  not including) a given item.
1359
1392
  """
1360
1393
  if isinstance(trItem, QTreeWidgetItem):
1361
- chCount = trItem.childCount()
1362
- for i in range(chCount):
1394
+ for i in range(trItem.childCount()):
1363
1395
  chItem = trItem.child(i)
1364
1396
  chItem.setExpanded(isExpanded)
1365
1397
  self._recursiveSetExpanded(chItem, isExpanded)
@@ -1633,6 +1665,71 @@ class GuiProjectTree(QTreeWidget):
1633
1665
  # END Class GuiProjectTree
1634
1666
 
1635
1667
 
1668
+ class _UpdatableMenu(QMenu):
1669
+
1670
+ menuItemTriggered = pyqtSignal(str)
1671
+
1672
+ def __init__(self, parent: QWidget) -> None:
1673
+ super().__init__(parent=parent)
1674
+ self._map: dict[str, QAction] = {}
1675
+ self.setTitle(self.tr("From Template"))
1676
+ self.triggered.connect(self._actionTriggered)
1677
+ return
1678
+
1679
+ def __contains__(self, tHandle: str) -> bool:
1680
+ """Look up a handle in the menu."""
1681
+ return tHandle in self._map
1682
+
1683
+ ##
1684
+ # Methods
1685
+ ##
1686
+
1687
+ def addUpdate(self, tHandle: str, name: str, icon: QIcon) -> None:
1688
+ """Add or update a template item."""
1689
+ if tHandle in self._map:
1690
+ action = self._map[tHandle]
1691
+ action.setText(name)
1692
+ action.setIcon(icon)
1693
+ else:
1694
+ action = QAction(icon, name, self)
1695
+ action.setData(tHandle)
1696
+ self.addAction(action)
1697
+ self._map[tHandle] = action
1698
+ self.setActionsVisible(True)
1699
+ return
1700
+
1701
+ def remove(self, tHandle: str) -> None:
1702
+ """Remove a template item."""
1703
+ if action := self._map.pop(tHandle, None):
1704
+ self.removeAction(action)
1705
+ if not self._map:
1706
+ self.setActionsVisible(False)
1707
+ return
1708
+
1709
+ def clearMenu(self) -> None:
1710
+ """Clear all menu content."""
1711
+ self._map.clear()
1712
+ self.clear()
1713
+ return
1714
+
1715
+ def setActionsVisible(self, value: bool) -> None:
1716
+ """Set the visibility of root action."""
1717
+ self.menuAction().setVisible(value)
1718
+ return
1719
+
1720
+ ##
1721
+ # Private Slots
1722
+ ##
1723
+
1724
+ @pyqtSlot(QAction)
1725
+ def _actionTriggered(self, action: QAction) -> None:
1726
+ """Translate the menu trigger into an item trigger."""
1727
+ self.menuItemTriggered.emit(str(action.data()))
1728
+ return
1729
+
1730
+ # END Class _UpdatableMenu
1731
+
1732
+
1636
1733
  class _TreeContextMenu(QMenu):
1637
1734
 
1638
1735
  def __init__(self, projTree: GuiProjectTree, nwItem: NWItem) -> None:
@@ -1643,7 +1740,7 @@ class _TreeContextMenu(QMenu):
1643
1740
 
1644
1741
  self._item = nwItem
1645
1742
  self._handle = nwItem.itemHandle
1646
- self._items: list[str] = []
1743
+ self._items: list[NWItem] = []
1647
1744
 
1648
1745
  logger.debug("Ready: _TreeContextMenu")
1649
1746
 
@@ -1674,10 +1771,15 @@ class _TreeContextMenu(QMenu):
1674
1771
  self._docActions()
1675
1772
  self.addSeparator()
1676
1773
 
1774
+ # Create New Items
1775
+ self._itemCreation()
1776
+ self.addSeparator()
1777
+
1677
1778
  # Edit Item Settings
1678
- aLabel = self.addAction(self.tr("Rename"))
1679
- aLabel.triggered.connect(lambda: self.projTree.renameTreeItem(self._handle))
1779
+ action = self.addAction(self.tr("Rename"))
1780
+ action.triggered.connect(lambda: self.projTree.renameTreeItem(self._handle))
1680
1781
  if isFile:
1782
+ self._itemHeader()
1681
1783
  self._itemActive(False)
1682
1784
  self._itemStatusImport(False)
1683
1785
 
@@ -1691,13 +1793,17 @@ class _TreeContextMenu(QMenu):
1691
1793
 
1692
1794
  return
1693
1795
 
1694
- def buildMultiSelectMenu(self, items: list[str]) -> None:
1796
+ def buildMultiSelectMenu(self, handles: list[str]) -> None:
1695
1797
  """Build the multi-select menu."""
1696
- self._items = items
1798
+ self._items = []
1799
+ for tHandle in handles:
1800
+ if (tItem := SHARED.project.tree[tHandle]):
1801
+ self._items.append(tItem)
1802
+
1697
1803
  self._itemActive(True)
1698
1804
  self._itemStatusImport(True)
1699
1805
  self.addSeparator()
1700
- self._moveToTrash(True)
1806
+ self._multiMoveToTrash()
1701
1807
  return
1702
1808
 
1703
1809
  ##
@@ -1716,13 +1822,32 @@ class _TreeContextMenu(QMenu):
1716
1822
  )
1717
1823
  return
1718
1824
 
1825
+ def _itemCreation(self) -> None:
1826
+ """Add create item actions."""
1827
+ menu = self.addMenu(self.tr("Create New ..."))
1828
+ menu.addAction(self.projView.projBar.aAddEmpty)
1829
+ menu.addAction(self.projView.projBar.aAddChap)
1830
+ menu.addAction(self.projView.projBar.aAddScene)
1831
+ menu.addAction(self.projView.projBar.aAddNote)
1832
+ menu.addAction(self.projView.projBar.aAddFolder)
1833
+ return
1834
+
1835
+ def _itemHeader(self) -> None:
1836
+ """Check if there is a header that can be used for rename."""
1837
+ if hItem := SHARED.project.index.getItemHeader(self._handle, "T0001"):
1838
+ action = self.addAction(self.tr("Rename to Heading"))
1839
+ action.triggered.connect(
1840
+ lambda: self.projTree.renameTreeItem(self._handle, hItem.title)
1841
+ )
1842
+ return
1843
+
1719
1844
  def _itemActive(self, multi: bool) -> None:
1720
1845
  """Add Active/Inactive actions."""
1721
1846
  if multi:
1722
1847
  mSub = self.addMenu(self.tr("Set Active to ..."))
1723
- aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.tr("Active"))
1848
+ aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive)
1724
1849
  aOne.triggered.connect(lambda: self._iterItemActive(True))
1725
- aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.tr("Inactive"))
1850
+ aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive)
1726
1851
  aTwo.triggered.connect(lambda: self._iterItemActive(False))
1727
1852
  else:
1728
1853
  action = self.addAction(self.tr("Toggle Active"))
@@ -1746,7 +1871,7 @@ class _TreeContextMenu(QMenu):
1746
1871
  menu.addSeparator()
1747
1872
  action = menu.addAction(self.tr("Manage Labels ..."))
1748
1873
  action.triggered.connect(
1749
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS)
1874
+ lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS)
1750
1875
  )
1751
1876
  else:
1752
1877
  menu = self.addMenu(self.tr("Set Importance to ..."))
@@ -1763,13 +1888,13 @@ class _TreeContextMenu(QMenu):
1763
1888
  menu.addSeparator()
1764
1889
  action = menu.addAction(self.tr("Manage Labels ..."))
1765
1890
  action.triggered.connect(
1766
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT)
1891
+ lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT)
1767
1892
  )
1768
1893
  return
1769
1894
 
1770
1895
  def _itemTransform(self, isFile: bool, isFolder: bool, hasChild: bool) -> None:
1771
1896
  """Add actions for the Transform menu."""
1772
- menu = self.addMenu(self.tr("Transform"))
1897
+ menu = self.addMenu(self.tr("Transform ..."))
1773
1898
 
1774
1899
  tree = self.projTree
1775
1900
  tHandle = self._handle
@@ -1837,10 +1962,14 @@ class _TreeContextMenu(QMenu):
1837
1962
 
1838
1963
  return
1839
1964
 
1840
- def _moveToTrash(self, multi: bool) -> None:
1965
+ def _multiMoveToTrash(self) -> None:
1841
1966
  """Add move to Trash action."""
1842
- action = self.addAction(self.tr("Move to Trash"))
1843
- if multi:
1967
+ areTrash = [i.itemClass == nwItemClass.TRASH for i in self._items]
1968
+ if all(areTrash):
1969
+ action = self.addAction(self.tr("Delete Permanently"))
1970
+ action.triggered.connect(self._iterPermDelete)
1971
+ elif not any(areTrash):
1972
+ action = self.addAction(self.tr("Move to Trash"))
1844
1973
  action.triggered.connect(self._iterMoveToTrash)
1845
1974
  return
1846
1975
 
@@ -1852,10 +1981,19 @@ class _TreeContextMenu(QMenu):
1852
1981
  def _iterMoveToTrash(self) -> None:
1853
1982
  """Iterate through files and move them to Trash."""
1854
1983
  if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))):
1855
- for tHandle in self._items:
1856
- tItem = SHARED.project.tree[tHandle]
1857
- if tItem and tItem.isFileType():
1858
- self.projTree.moveItemToTrash(tHandle, askFirst=False, flush=False)
1984
+ for tItem in self._items:
1985
+ if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH:
1986
+ self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False)
1987
+ self.projTree.saveTreeOrder()
1988
+ return
1989
+
1990
+ @pyqtSlot()
1991
+ def _iterPermDelete(self) -> None:
1992
+ """Iterate through files and delete them."""
1993
+ if SHARED.question(self.projTree.trPermDelete.format(len(self._items))):
1994
+ for tItem in self._items:
1995
+ if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH:
1996
+ self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False)
1859
1997
  self.projTree.saveTreeOrder()
1860
1998
  return
1861
1999
 
@@ -1873,12 +2011,11 @@ class _TreeContextMenu(QMenu):
1873
2011
 
1874
2012
  def _iterItemActive(self, isActive: bool) -> None:
1875
2013
  """Set the active status of multiple items."""
1876
- for tHandle in self._items:
1877
- tItem = SHARED.project.tree[tHandle]
2014
+ for tItem in self._items:
1878
2015
  if tItem and tItem.isFileType():
1879
2016
  tItem.setActive(isActive)
1880
- self.projTree.setTreeItemValues(tHandle)
1881
- self.projTree._alertTreeChange(tHandle, flush=False)
2017
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2018
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
1882
2019
  return
1883
2020
 
1884
2021
  def _changeItemStatus(self, key: str) -> None:
@@ -1890,12 +2027,11 @@ class _TreeContextMenu(QMenu):
1890
2027
 
1891
2028
  def _iterSetItemStatus(self, key: str) -> None:
1892
2029
  """Change the status value for multiple items."""
1893
- for tHandle in self._items:
1894
- tItem = SHARED.project.tree[tHandle]
2030
+ for tItem in self._items:
1895
2031
  if tItem and tItem.isNovelLike():
1896
2032
  tItem.setStatus(key)
1897
- self.projTree.setTreeItemValues(tHandle)
1898
- self.projTree._alertTreeChange(tHandle, flush=False)
2033
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2034
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
1899
2035
  return
1900
2036
 
1901
2037
  def _changeItemImport(self, key: str) -> None:
@@ -1907,12 +2043,11 @@ class _TreeContextMenu(QMenu):
1907
2043
 
1908
2044
  def _iterSetItemImport(self, key: str) -> None:
1909
2045
  """Change the status value for multiple items."""
1910
- for tHandle in self._items:
1911
- tItem = SHARED.project.tree[tHandle]
2046
+ for tItem in self._items:
1912
2047
  if tItem and not tItem.isNovelLike():
1913
2048
  tItem.setImport(key)
1914
- self.projTree.setTreeItemValues(tHandle)
1915
- self.projTree._alertTreeChange(tHandle, flush=False)
2049
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2050
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
1916
2051
  return
1917
2052
 
1918
2053
  def _changeItemLayout(self, itemLayout: nwItemLayout) -> None: