novelWriter 2.6b1__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.6b1.dist-info → novelWriter-2.6b2.dist-info}/METADATA +3 -3
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/RECORD +68 -52
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +49 -10
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
- novelwriter/assets/i18n/project_de_DE.json +2 -2
- novelwriter/assets/i18n/project_ru_RU.json +11 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +7 -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 +7 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/text/credits_en.htm +1 -0
- novelwriter/common.py +37 -2
- novelwriter/config.py +15 -12
- novelwriter/constants.py +24 -9
- novelwriter/core/coretools.py +111 -125
- novelwriter/core/docbuild.py +3 -2
- novelwriter/core/index.py +9 -19
- novelwriter/core/item.py +39 -6
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/project.py +67 -89
- novelwriter/core/status.py +7 -5
- novelwriter/core/tree.py +268 -287
- novelwriter/dialogs/docmerge.py +7 -17
- novelwriter/dialogs/preferences.py +3 -3
- novelwriter/dialogs/projectsettings.py +2 -2
- novelwriter/enum.py +7 -0
- novelwriter/extensions/configlayout.py +6 -4
- novelwriter/formats/todocx.py +34 -38
- novelwriter/formats/tohtml.py +14 -15
- novelwriter/formats/tokenizer.py +21 -17
- novelwriter/formats/toodt.py +53 -124
- novelwriter/formats/toqdoc.py +92 -44
- novelwriter/gui/doceditor.py +230 -219
- novelwriter/gui/docviewer.py +38 -9
- novelwriter/gui/docviewerpanel.py +14 -22
- novelwriter/gui/itemdetails.py +17 -24
- novelwriter/gui/mainmenu.py +13 -8
- novelwriter/gui/noveltree.py +12 -12
- novelwriter/gui/outline.py +10 -11
- novelwriter/gui/projtree.py +548 -1202
- novelwriter/gui/search.py +9 -10
- novelwriter/gui/theme.py +7 -3
- novelwriter/guimain.py +59 -43
- novelwriter/shared.py +52 -23
- novelwriter/text/patterns.py +17 -5
- novelwriter/tools/manusbuild.py +13 -11
- novelwriter/tools/manussettings.py +42 -52
- novelwriter/types.py +7 -1
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
novelwriter/core/tree.py
CHANGED
@@ -3,7 +3,8 @@ novelWriter – Project Tree Class
|
|
3
3
|
================================
|
4
4
|
|
5
5
|
File History:
|
6
|
-
Created:
|
6
|
+
Created: 2020-05-07 [0.4.5] NWTree
|
7
|
+
Rewritten: 2024-11-16 [2.6b2] NWTree
|
7
8
|
|
8
9
|
This file is a part of novelWriter
|
9
10
|
Copyright 2018–2024, Veronica Berglyd Olsen
|
@@ -30,10 +31,13 @@ from collections.abc import Iterable, Iterator
|
|
30
31
|
from pathlib import Path
|
31
32
|
from typing import TYPE_CHECKING, Literal, overload
|
32
33
|
|
33
|
-
from
|
34
|
-
|
34
|
+
from PyQt5.QtCore import QModelIndex
|
35
|
+
|
36
|
+
from novelwriter import SHARED
|
37
|
+
from novelwriter.constants import nwFiles, nwLabels, trConst
|
35
38
|
from novelwriter.core.item import NWItem
|
36
|
-
from novelwriter.
|
39
|
+
from novelwriter.core.itemmodel import ProjectModel, ProjectNode
|
40
|
+
from novelwriter.enum import nwChange, nwItemClass, nwItemLayout, nwItemType
|
37
41
|
from novelwriter.error import logException
|
38
42
|
|
39
43
|
if TYPE_CHECKING: # pragma: no cover
|
@@ -41,7 +45,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|
41
45
|
|
42
46
|
logger = logging.getLogger(__name__)
|
43
47
|
|
44
|
-
MAX_DEPTH =
|
48
|
+
MAX_DEPTH = 999 # Cap of tree traversing for loops (recursion limit)
|
45
49
|
|
46
50
|
|
47
51
|
class NWTree:
|
@@ -51,30 +55,51 @@ class NWTree:
|
|
51
55
|
This class holds all the project items of the project as instances
|
52
56
|
of NWItem.
|
53
57
|
|
54
|
-
For historical reasons, the order of the items is saved in a
|
55
|
-
separate list from the items themselves, which are stored in a
|
56
|
-
dictionary. This is somewhat redundant with the newer versions of
|
57
|
-
Python, but is still practical as it's easier to update the item
|
58
|
-
order as a list.
|
59
|
-
|
60
58
|
Each item has a handle, which is a random hex string of length 13.
|
61
59
|
The handle is the name of the item everywhere in novelWriter, and is
|
62
60
|
also used for file names.
|
63
61
|
"""
|
64
62
|
|
65
|
-
__slots__ = ("_project", "
|
63
|
+
__slots__ = ("_project", "_model", "_items", "_nodes", "_trash")
|
66
64
|
|
67
65
|
def __init__(self, project: NWProject) -> None:
|
68
|
-
|
69
66
|
self._project = project
|
67
|
+
self._model = ProjectModel(self)
|
68
|
+
self._items: dict[str, NWItem] = {}
|
69
|
+
self._nodes: dict[str, ProjectNode] = {}
|
70
|
+
self._trash = None
|
71
|
+
logger.debug("Ready: NWTree")
|
72
|
+
return
|
70
73
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
+
def __del__(self) -> None: # pragma: no cover
|
75
|
+
logger.debug("Delete: NWTree")
|
76
|
+
return
|
77
|
+
|
78
|
+
def __len__(self) -> int:
|
79
|
+
"""The number of items in the project."""
|
80
|
+
return len(self._items)
|
74
81
|
|
75
|
-
|
76
|
-
|
82
|
+
def __bool__(self) -> bool:
|
83
|
+
"""True if there are any items in the project."""
|
84
|
+
return bool(self._items)
|
77
85
|
|
86
|
+
def __getitem__(self, tHandle: str | None) -> NWItem | None:
|
87
|
+
"""Return a project item based on its handle. Returns None if
|
88
|
+
the handle doesn't exist in the project.
|
89
|
+
"""
|
90
|
+
if tHandle and tHandle in self._items:
|
91
|
+
return self._items[tHandle]
|
92
|
+
logger.error("No tree item with handle '%s'", str(tHandle))
|
93
|
+
return None
|
94
|
+
|
95
|
+
def __contains__(self, tHandle: str) -> bool:
|
96
|
+
"""Checks if a handle exists in the tree."""
|
97
|
+
return tHandle in self._items
|
98
|
+
|
99
|
+
def __iter__(self) -> Iterator[NWItem]:
|
100
|
+
"""Iterate through project items."""
|
101
|
+
for node in self._model.root.allChildren():
|
102
|
+
yield node.item
|
78
103
|
return
|
79
104
|
|
80
105
|
##
|
@@ -82,9 +107,19 @@ class NWTree:
|
|
82
107
|
##
|
83
108
|
|
84
109
|
@property
|
85
|
-
def
|
86
|
-
"""Return
|
87
|
-
|
110
|
+
def trash(self) -> ProjectNode | None:
|
111
|
+
"""Return trash node, if it exists."""
|
112
|
+
if self._trash:
|
113
|
+
return self._trash
|
114
|
+
return self._getTrashNode()
|
115
|
+
|
116
|
+
@property
|
117
|
+
def model(self) -> ProjectModel:
|
118
|
+
return self._model
|
119
|
+
|
120
|
+
@property
|
121
|
+
def nodes(self) -> dict[str, ProjectNode]:
|
122
|
+
return self._nodes
|
88
123
|
|
89
124
|
##
|
90
125
|
# Class Methods
|
@@ -92,83 +127,91 @@ class NWTree:
|
|
92
127
|
|
93
128
|
def clear(self) -> None:
|
94
129
|
"""Clear the item tree entirely."""
|
95
|
-
self.
|
96
|
-
|
97
|
-
self.
|
98
|
-
self.
|
99
|
-
self.
|
130
|
+
oldModel = self._model
|
131
|
+
oldModel.clear()
|
132
|
+
self._model = ProjectModel(self)
|
133
|
+
self._items.clear()
|
134
|
+
self._nodes.clear()
|
135
|
+
self._trash = None
|
136
|
+
oldModel.deleteLater()
|
137
|
+
del oldModel
|
100
138
|
return
|
101
139
|
|
102
|
-
def
|
103
|
-
"""
|
104
|
-
|
140
|
+
def add(self, item: NWItem, pos: int = -1) -> bool:
|
141
|
+
"""Add a project item into the project tree."""
|
142
|
+
if pHandle := item.itemParent:
|
143
|
+
if parent := self._nodes.get(pHandle):
|
144
|
+
node = ProjectNode(item)
|
145
|
+
index = self._model.indexFromNode(parent)
|
146
|
+
self._model.insertChild(node, index, pos)
|
147
|
+
self._nodes[item.itemHandle] = node
|
148
|
+
self._items[item.itemHandle] = item
|
149
|
+
self._itemChange(item, nwChange.CREATE)
|
150
|
+
else:
|
151
|
+
logger.error("Could not locate parent of '%s'", item.itemHandle)
|
152
|
+
return False
|
153
|
+
elif item.isRootType():
|
154
|
+
node = ProjectNode(item)
|
155
|
+
self._model.insertChild(node, QModelIndex(), pos)
|
156
|
+
self._nodes[item.itemHandle] = node
|
157
|
+
self._items[item.itemHandle] = item
|
158
|
+
self._itemChange(item, nwChange.CREATE)
|
159
|
+
else:
|
160
|
+
logger.error("Invalid project item '%s'", item.itemHandle)
|
161
|
+
return False
|
162
|
+
return True
|
163
|
+
|
164
|
+
def remove(self, tHandle: str) -> bool:
|
165
|
+
"""Remove an item from the project tree."""
|
166
|
+
if (node := self._nodes.get(tHandle)) and tHandle in self._items:
|
167
|
+
index = self._model.indexFromNode(node)
|
168
|
+
if index.isValid() and self._model.removeChild(index.parent(), index.row()):
|
169
|
+
self._itemChange(node.item, nwChange.DELETE)
|
170
|
+
del self._nodes[tHandle]
|
171
|
+
del self._items[tHandle]
|
172
|
+
return True
|
173
|
+
return False
|
105
174
|
|
106
175
|
@overload # pragma: no cover
|
107
|
-
def create(
|
108
|
-
|
176
|
+
def create(
|
177
|
+
self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
|
178
|
+
itemClass: nwItemClass, pos: int = -1
|
179
|
+
) -> str:
|
109
180
|
pass
|
110
181
|
|
111
182
|
@overload # pragma: no cover
|
112
|
-
def create(
|
113
|
-
|
183
|
+
def create(
|
184
|
+
self, label: str, parent: str | None, itemType: nwItemType,
|
185
|
+
itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1
|
186
|
+
) -> str | None:
|
114
187
|
pass
|
115
188
|
|
116
|
-
def create(
|
189
|
+
def create(
|
190
|
+
self, label: str, parent: str | None, itemType: nwItemType,
|
191
|
+
itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1,
|
192
|
+
) -> str | None:
|
117
193
|
"""Create a new item in the project tree, and return its handle.
|
118
194
|
If the item cannot be added to the project because of an invalid
|
119
195
|
parent, None is returned. For root elements, this cannot occur.
|
120
196
|
"""
|
121
197
|
parent = None if itemType == nwItemType.ROOT else parent
|
122
|
-
if parent is None or parent in self.
|
198
|
+
if parent is None or parent in self._nodes:
|
123
199
|
tHandle = self._makeHandle()
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
self.
|
130
|
-
|
131
|
-
return tHandle
|
200
|
+
nwItem = NWItem(self._project, tHandle)
|
201
|
+
nwItem.setName(label)
|
202
|
+
nwItem.setParent(parent)
|
203
|
+
nwItem.setType(itemType)
|
204
|
+
nwItem.setClass(itemClass)
|
205
|
+
if self.add(nwItem, pos):
|
206
|
+
return tHandle
|
132
207
|
return None
|
133
208
|
|
134
|
-
def
|
135
|
-
"""Add a new item to the end of the tree."""
|
136
|
-
tHandle = nwItem.itemHandle
|
137
|
-
pHandle = nwItem.itemParent
|
138
|
-
|
139
|
-
if not isHandle(tHandle):
|
140
|
-
logger.warning("Invalid item handle '%s' detected, skipping", tHandle)
|
141
|
-
return False
|
142
|
-
|
143
|
-
if tHandle in self._tree:
|
144
|
-
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
|
145
|
-
return False
|
146
|
-
|
147
|
-
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
148
|
-
|
149
|
-
if nwItem.isRootType():
|
150
|
-
logger.debug("Item '%s' is a root item", str(tHandle))
|
151
|
-
self._roots[tHandle] = nwItem
|
152
|
-
if nwItem.itemClass == nwItemClass.TRASH:
|
153
|
-
if self._trash is None:
|
154
|
-
logger.debug("Item '%s' is the trash folder", str(tHandle))
|
155
|
-
self._trash = tHandle
|
156
|
-
else:
|
157
|
-
logger.error("Only one trash folder allowed")
|
158
|
-
return False
|
159
|
-
|
160
|
-
self._tree[tHandle] = nwItem
|
161
|
-
self._order.append(tHandle)
|
162
|
-
self._setTreeChanged(True)
|
163
|
-
|
164
|
-
return True
|
165
|
-
|
166
|
-
def duplicate(self, sHandle: str) -> NWItem | None:
|
209
|
+
def duplicate(self, sHandle: str, pHandle: str | None, putAfter: bool) -> NWItem | None:
|
167
210
|
"""Duplicate an item and set a new handle."""
|
168
|
-
|
169
|
-
|
170
|
-
nItem
|
171
|
-
if self.
|
211
|
+
if sNode := self._nodes.get(sHandle):
|
212
|
+
nItem = NWItem.duplicate(sNode.item, self._makeHandle())
|
213
|
+
nItem.setParent(pHandle)
|
214
|
+
if self.add(nItem, (sNode.row() + 1) if putAfter else -1):
|
172
215
|
logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle)
|
173
216
|
return nItem
|
174
217
|
return None
|
@@ -177,23 +220,62 @@ class NWTree:
|
|
177
220
|
"""Pack the content of the tree into a list of dictionaries of
|
178
221
|
items. In the order defined by the _treeOrder list.
|
179
222
|
"""
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
223
|
+
nodes = self._model.root.allChildren()
|
224
|
+
if len(nodes) != len(self._nodes):
|
225
|
+
logger.warning(
|
226
|
+
"Model tree is inconsitent with nodes map, %d != %d",
|
227
|
+
len(nodes), len(self._nodes)
|
228
|
+
)
|
229
|
+
return [node.item.pack() for node in nodes]
|
186
230
|
|
187
231
|
def unpack(self, data: list[dict]) -> None:
|
188
232
|
"""Iterate through all items of a list and add them to the
|
189
233
|
project tree.
|
190
234
|
"""
|
191
235
|
self.clear()
|
236
|
+
items: dict[str, NWItem] = self._items.copy()
|
192
237
|
for item in data:
|
193
|
-
nwItem = NWItem(self._project, "")
|
238
|
+
nwItem = NWItem(self._project, "")
|
194
239
|
if nwItem.unpack(item):
|
195
|
-
|
196
|
-
|
240
|
+
items[nwItem.itemHandle] = nwItem
|
241
|
+
|
242
|
+
later = items
|
243
|
+
self._model.beginInsertRows(self._model.index(0, 0), 0, 0)
|
244
|
+
for _ in range(MAX_DEPTH):
|
245
|
+
later = self._addItems(later)
|
246
|
+
if len(later) == 0:
|
247
|
+
break
|
248
|
+
else:
|
249
|
+
logger.error("Not all items could be added to project tree")
|
250
|
+
|
251
|
+
self._trash = self._getTrashNode()
|
252
|
+
self._model.endInsertRows()
|
253
|
+
self._model.layoutChanged.emit()
|
254
|
+
|
255
|
+
return
|
256
|
+
|
257
|
+
def refreshItems(self, items: list[str]) -> None:
|
258
|
+
"""Refresh these items on the GUI. If they are an ordered range,
|
259
|
+
also set the isRange flag to True.
|
260
|
+
"""
|
261
|
+
for tHandle in items:
|
262
|
+
if node := self._nodes.get(tHandle):
|
263
|
+
node.refresh()
|
264
|
+
node.updateCount()
|
265
|
+
indexS = self._model.indexFromNode(node, 0)
|
266
|
+
indexE = self._model.indexFromNode(node, 3)
|
267
|
+
self._model.dataChanged.emit(indexS, indexE)
|
268
|
+
self._itemChange(node.item, nwChange.UPDATE)
|
269
|
+
return
|
270
|
+
|
271
|
+
def refreshAllItems(self) -> None:
|
272
|
+
"""Refresh all items in the tree."""
|
273
|
+
for node in reversed(self._model.root.allChildren()):
|
274
|
+
node.refresh()
|
275
|
+
node.updateCount(propagate=False)
|
276
|
+
self._model.root.refresh()
|
277
|
+
self._model.root.updateCount(propagate=False)
|
278
|
+
self._model.layoutChanged.emit()
|
197
279
|
return
|
198
280
|
|
199
281
|
def checkConsistency(self, prefix: str) -> tuple[int, int]:
|
@@ -205,29 +287,21 @@ class NWTree:
|
|
205
287
|
mark recovered files.
|
206
288
|
"""
|
207
289
|
storage = self._project.storage
|
208
|
-
|
209
|
-
|
210
|
-
if self.updateItemData(tHandle):
|
211
|
-
logger.debug("Checking item '%s' ... OK", tHandle)
|
212
|
-
files.discard(tHandle) # Remove it from the record
|
213
|
-
else:
|
214
|
-
logger.error("Checking item '%s' ... ERROR", tHandle)
|
215
|
-
self.__delitem__(tHandle) # The file will be re-added as orphaned
|
216
|
-
|
217
|
-
orphans = len(files)
|
290
|
+
remains = set(storage.scanContent()).difference(set(self._nodes.keys()))
|
291
|
+
orphans = len(remains)
|
218
292
|
if orphans == 0:
|
219
293
|
logger.info("Checked project files: OK")
|
220
294
|
return 0, 0
|
221
295
|
|
222
296
|
logger.warning("Found %d file(s) not tracked in project", orphans)
|
223
297
|
recovered = 0
|
224
|
-
for cHandle in
|
298
|
+
for cHandle in remains:
|
225
299
|
aDoc = storage.getDocument(cHandle)
|
226
300
|
aDoc.readDocument(isOrphan=True)
|
227
301
|
oName, oParent, oClass, oLayout = aDoc.getMeta()
|
228
302
|
|
229
303
|
oName = oName or cHandle
|
230
|
-
oParent = oParent if oParent in self.
|
304
|
+
oParent = oParent if oParent in self._nodes else None
|
231
305
|
oClass = oClass or nwItemClass.NOVEL
|
232
306
|
oLayout = oLayout or nwItemLayout.NOTE
|
233
307
|
|
@@ -248,8 +322,7 @@ class NWTree:
|
|
248
322
|
newItem.setType(nwItemType.FILE)
|
249
323
|
newItem.setClass(oClass)
|
250
324
|
newItem.setLayout(oLayout)
|
251
|
-
if self.
|
252
|
-
self.updateItemData(cHandle)
|
325
|
+
if self.add(newItem):
|
253
326
|
recovered += 1
|
254
327
|
|
255
328
|
return orphans, recovered
|
@@ -263,38 +336,33 @@ class NWTree:
|
|
263
336
|
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
|
264
337
|
return False
|
265
338
|
|
266
|
-
|
267
|
-
|
268
|
-
for
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
tFile = tHandle+".nwd"
|
274
|
-
if (contentPath / tFile).is_file():
|
339
|
+
entries = []
|
340
|
+
maxLen = 0
|
341
|
+
for node in self._model.root.allChildren():
|
342
|
+
item = node.item
|
343
|
+
file = f"{item.itemHandle}.nwd"
|
344
|
+
if (contentPath / file).is_file():
|
275
345
|
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
346
|
+
f"content/{file}",
|
347
|
+
item.itemClass.name,
|
348
|
+
item.itemLayout.name,
|
349
|
+
item.itemName,
|
280
350
|
)
|
281
|
-
|
282
|
-
|
351
|
+
entries.append(tocLine)
|
352
|
+
maxLen = max(maxLen, len(tocLine))
|
283
353
|
|
284
354
|
try:
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
outFile.write("\n")
|
292
|
-
outFile.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format(
|
355
|
+
with open(runtimePath / nwFiles.TOC_TXT, mode="w", encoding="utf-8") as toc:
|
356
|
+
toc.write("\n")
|
357
|
+
toc.write("Table of Contents\n")
|
358
|
+
toc.write("=================\n")
|
359
|
+
toc.write("\n")
|
360
|
+
toc.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format(
|
293
361
|
"File Name", "Class", "Layout", "Document Label"
|
294
362
|
))
|
295
|
-
|
296
|
-
|
297
|
-
|
363
|
+
toc.write("-"*max(maxLen, 62) + "\n")
|
364
|
+
toc.write("\n".join(entries))
|
365
|
+
toc.write("\n")
|
298
366
|
|
299
367
|
except Exception:
|
300
368
|
logger.error("Could not write ToC file")
|
@@ -307,74 +375,46 @@ class NWTree:
|
|
307
375
|
"""Loop over all entries and add up the word counts."""
|
308
376
|
noteWords = 0
|
309
377
|
novelWords = 0
|
310
|
-
for
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
pass
|
316
|
-
elif tItem.itemLayout == nwItemLayout.NOTE:
|
317
|
-
noteWords += tItem.wordCount
|
318
|
-
else:
|
319
|
-
novelWords += tItem.wordCount
|
378
|
+
for item in self._items.values():
|
379
|
+
if item.itemLayout == nwItemLayout.NOTE:
|
380
|
+
noteWords += item.wordCount
|
381
|
+
elif item.itemLayout == nwItemLayout.DOCUMENT:
|
382
|
+
novelWords += item.wordCount
|
320
383
|
return novelWords, noteWords
|
321
384
|
|
322
385
|
##
|
323
386
|
# Tree Item Methods
|
324
387
|
##
|
325
388
|
|
326
|
-
def updateItemData(self, tHandle: str) -> bool:
|
327
|
-
"""Update the root item handle of a given item. Returns True if
|
328
|
-
a root was found and data updated, otherwise False.
|
329
|
-
"""
|
330
|
-
tItem = self.__getitem__(tHandle)
|
331
|
-
if tItem is None:
|
332
|
-
return False
|
333
|
-
|
334
|
-
iItem = tItem
|
335
|
-
for _ in range(MAX_DEPTH):
|
336
|
-
if iItem.itemParent is None:
|
337
|
-
tItem.setRoot(iItem.itemHandle)
|
338
|
-
tItem.setClassDefaults(iItem.itemClass)
|
339
|
-
return True
|
340
|
-
else:
|
341
|
-
iItem = self.__getitem__(iItem.itemParent)
|
342
|
-
if iItem is None:
|
343
|
-
return False
|
344
|
-
else:
|
345
|
-
raise RecursionError("Critical internal error")
|
346
|
-
|
347
389
|
def checkType(self, tHandle: str, itemType: nwItemType) -> bool:
|
348
390
|
"""Check if item exists and is of the specified item type."""
|
349
|
-
tItem
|
350
|
-
|
351
|
-
|
352
|
-
return tItem.itemType == itemType
|
391
|
+
if tItem := self.__getitem__(tHandle):
|
392
|
+
return tItem.itemType == itemType
|
393
|
+
return False
|
353
394
|
|
354
|
-
def
|
395
|
+
def itemPath(self, tHandle: str, asName: bool = False) -> list[str]:
|
355
396
|
"""Iterate upwards in the tree until we find the item with
|
356
397
|
parent None, the root item, and return the list of handles, or
|
357
398
|
alternatively item names. We do this with a for loop with a
|
358
399
|
maximum depth to make infinite loops impossible.
|
359
400
|
"""
|
360
|
-
|
361
|
-
|
362
|
-
if tItem is not None:
|
363
|
-
tTree.append(tItem.itemName if asName else tHandle)
|
401
|
+
path = []
|
402
|
+
if node := self._nodes.get(tHandle):
|
364
403
|
for _ in range(MAX_DEPTH):
|
365
|
-
if
|
366
|
-
|
404
|
+
if parent := node.parent():
|
405
|
+
path.append(node.item.itemName if asName else tHandle)
|
406
|
+
node = parent
|
367
407
|
else:
|
368
|
-
|
369
|
-
tItem = self.__getitem__(tHandle)
|
370
|
-
if tItem is None:
|
371
|
-
return tTree
|
372
|
-
else:
|
373
|
-
tTree.append(tItem.itemName if asName else tHandle)
|
408
|
+
return path
|
374
409
|
else:
|
375
|
-
|
410
|
+
logger.error("Max project tree depth reached")
|
411
|
+
return path
|
376
412
|
|
377
|
-
|
413
|
+
def subTree(self, tHandle: str) -> list[str]:
|
414
|
+
"""Get the subtree from a given handle."""
|
415
|
+
if node := self._nodes.get(tHandle):
|
416
|
+
return [child.item.itemHandle for child in node.allChildren()]
|
417
|
+
return []
|
378
418
|
|
379
419
|
##
|
380
420
|
# Tree Root Methods
|
@@ -383,131 +423,72 @@ class NWTree:
|
|
383
423
|
def rootClasses(self) -> set[nwItemClass]:
|
384
424
|
"""Return a set of all root classes in use by the project."""
|
385
425
|
rootClasses = set()
|
386
|
-
for
|
387
|
-
rootClasses.add(
|
426
|
+
for node in self._model.root.children:
|
427
|
+
rootClasses.add(node.item.itemClass)
|
388
428
|
return rootClasses
|
389
429
|
|
390
430
|
def iterRoots(self, itemClass: nwItemClass | None) -> Iterable[tuple[str, NWItem]]:
|
391
431
|
"""Iterate over all root items of a given class in order."""
|
392
|
-
for
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
yield tHandle, nwItem
|
432
|
+
for node in self._model.root.children:
|
433
|
+
if node.item.isRootType():
|
434
|
+
if itemClass is None or node.item.itemClass == itemClass:
|
435
|
+
yield node.item.itemHandle, node.item
|
397
436
|
return
|
398
437
|
|
399
|
-
def isTrash(self, tHandle: str) -> bool:
|
400
|
-
"""Check if an item is in or is the trash folder."""
|
401
|
-
tItem = self.__getitem__(tHandle)
|
402
|
-
if tItem is None:
|
403
|
-
return True
|
404
|
-
if tItem.itemClass == nwItemClass.TRASH:
|
405
|
-
return True
|
406
|
-
if self._trash is not None:
|
407
|
-
if tHandle == self._trash:
|
408
|
-
return True
|
409
|
-
elif tItem.itemParent == self._trash:
|
410
|
-
return True
|
411
|
-
elif tItem.itemRoot == self._trash:
|
412
|
-
return True
|
413
|
-
return False
|
414
|
-
|
415
438
|
def findRoot(self, itemClass: nwItemClass | None) -> str | None:
|
416
439
|
"""Find the first root item for a given class."""
|
417
|
-
for
|
418
|
-
|
419
|
-
|
420
|
-
continue
|
421
|
-
if itemClass == tItem.itemClass:
|
422
|
-
return tItem.itemHandle
|
440
|
+
for node in self._model.root.children:
|
441
|
+
if node.item.itemClass == itemClass:
|
442
|
+
return node.item.itemHandle
|
423
443
|
return None
|
424
444
|
|
425
445
|
##
|
426
|
-
#
|
446
|
+
# Internal Functions
|
427
447
|
##
|
428
448
|
|
429
|
-
def
|
430
|
-
"""
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
for tHandle in self._order:
|
438
|
-
if tHandle not in tmpOrder:
|
439
|
-
logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
|
440
|
-
|
441
|
-
# Save the temp list
|
442
|
-
self._order = tmpOrder
|
443
|
-
self._setTreeChanged(True)
|
444
|
-
logger.debug("Project tree order updated")
|
445
|
-
|
449
|
+
def _itemChange(self, item: NWItem, change: nwChange) -> None:
|
450
|
+
"""Signal item change and notify project."""
|
451
|
+
tHandle = item.itemHandle
|
452
|
+
logger.debug("Item change: %s -> %s", tHandle, change.name)
|
453
|
+
self._project.setProjectChanged(True)
|
454
|
+
SHARED.emitProjectItemChanged(self._project, tHandle, change)
|
455
|
+
if item.isRootType():
|
456
|
+
SHARED.emitRootFolderChanged(self._project, tHandle, change)
|
446
457
|
return
|
447
458
|
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
def __bool__(self) -> bool:
|
457
|
-
"""True if there are any items in the project."""
|
458
|
-
return bool(self._order)
|
459
|
-
|
460
|
-
def __getitem__(self, tHandle: str | None) -> NWItem | None:
|
461
|
-
"""Return a project item based on its handle. Returns None if
|
462
|
-
the handle doesn't exist in the project.
|
463
|
-
"""
|
464
|
-
if tHandle and tHandle in self._tree:
|
465
|
-
return self._tree[tHandle]
|
466
|
-
logger.error("No tree item with handle '%s'", str(tHandle))
|
459
|
+
def _getTrashNode(self) -> ProjectNode | None:
|
460
|
+
"""Get the trash node. If it doesn't exist, create it."""
|
461
|
+
for node in self._model.root.children:
|
462
|
+
if node.item.itemClass == nwItemClass.TRASH:
|
463
|
+
return node
|
464
|
+
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
|
465
|
+
if handle := self.create(label, None, nwItemType.ROOT, nwItemClass.TRASH):
|
466
|
+
return self._nodes.get(handle)
|
467
467
|
return None
|
468
468
|
|
469
|
-
def
|
470
|
-
"""
|
471
|
-
|
472
|
-
self._order.remove(tHandle)
|
473
|
-
del self._tree[tHandle]
|
474
|
-
else:
|
475
|
-
logger.warning("Failed to delete item '%s': item not found", tHandle)
|
476
|
-
return
|
477
|
-
|
478
|
-
if tHandle in self._roots:
|
479
|
-
del self._roots[tHandle]
|
480
|
-
if tHandle == self._trash:
|
481
|
-
self._trash = None
|
482
|
-
|
483
|
-
self._setTreeChanged(True)
|
484
|
-
|
485
|
-
return
|
486
|
-
|
487
|
-
def __contains__(self, tHandle: str) -> bool:
|
488
|
-
"""Checks if a handle exists in the tree."""
|
489
|
-
return tHandle in self._order
|
490
|
-
|
491
|
-
def __iter__(self) -> Iterator[NWItem]:
|
492
|
-
"""Iterate through project items."""
|
493
|
-
for tHandle in self._order:
|
494
|
-
tItem = self._tree.get(tHandle)
|
495
|
-
if isinstance(tItem, NWItem):
|
496
|
-
yield tItem
|
497
|
-
return
|
498
|
-
|
499
|
-
##
|
500
|
-
# Internal Functions
|
501
|
-
##
|
502
|
-
|
503
|
-
def _setTreeChanged(self, state: bool) -> None:
|
504
|
-
"""Set the changed flag to state, and if being set to True,
|
505
|
-
propagate that state change to the parent NWProject class.
|
469
|
+
def _addItems(self, items: dict[str, NWItem]) -> dict[str, NWItem]:
|
470
|
+
"""Add a dictionary of items to the project tree. Returns a new
|
471
|
+
dictionary of items that could not be added yet, but can be.
|
506
472
|
"""
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
473
|
+
remains: dict[str, NWItem] = {}
|
474
|
+
for handle, item in items.items():
|
475
|
+
if pHandle := item.itemParent:
|
476
|
+
if parent := self._nodes.get(pHandle):
|
477
|
+
node = ProjectNode(item)
|
478
|
+
parent.addChild(node)
|
479
|
+
parent.updateCount()
|
480
|
+
self._items[handle] = item
|
481
|
+
self._nodes[handle] = node
|
482
|
+
elif pHandle in items:
|
483
|
+
remains[handle] = item
|
484
|
+
logger.warning("Item '%s' found before its parent", handle)
|
485
|
+
elif item.isRootType():
|
486
|
+
node = ProjectNode(item)
|
487
|
+
self._model.root.addChild(node)
|
488
|
+
self._model.root.updateCount()
|
489
|
+
self._items[handle] = item
|
490
|
+
self._nodes[handle] = node
|
491
|
+
return remains
|
511
492
|
|
512
493
|
def _makeHandle(self) -> str:
|
513
494
|
"""Generate a unique item handle. In the event that the key
|
@@ -515,7 +496,7 @@ class NWTree:
|
|
515
496
|
"""
|
516
497
|
logger.debug("Generating new handle")
|
517
498
|
handle = f"{random.getrandbits(52):013x}"
|
518
|
-
if handle in self.
|
499
|
+
if handle in self._items:
|
519
500
|
logger.warning("Duplicate handle encountered! Retrying ...")
|
520
501
|
handle = self._makeHandle()
|
521
502
|
|