novelWriter 2.6b1__py3-none-any.whl → 2.6rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/METADATA +4 -4
  2. {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/RECORD +114 -98
  3. {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +50 -11
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  13. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
  16. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  17. novelwriter/assets/i18n/project_de_DE.json +2 -2
  18. novelwriter/assets/i18n/project_ru_RU.json +11 -0
  19. novelwriter/assets/icons/typicons_dark/icons.conf +7 -0
  20. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  21. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  22. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  23. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  24. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  25. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  26. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  27. novelwriter/assets/icons/typicons_light/icons.conf +7 -0
  28. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  29. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  30. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  31. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  32. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  33. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  34. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  35. novelwriter/assets/manual.pdf +0 -0
  36. novelwriter/assets/sample.zip +0 -0
  37. novelwriter/assets/text/credits_en.htm +1 -0
  38. novelwriter/common.py +38 -3
  39. novelwriter/config.py +19 -13
  40. novelwriter/constants.py +60 -45
  41. novelwriter/core/buildsettings.py +1 -1
  42. novelwriter/core/coretools.py +112 -126
  43. novelwriter/core/docbuild.py +4 -3
  44. novelwriter/core/document.py +1 -1
  45. novelwriter/core/index.py +10 -20
  46. novelwriter/core/item.py +40 -7
  47. novelwriter/core/itemmodel.py +518 -0
  48. novelwriter/core/options.py +1 -1
  49. novelwriter/core/project.py +68 -90
  50. novelwriter/core/projectdata.py +8 -2
  51. novelwriter/core/projectxml.py +1 -1
  52. novelwriter/core/sessions.py +1 -1
  53. novelwriter/core/spellcheck.py +1 -1
  54. novelwriter/core/status.py +24 -8
  55. novelwriter/core/storage.py +1 -1
  56. novelwriter/core/tree.py +269 -288
  57. novelwriter/dialogs/about.py +1 -1
  58. novelwriter/dialogs/docmerge.py +8 -18
  59. novelwriter/dialogs/docsplit.py +1 -1
  60. novelwriter/dialogs/editlabel.py +1 -1
  61. novelwriter/dialogs/preferences.py +4 -4
  62. novelwriter/dialogs/projectsettings.py +148 -98
  63. novelwriter/dialogs/quotes.py +1 -1
  64. novelwriter/dialogs/wordlist.py +11 -10
  65. novelwriter/enum.py +8 -1
  66. novelwriter/error.py +2 -2
  67. novelwriter/extensions/configlayout.py +7 -5
  68. novelwriter/extensions/eventfilters.py +1 -1
  69. novelwriter/extensions/modified.py +17 -5
  70. novelwriter/extensions/novelselector.py +1 -1
  71. novelwriter/extensions/pagedsidebar.py +4 -4
  72. novelwriter/extensions/progressbars.py +4 -4
  73. novelwriter/extensions/statusled.py +3 -3
  74. novelwriter/extensions/switch.py +3 -3
  75. novelwriter/extensions/switchbox.py +1 -1
  76. novelwriter/extensions/versioninfo.py +1 -1
  77. novelwriter/formats/shared.py +1 -1
  78. novelwriter/formats/todocx.py +35 -39
  79. novelwriter/formats/tohtml.py +15 -16
  80. novelwriter/formats/tokenizer.py +26 -22
  81. novelwriter/formats/tomarkdown.py +1 -1
  82. novelwriter/formats/toodt.py +54 -125
  83. novelwriter/formats/toqdoc.py +93 -45
  84. novelwriter/formats/toraw.py +1 -1
  85. novelwriter/gui/doceditor.py +233 -220
  86. novelwriter/gui/dochighlight.py +1 -1
  87. novelwriter/gui/docviewer.py +39 -10
  88. novelwriter/gui/docviewerpanel.py +15 -23
  89. novelwriter/gui/editordocument.py +1 -1
  90. novelwriter/gui/itemdetails.py +20 -27
  91. novelwriter/gui/mainmenu.py +14 -9
  92. novelwriter/gui/noveltree.py +13 -13
  93. novelwriter/gui/outline.py +18 -20
  94. novelwriter/gui/projtree.py +545 -1201
  95. novelwriter/gui/search.py +11 -19
  96. novelwriter/gui/sidebar.py +1 -1
  97. novelwriter/gui/statusbar.py +20 -3
  98. novelwriter/gui/theme.py +8 -4
  99. novelwriter/guimain.py +60 -48
  100. novelwriter/shared.py +53 -24
  101. novelwriter/text/counting.py +1 -1
  102. novelwriter/text/patterns.py +18 -6
  103. novelwriter/tools/dictionaries.py +1 -1
  104. novelwriter/tools/lipsum.py +1 -1
  105. novelwriter/tools/manusbuild.py +14 -12
  106. novelwriter/tools/manuscript.py +7 -7
  107. novelwriter/tools/manussettings.py +43 -53
  108. novelwriter/tools/noveldetails.py +1 -1
  109. novelwriter/tools/welcome.py +1 -1
  110. novelwriter/tools/writingstats.py +1 -1
  111. novelwriter/types.py +9 -3
  112. {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/LICENSE.md +0 -0
  113. {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/entry_points.txt +0 -0
  114. {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,518 @@
1
+ """
2
+ novelWriter – Project Item Model
3
+ ================================
4
+
5
+ File History:
6
+ Created: 2024-11-16 [2.6b2] ProjectNode
7
+ Created: 2024-11-16 [2.6b2] ProjectModel
8
+
9
+ This file is a part of novelWriter
10
+ Copyright (C) 2024 Veronica Berglyd Olsen and novelWriter contributors
11
+
12
+ This program is free software: you can redistribute it and/or modify
13
+ it under the terms of the GNU General Public License as published by
14
+ the Free Software Foundation, either version 3 of the License, or
15
+ (at your option) any later version.
16
+
17
+ This program is distributed in the hope that it will be useful, but
18
+ WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20
+ General Public License for more details.
21
+
22
+ You should have received a copy of the GNU General Public License
23
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+
29
+ from typing import TYPE_CHECKING
30
+
31
+ from PyQt5.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt
32
+ from PyQt5.QtGui import QFont, QIcon
33
+
34
+ from novelwriter.common import decodeMimeHandles, encodeMimeHandles, minmax
35
+ from novelwriter.constants import nwConst
36
+ from novelwriter.core.item import NWItem
37
+ from novelwriter.enum import nwItemClass
38
+ from novelwriter.types import QtAlignRight
39
+
40
+ if TYPE_CHECKING: # pragma: no cover
41
+ from novelwriter.core.tree import NWTree
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ INV_ROOT = "invisibleRoot"
46
+ C_FACTOR = 0x0100
47
+
48
+ C_LABEL_TEXT = 0x0000 | Qt.ItemDataRole.DisplayRole
49
+ C_LABEL_ICON = 0x0000 | Qt.ItemDataRole.DecorationRole
50
+ C_LABEL_FONT = 0x0000 | Qt.ItemDataRole.FontRole
51
+ C_COUNT_TEXT = 0x0100 | Qt.ItemDataRole.DisplayRole
52
+ C_COUNT_ICON = 0x0100 | Qt.ItemDataRole.DecorationRole
53
+ C_COUNT_ALIGN = 0x0100 | Qt.ItemDataRole.TextAlignmentRole
54
+ C_ACTIVE_ICON = 0x0200 | Qt.ItemDataRole.DecorationRole
55
+ C_ACTIVE_TIP = 0x0200 | Qt.ItemDataRole.ToolTipRole
56
+ C_STATUS_ICON = 0x0300 | Qt.ItemDataRole.DecorationRole
57
+ C_STATUS_TIP = 0x0300 | Qt.ItemDataRole.ToolTipRole
58
+
59
+ NODE_FLAGS = Qt.ItemFlag.ItemIsEnabled
60
+ NODE_FLAGS |= Qt.ItemFlag.ItemIsSelectable
61
+ NODE_FLAGS |= Qt.ItemFlag.ItemIsDropEnabled
62
+
63
+ if TYPE_CHECKING: # pragma: no cover
64
+ # Requires Python 3.10
65
+ T_NodeData = str | QIcon | QFont | Qt.AlignmentFlag | None
66
+
67
+
68
+ class ProjectNode:
69
+ """Core: Project Model Node Class
70
+
71
+ The project tree structure is saved as nodes in a tree, starting
72
+ from a root node. This class makes up these nodes.
73
+
74
+ Each node is a wrapper around an NWItem object. The NWItem is the
75
+ object representing a single item in the project, and it only
76
+ contains a reference to its parent as well as it top level root, but
77
+ is itself not structured in a hierarchy in memory.
78
+
79
+ This class provides the necessary hierarchical structure, as well as
80
+ the data entries needed for populating the GUI project tree. It also
81
+ handles pushing and pulling information from its NWItem when
82
+ necessary.
83
+
84
+ The data to be displayed could in principle be pulled from the
85
+ NWItem whenever it is needed, but for performance reason it is
86
+ cached, as the GUI will pull this information often.
87
+ """
88
+
89
+ C_NAME = 0
90
+ C_COUNT = 1
91
+ C_ACTIVE = 2
92
+ C_STATUS = 3
93
+
94
+ __slots__ = ("_item", "_children", "_parent", "_row", "_cache", "_flags", "_count")
95
+
96
+ def __init__(self, item: NWItem) -> None:
97
+ self._item = item
98
+ self._children: list[ProjectNode] = []
99
+ self._parent: ProjectNode | None = None
100
+ self._row = 0
101
+ self._cache: dict[int, T_NodeData] = {}
102
+ self._flags = NODE_FLAGS
103
+ self._count = 0
104
+ self.refresh()
105
+ self.updateCount()
106
+ return
107
+
108
+ def __repr__(self) -> str:
109
+ return (
110
+ f"<ProjectNode handle={self._item.itemHandle} "
111
+ f"parent={self._parent.item.itemHandle if self._parent else None} "
112
+ f"row={self._row} "
113
+ f"children={len(self._children)}>"
114
+ )
115
+
116
+ def __bool__(self) -> bool:
117
+ """A node should always evaluate to True."""
118
+ return True
119
+
120
+ ##
121
+ # Properties
122
+ ##
123
+
124
+ @property
125
+ def item(self) -> NWItem:
126
+ """The project item of the node."""
127
+ return self._item
128
+
129
+ @property
130
+ def children(self) -> list[ProjectNode]:
131
+ """All children of the node."""
132
+ return self._children
133
+
134
+ @property
135
+ def count(self) -> int:
136
+ """The count of the node."""
137
+ return self._count
138
+
139
+ ##
140
+ # Data Maintenance
141
+ ##
142
+
143
+ def refresh(self) -> None:
144
+ """Refresh data values."""
145
+ # Label
146
+ self._cache[C_LABEL_ICON] = self._item.getMainIcon()
147
+ self._cache[C_LABEL_TEXT] = self._item.itemName
148
+ self._cache[C_LABEL_FONT] = self._item.getMainFont()
149
+
150
+ # Count
151
+ self._cache[C_COUNT_ALIGN] = QtAlignRight
152
+
153
+ # Active
154
+ aText, aIcon = self._item.getActiveStatus()
155
+ self._cache[C_ACTIVE_TIP] = aText
156
+ self._cache[C_ACTIVE_ICON] = aIcon
157
+
158
+ # Status
159
+ sText, sIcon = self._item.getImportStatus()
160
+ self._cache[C_STATUS_TIP] = sText
161
+ self._cache[C_STATUS_ICON] = sIcon
162
+
163
+ return
164
+
165
+ def updateCount(self, propagate: bool = True) -> None:
166
+ """Update counts, and propagate upwards in the tree."""
167
+ self._count = self._item.wordCount + sum(c._count for c in self._children)
168
+ self._cache[C_COUNT_TEXT] = f"{self._count:n}"
169
+ if propagate and (parent := self._parent):
170
+ parent.updateCount()
171
+ return
172
+
173
+ ##
174
+ # Data Access
175
+ ##
176
+
177
+ def row(self) -> int:
178
+ """Return the node's row number."""
179
+ return self._row
180
+
181
+ def childCount(self) -> int:
182
+ """Return the number of children of the node."""
183
+ return len(self._children)
184
+
185
+ def data(self, column: int, role: Qt.ItemDataRole) -> T_NodeData:
186
+ """Return cached node data."""
187
+ return self._cache.get(C_FACTOR*column | role)
188
+
189
+ def flags(self) -> Qt.ItemFlag:
190
+ """Return cached node flags."""
191
+ return self._flags
192
+
193
+ def parent(self) -> ProjectNode | None:
194
+ """Return the parent of the node."""
195
+ return self._parent
196
+
197
+ def child(self, row: int) -> ProjectNode | None:
198
+ """Return a child ofg the node."""
199
+ if 0 <= row < len(self._children):
200
+ return self._children[row]
201
+ return None
202
+
203
+ def allChildren(self) -> list[ProjectNode]:
204
+ """Return a recursive list of all children."""
205
+ nodes: list[ProjectNode] = []
206
+ self._recursiveAppendChildren(nodes)
207
+ return nodes
208
+
209
+ ##
210
+ # Data Edit
211
+ ##
212
+
213
+ def addChild(self, child: ProjectNode, pos: int = -1) -> None:
214
+ """Add a child item to this item."""
215
+ child._parent = self
216
+ self._updateRelationships(child)
217
+ if 0 <= pos < len(self._children):
218
+ self._children.insert(pos, child)
219
+ else:
220
+ child._row = len(self._children)
221
+ self._children.append(child)
222
+ self._refreshChildrenPos()
223
+ return
224
+
225
+ def takeChild(self, pos: int) -> ProjectNode | None:
226
+ """Remove a child item and return it."""
227
+ if 0 <= pos < len(self._children):
228
+ node = self._children.pop(pos)
229
+ self._refreshChildrenPos()
230
+ self.updateCount()
231
+ return node
232
+ return None
233
+
234
+ def moveChild(self, source: int, target: int) -> None:
235
+ """Move a child internally."""
236
+ count = len(self._children)
237
+ if (source != target) and (0 <= source < count) and (0 <= target <= count):
238
+ node = self._children.pop(source)
239
+ self._children.insert(target, node)
240
+ self._refreshChildrenPos()
241
+ return
242
+
243
+ def setExpanded(self, state: bool) -> None:
244
+ """Set the node's expanded state."""
245
+ if state and self._children:
246
+ self._item.setExpanded(True)
247
+ else:
248
+ self._item.setExpanded(False)
249
+ return
250
+
251
+ ##
252
+ # Internal Functions
253
+ ##
254
+
255
+ def _recursiveAppendChildren(self, children: list[ProjectNode]) -> None:
256
+ """Recursively add all nodes to a list."""
257
+ for node in self._children:
258
+ children.append(node)
259
+ node._recursiveAppendChildren(children)
260
+ return
261
+
262
+ def _refreshChildrenPos(self) -> None:
263
+ """Update the row value on all children."""
264
+ for n, child in enumerate(self._children):
265
+ child._row = n
266
+ child.item.setOrder(n)
267
+ return
268
+
269
+ def _updateRelationships(self, child: ProjectNode) -> None:
270
+ """Update a child item's relationships."""
271
+ if self._parent:
272
+ child.item.setParent(self._item.itemHandle)
273
+ child.item.setRoot(self._item.itemRoot)
274
+ child.item.setClassDefaults(self._item.itemClass)
275
+ child._flags = NODE_FLAGS | Qt.ItemFlag.ItemIsDragEnabled
276
+ else:
277
+ child.item.setParent(None)
278
+ child.item.setRoot(child.item.itemHandle)
279
+ child.item.setClassDefaults(child.item.itemClass)
280
+ return
281
+
282
+
283
+ class ProjectModel(QAbstractItemModel):
284
+ """Core: Project Model Class
285
+
286
+ This class provides the interface for the tree widget used on the
287
+ GUI. It implements the QModelIndex based interface required, adds
288
+ support for drag and drop, and a few other novelWriter-specific
289
+ methods needed primarily by the project tree GUI component.
290
+ """
291
+
292
+ __slots__ = ("_tree", "_root")
293
+
294
+ def __init__(self, tree: NWTree) -> None:
295
+ super().__init__()
296
+ self._tree = tree
297
+ self._root = ProjectNode(NWItem(tree._project, INV_ROOT))
298
+ self._root.item.setName("Invisible Root")
299
+ logger.debug("Ready: ProjectModel")
300
+ return
301
+
302
+ def __del__(self) -> None: # pragma: no cover
303
+ logger.debug("Delete: ProjectModel")
304
+ return
305
+
306
+ ##
307
+ # Properties
308
+ ##
309
+
310
+ @property
311
+ def root(self) -> ProjectNode:
312
+ """Return the model root item."""
313
+ return self._root
314
+
315
+ ##
316
+ # Model Interface
317
+ ##
318
+
319
+ def rowCount(self, index: QModelIndex) -> int:
320
+ """Return the number of rows for an entry."""
321
+ if index.isValid():
322
+ return index.internalPointer().childCount()
323
+ return self._root.childCount()
324
+
325
+ def columnCount(self, index: QModelIndex) -> int:
326
+ """Return the number of columns for an entry."""
327
+ return 4
328
+
329
+ def parent(self, index: QModelIndex) -> QModelIndex:
330
+ """Get the parent model index of another index."""
331
+ if index.isValid() and (parent := index.internalPointer().parent()):
332
+ return self.createIndex(parent.row(), 0, parent)
333
+ return QModelIndex()
334
+
335
+ def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:
336
+ """get the index of a child item of a parent."""
337
+ if self.hasIndex(row, column, parent):
338
+ node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
339
+ if child := node.child(row):
340
+ return self.createIndex(row, column, child)
341
+ return QModelIndex()
342
+
343
+ def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> T_NodeData:
344
+ """Return display data for a project node."""
345
+ if index.isValid():
346
+ return index.internalPointer().data(index.column(), role)
347
+ return None
348
+
349
+ def flags(self, index: QModelIndex) -> Qt.ItemFlag:
350
+ """Return flags for a project node."""
351
+ if index.isValid():
352
+ return index.internalPointer().flags()
353
+ return Qt.ItemFlag.NoItemFlags
354
+
355
+ ##
356
+ # Drag and Drop
357
+ ##
358
+
359
+ def supportedDropActions(self) -> Qt.DropAction:
360
+ """Return supported drop actions"""
361
+ return Qt.DropAction.MoveAction
362
+
363
+ def mimeTypes(self) -> list[str]:
364
+ """Return the supported mime types of the model."""
365
+ return [nwConst.MIME_HANDLE]
366
+
367
+ def mimeData(self, indices: list[QModelIndex]) -> QMimeData:
368
+ """Encode mime data about a selection."""
369
+ handles = [
370
+ i.internalPointer().item.itemHandle
371
+ for i in indices if i.isValid() and i.column() == 0
372
+ ]
373
+ mime = QMimeData()
374
+ encodeMimeHandles(mime, handles)
375
+ return mime
376
+
377
+ def canDropMimeData(
378
+ self, data: QMimeData, action: Qt.DropAction,
379
+ row: int, column: int, parent: QModelIndex
380
+ ) -> bool:
381
+ """Check if mime data can be dropped on the current location."""
382
+ return data.hasFormat(nwConst.MIME_HANDLE) and action == Qt.DropAction.MoveAction
383
+
384
+ def dropMimeData(
385
+ self, data: QMimeData, action: Qt.DropAction,
386
+ row: int, column: int, parent: QModelIndex
387
+ ) -> bool:
388
+ """Process mime data drop."""
389
+ if self.canDropMimeData(data, action, row, column, parent):
390
+ items = []
391
+ for handle in decodeMimeHandles(data):
392
+ if (index := self.indexFromHandle(handle)).isValid():
393
+ items.append(index)
394
+ self.multiMove(items, parent, row)
395
+ return True
396
+ return False
397
+
398
+ ##
399
+ # Data Access
400
+ ##
401
+
402
+ def row(self, index: QModelIndex) -> int:
403
+ """Return the row number of the index."""
404
+ if index.isValid():
405
+ return index.internalPointer().row()
406
+ return -1
407
+
408
+ def node(self, index: QModelIndex) -> ProjectNode | None:
409
+ """Return the node for a given model index."""
410
+ if index.isValid():
411
+ return index.internalPointer()
412
+ return None
413
+
414
+ def nodes(self, indices: list[QModelIndex]) -> list[ProjectNode]:
415
+ """Return the nodes for a list of model indices."""
416
+ return [i.internalPointer() for i in indices if i.isValid() and i.column() == 0]
417
+
418
+ def indexFromHandle(self, handle: str | None) -> QModelIndex:
419
+ """Get the index representing a node in the model."""
420
+ if handle and (node := self._tree.nodes.get(handle)):
421
+ return self.createIndex(node.row(), 0, node)
422
+ return QModelIndex()
423
+
424
+ def indexFromNode(self, node: ProjectNode, column: int = 0) -> QModelIndex:
425
+ """Get the index representing a node in the model."""
426
+ return self.createIndex(node.row(), column, node)
427
+
428
+ ##
429
+ # Model Edit
430
+ ##
431
+
432
+ def insertChild(self, child: ProjectNode, parent: QModelIndex, pos: int) -> None:
433
+ """Insert a node into the model at a given position."""
434
+ node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
435
+ count = node.childCount()
436
+ row = minmax(pos, 0, count) if pos >= 0 else count
437
+ self.beginInsertRows(parent, row, row)
438
+ node.addChild(child, row)
439
+ self.endInsertRows()
440
+ return
441
+
442
+ def removeChild(self, parent: QModelIndex, pos: int) -> ProjectNode | None:
443
+ """Remove a node from the model and return it."""
444
+ node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
445
+ if 0 <= pos < node.childCount():
446
+ self.beginRemoveRows(parent, pos, pos)
447
+ child = node.takeChild(pos)
448
+ self.endRemoveRows()
449
+ return child
450
+ return None
451
+
452
+ def internalMove(self, index: QModelIndex, step: int) -> None:
453
+ """Move an item internally among its siblings."""
454
+ if index.isValid():
455
+ node: ProjectNode = index.internalPointer()
456
+ if parent := node.parent():
457
+ pos = index.row()
458
+ new = minmax(pos + step, 0, parent.childCount() - 1)
459
+ if new != pos:
460
+ end = new if new < pos else new + 1
461
+ self.beginMoveRows(index.parent(), pos, pos, index.parent(), end)
462
+ parent.moveChild(pos, new)
463
+ self.endMoveRows()
464
+ return
465
+
466
+ def multiMove(self, indices: list[QModelIndex], target: QModelIndex, pos: int = -1) -> None:
467
+ """Move multiple items to a new location."""
468
+ if target.isValid():
469
+ # This is a two pass process. First we only select unique
470
+ # non-root items for move, then we do a second pass and only
471
+ # move those items that don't have a parent also scheduled
472
+ # for moving or have already been moved. Child items are
473
+ # moved with the parent.
474
+ pruned = []
475
+ handles = set()
476
+ for index in indices:
477
+ if index.isValid():
478
+ node: ProjectNode = index.internalPointer()
479
+ handle = node.item.itemHandle
480
+ if node.item.isRootType() is False and handle not in handles:
481
+ pruned.append(node)
482
+ handles.add(handle)
483
+ for node in (reversed(pruned) if pos >= 0 else pruned):
484
+ if node.item.itemParent not in handles:
485
+ index = self.indexFromNode(node)
486
+ if temp := self.removeChild(index.parent(), index.row()):
487
+ self.insertChild(temp, target, pos)
488
+ for child in reversed(node.allChildren()):
489
+ node._updateRelationships(child)
490
+ child.item.notifyToRefresh()
491
+ node.item.notifyToRefresh()
492
+ return
493
+
494
+ ##
495
+ # Other Methods
496
+ ##
497
+
498
+ def clear(self) -> None:
499
+ """Clear the project model."""
500
+ self._root._children.clear()
501
+ return
502
+
503
+ def allExpanded(self) -> list[QModelIndex]:
504
+ """Return a list of all expanded items."""
505
+ expanded = []
506
+ for node in self._root.allChildren():
507
+ if node._item.isExpanded:
508
+ expanded.append(self.createIndex(node.row(), 0, node))
509
+ return expanded
510
+
511
+ def trashSelection(self, indices: list[QModelIndex]) -> bool:
512
+ """Check if a selection of indices are all in trash or not."""
513
+ for index in indices:
514
+ if index.isValid():
515
+ node: ProjectNode = index.internalPointer()
516
+ if node.item.itemClass != nwItemClass.TRASH:
517
+ return False
518
+ return True
@@ -7,7 +7,7 @@ Created: 2019-10-21 [0.3.1] OptionState
7
7
  Rewritten: 2020-02-19 [0.4.5] OptionState
8
8
 
9
9
  This file is a part of novelWriter
10
- Copyright 2018–2024, Veronica Berglyd Olsen
10
+ Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
11
11
 
12
12
  This program is free software: you can redistribute it and/or modify
13
13
  it under the terms of the GNU General Public License as published by