novelWriter 2.5.3__py3-none-any.whl → 2.6b2__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.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
- novelwriter/__init__.py +49 -10
- novelwriter/assets/i18n/project_en_GB.json +1 -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/common.py +100 -2
- novelwriter/config.py +25 -15
- novelwriter/constants.py +168 -60
- novelwriter/core/buildsettings.py +66 -39
- novelwriter/core/coretools.py +145 -147
- novelwriter/core/docbuild.py +132 -170
- novelwriter/core/index.py +38 -37
- novelwriter/core/item.py +41 -8
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/options.py +4 -1
- novelwriter/core/project.py +67 -89
- novelwriter/core/spellcheck.py +9 -14
- novelwriter/core/status.py +7 -5
- novelwriter/core/tree.py +268 -287
- novelwriter/dialogs/docmerge.py +7 -17
- novelwriter/dialogs/preferences.py +46 -33
- novelwriter/dialogs/projectsettings.py +5 -5
- novelwriter/enum.py +36 -23
- novelwriter/extensions/configlayout.py +27 -12
- novelwriter/extensions/modified.py +13 -1
- novelwriter/extensions/pagedsidebar.py +5 -5
- novelwriter/formats/shared.py +155 -0
- novelwriter/formats/todocx.py +1191 -0
- novelwriter/formats/tohtml.py +451 -0
- novelwriter/{core → formats}/tokenizer.py +487 -491
- novelwriter/formats/tomarkdown.py +217 -0
- novelwriter/{core → formats}/toodt.py +311 -432
- novelwriter/formats/toqdoc.py +484 -0
- novelwriter/formats/toraw.py +91 -0
- novelwriter/gui/doceditor.py +342 -284
- novelwriter/gui/dochighlight.py +96 -84
- novelwriter/gui/docviewer.py +88 -31
- novelwriter/gui/docviewerpanel.py +17 -25
- novelwriter/gui/editordocument.py +17 -2
- novelwriter/gui/itemdetails.py +25 -28
- novelwriter/gui/mainmenu.py +129 -63
- novelwriter/gui/noveltree.py +45 -47
- novelwriter/gui/outline.py +196 -249
- novelwriter/gui/projtree.py +594 -1241
- novelwriter/gui/search.py +9 -10
- novelwriter/gui/sidebar.py +7 -6
- novelwriter/gui/theme.py +10 -5
- novelwriter/guimain.py +100 -196
- novelwriter/shared.py +66 -27
- novelwriter/text/counting.py +2 -0
- novelwriter/text/patterns.py +168 -60
- novelwriter/tools/manusbuild.py +14 -12
- novelwriter/tools/manuscript.py +120 -78
- novelwriter/tools/manussettings.py +424 -291
- novelwriter/tools/welcome.py +4 -4
- novelwriter/tools/writingstats.py +3 -3
- novelwriter/types.py +23 -7
- novelwriter/core/tohtml.py +0 -530
- novelwriter/core/tomarkdown.py +0 -252
- novelwriter/core/toqdoc.py +0 -419
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
novelwriter/gui/projtree.py
CHANGED
@@ -3,10 +3,12 @@ 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
14
|
Copyright 2018–2024, Veronica Berglyd Olsen
|
@@ -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,1043 +606,438 @@ 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
|
-
# If a non-root item ends up on root due to a bug, allow it
|
1004
|
-
# to still be deleted from invisible root (see #2108)
|
1005
|
-
trItemP = trItemS.parent() or self.invisibleRootItem()
|
1006
|
-
tIndex = trItemP.indexOfChild(trItemS)
|
1007
|
-
trItemP.takeChild(tIndex)
|
765
|
+
docSplit.setParentItem(tItem.itemParent)
|
1008
766
|
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
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
|
+
)
|
1016
774
|
|
1017
|
-
|
1018
|
-
|
1019
|
-
self.projView.treeItemChanged.emit(tHandle)
|
775
|
+
if data.get("moveToTrash", False):
|
776
|
+
self.processDeleteRequest([tHandle], False)
|
1020
777
|
|
1021
778
|
return True
|
1022
779
|
|
1023
|
-
def
|
1024
|
-
"""
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
for nwItem in SHARED.project.tree:
|
1031
|
-
if not nwItem.isNovelLike():
|
1032
|
-
self.setTreeItemValues(nwItem)
|
1033
|
-
return
|
1034
|
-
|
1035
|
-
def setTreeItemValues(self, nwItem: NWItem | None) -> None:
|
1036
|
-
"""Set the name and flag values for a tree item in the project
|
1037
|
-
tree. Does not trigger a tree change as the data is already
|
1038
|
-
coming from project data.
|
1039
|
-
"""
|
1040
|
-
if isinstance(nwItem, NWItem) and (trItem := self._getTreeItem(nwItem.itemHandle)):
|
1041
|
-
itemStatus, statusIcon = nwItem.getImportStatus()
|
1042
|
-
hLevel = nwItem.mainHeading
|
1043
|
-
itemIcon = SHARED.theme.getItemIcon(
|
1044
|
-
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
|
1045
|
-
)
|
1046
|
-
|
1047
|
-
trItem.setIcon(self.C_NAME, itemIcon)
|
1048
|
-
trItem.setText(self.C_NAME, nwItem.itemName)
|
1049
|
-
trItem.setIcon(self.C_STATUS, statusIcon)
|
1050
|
-
trItem.setToolTip(self.C_STATUS, itemStatus)
|
1051
|
-
|
1052
|
-
if nwItem.isFileType():
|
1053
|
-
iconName = "checked" if nwItem.isActive else "unchecked"
|
1054
|
-
toolTip = self.trActive if nwItem.isActive else self.trInactive
|
1055
|
-
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?")
|
1056
787
|
else:
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
trFont.setUnderline(hLevel == "H1")
|
1065
|
-
trItem.setFont(self.C_NAME, trFont)
|
1066
|
-
|
1067
|
-
# Emit Refresh Signal
|
1068
|
-
self.itemRefreshed.emit(nwItem.itemHandle, nwItem, itemIcon)
|
1069
|
-
|
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()
|
1070
795
|
return
|
1071
796
|
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
root item. This function is more efficient than recalculating
|
1076
|
-
everything each time the word count is updated, but is also
|
1077
|
-
prone to diverging from the true values if the counts are not
|
1078
|
-
properly reported to the function.
|
1079
|
-
"""
|
1080
|
-
tItem = self._getTreeItem(tHandle)
|
1081
|
-
if tItem is None:
|
1082
|
-
return
|
1083
|
-
|
1084
|
-
if countChildren:
|
1085
|
-
for i in range(tItem.childCount()):
|
1086
|
-
newCount += int(tItem.child(i).data(self.C_DATA, self.D_WORDS))
|
1087
|
-
|
1088
|
-
tItem.setText(self.C_COUNT, f"{newCount:n}")
|
1089
|
-
tItem.setData(self.C_DATA, self.D_WORDS, int(newCount))
|
1090
|
-
|
1091
|
-
pItem = tItem.parent()
|
1092
|
-
if pItem is None:
|
1093
|
-
return
|
1094
|
-
|
1095
|
-
pCount = 0
|
1096
|
-
pHandle = None
|
1097
|
-
for i in range(pItem.childCount()):
|
1098
|
-
pCount += int(pItem.child(i).data(self.C_DATA, self.D_WORDS))
|
1099
|
-
pHandle = pItem.data(self.C_DATA, self.D_HANDLE)
|
1100
|
-
|
1101
|
-
if pHandle:
|
1102
|
-
if SHARED.project.tree.checkType(pHandle, nwItemType.FILE):
|
1103
|
-
# A file has an internal word count we need to account
|
1104
|
-
# for, but a folder always has 0 words on its own.
|
1105
|
-
pCount += SHARED.project.index.getCounts(pHandle)[1]
|
1106
|
-
|
1107
|
-
self.propagateCount(pHandle, pCount, countChildren=False)
|
1108
|
-
|
1109
|
-
return
|
797
|
+
##
|
798
|
+
# Events and Overloads
|
799
|
+
##
|
1110
800
|
|
1111
|
-
def
|
1112
|
-
"""
|
1113
|
-
|
1114
|
-
|
1115
|
-
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.
|
1116
805
|
"""
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
self.
|
1123
|
-
|
1124
|
-
|
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
|
+
)
|
1125
815
|
return
|
1126
816
|
|
1127
|
-
def
|
1128
|
-
"""
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1132
|
-
return items[0].data(self.C_DATA, self.D_HANDLE)
|
1133
|
-
return None
|
1134
|
-
|
1135
|
-
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
|
1136
|
-
"""Set a specific handle as the selected item."""
|
1137
|
-
tItem = self._getTreeItem(tHandle)
|
1138
|
-
if tItem is None:
|
1139
|
-
return False
|
1140
|
-
|
1141
|
-
if tHandle in self._treeMap:
|
1142
|
-
self.setCurrentItem(self._treeMap[tHandle])
|
1143
|
-
|
1144
|
-
if (indexes := self.selectedIndexes()) and doScroll:
|
1145
|
-
self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
|
1146
|
-
|
1147
|
-
return True
|
1148
|
-
|
1149
|
-
def setExpandedFromHandle(self, tHandle: str | None, isExpanded: bool) -> None:
|
1150
|
-
"""Iterate through items below tHandle and change expanded
|
1151
|
-
status for all child items. If tHandle is None, it affects the
|
1152
|
-
entire tree.
|
1153
|
-
"""
|
1154
|
-
trItem = self._getTreeItem(tHandle) or self.invisibleRootItem()
|
1155
|
-
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)
|
1156
822
|
return
|
1157
823
|
|
1158
|
-
def openContextOnSelected(self) -> bool:
|
1159
|
-
"""Open the context menu on the current selected item."""
|
1160
|
-
if items := self.selectedItems():
|
1161
|
-
return self._openContextMenu(self.visualItemRect(items[0]).center())
|
1162
|
-
return False
|
1163
|
-
|
1164
|
-
def changedSince(self, checkTime: float) -> bool:
|
1165
|
-
"""Check if the tree has changed since a given time."""
|
1166
|
-
return self._timeChanged > checkTime
|
1167
|
-
|
1168
824
|
##
|
1169
|
-
#
|
825
|
+
# Public Slots
|
1170
826
|
##
|
1171
827
|
|
1172
828
|
@pyqtSlot()
|
1173
|
-
def
|
1174
|
-
"""
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
829
|
+
def moveItemUp(self) -> None:
|
830
|
+
"""Move an item up in the tree."""
|
831
|
+
if model := self._getModel():
|
832
|
+
model.internalMove(self.currentIndex(), -1)
|
833
|
+
return
|
1178
834
|
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
if
|
1183
|
-
|
1184
|
-
|
1185
|
-
item.setSelected(False)
|
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)
|
840
|
+
return
|
1186
841
|
|
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))
|
1187
848
|
return
|
1188
849
|
|
1189
|
-
@pyqtSlot(
|
1190
|
-
def
|
1191
|
-
"""
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
return
|
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
|
1197
857
|
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
858
|
+
@pyqtSlot()
|
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))
|
867
|
+
return
|
1201
868
|
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
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
|
1206
879
|
|
880
|
+
@pyqtSlot(QModelIndex)
|
881
|
+
def expandFromIndex(self, index: QModelIndex) -> None:
|
882
|
+
"""Expand all nodes from index."""
|
883
|
+
self.expandRecursively(index)
|
1207
884
|
return
|
1208
885
|
|
1209
|
-
@pyqtSlot(
|
1210
|
-
def
|
1211
|
-
"""
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
hasChild = False
|
1217
|
-
sItem = self.itemAt(clickPos)
|
1218
|
-
sItems = self.selectedItems()
|
1219
|
-
if isinstance(sItem, QTreeWidgetItem):
|
1220
|
-
tHandle = sItem.data(self.C_DATA, self.D_HANDLE)
|
1221
|
-
tItem = SHARED.project.tree[tHandle]
|
1222
|
-
hasChild = sItem.childCount() > 0
|
1223
|
-
|
1224
|
-
if tItem is None or tHandle is None:
|
1225
|
-
logger.debug("No item found")
|
1226
|
-
return False
|
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)
|
892
|
+
return
|
1227
893
|
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
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]
|
1235
901
|
else:
|
1236
|
-
|
902
|
+
indices = self._selectedRows()
|
1237
903
|
|
1238
|
-
|
1239
|
-
|
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
|
1240
911
|
|
1241
|
-
|
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))
|
1242
927
|
|
1243
|
-
@pyqtSlot()
|
1244
|
-
def _doAutoScroll(self) -> None:
|
1245
|
-
"""Scroll one item up or down based on direction value."""
|
1246
|
-
if self._scrollDirection == -1:
|
1247
|
-
self.scrollToItem(self.itemAbove(self.itemAt(1, 1)))
|
1248
|
-
elif self._scrollDirection == 1:
|
1249
|
-
self.scrollToItem(self.itemBelow(self.itemAt(1, self.height() - 1)))
|
1250
|
-
self._scrollDirection = 0
|
1251
|
-
self._scrollTimer.stop()
|
1252
928
|
return
|
1253
929
|
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
"""Overload mousePressEvent to clear selection if clicking the
|
1260
|
-
mouse in a blank area of the tree view, and to load a document
|
1261
|
-
for viewing if the user middle-clicked.
|
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.
|
1262
935
|
"""
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
return
|
1275
|
-
|
1276
|
-
def startDrag(self, dropAction: Qt.DropAction) -> None:
|
1277
|
-
"""Capture the drag and drop handling to pop alerts."""
|
1278
|
-
super().startDrag(dropAction)
|
1279
|
-
if self._popAlert:
|
1280
|
-
SHARED.error(self._popAlert)
|
1281
|
-
self._popAlert = None
|
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)
|
1282
947
|
return
|
1283
948
|
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
"""
|
1288
|
-
|
1289
|
-
if items and (parent := items[0].parent()) and all(x.parent() is parent for x in items):
|
1290
|
-
super().dragEnterEvent(event)
|
1291
|
-
else:
|
1292
|
-
logger.warning("Drag action is not allowed and has been cancelled")
|
1293
|
-
self._popAlert = self.tr(
|
1294
|
-
"Drag and drop is only allowed for single items, non-root "
|
1295
|
-
"items, or multiple items with the same parent."
|
1296
|
-
)
|
1297
|
-
event.mimeData().clear()
|
1298
|
-
event.ignore()
|
1299
|
-
return
|
1300
|
-
|
1301
|
-
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
|
1302
|
-
"""Capture the drag move event to enable edge auto scroll."""
|
1303
|
-
y = event.pos().y()
|
1304
|
-
if y < self._scrollMargin:
|
1305
|
-
if not self._scrollTimer.isActive():
|
1306
|
-
self._scrollDirection = -1
|
1307
|
-
self._scrollTimer.start()
|
1308
|
-
elif y > self.height() - self._scrollMargin:
|
1309
|
-
if not self._scrollTimer.isActive():
|
1310
|
-
self._scrollDirection = 1
|
1311
|
-
self._scrollTimer.start()
|
1312
|
-
super().dragMoveEvent(event)
|
1313
|
-
return
|
1314
|
-
|
1315
|
-
def dropEvent(self, event: QDropEvent) -> None:
|
1316
|
-
"""Overload the drop item event to ensure the drag and drop
|
1317
|
-
action is allowed, and update relevant data.
|
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.
|
1318
954
|
"""
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
self.propagateCount(mHandle, 0)
|
1336
|
-
|
1337
|
-
super().dropEvent(event)
|
1338
|
-
|
1339
|
-
for mHandle, (sItem, isExpanded) in mItems.items():
|
1340
|
-
self._postItemMove(mHandle)
|
1341
|
-
sItem.setExpanded(isExpanded)
|
1342
|
-
self._alertTreeChange(mHandle, flush=False)
|
955
|
+
if model := self._getModel():
|
956
|
+
if point is None:
|
957
|
+
point = self.visualRect(self.currentIndex()).center()
|
958
|
+
|
959
|
+
if (
|
960
|
+
point is not None
|
961
|
+
and (node := self._getNode(self.currentIndex()))
|
962
|
+
and (indices := self._selectedRows())
|
963
|
+
):
|
964
|
+
ctxMenu = _TreeContextMenu(self, model, node, indices)
|
965
|
+
if node is SHARED.project.tree.trash:
|
966
|
+
ctxMenu.buildTrashMenu()
|
967
|
+
elif len(indices) > 1:
|
968
|
+
ctxMenu.buildMultiSelectMenu()
|
969
|
+
else:
|
970
|
+
ctxMenu.buildSingleSelectMenu()
|
1343
971
|
|
1344
|
-
|
972
|
+
ctxMenu.exec(self.viewport().mapToGlobal(point))
|
973
|
+
ctxMenu.deleteLater()
|
1345
974
|
|
1346
975
|
return
|
1347
976
|
|
1348
977
|
##
|
1349
|
-
#
|
978
|
+
# Private Slots
|
1350
979
|
##
|
1351
980
|
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
if trItemP is None or nwItemS is None:
|
1358
|
-
logger.error("Failed to find new parent item of '%s'", tHandle)
|
1359
|
-
return
|
1360
|
-
|
1361
|
-
# Update item parent handle in the project
|
1362
|
-
pHandle = trItemP.data(self.C_DATA, self.D_HANDLE)
|
1363
|
-
nwItemS.setParent(pHandle)
|
1364
|
-
trItemP.setExpanded(True)
|
1365
|
-
logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
|
1366
|
-
|
1367
|
-
mHandles = self.getTreeFromHandle(tHandle)
|
1368
|
-
logger.debug("A total of %d item(s) were moved", len(mHandles))
|
1369
|
-
for mHandle in mHandles:
|
1370
|
-
logger.debug("Updating item '%s'", mHandle)
|
1371
|
-
SHARED.project.tree.updateItemData(mHandle)
|
1372
|
-
if nwItemS.isInactiveClass():
|
1373
|
-
SHARED.project.index.deleteHandle(mHandle)
|
1374
|
-
else:
|
1375
|
-
SHARED.project.index.reIndexHandle(mHandle)
|
1376
|
-
if mItem := SHARED.project.tree[mHandle]:
|
1377
|
-
self.setTreeItemValues(mItem)
|
1378
|
-
|
1379
|
-
# Update word count
|
1380
|
-
self.propagateCount(tHandle, nwItemS.wordCount, countChildren=True)
|
1381
|
-
|
981
|
+
@pyqtSlot(QModelIndex)
|
982
|
+
def _onSingleClick(self, index: QModelIndex) -> None:
|
983
|
+
"""The user changed which item is selected."""
|
984
|
+
if node := self._getNode(index):
|
985
|
+
self.projView.selectedItemChanged.emit(node.item.itemHandle)
|
1382
986
|
return
|
1383
987
|
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
def _getTreeItem(self, tHandle: str | None) -> QTreeWidgetItem | None:
|
1390
|
-
"""Return the QTreeWidgetItem of a given item handle."""
|
1391
|
-
return self._treeMap.get(tHandle, None) if tHandle else None
|
1392
|
-
|
1393
|
-
def _recursiveSetExpanded(self, trItem: QTreeWidgetItem, isExpanded: bool) -> None:
|
1394
|
-
"""Recursive function to set expanded status starting from (and
|
1395
|
-
not including) a given item.
|
988
|
+
@pyqtSlot(QModelIndex)
|
989
|
+
def _onDoubleClick(self, index: QModelIndex) -> None:
|
990
|
+
"""Capture a double-click event and either request the document
|
991
|
+
for editing if it is a file, or expand/close the node if not.
|
1396
992
|
"""
|
1397
|
-
if
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1401
|
-
self._recursiveSetExpanded(chItem, isExpanded)
|
1402
|
-
return
|
1403
|
-
|
1404
|
-
def _mergeDocuments(self, tHandle: str, newFile: bool) -> bool:
|
1405
|
-
"""Merge an item's child documents into a single document."""
|
1406
|
-
logger.info("Request to merge items under handle '%s'", tHandle)
|
1407
|
-
itemList = self.getTreeFromHandle(tHandle)
|
1408
|
-
|
1409
|
-
tItem = SHARED.project.tree[tHandle]
|
1410
|
-
if tItem is None:
|
1411
|
-
return False
|
1412
|
-
|
1413
|
-
if tItem.isRootType():
|
1414
|
-
logger.error("Cannot merge root item")
|
1415
|
-
return False
|
1416
|
-
|
1417
|
-
if not newFile:
|
1418
|
-
itemList.remove(tHandle)
|
1419
|
-
|
1420
|
-
data, status = GuiDocMerge.getData(SHARED.mainGui, tHandle, itemList)
|
1421
|
-
if status:
|
1422
|
-
items = data.get("finalItems", [])
|
1423
|
-
if not items:
|
1424
|
-
SHARED.info(self.tr("No documents selected for merging."))
|
1425
|
-
return False
|
1426
|
-
|
1427
|
-
# Save the open document first, in case it's part of merge
|
1428
|
-
SHARED.saveEditor()
|
1429
|
-
|
1430
|
-
# Create merge object, and append docs
|
1431
|
-
docMerger = DocMerger(SHARED.project)
|
1432
|
-
mLabel = self.tr("Merged")
|
1433
|
-
|
1434
|
-
if newFile:
|
1435
|
-
docLabel = f"[{mLabel}] {tItem.itemName}"
|
1436
|
-
mHandle = docMerger.newTargetDoc(tHandle, docLabel)
|
1437
|
-
elif tItem.isFileType():
|
1438
|
-
docMerger.setTargetDoc(tHandle)
|
1439
|
-
mHandle = tHandle
|
1440
|
-
else:
|
1441
|
-
return False
|
1442
|
-
|
1443
|
-
for sHandle in items:
|
1444
|
-
docMerger.appendText(sHandle, True, mLabel)
|
1445
|
-
|
1446
|
-
if not docMerger.writeTargetDoc():
|
1447
|
-
SHARED.error(
|
1448
|
-
self.tr("Could not write document content."),
|
1449
|
-
info=docMerger.getError()
|
993
|
+
if node := self._getNode(index):
|
994
|
+
if node.item.isFileType():
|
995
|
+
self.projView.openDocumentRequest.emit(
|
996
|
+
node.item.itemHandle, nwDocMode.EDIT, "", True
|
1450
997
|
)
|
1451
|
-
return False
|
1452
|
-
|
1453
|
-
SHARED.project.index.reIndexHandle(mHandle)
|
1454
|
-
if newFile:
|
1455
|
-
self.revealNewTreeItem(mHandle, nHandle=tHandle, wordCount=True)
|
1456
|
-
|
1457
|
-
self.projView.openDocumentRequest.emit(mHandle, nwDocMode.EDIT, "", False)
|
1458
|
-
self.projView.setSelectedHandle(mHandle, doScroll=True)
|
1459
|
-
|
1460
|
-
if data.get("moveToTrash", False):
|
1461
|
-
for sHandle in reversed(data.get("finalItems", [])):
|
1462
|
-
trItem = self._getTreeItem(sHandle)
|
1463
|
-
if isinstance(trItem, QTreeWidgetItem) and trItem.childCount() == 0:
|
1464
|
-
self.moveItemToTrash(sHandle, askFirst=False, flush=False)
|
1465
|
-
|
1466
|
-
self._alertTreeChange(mHandle, flush=True)
|
1467
|
-
self.projView.wordCountsChanged.emit()
|
1468
|
-
|
1469
|
-
else:
|
1470
|
-
logger.info("Action cancelled by user")
|
1471
|
-
return False
|
1472
|
-
|
1473
|
-
return True
|
1474
|
-
|
1475
|
-
def _splitDocument(self, tHandle: str) -> bool:
|
1476
|
-
"""Split a document into multiple documents."""
|
1477
|
-
logger.info("Request to split items with handle '%s'", tHandle)
|
1478
|
-
|
1479
|
-
tItem = SHARED.project.tree[tHandle]
|
1480
|
-
if tItem is None:
|
1481
|
-
return False
|
1482
|
-
|
1483
|
-
if not tItem.isFileType() or tItem.itemParent is None:
|
1484
|
-
logger.error("Only valid document items can be split")
|
1485
|
-
return False
|
1486
|
-
|
1487
|
-
data, text, status = GuiDocSplit.getData(SHARED.mainGui, tHandle)
|
1488
|
-
if status:
|
1489
|
-
headerList = data.get("headerList", [])
|
1490
|
-
intoFolder = data.get("intoFolder", False)
|
1491
|
-
docHierarchy = data.get("docHierarchy", False)
|
1492
|
-
|
1493
|
-
docSplit = DocSplitter(SHARED.project, tHandle)
|
1494
|
-
if intoFolder:
|
1495
|
-
fHandle = docSplit.newParentFolder(tItem.itemParent, tItem.itemName)
|
1496
|
-
self.revealNewTreeItem(fHandle, nHandle=tHandle)
|
1497
|
-
self._alertTreeChange(fHandle, flush=False)
|
1498
998
|
else:
|
1499
|
-
|
1500
|
-
|
1501
|
-
docSplit.splitDocument(headerList, text)
|
1502
|
-
for writeOk, dHandle, nHandle in docSplit.writeDocuments(docHierarchy):
|
1503
|
-
SHARED.project.index.reIndexHandle(dHandle)
|
1504
|
-
self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True)
|
1505
|
-
self._alertTreeChange(dHandle, flush=False)
|
1506
|
-
if not writeOk:
|
1507
|
-
SHARED.error(
|
1508
|
-
self.tr("Could not write document content."),
|
1509
|
-
info=docSplit.getError()
|
1510
|
-
)
|
1511
|
-
|
1512
|
-
if data.get("moveToTrash", False):
|
1513
|
-
self.moveItemToTrash(tHandle, askFirst=False, flush=True)
|
1514
|
-
|
1515
|
-
self.saveTreeOrder()
|
1516
|
-
|
1517
|
-
else:
|
1518
|
-
logger.info("Action cancelled by user")
|
1519
|
-
return False
|
1520
|
-
|
1521
|
-
return True
|
1522
|
-
|
1523
|
-
def _duplicateFromHandle(self, tHandle: str) -> bool:
|
1524
|
-
"""Duplicate the item hierarchy from a given item."""
|
1525
|
-
itemTree = self.getTreeFromHandle(tHandle)
|
1526
|
-
nItems = len(itemTree)
|
1527
|
-
if nItems == 0:
|
1528
|
-
return False
|
1529
|
-
elif nItems == 1:
|
1530
|
-
question = self.tr("Do you want to duplicate this document?")
|
1531
|
-
else:
|
1532
|
-
question = self.tr("Do you want to duplicate this item and all child items?")
|
1533
|
-
|
1534
|
-
if not SHARED.question(question):
|
1535
|
-
return False
|
1536
|
-
|
1537
|
-
docDup = DocDuplicator(SHARED.project)
|
1538
|
-
dupCount = 0
|
1539
|
-
for dHandle, nHandle in docDup.duplicate(itemTree):
|
1540
|
-
SHARED.project.index.reIndexHandle(dHandle)
|
1541
|
-
self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True)
|
1542
|
-
self._alertTreeChange(dHandle, flush=False)
|
1543
|
-
dupCount += 1
|
1544
|
-
|
1545
|
-
if dupCount != nItems:
|
1546
|
-
SHARED.warn(self.tr("Could not duplicate all items."))
|
1547
|
-
|
1548
|
-
self.saveTreeOrder()
|
1549
|
-
|
1550
|
-
return True
|
1551
|
-
|
1552
|
-
def _scanChildren(self, itemList: list, tItem: QTreeWidgetItem, tIndex: int) -> list[str]:
|
1553
|
-
"""This is a recursive function returning all items in a tree
|
1554
|
-
starting at a given QTreeWidgetItem.
|
1555
|
-
"""
|
1556
|
-
tHandle = tItem.data(self.C_DATA, self.D_HANDLE)
|
1557
|
-
cCount = tItem.childCount()
|
1558
|
-
|
1559
|
-
# Update tree-related meta data
|
1560
|
-
nwItem = SHARED.project.tree[tHandle]
|
1561
|
-
if nwItem is not None:
|
1562
|
-
nwItem.setExpanded(tItem.isExpanded() and cCount > 0)
|
1563
|
-
nwItem.setOrder(tIndex)
|
1564
|
-
|
1565
|
-
itemList.append(tHandle)
|
1566
|
-
for i in range(cCount):
|
1567
|
-
self._scanChildren(itemList, tItem.child(i), i)
|
1568
|
-
|
1569
|
-
return itemList
|
1570
|
-
|
1571
|
-
def _addTreeItem(self, nwItem: NWItem | None,
|
1572
|
-
nHandle: str | None = None) -> QTreeWidgetItem | None:
|
1573
|
-
"""Create a QTreeWidgetItem from an NWItem and add it to the
|
1574
|
-
project tree. Returns the widget if the item is valid, otherwise
|
1575
|
-
a None is returned.
|
1576
|
-
"""
|
1577
|
-
if not nwItem:
|
1578
|
-
logger.error("Invalid item cannot be added to project tree")
|
1579
|
-
return None
|
1580
|
-
|
1581
|
-
tHandle = nwItem.itemHandle
|
1582
|
-
pHandle = nwItem.itemParent
|
1583
|
-
newItem = QTreeWidgetItem()
|
1584
|
-
|
1585
|
-
newItem.setText(self.C_NAME, "")
|
1586
|
-
newItem.setText(self.C_COUNT, "0")
|
1587
|
-
newItem.setText(self.C_ACTIVE, "")
|
1588
|
-
newItem.setText(self.C_STATUS, "")
|
1589
|
-
|
1590
|
-
newItem.setTextAlignment(self.C_NAME, QtAlignLeft)
|
1591
|
-
newItem.setTextAlignment(self.C_COUNT, QtAlignRight)
|
1592
|
-
newItem.setTextAlignment(self.C_ACTIVE, QtAlignLeft)
|
1593
|
-
newItem.setTextAlignment(self.C_STATUS, QtAlignLeft)
|
1594
|
-
|
1595
|
-
newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
|
1596
|
-
newItem.setData(self.C_DATA, self.D_WORDS, 0)
|
1597
|
-
|
1598
|
-
if pHandle is None and nwItem.isRootType():
|
1599
|
-
pItem = self.invisibleRootItem()
|
1600
|
-
elif pHandle and pHandle in self._treeMap:
|
1601
|
-
pItem = self._treeMap[pHandle]
|
1602
|
-
else:
|
1603
|
-
SHARED.error(self.tr(
|
1604
|
-
"There is nowhere to add item with name '{0}'."
|
1605
|
-
).format(nwItem.itemName))
|
1606
|
-
return None
|
1607
|
-
|
1608
|
-
byIndex = -1
|
1609
|
-
if nHandle is not None and nHandle in self._treeMap:
|
1610
|
-
byIndex = pItem.indexOfChild(self._treeMap[nHandle])
|
1611
|
-
if byIndex >= 0:
|
1612
|
-
pItem.insertChild(byIndex + 1, newItem)
|
1613
|
-
else:
|
1614
|
-
pItem.addChild(newItem)
|
999
|
+
self.setExpanded(index, not self.isExpanded(index))
|
1000
|
+
return
|
1615
1001
|
|
1616
|
-
|
1617
|
-
|
1618
|
-
|
1619
|
-
|
1002
|
+
@pyqtSlot(QModelIndex)
|
1003
|
+
def _onNodeCollapsed(self, index: QModelIndex) -> None:
|
1004
|
+
"""Capture a node collapse, and pass it to the model."""
|
1005
|
+
if node := self._getNode(index):
|
1006
|
+
node.setExpanded(False)
|
1007
|
+
return
|
1620
1008
|
|
1621
|
-
|
1009
|
+
@pyqtSlot(QModelIndex)
|
1010
|
+
def _onNodeExpanded(self, index: QModelIndex) -> None:
|
1011
|
+
"""Capture a node expand, and pass it to the model."""
|
1012
|
+
if node := self._getNode(index):
|
1013
|
+
node.setExpanded(True)
|
1014
|
+
return
|
1622
1015
|
|
1623
|
-
|
1624
|
-
|
1625
|
-
|
1626
|
-
"""
|
1627
|
-
trashHandle = SHARED.project.trashFolder()
|
1628
|
-
if trashHandle is None:
|
1629
|
-
return None
|
1630
|
-
|
1631
|
-
trItem = self._getTreeItem(trashHandle)
|
1632
|
-
if trItem is None:
|
1633
|
-
trItem = self._addTreeItem(SHARED.project.tree[trashHandle])
|
1634
|
-
if trItem is not None:
|
1635
|
-
trItem.setExpanded(True)
|
1636
|
-
self._alertTreeChange(trashHandle, flush=True)
|
1637
|
-
|
1638
|
-
return trItem
|
1639
|
-
|
1640
|
-
def _alertTreeChange(self, tHandle: str | None, flush: bool = False) -> None:
|
1641
|
-
"""Update information on tree change state, and emit necessary
|
1642
|
-
signals. A flush is only needed if an item is moved, created or
|
1643
|
-
deleted.
|
1644
|
-
"""
|
1645
|
-
self._timeChanged = time()
|
1646
|
-
SHARED.project.setProjectChanged(True)
|
1647
|
-
if flush:
|
1648
|
-
self.saveTreeOrder()
|
1016
|
+
##
|
1017
|
+
# Internal Functions
|
1018
|
+
##
|
1649
1019
|
|
1650
|
-
|
1651
|
-
|
1020
|
+
def _clearSelection(self) -> None:
|
1021
|
+
"""Clear the currently selected items."""
|
1022
|
+
self.clearSelection()
|
1023
|
+
self.selectionModel().clearCurrentIndex()
|
1024
|
+
return
|
1652
1025
|
|
1653
|
-
|
1654
|
-
|
1655
|
-
|
1026
|
+
def _selectedRows(self) -> list[QModelIndex]:
|
1027
|
+
"""Return all column 0 indexes."""
|
1028
|
+
return [i for i in self.selectedIndexes() if i.column() == 0]
|
1656
1029
|
|
1657
|
-
|
1030
|
+
def _getModel(self) -> ProjectModel | None:
|
1031
|
+
"""Return a project node corresponding to a model index."""
|
1032
|
+
if isinstance(model := self.model(), ProjectModel):
|
1033
|
+
return model
|
1034
|
+
return None
|
1658
1035
|
|
1659
|
-
|
1036
|
+
def _getNode(self, index: QModelIndex) -> ProjectNode | None:
|
1037
|
+
"""Return a project node corresponding to a model index."""
|
1038
|
+
if isinstance(model := self.model(), ProjectModel) and (node := model.node(index)):
|
1039
|
+
return node
|
1040
|
+
return None
|
1660
1041
|
|
1661
1042
|
|
1662
1043
|
class _UpdatableMenu(QMenu):
|
@@ -1724,18 +1105,22 @@ class _UpdatableMenu(QMenu):
|
|
1724
1105
|
|
1725
1106
|
class _TreeContextMenu(QMenu):
|
1726
1107
|
|
1727
|
-
|
1728
|
-
super().__init__(parent=projTree)
|
1729
|
-
|
1730
|
-
self.projTree = projTree
|
1731
|
-
self.projView = projTree.projView
|
1732
|
-
|
1733
|
-
self._item = nwItem
|
1734
|
-
self._handle = nwItem.itemHandle
|
1735
|
-
self._items: list[NWItem] = []
|
1108
|
+
__slots__ = ("_tree", "_view", "_node", "_item", "_model", "_handle", "_indices", "_children")
|
1736
1109
|
|
1110
|
+
def __init__(
|
1111
|
+
self, projTree: GuiProjectTree, model: ProjectModel,
|
1112
|
+
node: ProjectNode, indices: list[QModelIndex]
|
1113
|
+
) -> None:
|
1114
|
+
super().__init__(parent=projTree)
|
1115
|
+
self._tree = projTree
|
1116
|
+
self._view = projTree.projView
|
1117
|
+
self._node = node
|
1118
|
+
self._item = node.item
|
1119
|
+
self._model = model
|
1120
|
+
self._handle = node.item.itemHandle
|
1121
|
+
self._indices = indices
|
1122
|
+
self._children = node.childCount() > 0
|
1737
1123
|
logger.debug("Ready: _TreeContextMenu")
|
1738
|
-
|
1739
1124
|
return
|
1740
1125
|
|
1741
1126
|
def __del__(self) -> None: # pragma: no cover
|
@@ -1749,14 +1134,15 @@ class _TreeContextMenu(QMenu):
|
|
1749
1134
|
def buildTrashMenu(self) -> None:
|
1750
1135
|
"""Build the special menu for the Trash folder."""
|
1751
1136
|
action = self.addAction(self.tr("Empty Trash"))
|
1752
|
-
action.triggered.connect(self.
|
1137
|
+
action.triggered.connect(self._tree.emptyTrash)
|
1138
|
+
if self._children:
|
1139
|
+
self._expandCollapse()
|
1753
1140
|
return
|
1754
1141
|
|
1755
|
-
def buildSingleSelectMenu(self
|
1142
|
+
def buildSingleSelectMenu(self) -> None:
|
1756
1143
|
"""Build the single-select menu."""
|
1757
1144
|
isFile = self._item.isFileType()
|
1758
1145
|
isFolder = self._item.isFolderType()
|
1759
|
-
isRoot = self._item.isRootType()
|
1760
1146
|
|
1761
1147
|
# Document Actions
|
1762
1148
|
if isFile:
|
@@ -1769,33 +1155,32 @@ class _TreeContextMenu(QMenu):
|
|
1769
1155
|
|
1770
1156
|
# Edit Item Settings
|
1771
1157
|
action = self.addAction(self.tr("Rename"))
|
1772
|
-
action.triggered.connect(
|
1158
|
+
action.triggered.connect(qtLambda(self._view.renameTreeItem, self._handle))
|
1773
1159
|
if isFile:
|
1774
1160
|
self._itemHeader()
|
1775
|
-
self._itemActive(
|
1161
|
+
self._itemActive()
|
1776
1162
|
self._itemStatusImport(False)
|
1777
1163
|
|
1778
1164
|
# Transform Item
|
1779
1165
|
if isFile or isFolder:
|
1780
|
-
self._itemTransform(isFile, isFolder
|
1166
|
+
self._itemTransform(isFile, isFolder)
|
1781
1167
|
self.addSeparator()
|
1782
1168
|
|
1783
1169
|
# Process Item
|
1784
|
-
self.
|
1170
|
+
if self._children:
|
1171
|
+
self._expandCollapse()
|
1172
|
+
action = self.addAction(self.tr("Duplicate"))
|
1173
|
+
action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle))
|
1174
|
+
self._deleteOrTrash()
|
1785
1175
|
|
1786
1176
|
return
|
1787
1177
|
|
1788
|
-
def buildMultiSelectMenu(self
|
1178
|
+
def buildMultiSelectMenu(self) -> None:
|
1789
1179
|
"""Build the multi-select menu."""
|
1790
|
-
self.
|
1791
|
-
for tHandle in handles:
|
1792
|
-
if (tItem := SHARED.project.tree[tHandle]):
|
1793
|
-
self._items.append(tItem)
|
1794
|
-
|
1795
|
-
self._itemActive(True)
|
1180
|
+
self._itemActive()
|
1796
1181
|
self._itemStatusImport(True)
|
1797
1182
|
self.addSeparator()
|
1798
|
-
self.
|
1183
|
+
self._deleteOrTrash()
|
1799
1184
|
return
|
1800
1185
|
|
1801
1186
|
##
|
@@ -1805,23 +1190,25 @@ class _TreeContextMenu(QMenu):
|
|
1805
1190
|
def _docActions(self) -> None:
|
1806
1191
|
"""Add document actions."""
|
1807
1192
|
action = self.addAction(self.tr("Open Document"))
|
1808
|
-
action.triggered.connect(
|
1809
|
-
|
1810
|
-
|
1193
|
+
action.triggered.connect(qtLambda(
|
1194
|
+
self._view.openDocumentRequest.emit,
|
1195
|
+
self._handle, nwDocMode.EDIT, "", True
|
1196
|
+
))
|
1811
1197
|
action = self.addAction(self.tr("View Document"))
|
1812
|
-
action.triggered.connect(
|
1813
|
-
|
1814
|
-
|
1198
|
+
action.triggered.connect(qtLambda(
|
1199
|
+
self._view.openDocumentRequest.emit,
|
1200
|
+
self._handle, nwDocMode.VIEW, "", False
|
1201
|
+
))
|
1815
1202
|
return
|
1816
1203
|
|
1817
1204
|
def _itemCreation(self) -> None:
|
1818
1205
|
"""Add create item actions."""
|
1819
1206
|
menu = self.addMenu(self.tr("Create New ..."))
|
1820
|
-
menu.addAction(self.
|
1821
|
-
menu.addAction(self.
|
1822
|
-
menu.addAction(self.
|
1823
|
-
menu.addAction(self.
|
1824
|
-
menu.addAction(self.
|
1207
|
+
menu.addAction(self._view.projBar.aAddEmpty)
|
1208
|
+
menu.addAction(self._view.projBar.aAddChap)
|
1209
|
+
menu.addAction(self._view.projBar.aAddScene)
|
1210
|
+
menu.addAction(self._view.projBar.aAddNote)
|
1211
|
+
menu.addAction(self._view.projBar.aAddFolder)
|
1825
1212
|
return
|
1826
1213
|
|
1827
1214
|
def _itemHeader(self) -> None:
|
@@ -1830,18 +1217,18 @@ class _TreeContextMenu(QMenu):
|
|
1830
1217
|
if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"):
|
1831
1218
|
action = self.addAction(self.tr("Rename to Heading"))
|
1832
1219
|
action.triggered.connect(
|
1833
|
-
|
1220
|
+
qtLambda(self._view.renameTreeItem, self._handle, hItem.title)
|
1834
1221
|
)
|
1835
1222
|
return
|
1836
1223
|
|
1837
|
-
def _itemActive(self
|
1224
|
+
def _itemActive(self) -> None:
|
1838
1225
|
"""Add Active/Inactive actions."""
|
1839
|
-
if
|
1226
|
+
if len(self._indices) > 1:
|
1840
1227
|
mSub = self.addMenu(self.tr("Set Active to ..."))
|
1841
|
-
aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.
|
1842
|
-
aOne.triggered.connect(
|
1843
|
-
aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.
|
1844
|
-
aTwo.triggered.connect(
|
1228
|
+
aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self._tree.trActive)
|
1229
|
+
aOne.triggered.connect(qtLambda(self._iterItemActive, True))
|
1230
|
+
aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self._tree.trInactive)
|
1231
|
+
aTwo.triggered.connect(qtLambda(self._iterItemActive, False))
|
1845
1232
|
else:
|
1846
1233
|
action = self.addAction(self.tr("Toggle Active"))
|
1847
1234
|
action.triggered.connect(self._toggleItemActive)
|
@@ -1852,46 +1239,45 @@ class _TreeContextMenu(QMenu):
|
|
1852
1239
|
if self._item.isNovelLike():
|
1853
1240
|
menu = self.addMenu(self.tr("Set Status to ..."))
|
1854
1241
|
current = self._item.itemStatus
|
1855
|
-
for
|
1242
|
+
for key, entry in SHARED.project.data.itemStatus.iterItems():
|
1856
1243
|
name = entry.name
|
1857
1244
|
if not multi and current == key:
|
1858
1245
|
name += f" ({nwUnicode.U_CHECK})"
|
1859
1246
|
action = menu.addAction(entry.icon, name)
|
1860
1247
|
if multi:
|
1861
|
-
action.triggered.connect(
|
1248
|
+
action.triggered.connect(qtLambda(self._iterSetItemStatus, key))
|
1862
1249
|
else:
|
1863
|
-
action.triggered.connect(
|
1250
|
+
action.triggered.connect(qtLambda(self._changeItemStatus, key))
|
1864
1251
|
menu.addSeparator()
|
1865
1252
|
action = menu.addAction(self.tr("Manage Labels ..."))
|
1866
|
-
action.triggered.connect(
|
1867
|
-
|
1868
|
-
|
1253
|
+
action.triggered.connect(qtLambda(
|
1254
|
+
self._view.projectSettingsRequest.emit,
|
1255
|
+
GuiProjectSettings.PAGE_STATUS
|
1256
|
+
))
|
1869
1257
|
else:
|
1870
1258
|
menu = self.addMenu(self.tr("Set Importance to ..."))
|
1871
1259
|
current = self._item.itemImport
|
1872
|
-
for
|
1260
|
+
for key, entry in SHARED.project.data.itemImport.iterItems():
|
1873
1261
|
name = entry.name
|
1874
1262
|
if not multi and current == key:
|
1875
1263
|
name += f" ({nwUnicode.U_CHECK})"
|
1876
1264
|
action = menu.addAction(entry.icon, name)
|
1877
1265
|
if multi:
|
1878
|
-
action.triggered.connect(
|
1266
|
+
action.triggered.connect(qtLambda(self._iterSetItemImport, key))
|
1879
1267
|
else:
|
1880
|
-
action.triggered.connect(
|
1268
|
+
action.triggered.connect(qtLambda(self._changeItemImport, key))
|
1881
1269
|
menu.addSeparator()
|
1882
1270
|
action = menu.addAction(self.tr("Manage Labels ..."))
|
1883
|
-
action.triggered.connect(
|
1884
|
-
|
1885
|
-
|
1271
|
+
action.triggered.connect(qtLambda(
|
1272
|
+
self._view.projectSettingsRequest.emit,
|
1273
|
+
GuiProjectSettings.PAGE_IMPORT
|
1274
|
+
))
|
1886
1275
|
return
|
1887
1276
|
|
1888
|
-
def _itemTransform(self, isFile: bool, isFolder: bool
|
1277
|
+
def _itemTransform(self, isFile: bool, isFolder: bool) -> None:
|
1889
1278
|
"""Add actions for the Transform menu."""
|
1890
1279
|
menu = self.addMenu(self.tr("Transform ..."))
|
1891
1280
|
|
1892
|
-
tree = self.projTree
|
1893
|
-
tHandle = self._handle
|
1894
|
-
|
1895
1281
|
trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
|
1896
1282
|
trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
|
1897
1283
|
loDoc = nwItemLayout.DOCUMENT
|
@@ -1901,159 +1287,128 @@ class _TreeContextMenu(QMenu):
|
|
1901
1287
|
|
1902
1288
|
if isNoteFile and self._item.documentAllowed():
|
1903
1289
|
action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
|
1904
|
-
action.triggered.connect(
|
1290
|
+
action.triggered.connect(qtLambda(self._changeItemLayout, loDoc))
|
1905
1291
|
|
1906
1292
|
if isDocFile:
|
1907
1293
|
action = menu.addAction(self.tr("Convert to {0}").format(trNote))
|
1908
|
-
action.triggered.connect(
|
1294
|
+
action.triggered.connect(qtLambda(self._changeItemLayout, loNote))
|
1909
1295
|
|
1910
1296
|
if isFolder and self._item.documentAllowed():
|
1911
1297
|
action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
|
1912
|
-
action.triggered.connect(
|
1298
|
+
action.triggered.connect(qtLambda(self._convertFolderToFile, loDoc))
|
1913
1299
|
|
1914
1300
|
if isFolder:
|
1915
1301
|
action = menu.addAction(self.tr("Convert to {0}").format(trNote))
|
1916
|
-
action.triggered.connect(
|
1302
|
+
action.triggered.connect(qtLambda(self._convertFolderToFile, loNote))
|
1917
1303
|
|
1918
|
-
if
|
1304
|
+
if self._children and isFile:
|
1919
1305
|
action = menu.addAction(self.tr("Merge Child Items into Self"))
|
1920
|
-
action.triggered.connect(
|
1306
|
+
action.triggered.connect(qtLambda(self._tree.mergeDocuments, self._handle, False))
|
1921
1307
|
action = menu.addAction(self.tr("Merge Child Items into New"))
|
1922
|
-
action.triggered.connect(
|
1308
|
+
action.triggered.connect(qtLambda(self._tree.mergeDocuments, self._handle, True))
|
1923
1309
|
|
1924
|
-
if
|
1310
|
+
if self._children and isFolder:
|
1925
1311
|
action = menu.addAction(self.tr("Merge Documents in Folder"))
|
1926
|
-
action.triggered.connect(
|
1312
|
+
action.triggered.connect(qtLambda(self._tree.mergeDocuments, self._handle, True))
|
1927
1313
|
|
1928
1314
|
if isFile:
|
1929
1315
|
action = menu.addAction(self.tr("Split Document by Headings"))
|
1930
|
-
action.triggered.connect(
|
1316
|
+
action.triggered.connect(qtLambda(self._tree.splitDocument, self._handle))
|
1931
1317
|
|
1932
1318
|
return
|
1933
1319
|
|
1934
|
-
def
|
1935
|
-
"""Add actions for
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
1939
|
-
|
1940
|
-
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, True))
|
1941
|
-
action = self.addAction(self.tr("Collapse All"))
|
1942
|
-
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, False))
|
1943
|
-
|
1944
|
-
action = self.addAction(self.tr("Duplicate"))
|
1945
|
-
action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
|
1946
|
-
|
1947
|
-
if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
|
1948
|
-
action = self.addAction(self.tr("Delete Permanently"))
|
1949
|
-
action.triggered.connect(lambda: tree.permDeleteItem(tHandle))
|
1950
|
-
else:
|
1951
|
-
action = self.addAction(self.tr("Move to Trash"))
|
1952
|
-
action.triggered.connect(lambda: tree.moveItemToTrash(tHandle))
|
1953
|
-
|
1320
|
+
def _expandCollapse(self) -> None:
|
1321
|
+
"""Add actions for expand and collapse."""
|
1322
|
+
action = self.addAction(self.tr("Expand All"))
|
1323
|
+
action.triggered.connect(qtLambda(self._tree.expandFromIndex, self._indices[0]))
|
1324
|
+
action = self.addAction(self.tr("Collapse All"))
|
1325
|
+
action.triggered.connect(qtLambda(self._tree.collapseFromIndex, self._indices[0]))
|
1954
1326
|
return
|
1955
1327
|
|
1956
|
-
def
|
1328
|
+
def _deleteOrTrash(self) -> None:
|
1957
1329
|
"""Add move to Trash action."""
|
1958
|
-
|
1959
|
-
|
1960
|
-
|
1961
|
-
|
1962
|
-
|
1963
|
-
|
1964
|
-
|
1330
|
+
if (
|
1331
|
+
self._model.trashSelection(self._indices)
|
1332
|
+
or len(self._indices) == 1 and self._item.isRootType()
|
1333
|
+
):
|
1334
|
+
text = self.tr("Delete Permanently")
|
1335
|
+
else:
|
1336
|
+
text = self.tr("Move to Trash")
|
1337
|
+
action = self.addAction(text)
|
1338
|
+
action.triggered.connect(self._tree.processDeleteRequest)
|
1965
1339
|
return
|
1966
1340
|
|
1967
1341
|
##
|
1968
1342
|
# Private Slots
|
1969
1343
|
##
|
1970
1344
|
|
1971
|
-
@pyqtSlot()
|
1972
|
-
def _iterMoveToTrash(self) -> None:
|
1973
|
-
"""Iterate through files and move them to Trash."""
|
1974
|
-
if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))):
|
1975
|
-
for tItem in self._items:
|
1976
|
-
if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH:
|
1977
|
-
self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False)
|
1978
|
-
self.projTree.saveTreeOrder()
|
1979
|
-
return
|
1980
|
-
|
1981
|
-
@pyqtSlot()
|
1982
|
-
def _iterPermDelete(self) -> None:
|
1983
|
-
"""Iterate through files and delete them."""
|
1984
|
-
if SHARED.question(self.projTree.trPermDelete.format(len(self._items))):
|
1985
|
-
for tItem in self._items:
|
1986
|
-
if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH:
|
1987
|
-
self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False)
|
1988
|
-
self.projTree.saveTreeOrder()
|
1989
|
-
return
|
1990
|
-
|
1991
1345
|
@pyqtSlot()
|
1992
1346
|
def _toggleItemActive(self) -> None:
|
1993
1347
|
"""Toggle the active status of an item."""
|
1994
|
-
self._item.
|
1995
|
-
|
1996
|
-
|
1348
|
+
if self._item.isFileType():
|
1349
|
+
self._item.setActive(not self._item.isActive)
|
1350
|
+
self._item.notifyToRefresh()
|
1997
1351
|
return
|
1998
1352
|
|
1999
1353
|
##
|
2000
1354
|
# Internal Functions
|
2001
1355
|
##
|
2002
1356
|
|
2003
|
-
def _iterItemActive(self,
|
1357
|
+
def _iterItemActive(self, state: bool) -> None:
|
2004
1358
|
"""Set the active status of multiple items."""
|
2005
|
-
|
2006
|
-
|
2007
|
-
|
2008
|
-
|
2009
|
-
|
1359
|
+
refresh = []
|
1360
|
+
for node in self._model.nodes(self._indices):
|
1361
|
+
if node.item.isFileType():
|
1362
|
+
node.item.setActive(state)
|
1363
|
+
refresh.append(node.item.itemHandle)
|
1364
|
+
SHARED.project.tree.refreshItems(refresh)
|
2010
1365
|
return
|
2011
1366
|
|
2012
1367
|
def _changeItemStatus(self, key: str) -> None:
|
2013
1368
|
"""Set a new status value of an item."""
|
2014
|
-
self._item.
|
2015
|
-
|
2016
|
-
|
1369
|
+
if self._item.isFileType():
|
1370
|
+
self._item.setStatus(key)
|
1371
|
+
self._item.notifyToRefresh()
|
2017
1372
|
return
|
2018
1373
|
|
2019
1374
|
def _iterSetItemStatus(self, key: str) -> None:
|
2020
1375
|
"""Change the status value for multiple items."""
|
2021
|
-
|
2022
|
-
|
2023
|
-
|
2024
|
-
|
2025
|
-
|
1376
|
+
refresh = []
|
1377
|
+
for node in self._model.nodes(self._indices):
|
1378
|
+
if node.item.isNovelLike():
|
1379
|
+
node.item.setStatus(key)
|
1380
|
+
refresh.append(node.item.itemHandle)
|
1381
|
+
SHARED.project.tree.refreshItems(refresh)
|
2026
1382
|
return
|
2027
1383
|
|
2028
1384
|
def _changeItemImport(self, key: str) -> None:
|
2029
1385
|
"""Set a new importance value of an item."""
|
2030
|
-
self._item.
|
2031
|
-
|
2032
|
-
|
1386
|
+
if self._item.isFileType():
|
1387
|
+
self._item.setImport(key)
|
1388
|
+
self._item.notifyToRefresh()
|
2033
1389
|
return
|
2034
1390
|
|
2035
1391
|
def _iterSetItemImport(self, key: str) -> None:
|
2036
1392
|
"""Change the status value for multiple items."""
|
2037
|
-
|
2038
|
-
|
2039
|
-
|
2040
|
-
|
2041
|
-
|
1393
|
+
refresh = []
|
1394
|
+
for node in self._model.nodes(self._indices):
|
1395
|
+
if not node.item.isNovelLike():
|
1396
|
+
node.item.setImport(key)
|
1397
|
+
refresh.append(node.item.itemHandle)
|
1398
|
+
SHARED.project.tree.refreshItems(refresh)
|
2042
1399
|
return
|
2043
1400
|
|
2044
1401
|
def _changeItemLayout(self, itemLayout: nwItemLayout) -> None:
|
2045
1402
|
"""Set a new item layout value of an item."""
|
2046
1403
|
if itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
|
2047
1404
|
self._item.setLayout(nwItemLayout.DOCUMENT)
|
2048
|
-
self.
|
2049
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1405
|
+
self._item.notifyToRefresh()
|
2050
1406
|
elif itemLayout == nwItemLayout.NOTE:
|
2051
1407
|
self._item.setLayout(nwItemLayout.NOTE)
|
2052
|
-
self.
|
2053
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1408
|
+
self._item.notifyToRefresh()
|
2054
1409
|
return
|
2055
1410
|
|
2056
|
-
def
|
1411
|
+
def _convertFolderToFile(self, itemLayout: nwItemLayout) -> None:
|
2057
1412
|
"""Convert a folder to a note or document."""
|
2058
1413
|
if self._item.isFolderType():
|
2059
1414
|
msgYes = SHARED.question(self.tr(
|
@@ -2063,13 +1418,11 @@ class _TreeContextMenu(QMenu):
|
|
2063
1418
|
if msgYes and itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
|
2064
1419
|
self._item.setType(nwItemType.FILE)
|
2065
1420
|
self._item.setLayout(nwItemLayout.DOCUMENT)
|
2066
|
-
self.
|
2067
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1421
|
+
self._item.notifyToRefresh()
|
2068
1422
|
elif msgYes and itemLayout == nwItemLayout.NOTE:
|
2069
1423
|
self._item.setType(nwItemType.FILE)
|
2070
1424
|
self._item.setLayout(nwItemLayout.NOTE)
|
2071
|
-
self.
|
2072
|
-
self.projTree._alertTreeChange(self._handle, flush=False)
|
1425
|
+
self._item.notifyToRefresh()
|
2073
1426
|
else:
|
2074
1427
|
logger.info("Folder conversion cancelled")
|
2075
1428
|
return
|