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/project.py
CHANGED
@@ -26,7 +26,6 @@ from __future__ import annotations
|
|
26
26
|
import json
|
27
27
|
import logging
|
28
28
|
|
29
|
-
from collections.abc import Iterable
|
30
29
|
from enum import Enum
|
31
30
|
from functools import partial
|
32
31
|
from pathlib import Path
|
@@ -52,7 +51,8 @@ from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
|
52
51
|
from novelwriter.error import logException
|
53
52
|
|
54
53
|
if TYPE_CHECKING: # pragma: no cover
|
55
|
-
|
54
|
+
# Requires Python 3.10
|
55
|
+
from novelwriter.core.status import T_StatusKind, T_UpdateEntry
|
56
56
|
|
57
57
|
logger = logging.getLogger(__name__)
|
58
58
|
|
@@ -67,6 +67,11 @@ class NWProjectState(Enum):
|
|
67
67
|
|
68
68
|
class NWProject:
|
69
69
|
|
70
|
+
__slots__ = (
|
71
|
+
"_options", "_storage", "_data", "_tree", "_index", "_session",
|
72
|
+
"_langData", "_changed", "_valid", "_state", "tr",
|
73
|
+
)
|
74
|
+
|
70
75
|
def __init__(self) -> None:
|
71
76
|
|
72
77
|
# Core Elements
|
@@ -94,6 +99,12 @@ class NWProject:
|
|
94
99
|
logger.debug("Delete: NWProject")
|
95
100
|
return
|
96
101
|
|
102
|
+
def clear(self) -> None:
|
103
|
+
"""Clear the project."""
|
104
|
+
self._tree.clear()
|
105
|
+
self._index.clear()
|
106
|
+
return
|
107
|
+
|
97
108
|
##
|
98
109
|
# Properties
|
99
110
|
##
|
@@ -150,24 +161,46 @@ class NWProject:
|
|
150
161
|
"""Return total edit time, including the current session."""
|
151
162
|
return self._data.editTime + round(time() - self._session.start)
|
152
163
|
|
164
|
+
@property
|
165
|
+
def currentTotalCount(self) -> int:
|
166
|
+
"""Return the current total word count from the tree."""
|
167
|
+
return self._tree.model.root.count
|
168
|
+
|
153
169
|
##
|
154
170
|
# Item Methods
|
155
171
|
##
|
156
172
|
|
157
|
-
def newRoot(self, itemClass: nwItemClass,
|
173
|
+
def newRoot(self, itemClass: nwItemClass, pos: int = -1) -> str:
|
158
174
|
"""Add a new root folder to the project. If label is not set,
|
159
175
|
use the class label.
|
160
176
|
"""
|
161
|
-
label =
|
162
|
-
return self._tree.create(label, None, nwItemType.ROOT, itemClass)
|
177
|
+
label = trConst(nwLabels.CLASS_NAME[itemClass])
|
178
|
+
return self._tree.create(label, None, nwItemType.ROOT, itemClass=itemClass, pos=pos)
|
163
179
|
|
164
|
-
def newFolder(self, label: str, parent: str) -> str | None:
|
180
|
+
def newFolder(self, label: str, parent: str, pos: int = -1) -> str | None:
|
165
181
|
"""Add a new folder with a given label and parent item."""
|
166
|
-
return self._tree.create(label, parent, nwItemType.FOLDER)
|
182
|
+
return self._tree.create(label, parent, nwItemType.FOLDER, pos=pos)
|
167
183
|
|
168
|
-
def newFile(self, label: str, parent: str) -> str | None:
|
184
|
+
def newFile(self, label: str, parent: str, pos: int = -1) -> str | None:
|
169
185
|
"""Add a new file with a given label and parent item."""
|
170
|
-
return self._tree.create(label, parent, nwItemType.FILE)
|
186
|
+
return self._tree.create(label, parent, nwItemType.FILE, pos=pos)
|
187
|
+
|
188
|
+
def removeItem(self, tHandle: str) -> bool:
|
189
|
+
"""Remove an item from the project. This will delete both the
|
190
|
+
project entry and a document file if it exists.
|
191
|
+
"""
|
192
|
+
if self._tree.checkType(tHandle, nwItemType.FILE):
|
193
|
+
SHARED.closeDocument(tHandle)
|
194
|
+
doc = self._storage.getDocument(tHandle)
|
195
|
+
if not doc.deleteDocument():
|
196
|
+
SHARED.error(
|
197
|
+
self.tr("Could not delete document file."),
|
198
|
+
info=doc.getError()
|
199
|
+
)
|
200
|
+
return False
|
201
|
+
self._index.deleteHandle(tHandle)
|
202
|
+
self._tree.remove(tHandle)
|
203
|
+
return True
|
171
204
|
|
172
205
|
def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, text: str = "") -> bool:
|
173
206
|
"""Write content to a new document after it is created. This
|
@@ -209,35 +242,21 @@ class NWProject:
|
|
209
242
|
text = self._storage.getDocumentText(sHandle)
|
210
243
|
self._storage.getDocument(tHandle).writeDocument(text)
|
211
244
|
sItem.setLayout(tItem.itemLayout)
|
212
|
-
self._index.
|
245
|
+
self._index.reIndexHandle(tHandle)
|
213
246
|
|
214
247
|
return True
|
215
248
|
|
216
|
-
def
|
217
|
-
"""
|
218
|
-
|
249
|
+
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
|
250
|
+
"""Create a new note. This function is used by the document
|
251
|
+
editor to create note files for unknown tags.
|
219
252
|
"""
|
220
|
-
if
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
return False
|
228
|
-
|
229
|
-
self._index.deleteHandle(tHandle)
|
230
|
-
del self._tree[tHandle]
|
231
|
-
|
232
|
-
return True
|
233
|
-
|
234
|
-
def trashFolder(self) -> str:
|
235
|
-
"""Add the special trash root folder to the project."""
|
236
|
-
trashHandle = self._tree.trashRoot
|
237
|
-
if trashHandle is None:
|
238
|
-
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
|
239
|
-
return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
|
240
|
-
return trashHandle
|
253
|
+
if itemClass != nwItemClass.NO_CLASS:
|
254
|
+
if not (rHandle := self._tree.findRoot(itemClass)):
|
255
|
+
rHandle = self.newRoot(itemClass)
|
256
|
+
if rHandle and (tHandle := SHARED.project.newFile(tag.title(), rHandle)):
|
257
|
+
self.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
|
258
|
+
self._tree.refreshItems([tHandle])
|
259
|
+
return
|
241
260
|
|
242
261
|
##
|
243
262
|
# Project Methods
|
@@ -349,7 +368,7 @@ class NWProject:
|
|
349
368
|
self._index.loadIndex()
|
350
369
|
if xmlReader.state == XMLReadState.WAS_LEGACY:
|
351
370
|
# Often, the index needs to be rebuilt when updating format
|
352
|
-
self._index.
|
371
|
+
self._index.rebuild()
|
353
372
|
|
354
373
|
self.updateWordCounts()
|
355
374
|
self._session.startSession()
|
@@ -414,12 +433,11 @@ class NWProject:
|
|
414
433
|
def closeProject(self, idleTime: float = 0.0) -> None:
|
415
434
|
"""Close the project."""
|
416
435
|
logger.info("Closing project")
|
417
|
-
self._index.
|
436
|
+
self._index.clear() # Triggers clear signal, see #1718
|
418
437
|
self._options.saveSettings()
|
419
438
|
self._tree.writeToCFile()
|
420
439
|
self._session.appendSession(idleTime)
|
421
440
|
self._storage.closeSession()
|
422
|
-
self._lockedBy = None
|
423
441
|
return
|
424
442
|
|
425
443
|
def backupProject(self, doNotify: bool) -> bool:
|
@@ -489,17 +507,6 @@ class NWProject:
|
|
489
507
|
self.setProjectChanged(True)
|
490
508
|
return
|
491
509
|
|
492
|
-
def setTreeOrder(self, order: list[str]) -> None:
|
493
|
-
"""A list representing the linear/flattened order of project
|
494
|
-
items in the GUI project tree. The user can rearrange the order
|
495
|
-
by drag-and-drop. Forwarded to the NWTree class.
|
496
|
-
"""
|
497
|
-
if len(self._tree) != len(order):
|
498
|
-
logger.warning("Sizes of new and old tree order do not match")
|
499
|
-
self._tree.setOrder(order)
|
500
|
-
self.setProjectChanged(True)
|
501
|
-
return
|
502
|
-
|
503
510
|
def setProjectChanged(self, status: bool) -> bool:
|
504
511
|
"""Toggle the project changed flag, and propagate the
|
505
512
|
information to the GUI statusbar.
|
@@ -513,47 +520,6 @@ class NWProject:
|
|
513
520
|
# Class Methods
|
514
521
|
##
|
515
522
|
|
516
|
-
def iterProjectItems(self) -> Iterable[NWItem]:
|
517
|
-
"""This function ensures that the item tree loaded is sent to
|
518
|
-
the GUI tree view in such a way that the tree can be built. That
|
519
|
-
is, the parent item must be sent before its child. In principle,
|
520
|
-
a proper XML file will already ensure that, but in the event the
|
521
|
-
order has been altered, or a file is orphaned, this function is
|
522
|
-
capable of handling it.
|
523
|
-
"""
|
524
|
-
sentItems = set()
|
525
|
-
iterItems = self._tree.handles()
|
526
|
-
n = 0
|
527
|
-
nMax = min(len(iterItems), 10000)
|
528
|
-
while n < nMax:
|
529
|
-
tHandle = iterItems[n]
|
530
|
-
tItem = self._tree[tHandle]
|
531
|
-
n += 1
|
532
|
-
if tItem is None:
|
533
|
-
# Technically a bug
|
534
|
-
continue
|
535
|
-
elif tItem.itemParent is None:
|
536
|
-
# Item is a root, or already been identified as orphaned
|
537
|
-
sentItems.add(tHandle)
|
538
|
-
yield tItem
|
539
|
-
elif tItem.itemParent in sentItems:
|
540
|
-
# Item's parent has been sent, so all is fine
|
541
|
-
sentItems.add(tHandle)
|
542
|
-
yield tItem
|
543
|
-
elif tItem.itemParent in iterItems:
|
544
|
-
# Item's parent exists, but hasn't been sent yet, so add
|
545
|
-
# it again to the end, but make sure this doesn't get
|
546
|
-
# out hand, so we cap at 10000 items
|
547
|
-
logger.warning("Item '%s' found before its parent", tHandle)
|
548
|
-
iterItems.append(tHandle)
|
549
|
-
nMax = min(len(iterItems), 10000)
|
550
|
-
else:
|
551
|
-
# Item is orphaned
|
552
|
-
logger.error("Item '%s' has no parent in current tree", tHandle)
|
553
|
-
tItem.setParent(None)
|
554
|
-
yield tItem
|
555
|
-
return
|
556
|
-
|
557
523
|
def updateWordCounts(self) -> None:
|
558
524
|
"""Update the total word count values."""
|
559
525
|
novel, notes = self._tree.sumWords()
|
@@ -574,6 +540,18 @@ class NWProject:
|
|
574
540
|
self._data.itemImport.increment(nwItem.itemImport)
|
575
541
|
return
|
576
542
|
|
543
|
+
def updateStatus(self, kind: T_StatusKind, update: T_UpdateEntry) -> None:
|
544
|
+
"""Update status or import entries."""
|
545
|
+
if kind == "s":
|
546
|
+
self._data.itemStatus.update(update)
|
547
|
+
SHARED.emitStatusLabelsChanged(self, kind)
|
548
|
+
self._tree.refreshAllItems()
|
549
|
+
elif kind == "i":
|
550
|
+
self._data.itemImport.update(update)
|
551
|
+
SHARED.emitStatusLabelsChanged(self, kind)
|
552
|
+
self._tree.refreshAllItems()
|
553
|
+
return
|
554
|
+
|
577
555
|
def localLookup(self, word: str | int) -> str:
|
578
556
|
"""Look up a word or number in the translation map for the
|
579
557
|
project and return it. The variable is cast to a string before
|
novelwriter/core/status.py
CHANGED
@@ -65,6 +65,11 @@ class StatusEntry:
|
|
65
65
|
|
66
66
|
NO_ENTRY = StatusEntry("", QColor(0, 0, 0), nwStatusShape.SQUARE, QIcon(), 0)
|
67
67
|
|
68
|
+
if TYPE_CHECKING: # pragma: no cover
|
69
|
+
# Requires Python 3.10
|
70
|
+
T_UpdateEntry = list[tuple[str | None, StatusEntry]]
|
71
|
+
T_StatusKind = Literal["s", "i"]
|
72
|
+
|
68
73
|
|
69
74
|
class NWStatus:
|
70
75
|
|
@@ -73,7 +78,7 @@ class NWStatus:
|
|
73
78
|
|
74
79
|
__slots__ = ("_store", "_default", "_prefix", "_height")
|
75
80
|
|
76
|
-
def __init__(self, prefix:
|
81
|
+
def __init__(self, prefix: T_StatusKind) -> None:
|
77
82
|
self._store: dict[str, StatusEntry] = {}
|
78
83
|
self._default = None
|
79
84
|
self._prefix = prefix[:1]
|
@@ -120,7 +125,7 @@ class NWStatus:
|
|
120
125
|
|
121
126
|
return key
|
122
127
|
|
123
|
-
def update(self, update:
|
128
|
+
def update(self, update: T_UpdateEntry) -> None:
|
124
129
|
"""Update the list of statuses."""
|
125
130
|
self._store.clear()
|
126
131
|
for key, entry in update:
|
@@ -130,9 +135,6 @@ class NWStatus:
|
|
130
135
|
if self._default not in self._store:
|
131
136
|
self._default = next(iter(self._store)) if self._store else None
|
132
137
|
|
133
|
-
# Emit the change signal
|
134
|
-
SHARED.projectSingalProxy({"event": "statusLabels", "kind": self._prefix})
|
135
|
-
|
136
138
|
return
|
137
139
|
|
138
140
|
def check(self, value: str) -> str:
|