novelWriter 2.2rc1__py3-none-any.whl → 2.3b1__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 (153) hide show
  1. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +141 -129
  3. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +11 -6
  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_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  13. novelwriter/assets/i18n/project_de_DE.json +1 -0
  14. novelwriter/assets/i18n/project_en_US.json +1 -0
  15. novelwriter/assets/i18n/project_es_419.json +11 -0
  16. novelwriter/assets/i18n/project_fr_FR.json +11 -0
  17. novelwriter/assets/i18n/project_it_IT.json +11 -0
  18. novelwriter/assets/i18n/project_ja_JP.json +2 -1
  19. novelwriter/assets/i18n/project_nb_NO.json +1 -0
  20. novelwriter/assets/i18n/project_zh_CN.json +11 -0
  21. novelwriter/assets/icons/typicons_dark/icons.conf +9 -2
  22. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  23. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
  24. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
  25. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
  27. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
  28. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
  29. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
  30. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
  31. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
  32. novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -0
  33. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  34. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  35. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  36. novelwriter/assets/icons/typicons_light/icons.conf +9 -2
  37. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  38. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
  40. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
  41. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
  42. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
  44. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
  45. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
  46. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
  47. novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -0
  48. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  49. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  50. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  51. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  52. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  53. novelwriter/assets/images/welcome-dark.jpg +0 -0
  54. novelwriter/assets/images/welcome-light.jpg +0 -0
  55. novelwriter/assets/manual.pdf +0 -0
  56. novelwriter/assets/sample.zip +0 -0
  57. novelwriter/assets/syntax/default_dark.conf +1 -0
  58. novelwriter/assets/syntax/default_light.conf +1 -0
  59. novelwriter/assets/syntax/grey_dark.conf +1 -0
  60. novelwriter/assets/syntax/grey_light.conf +1 -0
  61. novelwriter/assets/syntax/light_owl.conf +1 -0
  62. novelwriter/assets/syntax/night_owl.conf +1 -0
  63. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  64. novelwriter/assets/syntax/solarized_light.conf +1 -0
  65. novelwriter/assets/syntax/tomorrow.conf +1 -0
  66. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  67. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  68. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  69. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  70. novelwriter/assets/text/credits_en.htm +4 -2
  71. novelwriter/assets/themes/default_dark.conf +2 -2
  72. novelwriter/assets/themes/default_light.conf +2 -2
  73. novelwriter/common.py +64 -66
  74. novelwriter/config.py +39 -44
  75. novelwriter/constants.py +39 -17
  76. novelwriter/core/buildsettings.py +8 -8
  77. novelwriter/core/coretools.py +194 -155
  78. novelwriter/core/docbuild.py +7 -4
  79. novelwriter/core/document.py +7 -7
  80. novelwriter/core/index.py +90 -57
  81. novelwriter/core/item.py +23 -5
  82. novelwriter/core/options.py +11 -10
  83. novelwriter/core/project.py +72 -47
  84. novelwriter/core/projectdata.py +3 -16
  85. novelwriter/core/projectxml.py +14 -42
  86. novelwriter/core/sessions.py +4 -3
  87. novelwriter/core/spellcheck.py +6 -4
  88. novelwriter/core/status.py +5 -4
  89. novelwriter/core/storage.py +179 -141
  90. novelwriter/core/tohtml.py +6 -4
  91. novelwriter/core/tokenizer.py +74 -46
  92. novelwriter/core/tomd.py +2 -2
  93. novelwriter/core/toodt.py +41 -31
  94. novelwriter/core/tree.py +5 -4
  95. novelwriter/dialogs/about.py +88 -179
  96. novelwriter/dialogs/docmerge.py +30 -20
  97. novelwriter/dialogs/docsplit.py +33 -22
  98. novelwriter/dialogs/editlabel.py +20 -8
  99. novelwriter/dialogs/preferences.py +562 -725
  100. novelwriter/dialogs/{projsettings.py → projectsettings.py} +301 -270
  101. novelwriter/dialogs/quotes.py +47 -36
  102. novelwriter/dialogs/wordlist.py +128 -59
  103. novelwriter/enum.py +25 -22
  104. novelwriter/error.py +2 -2
  105. novelwriter/extensions/circularprogress.py +12 -12
  106. novelwriter/extensions/configlayout.py +185 -146
  107. novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
  108. novelwriter/extensions/modified.py +81 -0
  109. novelwriter/extensions/novelselector.py +27 -13
  110. novelwriter/extensions/pagedsidebar.py +15 -20
  111. novelwriter/extensions/simpleprogress.py +8 -9
  112. novelwriter/extensions/statusled.py +9 -9
  113. novelwriter/extensions/switch.py +32 -64
  114. novelwriter/extensions/switchbox.py +2 -7
  115. novelwriter/extensions/versioninfo.py +153 -0
  116. novelwriter/gui/doceditor.py +250 -214
  117. novelwriter/gui/dochighlight.py +66 -94
  118. novelwriter/gui/docviewer.py +71 -98
  119. novelwriter/gui/docviewerpanel.py +140 -47
  120. novelwriter/gui/editordocument.py +3 -3
  121. novelwriter/gui/itemdetails.py +9 -9
  122. novelwriter/gui/mainmenu.py +47 -46
  123. novelwriter/gui/noveltree.py +53 -61
  124. novelwriter/gui/outline.py +100 -76
  125. novelwriter/gui/projtree.py +193 -67
  126. novelwriter/gui/sidebar.py +9 -8
  127. novelwriter/gui/statusbar.py +49 -7
  128. novelwriter/gui/theme.py +65 -74
  129. novelwriter/guimain.py +173 -330
  130. novelwriter/shared.py +68 -30
  131. novelwriter/tools/dictionaries.py +7 -8
  132. novelwriter/tools/lipsum.py +34 -28
  133. novelwriter/tools/manusbuild.py +3 -4
  134. novelwriter/tools/manuscript.py +25 -32
  135. novelwriter/tools/manussettings.py +194 -225
  136. novelwriter/tools/noveldetails.py +525 -0
  137. novelwriter/tools/welcome.py +802 -0
  138. novelwriter/tools/writingstats.py +26 -13
  139. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
  140. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
  141. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
  142. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
  143. novelwriter/assets/images/wizard-back.jpg +0 -0
  144. novelwriter/assets/text/gplv3_en.htm +0 -641
  145. novelwriter/assets/text/release_notes.htm +0 -17
  146. novelwriter/dialogs/projdetails.py +0 -525
  147. novelwriter/dialogs/projload.py +0 -298
  148. novelwriter/dialogs/updates.py +0 -182
  149. novelwriter/extensions/pageddialog.py +0 -130
  150. novelwriter/tools/projwizard.py +0 -478
  151. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/LICENSE.md +0 -0
  152. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
  153. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.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
 
@@ -588,7 +619,7 @@ class GuiProjectTree(QTreeWidget):
588
619
  return False
589
620
 
590
621
  def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
591
- hLevel: int = 1, isNote: bool = False) -> bool:
622
+ hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
592
623
  """Add new item to the tree, with a given itemType (and
593
624
  itemClass if Root), and attach it to the selected handle. Also
594
625
  make sure the item is added in a place it can be added, and that
@@ -628,7 +659,10 @@ class GuiProjectTree(QTreeWidget):
628
659
  # Set default label and determine if new item is to be added
629
660
  # as child or sibling to the selected item
630
661
  if itemType == nwItemType.FILE:
631
- if isNote:
662
+ if copyDoc and (cItem := SHARED.project.tree[copyDoc]):
663
+ newLabel = cItem.itemName
664
+ asChild = sIsParent and pItem.isDocumentLayout()
665
+ elif isNote:
632
666
  newLabel = self.tr("New Note")
633
667
  asChild = sIsParent
634
668
  elif hLevel == 2:
@@ -676,12 +710,14 @@ class GuiProjectTree(QTreeWidget):
676
710
  return True
677
711
 
678
712
  # Handle new file creation
679
- if itemType == nwItemType.FILE and hLevel > 0:
713
+ if itemType == nwItemType.FILE and copyDoc:
714
+ SHARED.project.copyFileContent(tHandle, copyDoc)
715
+ elif itemType == nwItemType.FILE and hLevel > 0:
680
716
  SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
681
717
 
682
718
  # Add the new item to the project tree
683
719
  self.revealNewTreeItem(tHandle, nHandle=nHandle, wordCount=True)
684
- self.mainGui.switchFocus(nwWidget.TREE)
720
+ self.projView.setTreeFocus() # See issue #1376
685
721
 
686
722
  return True
687
723
 
@@ -771,8 +807,7 @@ class GuiProjectTree(QTreeWidget):
771
807
 
772
808
  def renameTreeItem(self, tHandle: str, name: str = "") -> None:
773
809
  """Open a dialog to edit the label of an item."""
774
- tItem = SHARED.project.tree[tHandle]
775
- if tItem:
810
+ if tItem := SHARED.project.tree[tHandle]:
776
811
  newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or tItem.itemName)
777
812
  if dlgOk:
778
813
  tItem.setName(newLabel)
@@ -786,24 +821,24 @@ class GuiProjectTree(QTreeWidget):
786
821
  project structure, and must be called before any code that
787
822
  depends on this order to be up to date.
788
823
  """
789
- theList = []
824
+ items = []
790
825
  for i in range(self.topLevelItemCount()):
791
826
  item = self.topLevelItem(i)
792
827
  if isinstance(item, QTreeWidgetItem):
793
- theList = self._scanChildren(theList, item, i)
828
+ items = self._scanChildren(items, item, i)
794
829
  logger.debug("Saving project tree item order")
795
- SHARED.project.setTreeOrder(theList)
830
+ SHARED.project.setTreeOrder(items)
796
831
  return
797
832
 
798
833
  def getTreeFromHandle(self, tHandle: str) -> list[str]:
799
834
  """Recursively return all the child items starting from a given
800
835
  item handle.
801
836
  """
802
- theList = []
803
- theItem = self._getTreeItem(tHandle)
804
- if theItem is not None:
805
- theList = self._scanChildren(theList, theItem, 0)
806
- return theList
837
+ result = []
838
+ tIten = self._getTreeItem(tHandle)
839
+ if tIten is not None:
840
+ result = self._scanChildren(result, tIten, 0)
841
+ return result
807
842
 
808
843
  def requestDeleteItem(self, tHandle: str | None = None) -> bool:
809
844
  """Request an item deleted from the project tree. This function
@@ -858,11 +893,11 @@ class GuiProjectTree(QTreeWidget):
858
893
  SHARED.info(self.tr("There is currently no Trash folder in this project."))
859
894
  return False
860
895
 
861
- theTrash = self.getTreeFromHandle(trashHandle)
862
- if trashHandle in theTrash:
863
- theTrash.remove(trashHandle)
896
+ trashItems = self.getTreeFromHandle(trashHandle)
897
+ if trashHandle in trashItems:
898
+ trashItems.remove(trashHandle)
864
899
 
865
- nTrash = len(theTrash)
900
+ nTrash = len(trashItems)
866
901
  if nTrash == 0:
867
902
  SHARED.info(self.tr("The Trash folder is already empty."))
868
903
  return False
@@ -1030,6 +1065,9 @@ class GuiProjectTree(QTreeWidget):
1030
1065
  trFont.setUnderline(hLevel == "H1")
1031
1066
  trItem.setFont(self.C_NAME, trFont)
1032
1067
 
1068
+ # Emit Refresh Signal
1069
+ self.itemRefreshed.emit(tHandle, nwItem, itemIcon)
1070
+
1033
1071
  return
1034
1072
 
1035
1073
  def propagateCount(self, tHandle: str, newCount: int, countChildren: bool = False) -> None:
@@ -1359,8 +1397,7 @@ class GuiProjectTree(QTreeWidget):
1359
1397
  not including) a given item.
1360
1398
  """
1361
1399
  if isinstance(trItem, QTreeWidgetItem):
1362
- chCount = trItem.childCount()
1363
- for i in range(chCount):
1400
+ for i in range(trItem.childCount()):
1364
1401
  chItem = trItem.child(i)
1365
1402
  chItem.setExpanded(isExpanded)
1366
1403
  self._recursiveSetExpanded(chItem, isExpanded)
@@ -1634,6 +1671,71 @@ class GuiProjectTree(QTreeWidget):
1634
1671
  # END Class GuiProjectTree
1635
1672
 
1636
1673
 
1674
+ class _UpdatableMenu(QMenu):
1675
+
1676
+ menuItemTriggered = pyqtSignal(str)
1677
+
1678
+ def __init__(self, parent: QWidget) -> None:
1679
+ super().__init__(parent=parent)
1680
+ self._map: dict[str, QAction] = {}
1681
+ self.setTitle(self.tr("From Template"))
1682
+ self.triggered.connect(self._actionTriggered)
1683
+ return
1684
+
1685
+ def __contains__(self, tHandle: str) -> bool:
1686
+ """Look up a handle in the menu."""
1687
+ return tHandle in self._map
1688
+
1689
+ ##
1690
+ # Methods
1691
+ ##
1692
+
1693
+ def addUpdate(self, tHandle: str, name: str, icon: QIcon) -> None:
1694
+ """Add or update a template item."""
1695
+ if tHandle in self._map:
1696
+ action = self._map[tHandle]
1697
+ action.setText(name)
1698
+ action.setIcon(icon)
1699
+ else:
1700
+ action = QAction(icon, name, self)
1701
+ action.setData(tHandle)
1702
+ self.addAction(action)
1703
+ self._map[tHandle] = action
1704
+ self.setActionsVisible(True)
1705
+ return
1706
+
1707
+ def remove(self, tHandle: str) -> None:
1708
+ """Remove a template item."""
1709
+ if action := self._map.pop(tHandle, None):
1710
+ self.removeAction(action)
1711
+ if not self._map:
1712
+ self.setActionsVisible(False)
1713
+ return
1714
+
1715
+ def clearMenu(self) -> None:
1716
+ """Clear all menu content."""
1717
+ self._map.clear()
1718
+ self.clear()
1719
+ return
1720
+
1721
+ def setActionsVisible(self, value: bool) -> None:
1722
+ """Set the visibility of root action."""
1723
+ self.menuAction().setVisible(value)
1724
+ return
1725
+
1726
+ ##
1727
+ # Private Slots
1728
+ ##
1729
+
1730
+ @pyqtSlot(QAction)
1731
+ def _actionTriggered(self, action: QAction) -> None:
1732
+ """Translate the menu trigger into an item trigger."""
1733
+ self.menuItemTriggered.emit(str(action.data()))
1734
+ return
1735
+
1736
+ # END Class _UpdatableMenu
1737
+
1738
+
1637
1739
  class _TreeContextMenu(QMenu):
1638
1740
 
1639
1741
  def __init__(self, projTree: GuiProjectTree, nwItem: NWItem) -> None:
@@ -1650,7 +1752,7 @@ class _TreeContextMenu(QMenu):
1650
1752
 
1651
1753
  return
1652
1754
 
1653
- def __del__(self): # pragma: no cover
1755
+ def __del__(self) -> None: # pragma: no cover
1654
1756
  logger.debug("Delete: _TreeContextMenu")
1655
1757
  return
1656
1758
 
@@ -1675,10 +1777,15 @@ class _TreeContextMenu(QMenu):
1675
1777
  self._docActions()
1676
1778
  self.addSeparator()
1677
1779
 
1780
+ # Create New Items
1781
+ self._itemCreation()
1782
+ self.addSeparator()
1783
+
1678
1784
  # Edit Item Settings
1679
- aLabel = self.addAction(self.tr("Rename"))
1680
- aLabel.triggered.connect(lambda: self.projTree.renameTreeItem(self._handle))
1785
+ action = self.addAction(self.tr("Rename"))
1786
+ action.triggered.connect(lambda: self.projTree.renameTreeItem(self._handle))
1681
1787
  if isFile:
1788
+ self._itemHeader()
1682
1789
  self._itemActive(False)
1683
1790
  self._itemStatusImport(False)
1684
1791
 
@@ -1717,6 +1824,25 @@ class _TreeContextMenu(QMenu):
1717
1824
  )
1718
1825
  return
1719
1826
 
1827
+ def _itemCreation(self) -> None:
1828
+ """Add create item actions."""
1829
+ menu = self.addMenu(self.tr("Create New ..."))
1830
+ menu.addAction(self.projView.projBar.aAddEmpty)
1831
+ menu.addAction(self.projView.projBar.aAddChap)
1832
+ menu.addAction(self.projView.projBar.aAddScene)
1833
+ menu.addAction(self.projView.projBar.aAddNote)
1834
+ menu.addAction(self.projView.projBar.aAddFolder)
1835
+ return
1836
+
1837
+ def _itemHeader(self) -> None:
1838
+ """Check if there is a header that can be used for rename."""
1839
+ if hItem := SHARED.project.index.getItemHeader(self._handle, "T0001"):
1840
+ action = self.addAction(self.tr("Rename to Heading"))
1841
+ action.triggered.connect(
1842
+ lambda: self.projTree.renameTreeItem(self._handle, hItem.title)
1843
+ )
1844
+ return
1845
+
1720
1846
  def _itemActive(self, multi: bool) -> None:
1721
1847
  """Add Active/Inactive actions."""
1722
1848
  if multi:
@@ -1747,7 +1873,7 @@ class _TreeContextMenu(QMenu):
1747
1873
  menu.addSeparator()
1748
1874
  action = menu.addAction(self.tr("Manage Labels ..."))
1749
1875
  action.triggered.connect(
1750
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS)
1876
+ lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS)
1751
1877
  )
1752
1878
  else:
1753
1879
  menu = self.addMenu(self.tr("Set Importance to ..."))
@@ -1764,13 +1890,13 @@ class _TreeContextMenu(QMenu):
1764
1890
  menu.addSeparator()
1765
1891
  action = menu.addAction(self.tr("Manage Labels ..."))
1766
1892
  action.triggered.connect(
1767
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT)
1893
+ lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT)
1768
1894
  )
1769
1895
  return
1770
1896
 
1771
1897
  def _itemTransform(self, isFile: bool, isFolder: bool, hasChild: bool) -> None:
1772
1898
  """Add actions for the Transform menu."""
1773
- menu = self.addMenu(self.tr("Transform"))
1899
+ menu = self.addMenu(self.tr("Transform ..."))
1774
1900
 
1775
1901
  tree = self.projTree
1776
1902
  tHandle = self._handle
@@ -3,10 +3,10 @@ novelWriter – GUI Main Window SideBar
3
3
  =====================================
4
4
 
5
5
  File History:
6
- Created: 2022-05-10 [2.0rc1]
6
+ Created: 2022-05-10 [2.0rc1] GuiSideBar
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
@@ -27,12 +27,13 @@ import logging
27
27
 
28
28
  from typing import TYPE_CHECKING
29
29
 
30
- from PyQt5.QtCore import QEvent, QPoint, Qt, QSize, pyqtSignal
31
30
  from PyQt5.QtGui import QPalette
31
+ from PyQt5.QtCore import QEvent, QPoint, Qt, QSize, pyqtSignal
32
32
  from PyQt5.QtWidgets import QMenu, QToolButton, QVBoxLayout, QWidget
33
33
 
34
34
  from novelwriter import CONFIG, SHARED
35
35
  from novelwriter.enum import nwView
36
+ from novelwriter.extensions.eventfilters import StatusTipFilter
36
37
 
37
38
  if TYPE_CHECKING: # pragma: no cover
38
39
  from novelwriter.guimain import GuiMain
@@ -54,6 +55,7 @@ class GuiSideBar(QWidget):
54
55
  iPx = CONFIG.pxInt(24)
55
56
  iconSize = QSize(iPx, iPx)
56
57
  self.setContentsMargins(0, 0, 0, 0)
58
+ self.installEventFilter(StatusTipFilter(mainGui))
57
59
 
58
60
  # Buttons
59
61
  self.tbProject = QToolButton(self)
@@ -77,9 +79,9 @@ class GuiSideBar(QWidget):
77
79
  self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
78
80
 
79
81
  self.tbDetails = QToolButton(self)
80
- self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Project Details")))
82
+ self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
81
83
  self.tbDetails.setIconSize(iconSize)
82
- self.tbDetails.clicked.connect(self.mainGui.showProjectDetailsDialog)
84
+ self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog)
83
85
 
84
86
  self.tbStats = QToolButton(self)
85
87
  self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
@@ -162,11 +164,10 @@ class GuiSideBar(QWidget):
162
164
 
163
165
  class _PopRightMenu(QMenu):
164
166
 
165
- def event(self, event: QEvent):
167
+ def event(self, event: QEvent) -> bool:
166
168
  """Overload the show event and move the menu popup location."""
167
169
  if event.type() == QEvent.Show:
168
- parent = self.parent()
169
- if isinstance(parent, QWidget):
170
+ if isinstance(parent := self.parent(), QWidget):
170
171
  offset = QPoint(parent.width(), parent.height() - self.height())
171
172
  self.move(parent.mapToGlobal(offset))
172
173
  return super(_PopRightMenu, self).event(event)
@@ -3,10 +3,10 @@ novelWriter – GUI Main Window Status Bar
3
3
  ========================================
4
4
 
5
5
  File History:
6
- Created: 2019-04-20 [0.0.1]
6
+ Created: 2019-04-20 [0.0.1] GuiMainStatus
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
@@ -27,9 +27,9 @@ import logging
27
27
 
28
28
  from time import time
29
29
  from typing import TYPE_CHECKING, Literal
30
+ from datetime import datetime
30
31
 
31
32
  from PyQt5.QtCore import pyqtSlot, QLocale
32
- from PyQt5.QtGui import QColor
33
33
  from PyQt5.QtWidgets import qApp, QStatusBar, QLabel
34
34
 
35
35
  from novelwriter import CONFIG, SHARED
@@ -50,12 +50,13 @@ class GuiMainStatus(QStatusBar):
50
50
 
51
51
  logger.debug("Create: GuiMainStatus")
52
52
 
53
- self._refTime = -1.0
53
+ self._refTime = -1.0
54
54
  self._userIdle = False
55
+ self._debugInfo = False
55
56
 
56
- colNone = QColor(*SHARED.theme.statNone)
57
- colSaved = QColor(*SHARED.theme.statSaved)
58
- colUnsaved = QColor(*SHARED.theme.statUnsaved)
57
+ colNone = SHARED.theme.statNone
58
+ colSaved = SHARED.theme.statSaved
59
+ colUnsaved = SHARED.theme.statUnsaved
59
60
 
60
61
  iPx = SHARED.theme.baseIconSize
61
62
 
@@ -223,4 +224,45 @@ class GuiMainStatus(QStatusBar):
223
224
  self.setDocumentStatus(StatusLED.S_BAD if status else StatusLED.S_GOOD)
224
225
  return
225
226
 
227
+ ##
228
+ # Debug
229
+ ##
230
+
231
+ def memInfo(self) -> None: # pragma: no cover
232
+ """Display memory info on the status bar. This is used to
233
+ investigate memory usage and Qt widgets that get left in memory.
234
+ Enabled by the --meminfo command line flag.
235
+
236
+ By default, this tracks memory usage diff after launch. To track
237
+ full memory usage, set environment variable PYTHONTRACEMALLOC=1
238
+ before starting novelWriter.
239
+ """
240
+ import tracemalloc
241
+ from collections import Counter
242
+
243
+ widgets = qApp.allWidgets()
244
+ if not self._debugInfo:
245
+ if tracemalloc.is_tracing():
246
+ self._traceMallocRef = "Total"
247
+ else:
248
+ self._traceMallocRef = "Relative"
249
+ tracemalloc.start()
250
+ self._debugInfo = True
251
+ self._wCounts = Counter([type(x).__name__ for x in widgets])
252
+
253
+ if hasattr(self, "_wCounts"):
254
+ diff = Counter([type(x).__name__ for x in widgets]) - self._wCounts
255
+ for name, count in diff.items():
256
+ logger.debug("Widget '%s': +%d", name, count)
257
+
258
+ mem = tracemalloc.get_traced_memory()
259
+ stamp = datetime.now().strftime("%H:%M:%S")
260
+ self.showMessage((
261
+ f"Debug [{stamp}]"
262
+ f" \u2013 Widgets: {len(widgets)}"
263
+ f" \u2013 {self._traceMallocRef} Memory: {mem[0]:n}"
264
+ f" \u2013 Peak: {mem[1]:n}"
265
+ ), 6000)
266
+ return
267
+
226
268
  # END Class GuiMainStatus