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.
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/METADATA +1 -1
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/RECORD +116 -101
- novelWriter-2.3.dist-info/entry_points.txt +2 -0
- novelwriter/__init__.py +4 -4
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/project_nl_NL.json +11 -0
- novelwriter/assets/i18n/project_pt_BR.json +11 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
- novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_th-list.svg +9 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -0
- novelwriter/assets/icons/typicons_light/mixed_document-new.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_th-list.svg +9 -0
- novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
- novelwriter/assets/images/novelwriter-text-light.svg +4 -0
- novelwriter/assets/images/welcome-dark.jpg +0 -0
- novelwriter/assets/images/welcome-light.jpg +0 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
- novelwriter/assets/syntax/default_dark.conf +1 -0
- novelwriter/assets/syntax/default_light.conf +1 -0
- novelwriter/assets/syntax/grey_dark.conf +1 -0
- novelwriter/assets/syntax/grey_light.conf +1 -0
- novelwriter/assets/syntax/light_owl.conf +1 -0
- novelwriter/assets/syntax/night_owl.conf +1 -0
- novelwriter/assets/syntax/solarized_dark.conf +1 -0
- novelwriter/assets/syntax/solarized_light.conf +1 -0
- novelwriter/assets/syntax/tango.conf +23 -0
- novelwriter/assets/syntax/tomorrow.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
- novelwriter/assets/text/credits_en.htm +4 -2
- novelwriter/assets/themes/cyberpunk_night.conf +29 -0
- novelwriter/assets/themes/default_dark.conf +2 -2
- novelwriter/assets/themes/default_light.conf +2 -2
- novelwriter/common.py +48 -37
- novelwriter/config.py +36 -41
- novelwriter/constants.py +38 -16
- novelwriter/core/buildsettings.py +7 -7
- novelwriter/core/coretools.py +196 -156
- novelwriter/core/docbuild.py +6 -3
- novelwriter/core/document.py +6 -6
- novelwriter/core/index.py +89 -56
- novelwriter/core/item.py +21 -3
- novelwriter/core/options.py +8 -7
- novelwriter/core/project.py +70 -44
- novelwriter/core/projectdata.py +1 -14
- novelwriter/core/projectxml.py +13 -41
- novelwriter/core/sessions.py +2 -1
- novelwriter/core/spellcheck.py +2 -1
- novelwriter/core/status.py +2 -1
- novelwriter/core/storage.py +182 -140
- novelwriter/core/tohtml.py +4 -2
- novelwriter/core/tokenizer.py +109 -82
- novelwriter/core/toodt.py +40 -30
- novelwriter/core/tree.py +3 -2
- novelwriter/dialogs/about.py +70 -160
- novelwriter/dialogs/docmerge.py +6 -5
- novelwriter/dialogs/docsplit.py +6 -6
- novelwriter/dialogs/editlabel.py +1 -1
- novelwriter/dialogs/preferences.py +553 -703
- novelwriter/dialogs/{projsettings.py → projectsettings.py} +288 -262
- novelwriter/dialogs/quotes.py +27 -23
- novelwriter/dialogs/wordlist.py +96 -40
- novelwriter/enum.py +20 -18
- novelwriter/error.py +1 -1
- novelwriter/extensions/circularprogress.py +11 -11
- novelwriter/extensions/configlayout.py +185 -134
- novelwriter/extensions/modified.py +81 -0
- novelwriter/extensions/novelselector.py +26 -12
- novelwriter/extensions/pagedsidebar.py +14 -16
- novelwriter/extensions/simpleprogress.py +5 -5
- novelwriter/extensions/statusled.py +8 -8
- novelwriter/extensions/switch.py +31 -63
- novelwriter/extensions/switchbox.py +1 -1
- novelwriter/extensions/versioninfo.py +153 -0
- novelwriter/gui/doceditor.py +178 -150
- novelwriter/gui/dochighlight.py +63 -92
- novelwriter/gui/docviewer.py +49 -51
- novelwriter/gui/docviewerpanel.py +72 -24
- novelwriter/gui/itemdetails.py +7 -7
- novelwriter/gui/mainmenu.py +14 -19
- novelwriter/gui/noveltree.py +9 -8
- novelwriter/gui/outline.py +98 -75
- novelwriter/gui/projtree.py +241 -106
- novelwriter/gui/sidebar.py +3 -4
- novelwriter/gui/statusbar.py +3 -4
- novelwriter/gui/theme.py +69 -70
- novelwriter/guimain.py +51 -156
- novelwriter/shared.py +15 -1
- novelwriter/tools/dictionaries.py +5 -6
- novelwriter/tools/manuscript.py +6 -6
- novelwriter/tools/manussettings.py +192 -221
- novelwriter/tools/noveldetails.py +525 -0
- novelwriter/tools/welcome.py +819 -0
- novelwriter/tools/writingstats.py +9 -9
- novelWriter-2.2.1.dist-info/entry_points.txt +0 -5
- novelwriter/assets/images/wizard-back.jpg +0 -0
- novelwriter/assets/text/gplv3_en.htm +0 -641
- novelwriter/assets/text/release_notes.htm +0 -60
- novelwriter/dialogs/projdetails.py +0 -518
- novelwriter/dialogs/projload.py +0 -294
- novelwriter/dialogs/updates.py +0 -172
- novelwriter/extensions/pageddialog.py +0 -130
- novelwriter/tools/projwizard.py +0 -478
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/WHEEL +0 -0
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/top_level.txt +0 -0
novelwriter/gui/projtree.py
CHANGED
@@ -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
|
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,
|
39
|
-
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget,
|
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.
|
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.
|
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
|
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.
|
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
|
-
"""
|
229
|
-
self.projBar.
|
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.
|
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
|
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
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
lambda
|
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
|
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
|
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
|
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
|
-
|
825
|
+
items = []
|
789
826
|
for i in range(self.topLevelItemCount()):
|
790
827
|
item = self.topLevelItem(i)
|
791
828
|
if isinstance(item, QTreeWidgetItem):
|
792
|
-
|
829
|
+
items = self._scanChildren(items, item, i)
|
793
830
|
logger.debug("Saving project tree item order")
|
794
|
-
SHARED.project.setTreeOrder(
|
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
|
-
|
802
|
-
|
803
|
-
if
|
804
|
-
|
805
|
-
return
|
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
|
-
|
861
|
-
if trashHandle in
|
862
|
-
|
897
|
+
trashItems = self.getTreeFromHandle(trashHandle)
|
898
|
+
if trashHandle in trashItems:
|
899
|
+
trashItems.remove(trashHandle)
|
863
900
|
|
864
|
-
nTrash = len(
|
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
|
-
|
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.
|
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
|
-
|
1094
|
-
|
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
|
-
|
1108
|
-
|
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
|
-
|
1125
|
-
|
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
|
-
|
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[
|
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
|
-
|
1679
|
-
|
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,
|
1796
|
+
def buildMultiSelectMenu(self, handles: list[str]) -> None:
|
1695
1797
|
"""Build the multi-select menu."""
|
1696
|
-
self._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.
|
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.
|
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.
|
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.
|
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.
|
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
|
1965
|
+
def _multiMoveToTrash(self) -> None:
|
1841
1966
|
"""Add move to Trash action."""
|
1842
|
-
|
1843
|
-
if
|
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
|
1856
|
-
tItem
|
1857
|
-
|
1858
|
-
|
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
|
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(
|
1881
|
-
self.projTree._alertTreeChange(
|
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
|
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(
|
1898
|
-
self.projTree._alertTreeChange(
|
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
|
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(
|
1915
|
-
self.projTree._alertTreeChange(
|
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:
|