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.
- {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/METADATA +4 -4
- {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/RECORD +114 -98
- {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/WHEEL +1 -1
- novelwriter/__init__.py +50 -11
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
- novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
- novelwriter/assets/i18n/project_de_DE.json +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 +38 -3
- novelwriter/config.py +19 -13
- novelwriter/constants.py +60 -45
- novelwriter/core/buildsettings.py +1 -1
- novelwriter/core/coretools.py +112 -126
- novelwriter/core/docbuild.py +4 -3
- novelwriter/core/document.py +1 -1
- novelwriter/core/index.py +10 -20
- novelwriter/core/item.py +40 -7
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/options.py +1 -1
- novelwriter/core/project.py +68 -90
- novelwriter/core/projectdata.py +8 -2
- novelwriter/core/projectxml.py +1 -1
- novelwriter/core/sessions.py +1 -1
- novelwriter/core/spellcheck.py +1 -1
- novelwriter/core/status.py +24 -8
- novelwriter/core/storage.py +1 -1
- novelwriter/core/tree.py +269 -288
- novelwriter/dialogs/about.py +1 -1
- novelwriter/dialogs/docmerge.py +8 -18
- novelwriter/dialogs/docsplit.py +1 -1
- novelwriter/dialogs/editlabel.py +1 -1
- novelwriter/dialogs/preferences.py +4 -4
- novelwriter/dialogs/projectsettings.py +148 -98
- novelwriter/dialogs/quotes.py +1 -1
- novelwriter/dialogs/wordlist.py +11 -10
- novelwriter/enum.py +8 -1
- novelwriter/error.py +2 -2
- novelwriter/extensions/configlayout.py +7 -5
- novelwriter/extensions/eventfilters.py +1 -1
- novelwriter/extensions/modified.py +17 -5
- novelwriter/extensions/novelselector.py +1 -1
- novelwriter/extensions/pagedsidebar.py +4 -4
- novelwriter/extensions/progressbars.py +4 -4
- novelwriter/extensions/statusled.py +3 -3
- novelwriter/extensions/switch.py +3 -3
- novelwriter/extensions/switchbox.py +1 -1
- novelwriter/extensions/versioninfo.py +1 -1
- novelwriter/formats/shared.py +1 -1
- novelwriter/formats/todocx.py +35 -39
- novelwriter/formats/tohtml.py +15 -16
- novelwriter/formats/tokenizer.py +26 -22
- novelwriter/formats/tomarkdown.py +1 -1
- novelwriter/formats/toodt.py +54 -125
- novelwriter/formats/toqdoc.py +93 -45
- novelwriter/formats/toraw.py +1 -1
- novelwriter/gui/doceditor.py +233 -220
- novelwriter/gui/dochighlight.py +1 -1
- novelwriter/gui/docviewer.py +39 -10
- novelwriter/gui/docviewerpanel.py +15 -23
- novelwriter/gui/editordocument.py +1 -1
- novelwriter/gui/itemdetails.py +20 -27
- novelwriter/gui/mainmenu.py +14 -9
- novelwriter/gui/noveltree.py +13 -13
- novelwriter/gui/outline.py +18 -20
- novelwriter/gui/projtree.py +545 -1201
- novelwriter/gui/search.py +11 -19
- novelwriter/gui/sidebar.py +1 -1
- novelwriter/gui/statusbar.py +20 -3
- novelwriter/gui/theme.py +8 -4
- novelwriter/guimain.py +60 -48
- novelwriter/shared.py +53 -24
- novelwriter/text/counting.py +1 -1
- novelwriter/text/patterns.py +18 -6
- novelwriter/tools/dictionaries.py +1 -1
- novelwriter/tools/lipsum.py +1 -1
- novelwriter/tools/manusbuild.py +14 -12
- novelwriter/tools/manuscript.py +7 -7
- novelwriter/tools/manussettings.py +43 -53
- novelwriter/tools/noveldetails.py +1 -1
- novelwriter/tools/welcome.py +1 -1
- novelwriter/tools/writingstats.py +1 -1
- novelwriter/types.py +9 -3
- {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.6b1.dist-info → novelWriter-2.6rc1.dist-info}/entry_points.txt +0 -0
- {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
|
novelwriter/core/options.py
CHANGED
@@ -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
|
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
|