novelWriter 2.2rc1__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 (162) hide show
  1. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/RECORD +149 -132
  3. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/WHEEL +1 -1
  4. novelWriter-2.3.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +11 -6
  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_zh_CN.qm +0 -0
  15. novelwriter/assets/i18n/project_de_DE.json +1 -0
  16. novelwriter/assets/i18n/project_en_US.json +1 -0
  17. novelwriter/assets/i18n/project_es_419.json +11 -0
  18. novelwriter/assets/i18n/project_fr_FR.json +11 -0
  19. novelwriter/assets/i18n/project_it_IT.json +11 -0
  20. novelwriter/assets/i18n/project_ja_JP.json +2 -1
  21. novelwriter/assets/i18n/project_nb_NO.json +1 -0
  22. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  23. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  24. novelwriter/assets/i18n/project_zh_CN.json +11 -0
  25. novelwriter/assets/icons/typicons_dark/icons.conf +11 -2
  26. novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +6 -0
  27. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  28. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
  29. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
  30. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
  31. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
  32. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
  33. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
  34. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
  35. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
  36. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
  37. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  38. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  39. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  40. novelwriter/assets/icons/typicons_dark/typ_th-list.svg +9 -0
  41. novelwriter/assets/icons/typicons_light/icons.conf +11 -2
  42. novelwriter/assets/icons/typicons_light/mixed_document-new.svg +6 -0
  43. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  44. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
  45. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
  46. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
  47. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
  48. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
  49. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
  50. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
  51. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
  52. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
  53. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  54. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  55. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  56. novelwriter/assets/icons/typicons_light/typ_th-list.svg +9 -0
  57. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  58. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  59. novelwriter/assets/images/welcome-dark.jpg +0 -0
  60. novelwriter/assets/images/welcome-light.jpg +0 -0
  61. novelwriter/assets/manual.pdf +0 -0
  62. novelwriter/assets/sample.zip +0 -0
  63. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  64. novelwriter/assets/syntax/default_dark.conf +1 -0
  65. novelwriter/assets/syntax/default_light.conf +1 -0
  66. novelwriter/assets/syntax/grey_dark.conf +1 -0
  67. novelwriter/assets/syntax/grey_light.conf +1 -0
  68. novelwriter/assets/syntax/light_owl.conf +1 -0
  69. novelwriter/assets/syntax/night_owl.conf +1 -0
  70. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  71. novelwriter/assets/syntax/solarized_light.conf +1 -0
  72. novelwriter/assets/syntax/tango.conf +23 -0
  73. novelwriter/assets/syntax/tomorrow.conf +1 -0
  74. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  75. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  76. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  77. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  78. novelwriter/assets/text/credits_en.htm +4 -2
  79. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  80. novelwriter/assets/themes/default_dark.conf +2 -2
  81. novelwriter/assets/themes/default_light.conf +2 -2
  82. novelwriter/common.py +64 -66
  83. novelwriter/config.py +39 -44
  84. novelwriter/constants.py +39 -17
  85. novelwriter/core/buildsettings.py +8 -8
  86. novelwriter/core/coretools.py +198 -157
  87. novelwriter/core/docbuild.py +7 -4
  88. novelwriter/core/document.py +7 -7
  89. novelwriter/core/index.py +90 -57
  90. novelwriter/core/item.py +23 -5
  91. novelwriter/core/options.py +11 -10
  92. novelwriter/core/project.py +73 -47
  93. novelwriter/core/projectdata.py +3 -16
  94. novelwriter/core/projectxml.py +14 -42
  95. novelwriter/core/sessions.py +4 -3
  96. novelwriter/core/spellcheck.py +6 -4
  97. novelwriter/core/status.py +5 -4
  98. novelwriter/core/storage.py +183 -141
  99. novelwriter/core/tohtml.py +6 -4
  100. novelwriter/core/tokenizer.py +110 -83
  101. novelwriter/core/tomd.py +2 -2
  102. novelwriter/core/toodt.py +41 -31
  103. novelwriter/core/tree.py +5 -4
  104. novelwriter/dialogs/about.py +88 -179
  105. novelwriter/dialogs/docmerge.py +30 -20
  106. novelwriter/dialogs/docsplit.py +33 -22
  107. novelwriter/dialogs/editlabel.py +20 -8
  108. novelwriter/dialogs/preferences.py +562 -725
  109. novelwriter/dialogs/{projsettings.py → projectsettings.py} +301 -270
  110. novelwriter/dialogs/quotes.py +47 -36
  111. novelwriter/dialogs/wordlist.py +128 -59
  112. novelwriter/enum.py +25 -22
  113. novelwriter/error.py +2 -2
  114. novelwriter/extensions/circularprogress.py +12 -12
  115. novelwriter/extensions/configlayout.py +185 -146
  116. novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
  117. novelwriter/extensions/modified.py +81 -0
  118. novelwriter/extensions/novelselector.py +27 -13
  119. novelwriter/extensions/pagedsidebar.py +15 -20
  120. novelwriter/extensions/simpleprogress.py +8 -9
  121. novelwriter/extensions/statusled.py +9 -9
  122. novelwriter/extensions/switch.py +32 -64
  123. novelwriter/extensions/switchbox.py +2 -7
  124. novelwriter/extensions/versioninfo.py +153 -0
  125. novelwriter/gui/doceditor.py +250 -214
  126. novelwriter/gui/dochighlight.py +66 -94
  127. novelwriter/gui/docviewer.py +71 -98
  128. novelwriter/gui/docviewerpanel.py +140 -47
  129. novelwriter/gui/editordocument.py +3 -3
  130. novelwriter/gui/itemdetails.py +9 -9
  131. novelwriter/gui/mainmenu.py +47 -47
  132. novelwriter/gui/noveltree.py +53 -61
  133. novelwriter/gui/outline.py +100 -76
  134. novelwriter/gui/projtree.py +246 -112
  135. novelwriter/gui/sidebar.py +9 -8
  136. novelwriter/gui/statusbar.py +49 -7
  137. novelwriter/gui/theme.py +74 -76
  138. novelwriter/guimain.py +175 -330
  139. novelwriter/shared.py +68 -30
  140. novelwriter/tools/dictionaries.py +7 -8
  141. novelwriter/tools/lipsum.py +34 -28
  142. novelwriter/tools/manusbuild.py +3 -4
  143. novelwriter/tools/manuscript.py +25 -32
  144. novelwriter/tools/manussettings.py +194 -225
  145. novelwriter/tools/noveldetails.py +525 -0
  146. novelwriter/tools/welcome.py +819 -0
  147. novelwriter/tools/writingstats.py +26 -13
  148. novelWriter-2.2rc1.dist-info/entry_points.txt +0 -5
  149. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
  150. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
  151. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
  152. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
  153. novelwriter/assets/images/wizard-back.jpg +0 -0
  154. novelwriter/assets/text/gplv3_en.htm +0 -641
  155. novelwriter/assets/text/release_notes.htm +0 -17
  156. novelwriter/dialogs/projdetails.py +0 -525
  157. novelwriter/dialogs/projload.py +0 -298
  158. novelwriter/dialogs/updates.py +0 -182
  159. novelwriter/extensions/pageddialog.py +0 -130
  160. novelwriter/tools/projwizard.py +0 -478
  161. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/LICENSE.md +0 -0
  162. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/top_level.txt +0 -0
@@ -6,9 +6,10 @@ File History:
6
6
  Created: 2018-09-29 [0.0.1] GuiProjectTree
7
7
  Created: 2022-06-06 [2.0rc1] GuiProjectView
8
8
  Created: 2022-06-06 [2.0rc1] GuiProjectToolBar
9
+ Created: 2023-11-22 [2.2rc1] _TreeContextMenu
9
10
 
10
11
  This file is a part of novelWriter
11
- Copyright 2018–2023, Veronica Berglyd Olsen
12
+ Copyright 2018–2024, Veronica Berglyd Olsen
12
13
 
13
14
  This program is free software: you can redistribute it and/or modify
14
15
  it under the terms of the GNU General Public License as published by
@@ -31,15 +32,18 @@ from enum import Enum
31
32
  from time import time
32
33
  from typing import TYPE_CHECKING
33
34
 
34
- from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent, QPalette
35
+ from PyQt5.QtGui import (
36
+ QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette
37
+ )
35
38
  from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
36
39
  from PyQt5.QtWidgets import (
37
- QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
38
- QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
39
- QVBoxLayout, QWidget
40
+ QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
41
+ QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget,
42
+ QTreeWidgetItem, QVBoxLayout, QWidget
40
43
  )
41
44
 
42
45
  from novelwriter import CONFIG, SHARED
46
+ from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
43
47
  from novelwriter.common import minmax
44
48
  from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels
45
49
  from novelwriter.core.item import NWItem
@@ -47,10 +51,7 @@ from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
47
51
  from novelwriter.dialogs.docmerge import GuiDocMerge
48
52
  from novelwriter.dialogs.docsplit import GuiDocSplit
49
53
  from novelwriter.dialogs.editlabel import GuiEditLabel
50
- from novelwriter.dialogs.projsettings import GuiProjectSettings
51
- from novelwriter.enum import (
52
- nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget
53
- )
54
+ from novelwriter.dialogs.projectsettings import GuiProjectSettings
54
55
 
55
56
  if TYPE_CHECKING: # pragma: no cover
56
57
  from novelwriter.guimain import GuiMain
@@ -132,7 +133,9 @@ class GuiProjectView(QWidget):
132
133
  self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected())
133
134
 
134
135
  # Signals
135
- 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)
136
139
 
137
140
  # Function Mappings
138
141
  self.emptyTrash = self.projTree.emptyTrash
@@ -158,7 +161,7 @@ class GuiProjectView(QWidget):
158
161
  self.projTree.initSettings()
159
162
  return
160
163
 
161
- def clearProjectView(self) -> None:
164
+ def closeProjectTasks(self) -> None:
162
165
  """Clear project-related GUI content."""
163
166
  self.projBar.clearContent()
164
167
  self.projBar.setEnabled(False)
@@ -167,7 +170,7 @@ class GuiProjectView(QWidget):
167
170
 
168
171
  def openProjectTasks(self) -> None:
169
172
  """Run open project tasks."""
170
- self.projBar.buildQuickLinkMenu()
173
+ self.projBar.buildQuickLinksMenu()
171
174
  self.projBar.setEnabled(True)
172
175
  return
173
176
 
@@ -213,10 +216,17 @@ class GuiProjectView(QWidget):
213
216
 
214
217
  @pyqtSlot(str)
215
218
  def updateItemValues(self, tHandle: str) -> None:
216
- """Update tree item"""
219
+ """Update tree item."""
217
220
  self.projTree.setTreeItemValues(tHandle)
218
221
  return
219
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
+
220
230
  @pyqtSlot(str, int, int, int)
221
231
  def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None:
222
232
  """Slot for updating the word count of a specific item."""
@@ -226,8 +236,8 @@ class GuiProjectView(QWidget):
226
236
 
227
237
  @pyqtSlot(str)
228
238
  def updateRootItem(self, tHandle: str) -> None:
229
- """If any root item changes, rebuild the quick link menu."""
230
- self.projBar.buildQuickLinkMenu()
239
+ """Process root item changes."""
240
+ self.projBar.buildQuickLinksMenu()
231
241
  return
232
242
 
233
243
  # END Class GuiProjectView
@@ -235,6 +245,8 @@ class GuiProjectView(QWidget):
235
245
 
236
246
  class GuiProjectToolBar(QWidget):
237
247
 
248
+ newDocumentFromTemplate = pyqtSignal(str)
249
+
238
250
  def __init__(self, projView: GuiProjectView) -> None:
239
251
  super().__init__(parent=projView)
240
252
 
@@ -304,6 +316,11 @@ class GuiProjectToolBar(QWidget):
304
316
  lambda: self.projTree.newTreeItem(nwItemType.FOLDER)
305
317
  )
306
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
+
307
324
  self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
308
325
  self._buildRootMenu()
309
326
 
@@ -384,7 +401,7 @@ class GuiProjectToolBar(QWidget):
384
401
  self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
385
402
  self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
386
403
 
387
- self.buildQuickLinkMenu()
404
+ self.buildQuickLinksMenu()
388
405
  self._buildRootMenu()
389
406
 
390
407
  return
@@ -392,21 +409,48 @@ class GuiProjectToolBar(QWidget):
392
409
  def clearContent(self) -> None:
393
410
  """Clear dynamic content on the tool bar."""
394
411
  self.mQuick.clear()
412
+ self.mTemplates.clearMenu()
395
413
  return
396
414
 
397
- def buildQuickLinkMenu(self) -> None:
415
+ def buildQuickLinksMenu(self) -> None:
398
416
  """Build the quick link menu."""
399
417
  logger.debug("Rebuilding quick links menu")
400
418
  self.mQuick.clear()
401
- for n, (tHandle, nwItem) in enumerate(SHARED.project.tree.iterRoots(None)):
402
- aRoot = self.mQuick.addAction(nwItem.itemName)
403
- aRoot.setData(tHandle)
404
- aRoot.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]))
405
- aRoot.triggered.connect(
406
- 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)
407
425
  )
408
426
  return
409
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
+
410
454
  ##
411
455
  # Internal Functions
412
456
  ##
@@ -422,7 +466,6 @@ class GuiProjectToolBar(QWidget):
422
466
 
423
467
  self.mAddRoot.clear()
424
468
  addClass(nwItemClass.NOVEL)
425
- addClass(nwItemClass.ARCHIVE)
426
469
  self.mAddRoot.addSeparator()
427
470
  addClass(nwItemClass.PLOT)
428
471
  addClass(nwItemClass.CHARACTER)
@@ -431,26 +474,12 @@ class GuiProjectToolBar(QWidget):
431
474
  addClass(nwItemClass.OBJECT)
432
475
  addClass(nwItemClass.ENTITY)
433
476
  addClass(nwItemClass.CUSTOM)
477
+ self.mAddRoot.addSeparator()
478
+ addClass(nwItemClass.ARCHIVE)
479
+ addClass(nwItemClass.TEMPLATE)
434
480
 
435
481
  return
436
482
 
437
- ##
438
- # Private Slots
439
- ##
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
-
454
483
  # END Class GuiProjectToolBar
455
484
 
456
485
 
@@ -465,6 +494,8 @@ class GuiProjectTree(QTreeWidget):
465
494
  D_HANDLE = Qt.ItemDataRole.UserRole
466
495
  D_WORDS = Qt.ItemDataRole.UserRole + 1
467
496
 
497
+ itemRefreshed = pyqtSignal(str, NWItem, QIcon)
498
+
468
499
  def __init__(self, projView: GuiProjectView) -> None:
469
500
  super().__init__(parent=projView)
470
501
 
@@ -474,10 +505,15 @@ class GuiProjectTree(QTreeWidget):
474
505
  self.mainGui = projView.mainGui
475
506
 
476
507
  # Internal Variables
477
- self._treeMap = {}
508
+ self._treeMap: dict[str, QTreeWidgetItem] = {}
478
509
  self._timeChanged = 0.0
479
510
  self._popAlert = None
480
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
+
481
517
  # Build GUI
482
518
  # =========
483
519
 
@@ -524,10 +560,6 @@ class GuiProjectTree(QTreeWidget):
524
560
  trRoot = self.invisibleRootItem()
525
561
  trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
526
562
 
527
- # Cached values
528
- self._lblActive = self.tr("Active")
529
- self._lblInactive = self.tr("Inactive")
530
-
531
563
  # Set selection options
532
564
  self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
533
565
  self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
@@ -588,7 +620,7 @@ class GuiProjectTree(QTreeWidget):
588
620
  return False
589
621
 
590
622
  def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
591
- hLevel: int = 1, isNote: bool = False) -> bool:
623
+ hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
592
624
  """Add new item to the tree, with a given itemType (and
593
625
  itemClass if Root), and attach it to the selected handle. Also
594
626
  make sure the item is added in a place it can be added, and that
@@ -628,7 +660,10 @@ class GuiProjectTree(QTreeWidget):
628
660
  # Set default label and determine if new item is to be added
629
661
  # as child or sibling to the selected item
630
662
  if itemType == nwItemType.FILE:
631
- if isNote:
663
+ if copyDoc and (cItem := SHARED.project.tree[copyDoc]):
664
+ newLabel = cItem.itemName
665
+ asChild = sIsParent and pItem.isDocumentLayout()
666
+ elif isNote:
632
667
  newLabel = self.tr("New Note")
633
668
  asChild = sIsParent
634
669
  elif hLevel == 2:
@@ -676,12 +711,14 @@ class GuiProjectTree(QTreeWidget):
676
711
  return True
677
712
 
678
713
  # Handle new file creation
679
- 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:
680
717
  SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
681
718
 
682
719
  # Add the new item to the project tree
683
720
  self.revealNewTreeItem(tHandle, nHandle=nHandle, wordCount=True)
684
- self.mainGui.switchFocus(nwWidget.TREE)
721
+ self.projView.setTreeFocus() # See issue #1376
685
722
 
686
723
  return True
687
724
 
@@ -771,8 +808,7 @@ class GuiProjectTree(QTreeWidget):
771
808
 
772
809
  def renameTreeItem(self, tHandle: str, name: str = "") -> None:
773
810
  """Open a dialog to edit the label of an item."""
774
- tItem = SHARED.project.tree[tHandle]
775
- if tItem:
811
+ if tItem := SHARED.project.tree[tHandle]:
776
812
  newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or tItem.itemName)
777
813
  if dlgOk:
778
814
  tItem.setName(newLabel)
@@ -786,24 +822,24 @@ class GuiProjectTree(QTreeWidget):
786
822
  project structure, and must be called before any code that
787
823
  depends on this order to be up to date.
788
824
  """
789
- theList = []
825
+ items = []
790
826
  for i in range(self.topLevelItemCount()):
791
827
  item = self.topLevelItem(i)
792
828
  if isinstance(item, QTreeWidgetItem):
793
- theList = self._scanChildren(theList, item, i)
829
+ items = self._scanChildren(items, item, i)
794
830
  logger.debug("Saving project tree item order")
795
- SHARED.project.setTreeOrder(theList)
831
+ SHARED.project.setTreeOrder(items)
796
832
  return
797
833
 
798
834
  def getTreeFromHandle(self, tHandle: str) -> list[str]:
799
835
  """Recursively return all the child items starting from a given
800
836
  item handle.
801
837
  """
802
- theList = []
803
- theItem = self._getTreeItem(tHandle)
804
- if theItem is not None:
805
- theList = self._scanChildren(theList, theItem, 0)
806
- 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
807
843
 
808
844
  def requestDeleteItem(self, tHandle: str | None = None) -> bool:
809
845
  """Request an item deleted from the project tree. This function
@@ -858,19 +894,16 @@ class GuiProjectTree(QTreeWidget):
858
894
  SHARED.info(self.tr("There is currently no Trash folder in this project."))
859
895
  return False
860
896
 
861
- theTrash = self.getTreeFromHandle(trashHandle)
862
- if trashHandle in theTrash:
863
- theTrash.remove(trashHandle)
897
+ trashItems = self.getTreeFromHandle(trashHandle)
898
+ if trashHandle in trashItems:
899
+ trashItems.remove(trashHandle)
864
900
 
865
- nTrash = len(theTrash)
901
+ nTrash = len(trashItems)
866
902
  if nTrash == 0:
867
903
  SHARED.info(self.tr("The Trash folder is already empty."))
868
904
  return False
869
905
 
870
- msgYes = SHARED.question(
871
- self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
872
- )
873
- if not msgYes:
906
+ if not SHARED.question(self.trPermDelete.format(nTrash)):
874
907
  logger.info("Action cancelled by user")
875
908
  return False
876
909
 
@@ -1017,7 +1050,7 @@ class GuiProjectTree(QTreeWidget):
1017
1050
 
1018
1051
  if nwItem.isFileType():
1019
1052
  iconName = "checked" if nwItem.isActive else "unchecked"
1020
- toolTip = self._lblActive if nwItem.isActive else self._lblInactive
1053
+ toolTip = self.trActive if nwItem.isActive else self.trInactive
1021
1054
  trItem.setToolTip(self.C_ACTIVE, toolTip)
1022
1055
  else:
1023
1056
  iconName = "noncheckable"
@@ -1030,6 +1063,9 @@ class GuiProjectTree(QTreeWidget):
1030
1063
  trFont.setUnderline(hLevel == "H1")
1031
1064
  trItem.setFont(self.C_NAME, trFont)
1032
1065
 
1066
+ # Emit Refresh Signal
1067
+ self.itemRefreshed.emit(tHandle, nwItem, itemIcon)
1068
+
1033
1069
  return
1034
1070
 
1035
1071
  def propagateCount(self, tHandle: str, newCount: int, countChildren: bool = False) -> None:
@@ -1091,9 +1127,8 @@ class GuiProjectTree(QTreeWidget):
1091
1127
  """Get the currently selected handle. If multiple items are
1092
1128
  selected, return the first.
1093
1129
  """
1094
- selItem = self.selectedItems()
1095
- if selItem:
1096
- 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)
1097
1132
  return None
1098
1133
 
1099
1134
  def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
@@ -1105,9 +1140,8 @@ class GuiProjectTree(QTreeWidget):
1105
1140
  if tHandle in self._treeMap:
1106
1141
  self.setCurrentItem(self._treeMap[tHandle])
1107
1142
 
1108
- selIndex = self.selectedIndexes()
1109
- if selIndex and doScroll:
1110
- self.scrollTo(selIndex[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1143
+ if (indexes := self.selectedIndexes()) and doScroll:
1144
+ self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
1111
1145
 
1112
1146
  return True
1113
1147
 
@@ -1122,10 +1156,8 @@ class GuiProjectTree(QTreeWidget):
1122
1156
 
1123
1157
  def openContextOnSelected(self) -> bool:
1124
1158
  """Open the context menu on the current selected item."""
1125
- selItem = self.selectedItems()
1126
- if selItem:
1127
- pos = self.visualItemRect(selItem[0]).center()
1128
- return self._openContextMenu(pos)
1159
+ if items := self.selectedItems():
1160
+ return self._openContextMenu(self.visualItemRect(items[0]).center())
1129
1161
  return False
1130
1162
 
1131
1163
  def changedSince(self, checkTime: float) -> bool:
@@ -1359,8 +1391,7 @@ class GuiProjectTree(QTreeWidget):
1359
1391
  not including) a given item.
1360
1392
  """
1361
1393
  if isinstance(trItem, QTreeWidgetItem):
1362
- chCount = trItem.childCount()
1363
- for i in range(chCount):
1394
+ for i in range(trItem.childCount()):
1364
1395
  chItem = trItem.child(i)
1365
1396
  chItem.setExpanded(isExpanded)
1366
1397
  self._recursiveSetExpanded(chItem, isExpanded)
@@ -1634,6 +1665,71 @@ class GuiProjectTree(QTreeWidget):
1634
1665
  # END Class GuiProjectTree
1635
1666
 
1636
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
+
1637
1733
  class _TreeContextMenu(QMenu):
1638
1734
 
1639
1735
  def __init__(self, projTree: GuiProjectTree, nwItem: NWItem) -> None:
@@ -1644,13 +1740,13 @@ class _TreeContextMenu(QMenu):
1644
1740
 
1645
1741
  self._item = nwItem
1646
1742
  self._handle = nwItem.itemHandle
1647
- self._items: list[str] = []
1743
+ self._items: list[NWItem] = []
1648
1744
 
1649
1745
  logger.debug("Ready: _TreeContextMenu")
1650
1746
 
1651
1747
  return
1652
1748
 
1653
- def __del__(self): # pragma: no cover
1749
+ def __del__(self) -> None: # pragma: no cover
1654
1750
  logger.debug("Delete: _TreeContextMenu")
1655
1751
  return
1656
1752
 
@@ -1675,10 +1771,15 @@ class _TreeContextMenu(QMenu):
1675
1771
  self._docActions()
1676
1772
  self.addSeparator()
1677
1773
 
1774
+ # Create New Items
1775
+ self._itemCreation()
1776
+ self.addSeparator()
1777
+
1678
1778
  # Edit Item Settings
1679
- aLabel = self.addAction(self.tr("Rename"))
1680
- 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))
1681
1781
  if isFile:
1782
+ self._itemHeader()
1682
1783
  self._itemActive(False)
1683
1784
  self._itemStatusImport(False)
1684
1785
 
@@ -1692,13 +1793,17 @@ class _TreeContextMenu(QMenu):
1692
1793
 
1693
1794
  return
1694
1795
 
1695
- def buildMultiSelectMenu(self, items: list[str]) -> None:
1796
+ def buildMultiSelectMenu(self, handles: list[str]) -> None:
1696
1797
  """Build the multi-select menu."""
1697
- self._items = items
1798
+ self._items = []
1799
+ for tHandle in handles:
1800
+ if (tItem := SHARED.project.tree[tHandle]):
1801
+ self._items.append(tItem)
1802
+
1698
1803
  self._itemActive(True)
1699
1804
  self._itemStatusImport(True)
1700
1805
  self.addSeparator()
1701
- self._moveToTrash(True)
1806
+ self._multiMoveToTrash()
1702
1807
  return
1703
1808
 
1704
1809
  ##
@@ -1717,13 +1822,32 @@ class _TreeContextMenu(QMenu):
1717
1822
  )
1718
1823
  return
1719
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
+
1720
1844
  def _itemActive(self, multi: bool) -> None:
1721
1845
  """Add Active/Inactive actions."""
1722
1846
  if multi:
1723
1847
  mSub = self.addMenu(self.tr("Set Active to ..."))
1724
- aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.tr("Active"))
1848
+ aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive)
1725
1849
  aOne.triggered.connect(lambda: self._iterItemActive(True))
1726
- aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.tr("Inactive"))
1850
+ aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive)
1727
1851
  aTwo.triggered.connect(lambda: self._iterItemActive(False))
1728
1852
  else:
1729
1853
  action = self.addAction(self.tr("Toggle Active"))
@@ -1747,7 +1871,7 @@ class _TreeContextMenu(QMenu):
1747
1871
  menu.addSeparator()
1748
1872
  action = menu.addAction(self.tr("Manage Labels ..."))
1749
1873
  action.triggered.connect(
1750
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS)
1874
+ lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS)
1751
1875
  )
1752
1876
  else:
1753
1877
  menu = self.addMenu(self.tr("Set Importance to ..."))
@@ -1764,13 +1888,13 @@ class _TreeContextMenu(QMenu):
1764
1888
  menu.addSeparator()
1765
1889
  action = menu.addAction(self.tr("Manage Labels ..."))
1766
1890
  action.triggered.connect(
1767
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT)
1891
+ lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT)
1768
1892
  )
1769
1893
  return
1770
1894
 
1771
1895
  def _itemTransform(self, isFile: bool, isFolder: bool, hasChild: bool) -> None:
1772
1896
  """Add actions for the Transform menu."""
1773
- menu = self.addMenu(self.tr("Transform"))
1897
+ menu = self.addMenu(self.tr("Transform ..."))
1774
1898
 
1775
1899
  tree = self.projTree
1776
1900
  tHandle = self._handle
@@ -1838,10 +1962,14 @@ class _TreeContextMenu(QMenu):
1838
1962
 
1839
1963
  return
1840
1964
 
1841
- def _moveToTrash(self, multi: bool) -> None:
1965
+ def _multiMoveToTrash(self) -> None:
1842
1966
  """Add move to Trash action."""
1843
- action = self.addAction(self.tr("Move to Trash"))
1844
- 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"))
1845
1973
  action.triggered.connect(self._iterMoveToTrash)
1846
1974
  return
1847
1975
 
@@ -1853,10 +1981,19 @@ class _TreeContextMenu(QMenu):
1853
1981
  def _iterMoveToTrash(self) -> None:
1854
1982
  """Iterate through files and move them to Trash."""
1855
1983
  if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))):
1856
- for tHandle in self._items:
1857
- tItem = SHARED.project.tree[tHandle]
1858
- if tItem and tItem.isFileType():
1859
- 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)
1860
1997
  self.projTree.saveTreeOrder()
1861
1998
  return
1862
1999
 
@@ -1874,12 +2011,11 @@ class _TreeContextMenu(QMenu):
1874
2011
 
1875
2012
  def _iterItemActive(self, isActive: bool) -> None:
1876
2013
  """Set the active status of multiple items."""
1877
- for tHandle in self._items:
1878
- tItem = SHARED.project.tree[tHandle]
2014
+ for tItem in self._items:
1879
2015
  if tItem and tItem.isFileType():
1880
2016
  tItem.setActive(isActive)
1881
- self.projTree.setTreeItemValues(tHandle)
1882
- self.projTree._alertTreeChange(tHandle, flush=False)
2017
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2018
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
1883
2019
  return
1884
2020
 
1885
2021
  def _changeItemStatus(self, key: str) -> None:
@@ -1891,12 +2027,11 @@ class _TreeContextMenu(QMenu):
1891
2027
 
1892
2028
  def _iterSetItemStatus(self, key: str) -> None:
1893
2029
  """Change the status value for multiple items."""
1894
- for tHandle in self._items:
1895
- tItem = SHARED.project.tree[tHandle]
2030
+ for tItem in self._items:
1896
2031
  if tItem and tItem.isNovelLike():
1897
2032
  tItem.setStatus(key)
1898
- self.projTree.setTreeItemValues(tHandle)
1899
- self.projTree._alertTreeChange(tHandle, flush=False)
2033
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2034
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
1900
2035
  return
1901
2036
 
1902
2037
  def _changeItemImport(self, key: str) -> None:
@@ -1908,12 +2043,11 @@ class _TreeContextMenu(QMenu):
1908
2043
 
1909
2044
  def _iterSetItemImport(self, key: str) -> None:
1910
2045
  """Change the status value for multiple items."""
1911
- for tHandle in self._items:
1912
- tItem = SHARED.project.tree[tHandle]
2046
+ for tItem in self._items:
1913
2047
  if tItem and not tItem.isNovelLike():
1914
2048
  tItem.setImport(key)
1915
- self.projTree.setTreeItemValues(tHandle)
1916
- self.projTree._alertTreeChange(tHandle, flush=False)
2049
+ self.projTree.setTreeItemValues(tItem.itemHandle)
2050
+ self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
1917
2051
  return
1918
2052
 
1919
2053
  def _changeItemLayout(self, itemLayout: nwItemLayout) -> None: