novelWriter 2.5.2__py3-none-any.whl → 2.6__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.5.2.dist-info → novelWriter-2.6.dist-info}/METADATA +5 -4
- {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/RECORD +126 -105
- {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +50 -11
- 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_fr_FR.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/nw_pl_PL.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
- novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
- novelwriter/assets/i18n/project_de_DE.json +4 -2
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/i18n/project_en_US.json +2 -0
- novelwriter/assets/i18n/project_it_IT.json +2 -0
- novelwriter/assets/i18n/project_ja_JP.json +2 -0
- novelwriter/assets/i18n/project_nb_NO.json +2 -0
- novelwriter/assets/i18n/project_nl_NL.json +2 -0
- novelwriter/assets/i18n/project_pl_PL.json +2 -0
- novelwriter/assets/i18n/project_pt_BR.json +2 -0
- novelwriter/assets/i18n/project_ru_RU.json +11 -0
- novelwriter/assets/i18n/project_zh_CN.json +2 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
- novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -0
- novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/text/credits_en.htm +1 -0
- novelwriter/assets/themes/default_light.conf +2 -2
- novelwriter/common.py +101 -3
- novelwriter/config.py +30 -17
- novelwriter/constants.py +189 -81
- novelwriter/core/buildsettings.py +74 -40
- novelwriter/core/coretools.py +146 -148
- novelwriter/core/docbuild.py +133 -171
- novelwriter/core/document.py +1 -1
- novelwriter/core/index.py +39 -38
- novelwriter/core/item.py +42 -9
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/options.py +5 -2
- novelwriter/core/project.py +68 -90
- novelwriter/core/projectdata.py +8 -2
- novelwriter/core/projectxml.py +1 -1
- novelwriter/core/sessions.py +1 -1
- novelwriter/core/spellcheck.py +10 -15
- novelwriter/core/status.py +24 -8
- novelwriter/core/storage.py +1 -1
- novelwriter/core/tree.py +269 -288
- novelwriter/dialogs/about.py +1 -1
- novelwriter/dialogs/docmerge.py +8 -18
- novelwriter/dialogs/docsplit.py +1 -1
- novelwriter/dialogs/editlabel.py +1 -1
- novelwriter/dialogs/preferences.py +47 -34
- novelwriter/dialogs/projectsettings.py +149 -99
- novelwriter/dialogs/quotes.py +1 -1
- novelwriter/dialogs/wordlist.py +11 -10
- novelwriter/enum.py +37 -24
- novelwriter/error.py +2 -2
- novelwriter/extensions/configlayout.py +28 -13
- novelwriter/extensions/eventfilters.py +1 -1
- novelwriter/extensions/modified.py +30 -6
- novelwriter/extensions/novelselector.py +4 -3
- novelwriter/extensions/pagedsidebar.py +9 -9
- novelwriter/extensions/progressbars.py +4 -4
- novelwriter/extensions/statusled.py +3 -3
- novelwriter/extensions/switch.py +3 -3
- novelwriter/extensions/switchbox.py +1 -1
- novelwriter/extensions/versioninfo.py +1 -1
- novelwriter/formats/shared.py +156 -0
- novelwriter/formats/todocx.py +1191 -0
- novelwriter/formats/tohtml.py +454 -0
- novelwriter/{core → formats}/tokenizer.py +497 -495
- novelwriter/formats/tomarkdown.py +218 -0
- novelwriter/{core → formats}/toodt.py +312 -433
- novelwriter/formats/toqdoc.py +486 -0
- novelwriter/formats/toraw.py +91 -0
- novelwriter/gui/doceditor.py +347 -287
- novelwriter/gui/dochighlight.py +97 -85
- novelwriter/gui/docviewer.py +90 -33
- novelwriter/gui/docviewerpanel.py +18 -26
- novelwriter/gui/editordocument.py +18 -3
- novelwriter/gui/itemdetails.py +27 -29
- novelwriter/gui/mainmenu.py +130 -64
- novelwriter/gui/noveltree.py +46 -48
- novelwriter/gui/outline.py +202 -256
- novelwriter/gui/projtree.py +590 -1238
- novelwriter/gui/search.py +11 -19
- novelwriter/gui/sidebar.py +8 -7
- novelwriter/gui/statusbar.py +20 -3
- novelwriter/gui/theme.py +11 -6
- novelwriter/guimain.py +101 -201
- novelwriter/shared.py +67 -28
- novelwriter/text/counting.py +3 -1
- novelwriter/text/patterns.py +169 -61
- novelwriter/tools/dictionaries.py +3 -3
- novelwriter/tools/lipsum.py +1 -1
- novelwriter/tools/manusbuild.py +15 -13
- novelwriter/tools/manuscript.py +121 -79
- novelwriter/tools/manussettings.py +424 -291
- novelwriter/tools/noveldetails.py +1 -1
- novelwriter/tools/welcome.py +6 -6
- novelwriter/tools/writingstats.py +4 -4
- novelwriter/types.py +25 -9
- novelwriter/core/tohtml.py +0 -530
- novelwriter/core/tomarkdown.py +0 -252
- novelwriter/core/toqdoc.py +0 -419
- {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5.2.dist-info → novelWriter-2.6.dist-info}/top_level.txt +0 -0
novelwriter/gui/projtree.py
CHANGED
@@ -3,13 +3,15 @@ novelWriter – GUI Project Tree
|
|
3
3
|
==============================
|
4
4
|
|
5
5
|
File History:
|
6
|
-
Created:
|
7
|
-
Created:
|
8
|
-
Created:
|
9
|
-
Created:
|
6
|
+
Created: 2018-09-29 [0.0.1] GuiProjectTree
|
7
|
+
Created: 2022-06-06 [2.0rc1] GuiProjectView
|
8
|
+
Created: 2022-06-06 [2.0rc1] GuiProjectToolBar
|
9
|
+
Created: 2023-11-22 [2.2rc1] _TreeContextMenu
|
10
|
+
Rewritten: 2024-11-17 [2.6b2] GuiProjectTree
|
11
|
+
Rewritten: 2024-11-20 [2.6b2] _TreeContextMenu
|
10
12
|
|
11
13
|
This file is a part of novelWriter
|
12
|
-
Copyright 2018
|
14
|
+
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
|
13
15
|
|
14
16
|
This program is free software: you can redistribute it and/or modify
|
15
17
|
it under the terms of the GNU General Public License as published by
|
@@ -29,30 +31,30 @@ from __future__ import annotations
|
|
29
31
|
import logging
|
30
32
|
|
31
33
|
from enum import Enum
|
32
|
-
from time import time
|
33
34
|
|
34
|
-
from PyQt5.QtCore import QPoint, Qt,
|
35
|
-
from PyQt5.QtGui import
|
35
|
+
from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot
|
36
|
+
from PyQt5.QtGui import QIcon, QMouseEvent, QPainter, QPalette
|
36
37
|
from PyQt5.QtWidgets import (
|
37
|
-
QAbstractItemView, QAction, QFrame, QHBoxLayout,
|
38
|
-
|
38
|
+
QAbstractItemView, QAction, QFrame, QHBoxLayout, QLabel, QMenu, QShortcut,
|
39
|
+
QStyleOptionViewItem, QTreeView, QVBoxLayout, QWidget
|
39
40
|
)
|
40
41
|
|
41
42
|
from novelwriter import CONFIG, SHARED
|
42
|
-
from novelwriter.common import
|
43
|
-
from novelwriter.constants import
|
43
|
+
from novelwriter.common import qtLambda
|
44
|
+
from novelwriter.constants import nwLabels, nwStyles, nwUnicode, trConst
|
44
45
|
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
|
45
46
|
from novelwriter.core.item import NWItem
|
47
|
+
from novelwriter.core.itemmodel import ProjectModel, ProjectNode
|
46
48
|
from novelwriter.dialogs.docmerge import GuiDocMerge
|
47
49
|
from novelwriter.dialogs.docsplit import GuiDocSplit
|
48
50
|
from novelwriter.dialogs.editlabel import GuiEditLabel
|
49
51
|
from novelwriter.dialogs.projectsettings import GuiProjectSettings
|
50
|
-
from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType
|
52
|
+
from novelwriter.enum import nwChange, nwDocMode, nwItemClass, nwItemLayout, nwItemType
|
51
53
|
from novelwriter.extensions.modified import NIconToolButton
|
52
54
|
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
|
53
55
|
from novelwriter.types import (
|
54
|
-
|
55
|
-
|
56
|
+
QtHeaderFixed, QtHeaderStretch, QtHeaderToContents, QtMouseLeft,
|
57
|
+
QtMouseMiddle, QtScrollAlwaysOff, QtScrollAsNeeded, QtSizeExpanding
|
56
58
|
)
|
57
59
|
|
58
60
|
logger = logging.getLogger(__name__)
|
@@ -64,17 +66,9 @@ class GuiProjectView(QWidget):
|
|
64
66
|
available are mapped through to the project tree class.
|
65
67
|
"""
|
66
68
|
|
67
|
-
# Signals triggered when the meta data values of items change
|
68
|
-
treeItemChanged = pyqtSignal(str)
|
69
|
-
rootFolderChanged = pyqtSignal(str)
|
70
|
-
wordCountsChanged = pyqtSignal()
|
71
|
-
|
72
|
-
# Signals for user interaction with the project tree
|
73
|
-
selectedItemChanged = pyqtSignal(str)
|
74
69
|
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
75
|
-
|
76
|
-
# Requests for the main GUI
|
77
70
|
projectSettingsRequest = pyqtSignal(int)
|
71
|
+
selectedItemChanged = pyqtSignal(str)
|
78
72
|
|
79
73
|
def __init__(self, parent: QWidget) -> None:
|
80
74
|
super().__init__(parent=parent)
|
@@ -97,48 +91,44 @@ class GuiProjectView(QWidget):
|
|
97
91
|
self.keyMoveUp = QShortcut(self.projTree)
|
98
92
|
self.keyMoveUp.setKey("Ctrl+Up")
|
99
93
|
self.keyMoveUp.setContext(Qt.ShortcutContext.WidgetShortcut)
|
100
|
-
self.keyMoveUp.activated.connect(
|
94
|
+
self.keyMoveUp.activated.connect(self.projTree.moveItemUp)
|
101
95
|
|
102
96
|
self.keyMoveDn = QShortcut(self.projTree)
|
103
97
|
self.keyMoveDn.setKey("Ctrl+Down")
|
104
98
|
self.keyMoveDn.setContext(Qt.ShortcutContext.WidgetShortcut)
|
105
|
-
self.keyMoveDn.activated.connect(
|
99
|
+
self.keyMoveDn.activated.connect(self.projTree.moveItemDown)
|
106
100
|
|
107
101
|
self.keyGoPrev = QShortcut(self.projTree)
|
108
102
|
self.keyGoPrev.setKey("Alt+Up")
|
109
103
|
self.keyGoPrev.setContext(Qt.ShortcutContext.WidgetShortcut)
|
110
|
-
self.keyGoPrev.activated.connect(
|
104
|
+
self.keyGoPrev.activated.connect(self.projTree.goToSiblingUp)
|
111
105
|
|
112
106
|
self.keyGoNext = QShortcut(self.projTree)
|
113
107
|
self.keyGoNext.setKey("Alt+Down")
|
114
108
|
self.keyGoNext.setContext(Qt.ShortcutContext.WidgetShortcut)
|
115
|
-
self.keyGoNext.activated.connect(
|
109
|
+
self.keyGoNext.activated.connect(self.projTree.goToSiblingDown)
|
116
110
|
|
117
111
|
self.keyGoUp = QShortcut(self.projTree)
|
118
112
|
self.keyGoUp.setKey("Alt+Left")
|
119
113
|
self.keyGoUp.setContext(Qt.ShortcutContext.WidgetShortcut)
|
120
|
-
self.keyGoUp.activated.connect(
|
114
|
+
self.keyGoUp.activated.connect(self.projTree.goToParent)
|
121
115
|
|
122
116
|
self.keyGoDown = QShortcut(self.projTree)
|
123
117
|
self.keyGoDown.setKey("Alt+Right")
|
124
118
|
self.keyGoDown.setContext(Qt.ShortcutContext.WidgetShortcut)
|
125
|
-
self.keyGoDown.activated.connect(
|
119
|
+
self.keyGoDown.activated.connect(self.projTree.goToFirstChild)
|
126
120
|
|
127
121
|
self.keyContext = QShortcut(self.projTree)
|
128
122
|
self.keyContext.setKey("Ctrl+.")
|
129
123
|
self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
|
130
|
-
self.keyContext.activated.connect(
|
124
|
+
self.keyContext.activated.connect(self.projTree.openContextMenu)
|
131
125
|
|
132
126
|
# Signals
|
133
127
|
self.selectedItemChanged.connect(self.projBar.treeSelectionChanged)
|
134
|
-
self.projTree.itemRefreshed.connect(self.projBar.treeItemRefreshed)
|
135
128
|
self.projBar.newDocumentFromTemplate.connect(self.createFileFromTemplate)
|
136
129
|
|
137
130
|
# Function Mappings
|
138
|
-
self.emptyTrash = self.projTree.emptyTrash
|
139
|
-
self.requestDeleteItem = self.projTree.requestDeleteItem
|
140
131
|
self.getSelectedHandle = self.projTree.getSelectedHandle
|
141
|
-
self.changedSince = self.projTree.changedSince
|
142
132
|
|
143
133
|
return
|
144
134
|
|
@@ -149,7 +139,6 @@ class GuiProjectView(QWidget):
|
|
149
139
|
def updateTheme(self) -> None:
|
150
140
|
"""Update theme elements."""
|
151
141
|
self.projBar.updateTheme()
|
152
|
-
self.populateTree()
|
153
142
|
return
|
154
143
|
|
155
144
|
def initSettings(self) -> None:
|
@@ -166,21 +155,12 @@ class GuiProjectView(QWidget):
|
|
166
155
|
|
167
156
|
def openProjectTasks(self) -> None:
|
168
157
|
"""Run open project tasks."""
|
169
|
-
self.
|
158
|
+
self.projTree.loadModel()
|
159
|
+
self.projBar.buildTemplatesMenu()
|
170
160
|
self.projBar.buildQuickLinksMenu()
|
171
161
|
self.projBar.setEnabled(True)
|
172
162
|
return
|
173
163
|
|
174
|
-
def saveProjectTasks(self) -> None:
|
175
|
-
"""Run save project tasks."""
|
176
|
-
self.projTree.saveTreeOrder()
|
177
|
-
return
|
178
|
-
|
179
|
-
def populateTree(self) -> None:
|
180
|
-
"""Build the tree structure from project data."""
|
181
|
-
self.projTree.buildTree()
|
182
|
-
return
|
183
|
-
|
184
164
|
def setTreeFocus(self) -> None:
|
185
165
|
"""Forward the set focus call to the tree widget."""
|
186
166
|
self.projTree.setFocus()
|
@@ -190,10 +170,20 @@ class GuiProjectView(QWidget):
|
|
190
170
|
"""Check if the project tree has focus."""
|
191
171
|
return self.projTree.hasFocus()
|
192
172
|
|
173
|
+
def connectMenuActions(self, rename: QAction, delete: QAction, trash: QAction) -> None:
|
174
|
+
"""Main menu actions passed to the project tree."""
|
175
|
+
self.projTree.addAction(rename)
|
176
|
+
self.projTree.addAction(delete)
|
177
|
+
self.projTree.addAction(trash)
|
178
|
+
rename.triggered.connect(self.renameTreeItem)
|
179
|
+
delete.triggered.connect(self.projTree.processDeleteRequest)
|
180
|
+
return
|
181
|
+
|
193
182
|
##
|
194
183
|
# Public Slots
|
195
184
|
##
|
196
185
|
|
186
|
+
@pyqtSlot()
|
197
187
|
@pyqtSlot(str, str)
|
198
188
|
def renameTreeItem(self, tHandle: str | None = None, name: str = "") -> None:
|
199
189
|
"""External request to rename an item or the currently selected
|
@@ -201,8 +191,11 @@ class GuiProjectView(QWidget):
|
|
201
191
|
"""
|
202
192
|
if tHandle is None:
|
203
193
|
tHandle = self.projTree.getSelectedHandle()
|
204
|
-
if tHandle:
|
205
|
-
|
194
|
+
if nwItem := SHARED.project.tree[tHandle]:
|
195
|
+
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or nwItem.itemName)
|
196
|
+
if dlgOk:
|
197
|
+
nwItem.setName(newLabel)
|
198
|
+
nwItem.notifyToRefresh()
|
206
199
|
return
|
207
200
|
|
208
201
|
@pyqtSlot(str, bool)
|
@@ -212,10 +205,15 @@ class GuiProjectView(QWidget):
|
|
212
205
|
return
|
213
206
|
|
214
207
|
@pyqtSlot(str)
|
215
|
-
def
|
216
|
-
"""
|
217
|
-
|
218
|
-
|
208
|
+
def setActiveHandle(self, tHandle: str | None) -> None:
|
209
|
+
"""Highlight the active handle."""
|
210
|
+
self.projTree.setActiveHandle(tHandle)
|
211
|
+
return
|
212
|
+
|
213
|
+
@pyqtSlot(str, Enum)
|
214
|
+
def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None:
|
215
|
+
"""Refresh other content when project item changed."""
|
216
|
+
self.projBar.processTemplateDocuments(tHandle)
|
219
217
|
return
|
220
218
|
|
221
219
|
@pyqtSlot(str)
|
@@ -225,31 +223,12 @@ class GuiProjectView(QWidget):
|
|
225
223
|
self.projTree.newTreeItem(nwItemType.FILE, copyDoc=tHandle)
|
226
224
|
return
|
227
225
|
|
228
|
-
@pyqtSlot(str,
|
229
|
-
def
|
230
|
-
"""Slot for updating the word count of a specific item."""
|
231
|
-
self.projTree.propagateCount(tHandle, wCount, countChildren=True)
|
232
|
-
self.wordCountsChanged.emit()
|
233
|
-
return
|
234
|
-
|
235
|
-
@pyqtSlot(str)
|
236
|
-
def updateRootItem(self, tHandle: str) -> None:
|
226
|
+
@pyqtSlot(str, Enum)
|
227
|
+
def updateRootItem(self, tHandle: str, change: nwChange) -> None:
|
237
228
|
"""Process root item changes."""
|
238
229
|
self.projBar.buildQuickLinksMenu()
|
239
230
|
return
|
240
231
|
|
241
|
-
@pyqtSlot(str, nwItemClass)
|
242
|
-
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
|
243
|
-
"""Process new not request."""
|
244
|
-
self.projTree.createNewNote(tag, itemClass)
|
245
|
-
return
|
246
|
-
|
247
|
-
@pyqtSlot(str)
|
248
|
-
def refreshUserLabels(self, kind: str) -> None:
|
249
|
-
"""Refresh status or importance labels."""
|
250
|
-
self.projTree.refreshUserLabels(kind)
|
251
|
-
return
|
252
|
-
|
253
232
|
|
254
233
|
class GuiProjectToolBar(QWidget):
|
255
234
|
|
@@ -286,38 +265,38 @@ class GuiProjectToolBar(QWidget):
|
|
286
265
|
# Move Buttons
|
287
266
|
self.tbMoveU = NIconToolButton(self, iSz)
|
288
267
|
self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
|
289
|
-
self.tbMoveU.clicked.connect(
|
268
|
+
self.tbMoveU.clicked.connect(self.projTree.moveItemUp)
|
290
269
|
|
291
270
|
self.tbMoveD = NIconToolButton(self, iSz)
|
292
271
|
self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
|
293
|
-
self.tbMoveD.clicked.connect(
|
272
|
+
self.tbMoveD.clicked.connect(self.projTree.moveItemDown)
|
294
273
|
|
295
274
|
# Add Item Menu
|
296
275
|
self.mAdd = QMenu(self)
|
297
276
|
|
298
277
|
self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"]))
|
299
278
|
self.aAddEmpty.triggered.connect(
|
300
|
-
|
279
|
+
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=0, isNote=False)
|
301
280
|
)
|
302
281
|
|
303
282
|
self.aAddChap = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"]))
|
304
283
|
self.aAddChap.triggered.connect(
|
305
|
-
|
284
|
+
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=2, isNote=False)
|
306
285
|
)
|
307
286
|
|
308
287
|
self.aAddScene = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"]))
|
309
288
|
self.aAddScene.triggered.connect(
|
310
|
-
|
289
|
+
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=3, isNote=False)
|
311
290
|
)
|
312
291
|
|
313
292
|
self.aAddNote = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["note"]))
|
314
293
|
self.aAddNote.triggered.connect(
|
315
|
-
|
294
|
+
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=1, isNote=True)
|
316
295
|
)
|
317
296
|
|
318
297
|
self.aAddFolder = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["folder"]))
|
319
298
|
self.aAddFolder.triggered.connect(
|
320
|
-
|
299
|
+
qtLambda(self.projTree.newTreeItem, nwItemType.FOLDER)
|
321
300
|
)
|
322
301
|
|
323
302
|
self.mTemplates = _UpdatableMenu(self.mAdd)
|
@@ -337,13 +316,13 @@ class GuiProjectToolBar(QWidget):
|
|
337
316
|
self.mMore = QMenu(self)
|
338
317
|
|
339
318
|
self.aExpand = self.mMore.addAction(self.tr("Expand All"))
|
340
|
-
self.aExpand.triggered.connect(
|
319
|
+
self.aExpand.triggered.connect(self.projTree.expandAll)
|
341
320
|
|
342
321
|
self.aCollapse = self.mMore.addAction(self.tr("Collapse All"))
|
343
|
-
self.aCollapse.triggered.connect(
|
322
|
+
self.aCollapse.triggered.connect(self.projTree.collapseAll)
|
344
323
|
|
345
324
|
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
|
346
|
-
self.aEmptyTrash.triggered.connect(
|
325
|
+
self.aEmptyTrash.triggered.connect(self.projTree.emptyTrash)
|
347
326
|
|
348
327
|
self.tbMore = NIconToolButton(self, iSz)
|
349
328
|
self.tbMore.setToolTip(self.tr("More Options"))
|
@@ -396,6 +375,7 @@ class GuiProjectToolBar(QWidget):
|
|
396
375
|
self.aAddNote.setIcon(SHARED.theme.getIcon("proj_note"))
|
397
376
|
self.aAddFolder.setIcon(SHARED.theme.getIcon("proj_folder"))
|
398
377
|
|
378
|
+
self.buildTemplatesMenu()
|
399
379
|
self.buildQuickLinksMenu()
|
400
380
|
self._buildRootMenu()
|
401
381
|
|
@@ -416,23 +396,30 @@ class GuiProjectToolBar(QWidget):
|
|
416
396
|
action.setData(tHandle)
|
417
397
|
action.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]))
|
418
398
|
action.triggered.connect(
|
419
|
-
|
399
|
+
qtLambda(self.projView.setSelectedHandle, tHandle, doScroll=True)
|
420
400
|
)
|
421
401
|
return
|
422
402
|
|
423
|
-
|
424
|
-
|
425
|
-
|
403
|
+
def buildTemplatesMenu(self) -> None:
|
404
|
+
"""Build the templates menu."""
|
405
|
+
for tHandle, _ in SHARED.project.tree.iterRoots(nwItemClass.TEMPLATE):
|
406
|
+
for dHandle in SHARED.project.tree.subTree(tHandle):
|
407
|
+
self.processTemplateDocuments(dHandle)
|
408
|
+
return
|
426
409
|
|
427
|
-
|
428
|
-
def treeItemRefreshed(self, tHandle: str, nwItem: NWItem, icon: QIcon) -> None:
|
410
|
+
def processTemplateDocuments(self, tHandle: str) -> None:
|
429
411
|
"""Process change in tree items to update menu content."""
|
430
|
-
if
|
431
|
-
|
432
|
-
|
433
|
-
self.mTemplates
|
412
|
+
if item := SHARED.project.tree[tHandle]:
|
413
|
+
if item.isTemplateFile() and item.isActive:
|
414
|
+
self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon())
|
415
|
+
elif tHandle in self.mTemplates:
|
416
|
+
self.mTemplates.remove(tHandle)
|
434
417
|
return
|
435
418
|
|
419
|
+
##
|
420
|
+
# Public Slots
|
421
|
+
##
|
422
|
+
|
436
423
|
@pyqtSlot(str)
|
437
424
|
def treeSelectionChanged(self, tHandle: str) -> None:
|
438
425
|
"""Toggle the visibility of the new item entries for novel
|
@@ -455,7 +442,9 @@ class GuiProjectToolBar(QWidget):
|
|
455
442
|
def addClass(itemClass: nwItemClass) -> None:
|
456
443
|
aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
|
457
444
|
aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]))
|
458
|
-
aNew.triggered.connect(
|
445
|
+
aNew.triggered.connect(
|
446
|
+
qtLambda(self.projTree.newTreeItem, nwItemType.ROOT, itemClass)
|
447
|
+
)
|
459
448
|
self.mAddRoot.addAction(aNew)
|
460
449
|
return
|
461
450
|
|
@@ -476,18 +465,7 @@ class GuiProjectToolBar(QWidget):
|
|
476
465
|
return
|
477
466
|
|
478
467
|
|
479
|
-
class GuiProjectTree(
|
480
|
-
|
481
|
-
C_DATA = 0
|
482
|
-
C_NAME = 0
|
483
|
-
C_COUNT = 1
|
484
|
-
C_ACTIVE = 2
|
485
|
-
C_STATUS = 3
|
486
|
-
|
487
|
-
D_HANDLE = QtUserRole
|
488
|
-
D_WORDS = QtUserRole + 1
|
489
|
-
|
490
|
-
itemRefreshed = pyqtSignal(str, NWItem, QIcon)
|
468
|
+
class GuiProjectTree(QTreeView):
|
491
469
|
|
492
470
|
def __init__(self, projView: GuiProjectView) -> None:
|
493
471
|
super().__init__(parent=projView)
|
@@ -497,25 +475,14 @@ class GuiProjectTree(QTreeWidget):
|
|
497
475
|
self.projView = projView
|
498
476
|
|
499
477
|
# Internal Variables
|
500
|
-
self.
|
501
|
-
self._timeChanged = 0.0
|
502
|
-
self._popAlert = None
|
478
|
+
self._actHandle = None
|
503
479
|
|
504
480
|
# Cached Translations
|
505
|
-
self.trActive =
|
506
|
-
self.trInactive =
|
507
|
-
self.trPermDelete = self.tr("Permanently delete {0} file(s) from Trash?")
|
508
|
-
|
509
|
-
# Build GUI
|
510
|
-
# =========
|
511
|
-
|
512
|
-
# Context Menu
|
513
|
-
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
514
|
-
self.customContextMenuRequested.connect(self._openContextMenu)
|
481
|
+
self.trActive = trConst(nwLabels.ACTIVE_NAME["checked"])
|
482
|
+
self.trInactive = trConst(nwLabels.ACTIVE_NAME["unchecked"])
|
515
483
|
|
516
484
|
# Tree Settings
|
517
485
|
iPx = SHARED.theme.baseIconHeight
|
518
|
-
cMg = CONFIG.pxInt(6)
|
519
486
|
|
520
487
|
self.setIconSize(SHARED.theme.baseIconSize)
|
521
488
|
self.setFrameStyle(QFrame.Shape.NoFrame)
|
@@ -525,47 +492,24 @@ class GuiProjectTree(QTreeWidget):
|
|
525
492
|
self.setAutoExpandDelay(1000)
|
526
493
|
self.setHeaderHidden(True)
|
527
494
|
self.setIndentation(iPx)
|
528
|
-
self.setColumnCount(4)
|
529
|
-
|
530
|
-
# Lock the column sizes
|
531
|
-
treeHeader = self.header()
|
532
|
-
treeHeader.setStretchLastSection(False)
|
533
|
-
treeHeader.setMinimumSectionSize(iPx + cMg)
|
534
|
-
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.Stretch)
|
535
|
-
treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeMode.ResizeToContents)
|
536
|
-
treeHeader.setSectionResizeMode(self.C_ACTIVE, QHeaderView.ResizeMode.Fixed)
|
537
|
-
treeHeader.setSectionResizeMode(self.C_STATUS, QHeaderView.ResizeMode.Fixed)
|
538
|
-
treeHeader.resizeSection(self.C_ACTIVE, iPx + cMg)
|
539
|
-
treeHeader.resizeSection(self.C_STATUS, iPx + cMg)
|
540
495
|
|
541
496
|
# Allow Move by Drag & Drop
|
542
497
|
self.setDragEnabled(True)
|
543
498
|
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
544
499
|
|
545
|
-
# Disable built-in auto scroll as it isn't working in some Qt
|
546
|
-
# releases (see #1561) and instead use our own implementation
|
547
|
-
self.setAutoScroll(False)
|
548
|
-
|
549
|
-
# But don't allow drop on root level
|
550
|
-
# Due to a bug, this stops working somewhere between Qt 5.15.3
|
551
|
-
# and 5.15.8, so this is also blocked in dropEvent (see #1569)
|
552
|
-
trRoot = self.invisibleRootItem()
|
553
|
-
trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
|
554
|
-
|
555
500
|
# Set selection options
|
556
501
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
557
502
|
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
558
503
|
|
559
|
-
#
|
560
|
-
self.
|
561
|
-
self.
|
504
|
+
# Context Menu
|
505
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
506
|
+
self.customContextMenuRequested.connect(self.openContextMenu)
|
562
507
|
|
563
|
-
#
|
564
|
-
self.
|
565
|
-
self.
|
566
|
-
self.
|
567
|
-
self.
|
568
|
-
self._scrollTimer.setInterval(250)
|
508
|
+
# Connect signals
|
509
|
+
self.clicked.connect(self._onSingleClick)
|
510
|
+
self.doubleClicked.connect(self._onDoubleClick)
|
511
|
+
self.collapsed.connect(self._onNodeCollapsed)
|
512
|
+
self.expanded.connect(self._onNodeExpanded)
|
569
513
|
|
570
514
|
# Set custom settings
|
571
515
|
self.initSettings()
|
@@ -578,41 +522,83 @@ class GuiProjectTree(QTreeWidget):
|
|
578
522
|
"""Set or update tree widget settings."""
|
579
523
|
# Scroll bars
|
580
524
|
if CONFIG.hideVScroll:
|
581
|
-
self.setVerticalScrollBarPolicy(
|
525
|
+
self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
|
582
526
|
else:
|
583
|
-
self.setVerticalScrollBarPolicy(
|
527
|
+
self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
|
584
528
|
if CONFIG.hideHScroll:
|
585
|
-
self.setHorizontalScrollBarPolicy(
|
529
|
+
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
|
586
530
|
else:
|
587
|
-
self.setHorizontalScrollBarPolicy(
|
531
|
+
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
532
|
+
return
|
533
|
+
|
534
|
+
##
|
535
|
+
# External Methods
|
536
|
+
##
|
537
|
+
|
538
|
+
def setActiveHandle(self, tHandle: str | None) -> None:
|
539
|
+
"""Set the handle to be highlighted."""
|
540
|
+
self._actHandle = tHandle
|
588
541
|
return
|
589
542
|
|
543
|
+
def getSelectedHandle(self) -> str | None:
|
544
|
+
"""Get the currently selected handle."""
|
545
|
+
if (indexes := self.selectedIndexes()) and (node := self._getNode(indexes[0])):
|
546
|
+
return node.item.itemHandle
|
547
|
+
return None
|
548
|
+
|
590
549
|
##
|
591
|
-
#
|
550
|
+
# Module Internal Methods
|
592
551
|
##
|
593
552
|
|
594
553
|
def clearTree(self) -> None:
|
595
|
-
"""Clear the
|
596
|
-
self.
|
597
|
-
self._treeMap = {}
|
598
|
-
self._timeChanged = 0.0
|
554
|
+
"""Clear the tree view."""
|
555
|
+
self.setModel(None)
|
599
556
|
return
|
600
557
|
|
601
|
-
def
|
602
|
-
"""
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
558
|
+
def loadModel(self) -> None:
|
559
|
+
"""Load and prepare a new project model."""
|
560
|
+
self.setModel(SHARED.project.tree.model)
|
561
|
+
|
562
|
+
# Lock the column sizes
|
563
|
+
iPx = SHARED.theme.baseIconHeight
|
564
|
+
cMg = CONFIG.pxInt(6)
|
565
|
+
|
566
|
+
treeHeader = self.header()
|
567
|
+
treeHeader.setStretchLastSection(False)
|
568
|
+
treeHeader.setMinimumSectionSize(iPx + cMg)
|
569
|
+
treeHeader.setSectionResizeMode(ProjectNode.C_NAME, QtHeaderStretch)
|
570
|
+
treeHeader.setSectionResizeMode(ProjectNode.C_COUNT, QtHeaderToContents)
|
571
|
+
treeHeader.setSectionResizeMode(ProjectNode.C_ACTIVE, QtHeaderFixed)
|
572
|
+
treeHeader.setSectionResizeMode(ProjectNode.C_STATUS, QtHeaderFixed)
|
573
|
+
treeHeader.resizeSection(ProjectNode.C_ACTIVE, iPx + cMg)
|
574
|
+
treeHeader.resizeSection(ProjectNode.C_STATUS, iPx + cMg)
|
575
|
+
|
576
|
+
self.restoreExpandedState()
|
577
|
+
|
578
|
+
return
|
579
|
+
|
580
|
+
def restoreExpandedState(self) -> None:
|
581
|
+
"""Expand all nodes that were previously expanded."""
|
582
|
+
if model := self._getModel():
|
583
|
+
self.blockSignals(True)
|
584
|
+
for index in model.allExpanded():
|
585
|
+
self.setExpanded(index, True)
|
586
|
+
self.blockSignals(False)
|
612
587
|
return
|
613
588
|
|
614
|
-
def
|
615
|
-
|
589
|
+
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
|
590
|
+
"""Set a specific handle as the selected item."""
|
591
|
+
if (model := self._getModel()) and (index := model.indexFromHandle(tHandle)).isValid():
|
592
|
+
self.setCurrentIndex(index)
|
593
|
+
if doScroll:
|
594
|
+
self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter)
|
595
|
+
self.projView.selectedItemChanged.emit(tHandle)
|
596
|
+
return
|
597
|
+
|
598
|
+
def newTreeItem(
|
599
|
+
self, itemType: nwItemType, itemClass: nwItemClass | None = None,
|
600
|
+
hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None,
|
601
|
+
) -> None:
|
616
602
|
"""Add new item to the tree, with a given itemType (and
|
617
603
|
itemClass if Root), and attach it to the selected handle. Also
|
618
604
|
make sure the item is added in a place it can be added, and that
|
@@ -620,1039 +606,435 @@ class GuiProjectTree(QTreeWidget):
|
|
620
606
|
"""
|
621
607
|
if not SHARED.hasProject:
|
622
608
|
logger.error("No project open")
|
623
|
-
return
|
609
|
+
return
|
624
610
|
|
625
|
-
nHandle = None
|
626
611
|
tHandle = None
|
627
|
-
|
628
612
|
if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
|
629
613
|
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
614
|
+
pos = -1
|
615
|
+
if (node := self._getNode(self.currentIndex())) and (itemRoot := node.item.itemRoot):
|
616
|
+
if root := SHARED.project.tree.nodes.get(itemRoot):
|
617
|
+
pos = root.row() + 1
|
618
|
+
|
619
|
+
tHandle = SHARED.project.newRoot(itemClass, pos)
|
620
|
+
self.restoreExpandedState()
|
634
621
|
|
635
622
|
elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
|
636
623
|
|
637
|
-
|
638
|
-
pItem = SHARED.project.tree[sHandle] if sHandle else None
|
639
|
-
if sHandle is None or pItem is None:
|
624
|
+
if not ((model := self._getModel()) and (node := model.node(self.currentIndex()))):
|
640
625
|
SHARED.error(self.tr("Did not find anywhere to add the file or folder!"))
|
641
|
-
return
|
642
|
-
|
643
|
-
# Collect some information about the selected item
|
644
|
-
qItem = self._getTreeItem(sHandle)
|
645
|
-
sLevel = nwHeaders.H_LEVEL.get(pItem.mainHeading, 0)
|
646
|
-
sIsParent = False if qItem is None else qItem.childCount() > 0
|
626
|
+
return
|
647
627
|
|
648
|
-
if
|
628
|
+
if node.item.itemClass == nwItemClass.TRASH:
|
649
629
|
SHARED.error(self.tr("Cannot add new files or folders to the Trash folder."))
|
650
|
-
return
|
630
|
+
return
|
631
|
+
|
632
|
+
# Collect some information about the selected item
|
633
|
+
sLevel = nwStyles.H_LEVEL.get(node.item.mainHeading, 0)
|
634
|
+
sIsParent = node.childCount() > 0
|
651
635
|
|
652
636
|
# Set default label and determine if new item is to be added
|
653
637
|
# as child or sibling to the selected item
|
654
638
|
if itemType == nwItemType.FILE:
|
655
639
|
if copyDoc and (cItem := SHARED.project.tree[copyDoc]):
|
656
640
|
newLabel = cItem.itemName
|
657
|
-
asChild = sIsParent and
|
641
|
+
asChild = sIsParent and node.item.isDocumentLayout()
|
658
642
|
elif isNote:
|
659
643
|
newLabel = self.tr("New Note")
|
660
644
|
asChild = sIsParent
|
661
645
|
elif hLevel == 2:
|
662
646
|
newLabel = self.tr("New Chapter")
|
663
|
-
asChild = sIsParent and
|
647
|
+
asChild = sIsParent and node.item.isDocumentLayout() and sLevel < 2
|
664
648
|
elif hLevel == 3:
|
665
649
|
newLabel = self.tr("New Scene")
|
666
|
-
asChild = sIsParent and
|
650
|
+
asChild = sIsParent and node.item.isDocumentLayout() and sLevel < 3
|
667
651
|
else:
|
668
652
|
newLabel = self.tr("New Document")
|
669
|
-
asChild = sIsParent and
|
653
|
+
asChild = sIsParent and node.item.isDocumentLayout()
|
670
654
|
else:
|
671
655
|
newLabel = self.tr("New Folder")
|
672
656
|
asChild = False
|
673
657
|
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
sHandle =
|
679
|
-
if sHandle is None:
|
680
|
-
# Bug: We have a condition that is unhandled
|
681
|
-
logger.error("Internal error")
|
682
|
-
return False
|
683
|
-
|
684
|
-
# Ask for label
|
685
|
-
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=newLabel)
|
686
|
-
if not dlgOk:
|
687
|
-
logger.info("New item creation cancelled by user")
|
688
|
-
return False
|
689
|
-
|
690
|
-
# Add the file or folder
|
691
|
-
if itemType == nwItemType.FILE:
|
692
|
-
tHandle = SHARED.project.newFile(newLabel, sHandle)
|
693
|
-
else:
|
694
|
-
tHandle = SHARED.project.newFolder(newLabel, sHandle)
|
695
|
-
|
696
|
-
else:
|
697
|
-
logger.error("Failed to add new item")
|
698
|
-
return False
|
699
|
-
|
700
|
-
# If there is no handle set, return here. This is a bug.
|
701
|
-
if tHandle is None: # pragma: no cover
|
702
|
-
logger.error("Internal error")
|
703
|
-
return True
|
658
|
+
pos = -1
|
659
|
+
sHandle = None
|
660
|
+
if not (asChild or node.item.isFolderType() or node.item.isRootType()):
|
661
|
+
pos = node.row() + 1
|
662
|
+
sHandle = node.item.itemParent
|
704
663
|
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
664
|
+
sHandle = sHandle or node.item.itemHandle
|
665
|
+
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=newLabel)
|
666
|
+
if dlgOk:
|
667
|
+
# Add the file or folder
|
668
|
+
if itemType == nwItemType.FILE:
|
669
|
+
if tHandle := SHARED.project.newFile(newLabel, sHandle, pos):
|
670
|
+
if copyDoc:
|
671
|
+
SHARED.project.copyFileContent(tHandle, copyDoc)
|
672
|
+
elif hLevel > 0:
|
673
|
+
SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
|
674
|
+
SHARED.project.index.reIndexHandle(tHandle)
|
675
|
+
SHARED.project.tree.refreshItems([tHandle])
|
676
|
+
else:
|
677
|
+
tHandle = SHARED.project.newFolder(newLabel, sHandle, pos)
|
710
678
|
|
711
|
-
#
|
712
|
-
|
713
|
-
|
679
|
+
# Select the new item automatically
|
680
|
+
if tHandle:
|
681
|
+
self.setSelectedHandle(tHandle)
|
714
682
|
|
715
|
-
return
|
683
|
+
return
|
716
684
|
|
717
|
-
def
|
718
|
-
|
719
|
-
"
|
720
|
-
nwItem = SHARED.project.tree[tHandle] if tHandle else None
|
721
|
-
if tHandle is None or nwItem is None:
|
722
|
-
return False
|
685
|
+
def mergeDocuments(self, tHandle: str, newFile: bool) -> bool:
|
686
|
+
"""Merge an item's child documents into a single document."""
|
687
|
+
logger.info("Request to merge items under handle '%s'", tHandle)
|
723
688
|
|
724
|
-
|
725
|
-
if trItem is None:
|
689
|
+
if not (tItem := SHARED.project.tree[tHandle]):
|
726
690
|
return False
|
727
691
|
|
728
|
-
if
|
729
|
-
|
730
|
-
self.propagateCount(tHandle, wC)
|
731
|
-
self.projView.wordCountsChanged.emit()
|
732
|
-
|
733
|
-
pHandle = nwItem.itemParent
|
734
|
-
if pHandle is not None and pHandle in self._treeMap:
|
735
|
-
self._treeMap[pHandle].setExpanded(True)
|
736
|
-
|
737
|
-
self._alertTreeChange(tHandle, flush=True)
|
738
|
-
self.setCurrentItem(trItem)
|
739
|
-
|
740
|
-
return True
|
741
|
-
|
742
|
-
def moveTreeItem(self, step: int) -> bool:
|
743
|
-
"""Move an item up or down in the tree."""
|
744
|
-
tHandle = self.getSelectedHandle()
|
745
|
-
tItem = self._getTreeItem(tHandle)
|
746
|
-
if tItem is None:
|
747
|
-
logger.debug("No item selected")
|
692
|
+
if tItem.isRootType():
|
693
|
+
logger.error("Cannot merge root item")
|
748
694
|
return False
|
749
695
|
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
tIndex = self.indexOfTopLevelItem(tItem)
|
754
|
-
nChild = self.topLevelItemCount()
|
755
|
-
|
756
|
-
nIndex = tIndex + step
|
757
|
-
if nIndex < 0 or nIndex >= nChild:
|
758
|
-
return False
|
759
|
-
|
760
|
-
cItem = self.takeTopLevelItem(tIndex)
|
761
|
-
self.insertTopLevelItem(nIndex, cItem)
|
762
|
-
|
763
|
-
else:
|
764
|
-
tIndex = pItem.indexOfChild(tItem)
|
765
|
-
nChild = pItem.childCount()
|
766
|
-
|
767
|
-
nIndex = tIndex + step
|
768
|
-
if nIndex < 0 or nIndex >= nChild:
|
769
|
-
return False
|
770
|
-
|
771
|
-
cItem = pItem.takeChild(tIndex)
|
772
|
-
pItem.insertChild(nIndex, cItem)
|
773
|
-
|
774
|
-
self._alertTreeChange(tHandle, flush=True)
|
775
|
-
self.setCurrentItem(tItem)
|
776
|
-
tItem.setExpanded(isExp)
|
696
|
+
itemList = SHARED.project.tree.subTree(tHandle)
|
697
|
+
if newFile:
|
698
|
+
itemList.insert(0, tHandle)
|
777
699
|
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
"""Move to the next item of the same tree level."""
|
782
|
-
tHandle = self.getSelectedHandle()
|
783
|
-
tItem = self._getTreeItem(tHandle) if tHandle else None
|
784
|
-
if tItem:
|
785
|
-
pItem = tItem.parent() or self.invisibleRootItem()
|
786
|
-
next = minmax(pItem.indexOfChild(tItem) + step, 0, pItem.childCount() - 1)
|
787
|
-
self.setCurrentItem(pItem.child(next))
|
788
|
-
return
|
789
|
-
|
790
|
-
def moveToLevel(self, step: int) -> None:
|
791
|
-
"""Move to the next item in the parent/child chain."""
|
792
|
-
tHandle = self.getSelectedHandle()
|
793
|
-
tItem = self._getTreeItem(tHandle) if tHandle else None
|
794
|
-
if tItem:
|
795
|
-
if step < 0 and tItem.parent():
|
796
|
-
self.setCurrentItem(tItem.parent())
|
797
|
-
elif step > 0 and tItem.childCount() > 0:
|
798
|
-
self.setCurrentItem(tItem.child(0))
|
799
|
-
return
|
800
|
-
|
801
|
-
def renameTreeItem(self, tHandle: str, name: str = "") -> None:
|
802
|
-
"""Open a dialog to edit the label of an item."""
|
803
|
-
if nwItem := SHARED.project.tree[tHandle]:
|
804
|
-
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or nwItem.itemName)
|
805
|
-
if dlgOk:
|
806
|
-
nwItem.setName(newLabel)
|
807
|
-
self.setTreeItemValues(nwItem)
|
808
|
-
self._alertTreeChange(tHandle, flush=False)
|
809
|
-
return
|
810
|
-
|
811
|
-
def saveTreeOrder(self) -> None:
|
812
|
-
"""Build a list of the items in the project tree and send them
|
813
|
-
to the project class. This syncs up the two versions of the
|
814
|
-
project structure, and must be called before any code that
|
815
|
-
depends on this order to be up to date.
|
816
|
-
"""
|
817
|
-
items = []
|
818
|
-
for i in range(self.topLevelItemCount()):
|
819
|
-
item = self.topLevelItem(i)
|
820
|
-
if isinstance(item, QTreeWidgetItem):
|
821
|
-
items = self._scanChildren(items, item, i)
|
822
|
-
logger.debug("Saving project tree item order")
|
823
|
-
SHARED.project.setTreeOrder(items)
|
824
|
-
return
|
825
|
-
|
826
|
-
def getTreeFromHandle(self, tHandle: str) -> list[str]:
|
827
|
-
"""Recursively return all the child items starting from a given
|
828
|
-
item handle.
|
829
|
-
"""
|
830
|
-
result = []
|
831
|
-
tIten = self._getTreeItem(tHandle)
|
832
|
-
if tIten is not None:
|
833
|
-
result = self._scanChildren(result, tIten, 0)
|
834
|
-
return result
|
835
|
-
|
836
|
-
def requestDeleteItem(self, tHandle: str | None = None) -> bool:
|
837
|
-
"""Request an item deleted from the project tree. This function
|
838
|
-
can be called on any item, and will check whether to attempt a
|
839
|
-
permanent deletion or moving the item to Trash.
|
840
|
-
"""
|
841
|
-
if not SHARED.hasProject:
|
842
|
-
logger.error("No project open")
|
700
|
+
data, status = GuiDocMerge.getData(SHARED.mainGui, tHandle, itemList)
|
701
|
+
if not status:
|
702
|
+
logger.info("Action cancelled by user")
|
843
703
|
return False
|
844
|
-
|
845
|
-
|
846
|
-
logger.info("Delete action blocked due to no widget focus")
|
704
|
+
if not (items := data.get("finalItems", [])):
|
705
|
+
SHARED.info(self.tr("No documents selected for merging."))
|
847
706
|
return False
|
848
707
|
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
if tHandle is None:
|
853
|
-
logger.error("There is no item to delete")
|
854
|
-
return False
|
855
|
-
|
856
|
-
trashHandle = SHARED.project.tree.trashRoot
|
857
|
-
if tHandle == trashHandle:
|
858
|
-
logger.error("Cannot delete the Trash folder")
|
859
|
-
return False
|
708
|
+
# Save the open document first, in case it's part of merge
|
709
|
+
SHARED.saveEditor()
|
860
710
|
|
861
|
-
|
862
|
-
|
863
|
-
|
711
|
+
# Create merge object, and append docs
|
712
|
+
docMerger = DocMerger(SHARED.project)
|
713
|
+
mLabel = self.tr("Merged")
|
864
714
|
|
865
|
-
if
|
866
|
-
|
715
|
+
if newFile:
|
716
|
+
docMerger.newTargetDoc(tHandle, f"[{mLabel}] {tItem.itemName}")
|
717
|
+
elif tItem.isFileType():
|
718
|
+
docMerger.setTargetDoc(tHandle)
|
867
719
|
else:
|
868
|
-
status = self.moveItemToTrash(tHandle)
|
869
|
-
|
870
|
-
return status
|
871
|
-
|
872
|
-
@pyqtSlot()
|
873
|
-
def emptyTrash(self) -> bool:
|
874
|
-
"""Permanently delete all documents in the Trash folder. This
|
875
|
-
function only asks for confirmation once, and calls the regular
|
876
|
-
deleteItem function for each document in the Trash folder.
|
877
|
-
"""
|
878
|
-
if not SHARED.hasProject:
|
879
|
-
logger.error("No project open")
|
880
|
-
return False
|
881
|
-
|
882
|
-
trashHandle = SHARED.project.tree.trashRoot
|
883
|
-
|
884
|
-
logger.debug("Emptying Trash folder")
|
885
|
-
if trashHandle is None:
|
886
|
-
SHARED.info(self.tr("There is currently no Trash folder in this project."))
|
887
720
|
return False
|
888
721
|
|
889
|
-
|
890
|
-
|
891
|
-
trashItems.remove(trashHandle)
|
892
|
-
|
893
|
-
nTrash = len(trashItems)
|
894
|
-
if nTrash == 0:
|
895
|
-
SHARED.info(self.tr("The Trash folder is already empty."))
|
896
|
-
return False
|
722
|
+
for sHandle in items:
|
723
|
+
docMerger.appendText(sHandle, True, mLabel)
|
897
724
|
|
898
|
-
if not
|
899
|
-
|
725
|
+
if not docMerger.writeTargetDoc():
|
726
|
+
SHARED.error(
|
727
|
+
self.tr("Could not write document content."),
|
728
|
+
info=docMerger.getError()
|
729
|
+
)
|
900
730
|
return False
|
901
731
|
|
902
|
-
|
903
|
-
|
904
|
-
if tHandle == trashHandle:
|
905
|
-
continue
|
906
|
-
self.permDeleteItem(tHandle, askFirst=False, flush=False)
|
732
|
+
if data.get("moveToTrash", False):
|
733
|
+
self.processDeleteRequest(data.get("finalItems", []), False)
|
907
734
|
|
908
|
-
if
|
909
|
-
self.
|
735
|
+
if mHandle := docMerger.targetHandle:
|
736
|
+
self.projView.openDocumentRequest.emit(mHandle, nwDocMode.EDIT, "", False)
|
737
|
+
self.projView.setSelectedHandle(mHandle, doScroll=True)
|
910
738
|
|
911
739
|
return True
|
912
740
|
|
913
|
-
def
|
914
|
-
"""
|
915
|
-
|
916
|
-
"""
|
917
|
-
trItemS = self._getTreeItem(tHandle)
|
918
|
-
nwItemS = SHARED.project.tree[tHandle]
|
919
|
-
|
920
|
-
if trItemS is None or nwItemS is None:
|
921
|
-
logger.error("Could not find tree item for deletion")
|
922
|
-
return False
|
923
|
-
|
924
|
-
if SHARED.project.tree.isTrash(tHandle):
|
925
|
-
logger.error("Item is already in the Trash folder")
|
926
|
-
return False
|
741
|
+
def splitDocument(self, tHandle: str) -> bool:
|
742
|
+
"""Split a document into multiple documents."""
|
743
|
+
logger.info("Request to split items with handle '%s'", tHandle)
|
927
744
|
|
928
|
-
if
|
929
|
-
logger.error("Root folders cannot be moved to Trash")
|
745
|
+
if not (tItem := SHARED.project.tree[tHandle]):
|
930
746
|
return False
|
931
747
|
|
932
|
-
|
933
|
-
|
934
|
-
trItemP = trItemS.parent()
|
935
|
-
trItemT = self._addTrashRoot()
|
936
|
-
if trItemP is None or trItemT is None:
|
937
|
-
logger.error("Could not delete item")
|
748
|
+
if not tItem.isFileType() or tItem.itemParent is None:
|
749
|
+
logger.error("Only valid document items can be split")
|
938
750
|
return False
|
939
751
|
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
)
|
944
|
-
if not msgYes:
|
945
|
-
logger.info("Action cancelled by user")
|
946
|
-
return False
|
947
|
-
|
948
|
-
self.propagateCount(tHandle, 0)
|
949
|
-
|
950
|
-
tIndex = trItemP.indexOfChild(trItemS)
|
951
|
-
trItemC = trItemP.takeChild(tIndex)
|
952
|
-
trItemT.addChild(trItemC)
|
953
|
-
|
954
|
-
self._postItemMove(tHandle)
|
955
|
-
self._alertTreeChange(tHandle, flush=flush)
|
956
|
-
|
957
|
-
logger.debug("Moved item '%s' to Trash", tHandle)
|
958
|
-
|
959
|
-
return True
|
960
|
-
|
961
|
-
def permDeleteItem(self, tHandle: str, askFirst: bool = True, flush: bool = True) -> bool:
|
962
|
-
"""Permanently delete a tree item from the project and the map.
|
963
|
-
Root items are handled a little different than other items.
|
964
|
-
"""
|
965
|
-
trItemS = self._getTreeItem(tHandle)
|
966
|
-
nwItemS = SHARED.project.tree[tHandle]
|
967
|
-
if trItemS is None or nwItemS is None:
|
968
|
-
logger.error("Could not find tree item for deletion")
|
752
|
+
data, text, status = GuiDocSplit.getData(SHARED.mainGui, tHandle)
|
753
|
+
if not status:
|
754
|
+
logger.info("Action cancelled by user")
|
969
755
|
return False
|
970
756
|
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
SHARED.error(self.tr("Root folders can only be deleted when they are empty."))
|
975
|
-
return False
|
976
|
-
|
977
|
-
logger.debug("Permanently deleting root folder '%s'", tHandle)
|
978
|
-
|
979
|
-
tIndex = self.indexOfTopLevelItem(trItemS)
|
980
|
-
self.takeTopLevelItem(tIndex)
|
981
|
-
SHARED.project.removeItem(tHandle)
|
982
|
-
self._treeMap.pop(tHandle, None)
|
983
|
-
self._alertTreeChange(tHandle, flush=True)
|
984
|
-
|
985
|
-
# These are not emitted by the alert function because the
|
986
|
-
# item has already been deleted
|
987
|
-
self.projView.rootFolderChanged.emit(tHandle)
|
988
|
-
self.projView.treeItemChanged.emit(tHandle)
|
757
|
+
headerList = data.get("headerList", [])
|
758
|
+
intoFolder = data.get("intoFolder", False)
|
759
|
+
docHierarchy = data.get("docHierarchy", False)
|
989
760
|
|
761
|
+
docSplit = DocSplitter(SHARED.project, tHandle)
|
762
|
+
if intoFolder:
|
763
|
+
docSplit.newParentFolder(tItem.itemParent, tItem.itemName)
|
990
764
|
else:
|
991
|
-
|
992
|
-
msgYes = SHARED.question(
|
993
|
-
self.tr("Permanently delete '{0}'?").format(nwItemS.itemName)
|
994
|
-
)
|
995
|
-
if not msgYes:
|
996
|
-
logger.info("Action cancelled by user")
|
997
|
-
return False
|
998
|
-
|
999
|
-
logger.debug("Permanently deleting item '%s'", tHandle)
|
1000
|
-
|
1001
|
-
self.propagateCount(tHandle, 0)
|
1002
|
-
|
1003
|
-
trItemP = trItemS.parent()
|
1004
|
-
tIndex = trItemP.indexOfChild(trItemS)
|
1005
|
-
trItemP.takeChild(tIndex)
|
765
|
+
docSplit.setParentItem(tItem.itemParent)
|
1006
766
|
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
767
|
+
docSplit.splitDocument(headerList, text)
|
768
|
+
for writeOk in docSplit.writeDocuments(docHierarchy):
|
769
|
+
if not writeOk:
|
770
|
+
SHARED.error(
|
771
|
+
self.tr("Could not write document content."),
|
772
|
+
info=docSplit.getError()
|
773
|
+
)
|
1014
774
|
|
1015
|
-
|
1016
|
-
|
1017
|
-
self.projView.treeItemChanged.emit(tHandle)
|
775
|
+
if data.get("moveToTrash", False):
|
776
|
+
self.processDeleteRequest([tHandle], False)
|
1018
777
|
|
1019
778
|
return True
|
1020
779
|
|
1021
|
-
def
|
1022
|
-
"""
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
for nwItem in SHARED.project.tree:
|
1029
|
-
if not nwItem.isNovelLike():
|
1030
|
-
self.setTreeItemValues(nwItem)
|
1031
|
-
return
|
1032
|
-
|
1033
|
-
def setTreeItemValues(self, nwItem: NWItem | None) -> None:
|
1034
|
-
"""Set the name and flag values for a tree item in the project
|
1035
|
-
tree. Does not trigger a tree change as the data is already
|
1036
|
-
coming from project data.
|
1037
|
-
"""
|
1038
|
-
if isinstance(nwItem, NWItem) and (trItem := self._getTreeItem(nwItem.itemHandle)):
|
1039
|
-
itemStatus, statusIcon = nwItem.getImportStatus()
|
1040
|
-
hLevel = nwItem.mainHeading
|
1041
|
-
itemIcon = SHARED.theme.getItemIcon(
|
1042
|
-
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
|
1043
|
-
)
|
1044
|
-
|
1045
|
-
trItem.setIcon(self.C_NAME, itemIcon)
|
1046
|
-
trItem.setText(self.C_NAME, nwItem.itemName)
|
1047
|
-
trItem.setIcon(self.C_STATUS, statusIcon)
|
1048
|
-
trItem.setToolTip(self.C_STATUS, itemStatus)
|
1049
|
-
|
1050
|
-
if nwItem.isFileType():
|
1051
|
-
iconName = "checked" if nwItem.isActive else "unchecked"
|
1052
|
-
toolTip = self.trActive if nwItem.isActive else self.trInactive
|
1053
|
-
trItem.setToolTip(self.C_ACTIVE, toolTip)
|
780
|
+
def duplicateFromHandle(self, tHandle: str) -> None:
|
781
|
+
"""Duplicate the item hierarchy from a given item."""
|
782
|
+
itemTree = [tHandle]
|
783
|
+
itemTree.extend(SHARED.project.tree.subTree(tHandle))
|
784
|
+
if itemTree:
|
785
|
+
if len(itemTree) == 1:
|
786
|
+
question = self.tr("Do you want to duplicate this document?")
|
1054
787
|
else:
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
trFont.setUnderline(hLevel == "H1")
|
1063
|
-
trItem.setFont(self.C_NAME, trFont)
|
1064
|
-
|
1065
|
-
# Emit Refresh Signal
|
1066
|
-
self.itemRefreshed.emit(nwItem.itemHandle, nwItem, itemIcon)
|
1067
|
-
|
788
|
+
question = self.tr("Do you want to duplicate this item and all child items?")
|
789
|
+
if SHARED.question(question):
|
790
|
+
docDup = DocDuplicator(SHARED.project)
|
791
|
+
dHandles = docDup.duplicate(itemTree)
|
792
|
+
if len(dHandles) != len(itemTree):
|
793
|
+
SHARED.warn(self.tr("Could not duplicate all items."))
|
794
|
+
self.restoreExpandedState()
|
1068
795
|
return
|
1069
796
|
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
root item. This function is more efficient than recalculating
|
1074
|
-
everything each time the word count is updated, but is also
|
1075
|
-
prone to diverging from the true values if the counts are not
|
1076
|
-
properly reported to the function.
|
1077
|
-
"""
|
1078
|
-
tItem = self._getTreeItem(tHandle)
|
1079
|
-
if tItem is None:
|
1080
|
-
return
|
1081
|
-
|
1082
|
-
if countChildren:
|
1083
|
-
for i in range(tItem.childCount()):
|
1084
|
-
newCount += int(tItem.child(i).data(self.C_DATA, self.D_WORDS))
|
1085
|
-
|
1086
|
-
tItem.setText(self.C_COUNT, f"{newCount:n}")
|
1087
|
-
tItem.setData(self.C_DATA, self.D_WORDS, int(newCount))
|
1088
|
-
|
1089
|
-
pItem = tItem.parent()
|
1090
|
-
if pItem is None:
|
1091
|
-
return
|
1092
|
-
|
1093
|
-
pCount = 0
|
1094
|
-
pHandle = None
|
1095
|
-
for i in range(pItem.childCount()):
|
1096
|
-
pCount += int(pItem.child(i).data(self.C_DATA, self.D_WORDS))
|
1097
|
-
pHandle = pItem.data(self.C_DATA, self.D_HANDLE)
|
1098
|
-
|
1099
|
-
if pHandle:
|
1100
|
-
if SHARED.project.tree.checkType(pHandle, nwItemType.FILE):
|
1101
|
-
# A file has an internal word count we need to account
|
1102
|
-
# for, but a folder always has 0 words on its own.
|
1103
|
-
pCount += SHARED.project.index.getCounts(pHandle)[1]
|
1104
|
-
|
1105
|
-
self.propagateCount(pHandle, pCount, countChildren=False)
|
1106
|
-
|
1107
|
-
return
|
797
|
+
##
|
798
|
+
# Events and Overloads
|
799
|
+
##
|
1108
800
|
|
1109
|
-
def
|
1110
|
-
"""
|
1111
|
-
|
1112
|
-
|
1113
|
-
sent first.
|
801
|
+
def mousePressEvent(self, event: QMouseEvent) -> None:
|
802
|
+
"""Overload mousePressEvent to clear selection if clicking the
|
803
|
+
mouse in a blank area of the tree view, and to load a document
|
804
|
+
for viewing if the user middle-clicked.
|
1114
805
|
"""
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
self.
|
1121
|
-
|
1122
|
-
|
806
|
+
super().mousePressEvent(event)
|
807
|
+
if event.button() == QtMouseLeft:
|
808
|
+
if not self.indexAt(event.pos()).isValid():
|
809
|
+
self._clearSelection()
|
810
|
+
elif event.button() == QtMouseMiddle:
|
811
|
+
if (node := self._getNode(self.indexAt(event.pos()))) and node.item.isFileType():
|
812
|
+
self.projView.openDocumentRequest.emit(
|
813
|
+
node.item.itemHandle, nwDocMode.VIEW, "", False
|
814
|
+
)
|
1123
815
|
return
|
1124
816
|
|
1125
|
-
def
|
1126
|
-
"""
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1130
|
-
return items[0].data(self.C_DATA, self.D_HANDLE)
|
1131
|
-
return None
|
1132
|
-
|
1133
|
-
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
|
1134
|
-
"""Set a specific handle as the selected item."""
|
1135
|
-
tItem = self._getTreeItem(tHandle)
|
1136
|
-
if tItem is None:
|
1137
|
-
return False
|
1138
|
-
|
1139
|
-
if tHandle in self._treeMap:
|
1140
|
-
self.setCurrentItem(self._treeMap[tHandle])
|
1141
|
-
|
1142
|
-
if (indexes := self.selectedIndexes()) and doScroll:
|
1143
|
-
self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
|
1144
|
-
|
1145
|
-
return True
|
1146
|
-
|
1147
|
-
def setExpandedFromHandle(self, tHandle: str | None, isExpanded: bool) -> None:
|
1148
|
-
"""Iterate through items below tHandle and change expanded
|
1149
|
-
status for all child items. If tHandle is None, it affects the
|
1150
|
-
entire tree.
|
1151
|
-
"""
|
1152
|
-
trItem = self._getTreeItem(tHandle) or self.invisibleRootItem()
|
1153
|
-
self._recursiveSetExpanded(trItem, isExpanded)
|
817
|
+
def drawRow(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None:
|
818
|
+
"""Draw a box on the active row."""
|
819
|
+
if (node := self._getNode(index)) and node.item.itemHandle == self._actHandle:
|
820
|
+
painter.fillRect(opt.rect, self.palette().alternateBase())
|
821
|
+
super().drawRow(painter, opt, index)
|
1154
822
|
return
|
1155
823
|
|
1156
|
-
def openContextOnSelected(self) -> bool:
|
1157
|
-
"""Open the context menu on the current selected item."""
|
1158
|
-
if items := self.selectedItems():
|
1159
|
-
return self._openContextMenu(self.visualItemRect(items[0]).center())
|
1160
|
-
return False
|
1161
|
-
|
1162
|
-
def changedSince(self, checkTime: float) -> bool:
|
1163
|
-
"""Check if the tree has changed since a given time."""
|
1164
|
-
return self._timeChanged > checkTime
|
1165
|
-
|
1166
824
|
##
|
1167
|
-
#
|
825
|
+
# Public Slots
|
1168
826
|
##
|
1169
827
|
|
1170
828
|
@pyqtSlot()
|
1171
|
-
def
|
1172
|
-
"""
|
1173
|
-
|
1174
|
-
|
1175
|
-
self.projView.selectedItemChanged.emit(tHandle)
|
1176
|
-
|
1177
|
-
# When selecting multiple items, don't allow including root
|
1178
|
-
# items in the selection and instead deselect them
|
1179
|
-
items = self.selectedItems()
|
1180
|
-
if items and len(items) > 1:
|
1181
|
-
for item in items:
|
1182
|
-
if item.parent() is None:
|
1183
|
-
item.setSelected(False)
|
1184
|
-
|
829
|
+
def moveItemUp(self) -> None:
|
830
|
+
"""Move an item up in the tree."""
|
831
|
+
if model := self._getModel():
|
832
|
+
model.internalMove(self.currentIndex(), -1)
|
1185
833
|
return
|
1186
834
|
|
1187
|
-
@pyqtSlot(
|
1188
|
-
def
|
1189
|
-
"""
|
1190
|
-
|
1191
|
-
|
1192
|
-
tHandle = self.getSelectedHandle()
|
1193
|
-
if tHandle is None:
|
1194
|
-
return
|
1195
|
-
|
1196
|
-
tItem = SHARED.project.tree[tHandle]
|
1197
|
-
if tItem is None:
|
1198
|
-
return
|
1199
|
-
|
1200
|
-
if tItem.isFileType():
|
1201
|
-
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
|
1202
|
-
else:
|
1203
|
-
trItem.setExpanded(not trItem.isExpanded())
|
1204
|
-
|
835
|
+
@pyqtSlot()
|
836
|
+
def moveItemDown(self) -> None:
|
837
|
+
"""Move an item down in the tree."""
|
838
|
+
if model := self._getModel():
|
839
|
+
model.internalMove(self.currentIndex(), 1)
|
1205
840
|
return
|
1206
841
|
|
1207
|
-
@pyqtSlot(
|
1208
|
-
def
|
1209
|
-
"""
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
hasChild = False
|
1215
|
-
sItem = self.itemAt(clickPos)
|
1216
|
-
sItems = self.selectedItems()
|
1217
|
-
if isinstance(sItem, QTreeWidgetItem):
|
1218
|
-
tHandle = sItem.data(self.C_DATA, self.D_HANDLE)
|
1219
|
-
tItem = SHARED.project.tree[tHandle]
|
1220
|
-
hasChild = sItem.childCount() > 0
|
1221
|
-
|
1222
|
-
if tItem is None or tHandle is None:
|
1223
|
-
logger.debug("No item found")
|
1224
|
-
return False
|
1225
|
-
|
1226
|
-
ctxMenu = _TreeContextMenu(self, tItem)
|
1227
|
-
trashHandle = SHARED.project.tree.trashRoot
|
1228
|
-
if trashHandle and tHandle == trashHandle:
|
1229
|
-
ctxMenu.buildTrashMenu()
|
1230
|
-
elif len(sItems) > 1:
|
1231
|
-
handles = [str(x.data(self.C_DATA, self.D_HANDLE)) for x in sItems]
|
1232
|
-
ctxMenu.buildMultiSelectMenu(handles)
|
1233
|
-
else:
|
1234
|
-
ctxMenu.buildSingleSelectMenu(hasChild)
|
1235
|
-
|
1236
|
-
ctxMenu.exec(self.viewport().mapToGlobal(clickPos))
|
1237
|
-
ctxMenu.deleteLater()
|
842
|
+
@pyqtSlot()
|
843
|
+
def goToSiblingUp(self) -> None:
|
844
|
+
"""Skip to the previous sibling."""
|
845
|
+
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
|
846
|
+
if (move := parent.child(node.row() - 1)) and (model := self._getModel()):
|
847
|
+
self.setCurrentIndex(model.indexFromNode(move))
|
848
|
+
return
|
1238
849
|
|
1239
|
-
|
850
|
+
@pyqtSlot()
|
851
|
+
def goToSiblingDown(self) -> None:
|
852
|
+
"""Skip to the next sibling."""
|
853
|
+
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
|
854
|
+
if (move := parent.child(node.row() + 1)) and (model := self._getModel()):
|
855
|
+
self.setCurrentIndex(model.indexFromNode(move))
|
856
|
+
return
|
1240
857
|
|
1241
858
|
@pyqtSlot()
|
1242
|
-
def
|
1243
|
-
"""
|
1244
|
-
if
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
859
|
+
def goToParent(self) -> None:
|
860
|
+
"""Move to parent item."""
|
861
|
+
if (
|
862
|
+
(model := self._getModel())
|
863
|
+
and (node := model.node(self.currentIndex()))
|
864
|
+
and (parent := node.parent())
|
865
|
+
):
|
866
|
+
self.setCurrentIndex(model.indexFromNode(parent))
|
1250
867
|
return
|
1251
868
|
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
869
|
+
@pyqtSlot()
|
870
|
+
def goToFirstChild(self) -> None:
|
871
|
+
"""Move to first child item."""
|
872
|
+
if (
|
873
|
+
(model := self._getModel())
|
874
|
+
and (node := model.node(self.currentIndex()))
|
875
|
+
and (child := node.child(0))
|
876
|
+
):
|
877
|
+
self.setCurrentIndex(model.indexFromNode(child))
|
878
|
+
return
|
1255
879
|
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
"""
|
1261
|
-
super().mousePressEvent(event)
|
1262
|
-
if event.button() == QtMouseLeft:
|
1263
|
-
selItem = self.indexAt(event.pos())
|
1264
|
-
if not selItem.isValid():
|
1265
|
-
self.clearSelection()
|
1266
|
-
elif event.button() == QtMouseMiddle:
|
1267
|
-
selItem = self.itemAt(event.pos())
|
1268
|
-
if selItem:
|
1269
|
-
tHandle = selItem.data(self.C_DATA, self.D_HANDLE)
|
1270
|
-
if (tItem := SHARED.project.tree[tHandle]) and tItem.isFileType():
|
1271
|
-
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
|
880
|
+
@pyqtSlot(QModelIndex)
|
881
|
+
def expandFromIndex(self, index: QModelIndex) -> None:
|
882
|
+
"""Expand all nodes from index."""
|
883
|
+
self.expandRecursively(index)
|
1272
884
|
return
|
1273
885
|
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
if self.
|
1278
|
-
|
1279
|
-
|
886
|
+
@pyqtSlot(QModelIndex)
|
887
|
+
def collapseFromIndex(self, index: QModelIndex) -> None:
|
888
|
+
"""Collapse all nodes from index."""
|
889
|
+
if (model := self._getModel()) and (node := model.node(index)):
|
890
|
+
for child in node.allChildren():
|
891
|
+
self.setExpanded(model.indexFromNode(child), False)
|
1280
892
|
return
|
1281
893
|
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
items
|
1287
|
-
if
|
1288
|
-
|
894
|
+
@pyqtSlot()
|
895
|
+
def processDeleteRequest(
|
896
|
+
self, handles: list[str] | None = None, askFirst: bool = True
|
897
|
+
) -> None:
|
898
|
+
"""Move selected items to Trash."""
|
899
|
+
if handles and (model := self._getModel()):
|
900
|
+
indices = [model.indexFromHandle(handle) for handle in handles]
|
1289
901
|
else:
|
1290
|
-
|
1291
|
-
self._popAlert = self.tr(
|
1292
|
-
"Drag and drop is only allowed for single items, non-root "
|
1293
|
-
"items, or multiple items with the same parent."
|
1294
|
-
)
|
1295
|
-
event.mimeData().clear()
|
1296
|
-
event.ignore()
|
1297
|
-
return
|
1298
|
-
|
1299
|
-
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
|
1300
|
-
"""Capture the drag move event to enable edge auto scroll."""
|
1301
|
-
y = event.pos().y()
|
1302
|
-
if y < self._scrollMargin:
|
1303
|
-
if not self._scrollTimer.isActive():
|
1304
|
-
self._scrollDirection = -1
|
1305
|
-
self._scrollTimer.start()
|
1306
|
-
elif y > self.height() - self._scrollMargin:
|
1307
|
-
if not self._scrollTimer.isActive():
|
1308
|
-
self._scrollDirection = 1
|
1309
|
-
self._scrollTimer.start()
|
1310
|
-
super().dragMoveEvent(event)
|
1311
|
-
return
|
1312
|
-
|
1313
|
-
def dropEvent(self, event: QDropEvent) -> None:
|
1314
|
-
"""Overload the drop item event to ensure the drag and drop
|
1315
|
-
action is allowed, and update relevant data.
|
1316
|
-
"""
|
1317
|
-
tItem = self.itemAt(event.pos())
|
1318
|
-
dropOn = self.dropIndicatorPosition() == QAbstractItemView.DropIndicatorPosition.OnItem
|
1319
|
-
# Make sure nothing can be dropped on invisible root (see #1569)
|
1320
|
-
if not tItem or tItem.parent() is None and not dropOn:
|
1321
|
-
logger.error("Invalid drop location")
|
1322
|
-
event.ignore()
|
1323
|
-
return
|
902
|
+
indices = self._selectedRows()
|
1324
903
|
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
904
|
+
if indices and (model := self._getModel()):
|
905
|
+
if len(indices) == 1 and (node := model.node(indices[0])) and node.item.isRootType():
|
906
|
+
if node.childCount() == 0:
|
907
|
+
SHARED.project.removeItem(node.item.itemHandle)
|
908
|
+
else:
|
909
|
+
SHARED.error(self.tr("Root folders can only be deleted when they are empty."))
|
910
|
+
return
|
1332
911
|
|
1333
|
-
|
912
|
+
if model.trashSelection(indices):
|
913
|
+
if not SHARED.question(self.tr("Permanently delete selected item(s)?")):
|
914
|
+
logger.info("Action cancelled by user")
|
915
|
+
return
|
916
|
+
for index in indices:
|
917
|
+
if node := model.node(index):
|
918
|
+
for child in reversed(node.allChildren()):
|
919
|
+
SHARED.project.removeItem(child.item.itemHandle)
|
920
|
+
SHARED.project.removeItem(node.item.itemHandle)
|
921
|
+
|
922
|
+
elif trashNode := SHARED.project.tree.trash:
|
923
|
+
if askFirst and not SHARED.question(self.tr("Move selected item(s) to Trash?")):
|
924
|
+
logger.info("Action cancelled by user")
|
925
|
+
return
|
926
|
+
model.multiMove(indices, model.indexFromNode(trashNode))
|
1334
927
|
|
1335
|
-
|
1336
|
-
self._postItemMove(mHandle)
|
1337
|
-
sItem.setExpanded(isExpanded)
|
1338
|
-
self._alertTreeChange(mHandle, flush=False)
|
928
|
+
return
|
1339
929
|
|
1340
|
-
|
930
|
+
@pyqtSlot()
|
931
|
+
def emptyTrash(self) -> None:
|
932
|
+
"""Permanently delete all documents in the Trash folder. This
|
933
|
+
function only asks for confirmation once, and calls the regular
|
934
|
+
deleteItem function for each document in the Trash folder.
|
935
|
+
"""
|
936
|
+
if trash := SHARED.project.tree.trash:
|
937
|
+
if not (nodes := trash.allChildren()):
|
938
|
+
SHARED.info(self.tr("The Trash folder is already empty."))
|
939
|
+
return
|
940
|
+
if not SHARED.question(
|
941
|
+
self.tr("Permanently delete {0} file(s) from Trash?").format(len(nodes))
|
942
|
+
):
|
943
|
+
logger.info("Action cancelled by user")
|
944
|
+
return
|
945
|
+
for node in reversed(nodes):
|
946
|
+
SHARED.project.removeItem(node.item.itemHandle)
|
947
|
+
return
|
1341
948
|
|
949
|
+
@pyqtSlot()
|
950
|
+
@pyqtSlot("QPoint")
|
951
|
+
def openContextMenu(self, point: QPoint | None = None) -> None:
|
952
|
+
"""The user right clicked an element in the project tree, so we
|
953
|
+
open a context menu in-place.
|
954
|
+
"""
|
955
|
+
if model := self._getModel():
|
956
|
+
if point is None:
|
957
|
+
point = self.visualRect(self.currentIndex()).center()
|
958
|
+
if point is not None:
|
959
|
+
index = self.indexAt(point)
|
960
|
+
if (node := self._getNode(index)) and (indices := self._selectedRows()):
|
961
|
+
ctxMenu = _TreeContextMenu(self, model, node, indices)
|
962
|
+
if node is SHARED.project.tree.trash:
|
963
|
+
ctxMenu.buildTrashMenu()
|
964
|
+
elif len(indices) > 1:
|
965
|
+
ctxMenu.buildMultiSelectMenu()
|
966
|
+
else:
|
967
|
+
ctxMenu.buildSingleSelectMenu()
|
968
|
+
ctxMenu.exec(self.viewport().mapToGlobal(point))
|
969
|
+
ctxMenu.setParent(None)
|
1342
970
|
return
|
1343
971
|
|
1344
972
|
##
|
1345
|
-
#
|
973
|
+
# Private Slots
|
1346
974
|
##
|
1347
975
|
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
if trItemP is None or nwItemS is None:
|
1354
|
-
logger.error("Failed to find new parent item of '%s'", tHandle)
|
1355
|
-
return
|
1356
|
-
|
1357
|
-
# Update item parent handle in the project
|
1358
|
-
pHandle = trItemP.data(self.C_DATA, self.D_HANDLE)
|
1359
|
-
nwItemS.setParent(pHandle)
|
1360
|
-
trItemP.setExpanded(True)
|
1361
|
-
logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
|
1362
|
-
|
1363
|
-
mHandles = self.getTreeFromHandle(tHandle)
|
1364
|
-
logger.debug("A total of %d item(s) were moved", len(mHandles))
|
1365
|
-
for mHandle in mHandles:
|
1366
|
-
logger.debug("Updating item '%s'", mHandle)
|
1367
|
-
SHARED.project.tree.updateItemData(mHandle)
|
1368
|
-
if nwItemS.isInactiveClass():
|
1369
|
-
SHARED.project.index.deleteHandle(mHandle)
|
1370
|
-
else:
|
1371
|
-
SHARED.project.index.reIndexHandle(mHandle)
|
1372
|
-
if mItem := SHARED.project.tree[mHandle]:
|
1373
|
-
self.setTreeItemValues(mItem)
|
1374
|
-
|
1375
|
-
# Update word count
|
1376
|
-
self.propagateCount(tHandle, nwItemS.wordCount, countChildren=True)
|
1377
|
-
|
976
|
+
@pyqtSlot(QModelIndex)
|
977
|
+
def _onSingleClick(self, index: QModelIndex) -> None:
|
978
|
+
"""The user changed which item is selected."""
|
979
|
+
if node := self._getNode(index):
|
980
|
+
self.projView.selectedItemChanged.emit(node.item.itemHandle)
|
1378
981
|
return
|
1379
982
|
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
def _getTreeItem(self, tHandle: str | None) -> QTreeWidgetItem | None:
|
1386
|
-
"""Return the QTreeWidgetItem of a given item handle."""
|
1387
|
-
return self._treeMap.get(tHandle, None) if tHandle else None
|
1388
|
-
|
1389
|
-
def _recursiveSetExpanded(self, trItem: QTreeWidgetItem, isExpanded: bool) -> None:
|
1390
|
-
"""Recursive function to set expanded status starting from (and
|
1391
|
-
not including) a given item.
|
983
|
+
@pyqtSlot(QModelIndex)
|
984
|
+
def _onDoubleClick(self, index: QModelIndex) -> None:
|
985
|
+
"""Capture a double-click event and either request the document
|
986
|
+
for editing if it is a file, or expand/close the node if not.
|
1392
987
|
"""
|
1393
|
-
if
|
1394
|
-
|
1395
|
-
|
1396
|
-
|
1397
|
-
self._recursiveSetExpanded(chItem, isExpanded)
|
1398
|
-
return
|
1399
|
-
|
1400
|
-
def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool:
|
1401
|
-
"""Merge an item's child documents into a single document."""
|
1402
|
-
logger.info("Request to merge items under handle '%s'", tHandle)
|
1403
|
-
itemList = self.getTreeFromHandle(tHandle)
|
1404
|
-
|
1405
|
-
tItem = SHARED.project.tree[tHandle]
|
1406
|
-
if tItem is None:
|
1407
|
-
return False
|
1408
|
-
|
1409
|
-
if tItem.isRootType():
|
1410
|
-
logger.error("Cannot merge root item")
|
1411
|
-
return False
|
1412
|
-
|
1413
|
-
if not newFile:
|
1414
|
-
itemList.remove(tHandle)
|
1415
|
-
|
1416
|
-
data, status = GuiDocMerge.getData(SHARED.mainGui, tHandle, itemList)
|
1417
|
-
if status:
|
1418
|
-
items = data.get("finalItems", [])
|
1419
|
-
if not items:
|
1420
|
-
SHARED.info(self.tr("No documents selected for merging."))
|
1421
|
-
return False
|
1422
|
-
|
1423
|
-
# Save the open document first, in case it's part of merge
|
1424
|
-
SHARED.saveEditor()
|
1425
|
-
|
1426
|
-
# Create merge object, and append docs
|
1427
|
-
docMerger = DocMerger(SHARED.project)
|
1428
|
-
mLabel = self.tr("Merged")
|
1429
|
-
|
1430
|
-
if newFile:
|
1431
|
-
docLabel = f"[{mLabel}] {tItem.itemName}"
|
1432
|
-
mHandle = docMerger.newTargetDoc(tHandle, docLabel)
|
1433
|
-
elif tItem.isFileType():
|
1434
|
-
docMerger.setTargetDoc(tHandle)
|
1435
|
-
mHandle = tHandle
|
1436
|
-
else:
|
1437
|
-
return False
|
1438
|
-
|
1439
|
-
for sHandle in items:
|
1440
|
-
docMerger.appendText(sHandle, True, mLabel)
|
1441
|
-
|
1442
|
-
if not docMerger.writeTargetDoc():
|
1443
|
-
SHARED.error(
|
1444
|
-
self.tr("Could not write document content."),
|
1445
|
-
info=docMerger.getError()
|
988
|
+
if node := self._getNode(index):
|
989
|
+
if node.item.isFileType():
|
990
|
+
self.projView.openDocumentRequest.emit(
|
991
|
+
node.item.itemHandle, nwDocMode.EDIT, "", True
|
1446
992
|
)
|
1447
|
-
return False
|
1448
|
-
|
1449
|
-
SHARED.project.index.reIndexHandle(mHandle)
|
1450
|
-
if newFile:
|
1451
|
-
self.revealNewTreeItem(mHandle, nHandle=tHandle, wordCount=True)
|
1452
|
-
|
1453
|
-
self.projView.openDocumentRequest.emit(mHandle, nwDocMode.EDIT, "", False)
|
1454
|
-
self.projView.setSelectedHandle(mHandle, doScroll=True)
|
1455
|
-
|
1456
|
-
if data.get("moveToTrash", False):
|
1457
|
-
for sHandle in reversed(data.get("finalItems", [])):
|
1458
|
-
trItem = self._getTreeItem(sHandle)
|
1459
|
-
if isinstance(trItem, QTreeWidgetItem) and trItem.childCount() == 0:
|
1460
|
-
self.moveItemToTrash(sHandle, askFirst=False, flush=False)
|
1461
|
-
|
1462
|
-
self._alertTreeChange(mHandle, flush=True)
|
1463
|
-
self.projView.wordCountsChanged.emit()
|
1464
|
-
|
1465
|
-
else:
|
1466
|
-
logger.info("Action cancelled by user")
|
1467
|
-
return False
|
1468
|
-
|
1469
|
-
return True
|
1470
|
-
|
1471
|
-
def _splitDocument(self, tHandle: str) -> bool:
|
1472
|
-
"""Split a document into multiple documents."""
|
1473
|
-
logger.info("Request to split items with handle '%s'", tHandle)
|
1474
|
-
|
1475
|
-
tItem = SHARED.project.tree[tHandle]
|
1476
|
-
if tItem is None:
|
1477
|
-
return False
|
1478
|
-
|
1479
|
-
if not tItem.isFileType() or tItem.itemParent is None:
|
1480
|
-
logger.error("Only valid document items can be split")
|
1481
|
-
return False
|
1482
|
-
|
1483
|
-
data, text, status = GuiDocSplit.getData(SHARED.mainGui, tHandle)
|
1484
|
-
if status:
|
1485
|
-
headerList = data.get("headerList", [])
|
1486
|
-
intoFolder = data.get("intoFolder", False)
|
1487
|
-
docHierarchy = data.get("docHierarchy", False)
|
1488
|
-
|
1489
|
-
docSplit = DocSplitter(SHARED.project, tHandle)
|
1490
|
-
if intoFolder:
|
1491
|
-
fHandle = docSplit.newParentFolder(tItem.itemParent, tItem.itemName)
|
1492
|
-
self.revealNewTreeItem(fHandle, nHandle=tHandle)
|
1493
|
-
self._alertTreeChange(fHandle, flush=False)
|
1494
993
|
else:
|
1495
|
-
|
1496
|
-
|
1497
|
-
docSplit.splitDocument(headerList, text)
|
1498
|
-
for writeOk, dHandle, nHandle in docSplit.writeDocuments(docHierarchy):
|
1499
|
-
SHARED.project.index.reIndexHandle(dHandle)
|
1500
|
-
self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True)
|
1501
|
-
self._alertTreeChange(dHandle, flush=False)
|
1502
|
-
if not writeOk:
|
1503
|
-
SHARED.error(
|
1504
|
-
self.tr("Could not write document content."),
|
1505
|
-
info=docSplit.getError()
|
1506
|
-
)
|
1507
|
-
|
1508
|
-
if data.get("moveToTrash", False):
|
1509
|
-
self.moveItemToTrash(tHandle, askFirst=False, flush=True)
|
1510
|
-
|
1511
|
-
self.saveTreeOrder()
|
1512
|
-
|
1513
|
-
else:
|
1514
|
-
logger.info("Action cancelled by user")
|
1515
|
-
return False
|
1516
|
-
|
1517
|
-
return True
|
1518
|
-
|
1519
|
-
def _duplicateFromHandle(self, tHandle: str) -> bool:
|
1520
|
-
"""Duplicate the item hierarchy from a given item."""
|
1521
|
-
itemTree = self.getTreeFromHandle(tHandle)
|
1522
|
-
nItems = len(itemTree)
|
1523
|
-
if nItems == 0:
|
1524
|
-
return False
|
1525
|
-
elif nItems == 1:
|
1526
|
-
question = self.tr("Do you want to duplicate this document?")
|
1527
|
-
else:
|
1528
|
-
question = self.tr("Do you want to duplicate this item and all child items?")
|
1529
|
-
|
1530
|
-
if not SHARED.question(question):
|
1531
|
-
return False
|
1532
|
-
|
1533
|
-
docDup = DocDuplicator(SHARED.project)
|
1534
|
-
dupCount = 0
|
1535
|
-
for dHandle, nHandle in docDup.duplicate(itemTree):
|
1536
|
-
SHARED.project.index.reIndexHandle(dHandle)
|
1537
|
-
self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True)
|
1538
|
-
self._alertTreeChange(dHandle, flush=False)
|
1539
|
-
dupCount += 1
|
1540
|
-
|
1541
|
-
if dupCount != nItems:
|
1542
|
-
SHARED.warn(self.tr("Could not duplicate all items."))
|
1543
|
-
|
1544
|
-
self.saveTreeOrder()
|
1545
|
-
|
1546
|
-
return True
|
1547
|
-
|
1548
|
-
def _scanChildren(self, itemList: list, tItem: QTreeWidgetItem, tIndex: int) -> list[str]:
|
1549
|
-
"""This is a recursive function returning all items in a tree
|
1550
|
-
starting at a given QTreeWidgetItem.
|
1551
|
-
"""
|
1552
|
-
tHandle = tItem.data(self.C_DATA, self.D_HANDLE)
|
1553
|
-
cCount = tItem.childCount()
|
1554
|
-
|
1555
|
-
# Update tree-related meta data
|
1556
|
-
nwItem = SHARED.project.tree[tHandle]
|
1557
|
-
if nwItem is not None:
|
1558
|
-
nwItem.setExpanded(tItem.isExpanded() and cCount > 0)
|
1559
|
-
nwItem.setOrder(tIndex)
|
1560
|
-
|
1561
|
-
itemList.append(tHandle)
|
1562
|
-
for i in range(cCount):
|
1563
|
-
self._scanChildren(itemList, tItem.child(i), i)
|
1564
|
-
|
1565
|
-
return itemList
|
1566
|
-
|
1567
|
-
def _addTreeItem(self, nwItem: NWItem | None,
|
1568
|
-
nHandle: str | None = None) -> QTreeWidgetItem | None:
|
1569
|
-
"""Create a QTreeWidgetItem from an NWItem and add it to the
|
1570
|
-
project tree. Returns the widget if the item is valid, otherwise
|
1571
|
-
a None is returned.
|
1572
|
-
"""
|
1573
|
-
if not nwItem:
|
1574
|
-
logger.error("Invalid item cannot be added to project tree")
|
1575
|
-
return None
|
1576
|
-
|
1577
|
-
tHandle = nwItem.itemHandle
|
1578
|
-
pHandle = nwItem.itemParent
|
1579
|
-
newItem = QTreeWidgetItem()
|
1580
|
-
|
1581
|
-
newItem.setText(self.C_NAME, "")
|
1582
|
-
newItem.setText(self.C_COUNT, "0")
|
1583
|
-
newItem.setText(self.C_ACTIVE, "")
|
1584
|
-
newItem.setText(self.C_STATUS, "")
|
1585
|
-
|
1586
|
-
newItem.setTextAlignment(self.C_NAME, QtAlignLeft)
|
1587
|
-
newItem.setTextAlignment(self.C_COUNT, QtAlignRight)
|
1588
|
-
newItem.setTextAlignment(self.C_ACTIVE, QtAlignLeft)
|
1589
|
-
newItem.setTextAlignment(self.C_STATUS, QtAlignLeft)
|
1590
|
-
|
1591
|
-
newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
|
1592
|
-
newItem.setData(self.C_DATA, self.D_WORDS, 0)
|
1593
|
-
|
1594
|
-
if pHandle is None and nwItem.isRootType():
|
1595
|
-
pItem = self.invisibleRootItem()
|
1596
|
-
elif pHandle and pHandle in self._treeMap:
|
1597
|
-
pItem = self._treeMap[pHandle]
|
1598
|
-
else:
|
1599
|
-
SHARED.error(self.tr(
|
1600
|
-
"There is nowhere to add item with name '{0}'."
|
1601
|
-
).format(nwItem.itemName))
|
1602
|
-
return None
|
1603
|
-
|
1604
|
-
byIndex = -1
|
1605
|
-
if nHandle is not None and nHandle in self._treeMap:
|
1606
|
-
byIndex = pItem.indexOfChild(self._treeMap[nHandle])
|
1607
|
-
if byIndex >= 0:
|
1608
|
-
pItem.insertChild(byIndex + 1, newItem)
|
1609
|
-
else:
|
1610
|
-
pItem.addChild(newItem)
|
994
|
+
self.setExpanded(index, not self.isExpanded(index))
|
995
|
+
return
|
1611
996
|
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1615
|
-
|
997
|
+
@pyqtSlot(QModelIndex)
|
998
|
+
def _onNodeCollapsed(self, index: QModelIndex) -> None:
|
999
|
+
"""Capture a node collapse, and pass it to the model."""
|
1000
|
+
if node := self._getNode(index):
|
1001
|
+
node.setExpanded(False)
|
1002
|
+
return
|
1616
1003
|
|
1617
|
-
|
1004
|
+
@pyqtSlot(QModelIndex)
|
1005
|
+
def _onNodeExpanded(self, index: QModelIndex) -> None:
|
1006
|
+
"""Capture a node expand, and pass it to the model."""
|
1007
|
+
if node := self._getNode(index):
|
1008
|
+
node.setExpanded(True)
|
1009
|
+
return
|
1618
1010
|
|
1619
|
-
|
1620
|
-
|
1621
|
-
|
1622
|
-
"""
|
1623
|
-
trashHandle = SHARED.project.trashFolder()
|
1624
|
-
if trashHandle is None:
|
1625
|
-
return None
|
1626
|
-
|
1627
|
-
trItem = self._getTreeItem(trashHandle)
|
1628
|
-
if trItem is None:
|
1629
|
-
trItem = self._addTreeItem(SHARED.project.tree[trashHandle])
|
1630
|
-
if trItem is not None:
|
1631
|
-
trItem.setExpanded(True)
|
1632
|
-
self._alertTreeChange(trashHandle, flush=True)
|
1633
|
-
|
1634
|
-
return trItem
|
1635
|
-
|
1636
|
-
def _alertTreeChange(self, tHandle: str | None, flush: bool = False) -> None:
|
1637
|
-
"""Update information on tree change state, and emit necessary
|
1638
|
-
signals. A flush is only needed if an item is moved, created or
|
1639
|
-
deleted.
|
1640
|
-
"""
|
1641
|
-
self._timeChanged = time()
|
1642
|
-
SHARED.project.setProjectChanged(True)
|
1643
|
-
if flush:
|
1644
|
-
self.saveTreeOrder()
|
1011
|
+
##
|
1012
|
+
# Internal Functions
|
1013
|
+
##
|
1645
1014
|
|
1646
|
-
|
1647
|
-
|
1015
|
+
def _clearSelection(self) -> None:
|
1016
|
+
"""Clear the currently selected items."""
|
1017
|
+
self.clearSelection()
|
1018
|
+
if model := self.selectionModel():
|
1019
|
+
# Selection model can be None (#2173)
|
1020
|
+
model.clearCurrentIndex()
|
1021
|
+
return
|
1648
1022
|
|
1649
|
-
|
1650
|
-
|
1651
|
-
|
1023
|
+
def _selectedRows(self) -> list[QModelIndex]:
|
1024
|
+
"""Return all column 0 indexes."""
|
1025
|
+
return [i for i in self.selectedIndexes() if i.column() == 0]
|
1652
1026
|
|
1653
|
-
|
1027
|
+
def _getModel(self) -> ProjectModel | None:
|
1028
|
+
"""Return a project node corresponding to a model index."""
|
1029
|
+
if isinstance(model := self.model(), ProjectModel):
|
1030
|
+
return model
|
1031
|
+
return None
|
1654
1032
|
|
1655
|
-
|
1033
|
+
def _getNode(self, index: QModelIndex) -> ProjectNode | None:
|
1034
|
+
"""Return a project node corresponding to a model index."""
|
1035
|
+
if isinstance(model := self.model(), ProjectModel) and (node := model.node(index)):
|
1036
|
+
return node
|
1037
|
+
return None
|
1656
1038
|
|
1657
1039
|
|
1658
1040
|
class _UpdatableMenu(QMenu):
|
@@ -1720,18 +1102,22 @@ class _UpdatableMenu(QMenu):
|
|
1720
1102
|
|
1721
1103
|
class _TreeContextMenu(QMenu):
|
1722
1104
|
|
1723
|
-
|
1724
|
-
super().__init__(parent=projTree)
|
1725
|
-
|
1726
|
-
self.projTree = projTree
|
1727
|
-
self.projView = projTree.projView
|
1728
|
-
|
1729
|
-
self._item = nwItem
|
1730
|
-
self._handle = nwItem.itemHandle
|
1731
|
-
self._items: list[NWItem] = []
|
1105
|
+
__slots__ = ("_tree", "_view", "_node", "_item", "_model", "_handle", "_indices", "_children")
|
1732
1106
|
|
1107
|
+
def __init__(
|
1108
|
+
self, projTree: GuiProjectTree, model: ProjectModel,
|
1109
|
+
node: ProjectNode, indices: list[QModelIndex]
|
1110
|
+
) -> None:
|
1111
|
+
super().__init__(parent=projTree)
|
1112
|
+
self._tree = projTree
|
1113
|
+
self._view = projTree.projView
|
1114
|
+
self._node = node
|
1115
|
+
self._item = node.item
|
1116
|
+
self._model = model
|
1117
|
+
self._handle = node.item.itemHandle
|
1118
|
+
self._indices = indices
|
1119
|
+
self._children = node.childCount() > 0
|
1733
1120
|
logger.debug("Ready: _TreeContextMenu")
|
1734
|
-
|
1735
1121
|
return
|
1736
1122
|
|
1737
1123
|
def __del__(self) -> None: # pragma: no cover
|
@@ -1745,14 +1131,15 @@ class _TreeContextMenu(QMenu):
|
|
1745
1131
|
def buildTrashMenu(self) -> None:
|
1746
1132
|
"""Build the special menu for the Trash folder."""
|
1747
1133
|
action = self.addAction(self.tr("Empty Trash"))
|
1748
|
-
action.triggered.connect(self.
|
1134
|
+
action.triggered.connect(self._tree.emptyTrash)
|
1135
|
+
if self._children:
|
1136
|
+
self._expandCollapse()
|
1749
1137
|
return
|
1750
1138
|
|
1751
|
-
def buildSingleSelectMenu(self
|
1139
|
+
def buildSingleSelectMenu(self) -> None:
|
1752
1140
|
"""Build the single-select menu."""
|
1753
1141
|
isFile = self._item.isFileType()
|
1754
1142
|
isFolder = self._item.isFolderType()
|
1755
|
-
isRoot = self._item.isRootType()
|
1756
1143
|
|
1757
1144
|
# Document Actions
|
1758
1145
|
if isFile:
|
@@ -1765,33 +1152,32 @@ class _TreeContextMenu(QMenu):
|
|
1765
1152
|
|
1766
1153
|
# Edit Item Settings
|
1767
1154
|
action = self.addAction(self.tr("Rename"))
|
1768
|
-
action.triggered.connect(
|
1155
|
+
action.triggered.connect(qtLambda(self._view.renameTreeItem, self._handle))
|
1769
1156
|
if isFile:
|
1770
1157
|
self._itemHeader()
|
1771
|
-
self._itemActive(
|
1158
|
+
self._itemActive()
|
1772
1159
|
self._itemStatusImport(False)
|
1773
1160
|
|
1774
1161
|
# Transform Item
|
1775
1162
|
if isFile or isFolder:
|
1776
|
-
self._itemTransform(isFile, isFolder
|
1163
|
+
self._itemTransform(isFile, isFolder)
|
1777
1164
|
self.addSeparator()
|
1778
1165
|
|
1779
1166
|
# Process Item
|
1780
|
-
self.
|
1167
|
+
if self._children:
|
1168
|
+
self._expandCollapse()
|
1169
|
+
action = self.addAction(self.tr("Duplicate"))
|
1170
|
+
action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle))
|
1171
|
+
self._deleteOrTrash()
|
1781
1172
|
|
1782
1173
|
return
|
1783
1174
|
|
1784
|
-
def buildMultiSelectMenu(self
|
1175
|
+
def buildMultiSelectMenu(self) -> None:
|
1785
1176
|
"""Build the multi-select menu."""
|
1786
|
-
self.
|
1787
|
-
for tHandle in handles:
|
1788
|
-
if (tItem := SHARED.project.tree[tHandle]):
|
1789
|
-
self._items.append(tItem)
|
1790
|
-
|
1791
|
-
self._itemActive(True)
|
1177
|
+
self._itemActive()
|
1792
1178
|
self._itemStatusImport(True)
|
1793
1179
|
self.addSeparator()
|
1794
|
-
self.
|
1180
|
+
self._deleteOrTrash()
|
1795
1181
|
return
|
1796
1182
|
|
1797
1183
|
##
|
@@ -1801,23 +1187,25 @@ class _TreeContextMenu(QMenu):
|
|
1801
1187
|
def _docActions(self) -> None:
|
1802
1188
|
"""Add document actions."""
|
1803
1189
|
action = self.addAction(self.tr("Open Document"))
|
1804
|
-
action.triggered.connect(
|
1805
|
-
|
1806
|
-
|
1190
|
+
action.triggered.connect(qtLambda(
|
1191
|
+
self._view.openDocumentRequest.emit,
|
1192
|
+
self._handle, nwDocMode.EDIT, "", True
|
1193
|
+
))
|
1807
1194
|
action = self.addAction(self.tr("View Document"))
|
1808
|
-
action.triggered.connect(
|
1809
|
-
|
1810
|
-
|
1195
|
+
action.triggered.connect(qtLambda(
|
1196
|
+
self._view.openDocumentRequest.emit,
|
1197
|
+
self._handle, nwDocMode.VIEW, "", False
|
1198
|
+
))
|
1811
1199
|
return
|
1812
1200
|
|
1813
1201
|
def _itemCreation(self) -> None:
|
1814
1202
|
"""Add create item actions."""
|
1815
1203
|
menu = self.addMenu(self.tr("Create New ..."))
|
1816
|
-
menu.addAction(self.
|
1817
|
-
menu.addAction(self.
|
1818
|
-
menu.addAction(self.
|
1819
|
-
menu.addAction(self.
|
1820
|
-
menu.addAction(self.
|
1204
|
+
menu.addAction(self._view.projBar.aAddEmpty)
|
1205
|
+
menu.addAction(self._view.projBar.aAddChap)
|
1206
|
+
menu.addAction(self._view.projBar.aAddScene)
|
1207
|
+
menu.addAction(self._view.projBar.aAddNote)
|
1208
|
+
menu.addAction(self._view.projBar.aAddFolder)
|
1821
1209
|
return
|
1822
1210
|
|
1823
1211
|
def _itemHeader(self) -> None:
|
@@ -1826,18 +1214,18 @@ class _TreeContextMenu(QMenu):
|
|
1826
1214
|
if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"):
|
1827
1215
|
action = self.addAction(self.tr("Rename to Heading"))
|
1828
1216
|
action.triggered.connect(
|
1829
|
-
|
1217
|
+
qtLambda(self._view.renameTreeItem, self._handle, hItem.title)
|
1830
1218
|
)
|
1831
1219
|
return
|
1832
1220
|
|
1833
|
-
def _itemActive(self
|
1221
|
+
def _itemActive(self) -> None:
|
1834
1222
|
"""Add Active/Inactive actions."""
|
1835
|
-
if
|
1223
|
+
if len(self._indices) > 1:
|
1836
1224
|
mSub = self.addMenu(self.tr("Set Active to ..."))
|
1837
|
-
aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.
|
1838
|
-
aOne.triggered.connect(
|
1839
|
-
aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.
|
1840
|
-
aTwo.triggered.connect(
|
1225
|
+
aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self._tree.trActive)
|
1226
|
+
aOne.triggered.connect(qtLambda(self._iterItemActive, True))
|
1227
|
+
aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self._tree.trInactive)
|
1228
|
+
aTwo.triggered.connect(qtLambda(self._iterItemActive, False))
|
1841
1229
|
else:
|
1842
1230
|
action = self.addAction(self.tr("Toggle Active"))
|
1843
1231
|
action.triggered.connect(self._toggleItemActive)
|
@@ -1848,46 +1236,45 @@ class _TreeContextMenu(QMenu):
|
|
1848
1236
|
if self._item.isNovelLike():
|
1849
1237
|
menu = self.addMenu(self.tr("Set Status to ..."))
|
1850
1238
|
current = self._item.itemStatus
|
1851
|
-
for
|
1239
|
+
for key, entry in SHARED.project.data.itemStatus.iterItems():
|
1852
1240
|
name = entry.name
|
1853
1241
|
if not multi and current == key:
|
1854
1242
|
name += f" ({nwUnicode.U_CHECK})"
|
1855
1243
|
action = menu.addAction(entry.icon, name)
|
1856
1244
|
if multi:
|
1857
|
-
action.triggered.connect(
|
1245
|
+
action.triggered.connect(qtLambda(self._iterSetItemStatus, key))
|
1858
1246
|
else:
|
1859
|
-
action.triggered.connect(
|
1247
|
+
action.triggered.connect(qtLambda(self._changeItemStatus, key))
|
1860
1248
|
menu.addSeparator()
|
1861
1249
|
action = menu.addAction(self.tr("Manage Labels ..."))
|
1862
|
-
action.triggered.connect(
|
1863
|
-
|
1864
|
-
|
1250
|
+
action.triggered.connect(qtLambda(
|
1251
|
+
self._view.projectSettingsRequest.emit,
|
1252
|
+
GuiProjectSettings.PAGE_STATUS
|
1253
|
+
))
|
1865
1254
|
else:
|
1866
1255
|
menu = self.addMenu(self.tr("Set Importance to ..."))
|
1867
1256
|
current = self._item.itemImport
|
1868
|
-
for
|
1257
|
+
for key, entry in SHARED.project.data.itemImport.iterItems():
|
1869
1258
|
name = entry.name
|
1870
1259
|
if not multi and current == key:
|
1871
1260
|
name += f" ({nwUnicode.U_CHECK})"
|
1872
1261
|
action = menu.addAction(entry.icon, name)
|
1873
1262
|
if multi:
|
1874
|
-
action.triggered.connect(
|
1263
|
+
action.triggered.connect(qtLambda(self._iterSetItemImport, key))
|
1875
1264
|
else:
|
1876
|
-
action.triggered.connect(
|
1265
|
+
action.triggered.connect(qtLambda(self._changeItemImport, key))
|
1877
1266
|
menu.addSeparator()
|
1878
1267
|
action = menu.addAction(self.tr("Manage Labels ..."))
|
1879
|
-
action.triggered.connect(
|
1880
|
-
|
1881
|
-
|
1268
|
+
action.triggered.connect(qtLambda(
|
1269
|
+
self._view.projectSettingsRequest.emit,
|
1270
|
+
GuiProjectSettings.PAGE_IMPORT
|
1271
|
+
))
|
1882
1272
|
return
|
1883
1273
|
|
1884
|
-
def _itemTransform(self, isFile: bool, isFolder: bool
|
1274
|
+
def _itemTransform(self, isFile: bool, isFolder: bool) -> None:
|
1885
1275
|
"""Add actions for the Transform menu."""
|
1886
1276
|
menu = self.addMenu(self.tr("Transform ..."))
|
1887
1277
|
|
1888
|
-
tree = self.projTree
|
1889
|
-
tHandle = self._handle
|
1890
|
-
|
1891
1278
|
trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
|
1892
1279
|
trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
|
1893
1280
|
loDoc = nwItemLayout.DOCUMENT
|
@@ -1897,159 +1284,126 @@ class _TreeContextMenu(QMenu):
|
|
1897
1284
|
|
1898
1285
|
if isNoteFile and self._item.documentAllowed():
|
1899
1286
|
action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
|
1900
|
-
action.triggered.connect(
|
1287
|
+
action.triggered.connect(qtLambda(self._changeItemLayout, loDoc))
|
1901
1288
|
|
1902
1289
|
if isDocFile:
|
1903
1290
|
action = menu.addAction(self.tr("Convert to {0}").format(trNote))
|
1904
|
-
action.triggered.connect(
|
1291
|
+
action.triggered.connect(qtLambda(self._changeItemLayout, loNote))
|
1905
1292
|
|
1906
1293
|
if isFolder and self._item.documentAllowed():
|
1907
1294
|
action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
|
1908
|
-
action.triggered.connect(
|
1295
|
+
action.triggered.connect(qtLambda(self._convertFolderToFile, loDoc))
|
1909
1296
|
|
1910
1297
|
if isFolder:
|
1911
1298
|
action = menu.addAction(self.tr("Convert to {0}").format(trNote))
|
1912
|
-
action.triggered.connect(
|
1299
|
+
action.triggered.connect(qtLambda(self._convertFolderToFile, loNote))
|
1913
1300
|
|
1914
|
-
if
|
1301
|
+
if self._children and isFile:
|
1915
1302
|
action = menu.addAction(self.tr("Merge Child Items into Self"))
|
1916
|
-
action.triggered.connect(
|
1303
|
+
action.triggered.connect(qtLambda(self._tree.mergeDocuments, self._handle, False))
|
1917
1304
|
action = menu.addAction(self.tr("Merge Child Items into New"))
|
1918
|
-
action.triggered.connect(
|
1305
|
+
action.triggered.connect(qtLambda(self._tree.mergeDocuments, self._handle, True))
|
1919
1306
|
|
1920
|
-
if
|
1307
|
+
if self._children and isFolder:
|
1921
1308
|
action = menu.addAction(self.tr("Merge Documents in Folder"))
|
1922
|
-
action.triggered.connect(
|
1309
|
+
action.triggered.connect(qtLambda(self._tree.mergeDocuments, self._handle, True))
|
1923
1310
|
|
1924
1311
|
if isFile:
|
1925
1312
|
action = menu.addAction(self.tr("Split Document by Headings"))
|
1926
|
-
action.triggered.connect(
|
1313
|
+
action.triggered.connect(qtLambda(self._tree.splitDocument, self._handle))
|
1927
1314
|
|
1928
1315
|
return
|
1929
1316
|
|
1930
|
-
def
|
1931
|
-
"""Add actions for
|
1932
|
-
|
1933
|
-
|
1934
|
-
|
1935
|
-
|
1936
|
-
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, True))
|
1937
|
-
action = self.addAction(self.tr("Collapse All"))
|
1938
|
-
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, False))
|
1939
|
-
|
1940
|
-
action = self.addAction(self.tr("Duplicate"))
|
1941
|
-
action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
|
1942
|
-
|
1943
|
-
if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
1944
|
-
action = self.addAction(self.tr("Delete Permanently"))
|
1945
|
-
action.triggered.connect(lambda: tree.permDeleteItem(tHandle))
|
1946
|
-
else:
|
1947
|
-
action = self.addAction(self.tr("Move to Trash"))
|
1948
|
-
action.triggered.connect(lambda: tree.moveItemToTrash(tHandle))
|
1949
|
-
|
1317
|
+
def _expandCollapse(self) -> None:
|
1318
|
+
"""Add actions for expand and collapse."""
|
1319
|
+
action = self.addAction(self.tr("Expand All"))
|
1320
|
+
action.triggered.connect(qtLambda(self._tree.expandFromIndex, self._indices[0]))
|
1321
|
+
action = self.addAction(self.tr("Collapse All"))
|
1322
|
+
action.triggered.connect(qtLambda(self._tree.collapseFromIndex, self._indices[0]))
|
1950
1323
|
return
|
1951
1324
|
|
1952
|
-
def
|
1325
|
+
def _deleteOrTrash(self) -> None:
|
1953
1326
|
"""Add move to Trash action."""
|
1954
|
-
|
1955
|
-
|
1956
|
-
|
1957
|
-
|
1958
|
-
|
1959
|
-
|
1960
|
-
|
1327
|
+
if (
|
1328
|
+
self._model.trashSelection(self._indices)
|
1329
|
+
or len(self._indices) == 1 and self._item.isRootType()
|
1330
|
+
):
|
1331
|
+
text = self.tr("Delete Permanently")
|
1332
|
+
else:
|
1333
|
+
text = self.tr("Move to Trash")
|
1334
|
+
action = self.addAction(text)
|
1335
|
+
action.triggered.connect(self._tree.processDeleteRequest)
|
1961
1336
|
return
|
1962
1337
|
|
1963
1338
|
##
|
1964
1339
|
# Private Slots
|
1965
1340
|
##
|
1966
1341
|
|
1967
|
-
@pyqtSlot()
|
1968
|
-
def _iterMoveToTrash(self) -> None:
|
1969
|
-
"""Iterate through files and move them to Trash."""
|
1970
|
-
if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))):
|
1971
|
-
for tItem in self._items:
|
1972
|
-
if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH:
|
1973
|
-
self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False)
|
1974
|
-
self.projTree.saveTreeOrder()
|
1975
|
-
return
|
1976
|
-
|
1977
|
-
@pyqtSlot()
|
1978
|
-
def _iterPermDelete(self) -> None:
|
1979
|
-
"""Iterate through files and delete them."""
|
1980
|
-
if SHARED.question(self.projTree.trPermDelete.format(len(self._items))):
|
1981
|
-
for tItem in self._items:
|
1982
|
-
if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH:
|
1983
|
-
self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False)
|
1984
|
-
self.projTree.saveTreeOrder()
|
1985
|
-
return
|
1986
|
-
|
1987
1342
|
@pyqtSlot()
|
1988
1343
|
def _toggleItemActive(self) -> None:
|
1989
1344
|
"""Toggle the active status of an item."""
|
1990
|
-
self._item.
|
1991
|
-
|
1992
|
-
|
1345
|
+
if self._item.isFileType():
|
1346
|
+
self._item.setActive(not self._item.isActive)
|
1347
|
+
self._item.notifyToRefresh()
|
1993
1348
|
return
|
1994
1349
|
|
1995
1350
|
##
|
1996
1351
|
# Internal Functions
|
1997
1352
|
##
|
1998
1353
|
|
1999
|
-
def _iterItemActive(self,
|
1354
|
+
def _iterItemActive(self, state: bool) -> None:
|
2000
1355
|
"""Set the active status of multiple items."""
|
2001
|
-
|
2002
|
-
|
2003
|
-
|
2004
|
-
|
2005
|
-
|
1356
|
+
refresh = []
|
1357
|
+
for node in self._model.nodes(self._indices):
|
1358
|
+
if node.item.isFileType():
|
1359
|
+
node.item.setActive(state)
|
1360
|
+
refresh.append(node.item.itemHandle)
|
1361
|
+
SHARED.project.tree.refreshItems(refresh)
|
2006
1362
|
return
|
2007
1363
|
|
2008
1364
|
def _changeItemStatus(self, key: str) -> None:
|
2009
1365
|
"""Set a new status value of an item."""
|
2010
1366
|
self._item.setStatus(key)
|
2011
|
-
self.
|
2012
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1367
|
+
self._item.notifyToRefresh()
|
2013
1368
|
return
|
2014
1369
|
|
2015
1370
|
def _iterSetItemStatus(self, key: str) -> None:
|
2016
1371
|
"""Change the status value for multiple items."""
|
2017
|
-
|
2018
|
-
|
2019
|
-
|
2020
|
-
|
2021
|
-
|
1372
|
+
refresh = []
|
1373
|
+
for node in self._model.nodes(self._indices):
|
1374
|
+
if node.item.isNovelLike():
|
1375
|
+
node.item.setStatus(key)
|
1376
|
+
refresh.append(node.item.itemHandle)
|
1377
|
+
SHARED.project.tree.refreshItems(refresh)
|
2022
1378
|
return
|
2023
1379
|
|
2024
1380
|
def _changeItemImport(self, key: str) -> None:
|
2025
1381
|
"""Set a new importance value of an item."""
|
2026
1382
|
self._item.setImport(key)
|
2027
|
-
self.
|
2028
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1383
|
+
self._item.notifyToRefresh()
|
2029
1384
|
return
|
2030
1385
|
|
2031
1386
|
def _iterSetItemImport(self, key: str) -> None:
|
2032
1387
|
"""Change the status value for multiple items."""
|
2033
|
-
|
2034
|
-
|
2035
|
-
|
2036
|
-
|
2037
|
-
|
1388
|
+
refresh = []
|
1389
|
+
for node in self._model.nodes(self._indices):
|
1390
|
+
if not node.item.isNovelLike():
|
1391
|
+
node.item.setImport(key)
|
1392
|
+
refresh.append(node.item.itemHandle)
|
1393
|
+
SHARED.project.tree.refreshItems(refresh)
|
2038
1394
|
return
|
2039
1395
|
|
2040
1396
|
def _changeItemLayout(self, itemLayout: nwItemLayout) -> None:
|
2041
1397
|
"""Set a new item layout value of an item."""
|
2042
1398
|
if itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
|
2043
1399
|
self._item.setLayout(nwItemLayout.DOCUMENT)
|
2044
|
-
self.
|
2045
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1400
|
+
self._item.notifyToRefresh()
|
2046
1401
|
elif itemLayout == nwItemLayout.NOTE:
|
2047
1402
|
self._item.setLayout(nwItemLayout.NOTE)
|
2048
|
-
self.
|
2049
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1403
|
+
self._item.notifyToRefresh()
|
2050
1404
|
return
|
2051
1405
|
|
2052
|
-
def
|
1406
|
+
def _convertFolderToFile(self, itemLayout: nwItemLayout) -> None:
|
2053
1407
|
"""Convert a folder to a note or document."""
|
2054
1408
|
if self._item.isFolderType():
|
2055
1409
|
msgYes = SHARED.question(self.tr(
|
@@ -2059,13 +1413,11 @@ class _TreeContextMenu(QMenu):
|
|
2059
1413
|
if msgYes and itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
|
2060
1414
|
self._item.setType(nwItemType.FILE)
|
2061
1415
|
self._item.setLayout(nwItemLayout.DOCUMENT)
|
2062
|
-
self.
|
2063
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1416
|
+
self._item.notifyToRefresh()
|
2064
1417
|
elif msgYes and itemLayout == nwItemLayout.NOTE:
|
2065
1418
|
self._item.setType(nwItemType.FILE)
|
2066
1419
|
self._item.setLayout(nwItemLayout.NOTE)
|
2067
|
-
self.
|
2068
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1420
|
+
self._item.notifyToRefresh()
|
2069
1421
|
else:
|
2070
1422
|
logger.info("Folder conversion cancelled")
|
2071
1423
|
return
|