novelWriter 2.7b1__py3-none-any.whl → 2.7rc1__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/__init__.py +17 -4
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/icons/remix_filled.icons +1 -0
- novelwriter/assets/icons/remix_outline.icons +1 -0
- novelwriter/assets/images/splash.png +0 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/manual_fr.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/snazzy.conf +3 -3
- novelwriter/assets/themes/snazzy.conf +48 -0
- novelwriter/common.py +10 -1
- novelwriter/config.py +96 -25
- novelwriter/constants.py +17 -0
- novelwriter/core/buildsettings.py +2 -0
- novelwriter/core/coretools.py +41 -34
- novelwriter/core/docbuild.py +1 -0
- novelwriter/core/index.py +25 -1
- novelwriter/core/indexdata.py +4 -1
- novelwriter/core/item.py +35 -24
- novelwriter/core/itemmodel.py +17 -13
- novelwriter/core/novelmodel.py +2 -0
- novelwriter/core/project.py +14 -14
- novelwriter/core/projectdata.py +42 -24
- novelwriter/core/projectxml.py +17 -7
- novelwriter/core/sessions.py +29 -13
- novelwriter/core/tree.py +9 -5
- novelwriter/dialogs/docmerge.py +2 -1
- novelwriter/dialogs/docsplit.py +6 -3
- novelwriter/dialogs/editlabel.py +11 -8
- novelwriter/dialogs/preferences.py +37 -26
- novelwriter/dialogs/projectsettings.py +3 -0
- novelwriter/extensions/configlayout.py +6 -2
- novelwriter/extensions/switch.py +16 -15
- novelwriter/extensions/switchbox.py +1 -0
- novelwriter/formats/tokenizer.py +2 -1
- novelwriter/gui/doceditor.py +98 -40
- novelwriter/gui/noveltree.py +11 -5
- novelwriter/gui/outline.py +9 -1
- novelwriter/gui/projtree.py +1 -0
- novelwriter/gui/search.py +1 -0
- novelwriter/gui/statusbar.py +14 -6
- novelwriter/gui/theme.py +25 -10
- novelwriter/guimain.py +29 -9
- novelwriter/splash.py +74 -0
- novelwriter/tools/lipsum.py +2 -1
- novelwriter/tools/manuscript.py +1 -1
- novelwriter/tools/manussettings.py +38 -13
- novelwriter/tools/noveldetails.py +9 -8
- novelwriter/tools/welcome.py +1 -0
- novelwriter/tools/writingstats.py +68 -45
- {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/METADATA +1 -1
- {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/RECORD +56 -53
- {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/WHEEL +1 -1
- {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/entry_points.txt +0 -0
- {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/licenses/LICENSE.md +0 -0
- {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/top_level.txt +0 -0
novelwriter/core/coretools.py
CHANGED
@@ -383,10 +383,12 @@ class ProjectBuilder:
|
|
383
383
|
"""Build or copy a project from a data dictionary."""
|
384
384
|
if isinstance(data, dict):
|
385
385
|
path = data.get("path", None) or None
|
386
|
+
if author := data.get("author"):
|
387
|
+
CONFIG.setLastAuthor(author)
|
386
388
|
if isinstance(path, str | Path):
|
387
389
|
self._path = Path(path).resolve()
|
388
390
|
if data.get("sample"):
|
389
|
-
return self._extractSampleProject(self._path)
|
391
|
+
return self._extractSampleProject(self._path, data)
|
390
392
|
elif data.get("template"):
|
391
393
|
return self._copyProject(self._path, data)
|
392
394
|
else:
|
@@ -416,20 +418,21 @@ class ProjectBuilder:
|
|
416
418
|
|
417
419
|
self._path = project.storage.storagePath
|
418
420
|
|
419
|
-
|
420
|
-
|
421
|
+
trName = self.tr("New Project")
|
422
|
+
trAuthor = self.tr("Author Name")
|
423
|
+
trTitlePage = self.tr("Title Page")
|
421
424
|
|
422
425
|
# Settings
|
423
426
|
project.data.setUuid(None)
|
424
|
-
project.data.setName(data.get("name",
|
425
|
-
project.data.setAuthor(data.get("author",
|
427
|
+
project.data.setName(data.get("name", trName))
|
428
|
+
project.data.setAuthor(data.get("author", trAuthor))
|
426
429
|
project.data.setLanguage(CONFIG.guiLocale)
|
427
430
|
project.setDefaultStatusImport()
|
428
431
|
project.session.startSession()
|
429
432
|
|
430
433
|
# Add Root Folders
|
431
434
|
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
|
432
|
-
hTitlePage = project.newFile(
|
435
|
+
hTitlePage = project.newFile(trTitlePage, hNovelRoot)
|
433
436
|
|
434
437
|
# Generate Title Page
|
435
438
|
aDoc = project.storage.getDocument(hTitlePage)
|
@@ -446,25 +449,21 @@ class ProjectBuilder:
|
|
446
449
|
"\n"
|
447
450
|
">> {count}: [field:{field}] <<\n"
|
448
451
|
).format(
|
449
|
-
author=project.data.author or
|
450
|
-
address=self.tr("Address"),
|
451
|
-
title=project.data.name or
|
452
|
+
author=project.data.author or trAuthor,
|
453
|
+
address=self.tr("Address Line"),
|
454
|
+
title=project.data.name or trName,
|
452
455
|
by=self.tr("By"),
|
453
456
|
count=self.tr("Word Count"),
|
454
457
|
field=nwStats.WORDS_TEXT,
|
455
458
|
))
|
456
459
|
|
457
|
-
# Create a project structure based on selected root folders
|
458
|
-
# and a number of chapters and scenes selected in the
|
459
|
-
# wizard's custom page.
|
460
|
-
|
461
460
|
# Create chapters and scenes
|
462
461
|
numChapters = data.get("chapters", 0)
|
463
462
|
numScenes = data.get("scenes", 0)
|
464
463
|
|
465
|
-
|
466
|
-
|
467
|
-
|
464
|
+
trChSynop = self.tr("Summary of the chapter.")
|
465
|
+
trScSynop = self.tr("Summary of the scene.")
|
466
|
+
trNoteDesc = self.tr("A short description.")
|
468
467
|
|
469
468
|
# Create chapters
|
470
469
|
if numChapters > 0:
|
@@ -472,7 +471,7 @@ class ProjectBuilder:
|
|
472
471
|
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
|
473
472
|
cHandle = project.newFile(chTitle, hNovelRoot)
|
474
473
|
aDoc = project.storage.getDocument(cHandle)
|
475
|
-
aDoc.writeDocument(f"## {chTitle}\n\n%Synopsis: {
|
474
|
+
aDoc.writeDocument(f"## {chTitle}\n\n%Synopsis: {trChSynop}\n\n")
|
476
475
|
|
477
476
|
# Create chapter scenes
|
478
477
|
if numScenes > 0 and cHandle:
|
@@ -480,7 +479,7 @@ class ProjectBuilder:
|
|
480
479
|
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
|
481
480
|
sHandle = project.newFile(scTitle, cHandle)
|
482
481
|
aDoc = project.storage.getDocument(sHandle)
|
483
|
-
aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {
|
482
|
+
aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {trScSynop}\n\n")
|
484
483
|
|
485
484
|
# Create scenes (no chapters)
|
486
485
|
elif numScenes > 0:
|
@@ -488,7 +487,7 @@ class ProjectBuilder:
|
|
488
487
|
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
|
489
488
|
sHandle = project.newFile(scTitle, hNovelRoot)
|
490
489
|
aDoc = project.storage.getDocument(sHandle)
|
491
|
-
aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {
|
490
|
+
aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {trScSynop}\n\n")
|
492
491
|
|
493
492
|
# Create notes folders
|
494
493
|
noteTitles = {
|
@@ -508,7 +507,7 @@ class ProjectBuilder:
|
|
508
507
|
aDoc.writeDocument(
|
509
508
|
f"# {noteTitles[newRoot]}\n\n"
|
510
509
|
f"@tag: {ntTag}\n\n"
|
511
|
-
f"%Short: {
|
510
|
+
f"%Short: {trNoteDesc}\n\n"
|
512
511
|
)
|
513
512
|
|
514
513
|
# Also add the archive and trash folders
|
@@ -563,23 +562,11 @@ class ProjectBuilder:
|
|
563
562
|
return False
|
564
563
|
|
565
564
|
# Open the copied project and update settings
|
566
|
-
|
567
|
-
project.openProject(dstPath)
|
568
|
-
project.data.setUuid("") # Creates a fresh uuid
|
569
|
-
project.data.setName(data.get("name", "None"))
|
570
|
-
project.data.setAuthor(data.get("author", ""))
|
571
|
-
project.data.setSpellCheck(True)
|
572
|
-
project.data.setSpellLang(None)
|
573
|
-
project.data.setDoBackup(True)
|
574
|
-
project.data.setSaveCount(0)
|
575
|
-
project.data.setAutoCount(0)
|
576
|
-
project.data.setEditTime(0)
|
577
|
-
project.saveProject()
|
578
|
-
project.closeProject()
|
565
|
+
self._resetProject(dstPath, data)
|
579
566
|
|
580
567
|
return True
|
581
568
|
|
582
|
-
def _extractSampleProject(self, path: Path) -> bool:
|
569
|
+
def _extractSampleProject(self, path: Path, data: dict) -> bool:
|
583
570
|
"""Make a copy of the sample project by extracting the
|
584
571
|
sample.zip file to the new path.
|
585
572
|
"""
|
@@ -593,6 +580,7 @@ class ProjectBuilder:
|
|
593
580
|
if (sample := CONFIG.assetPath("sample.zip")).is_file():
|
594
581
|
try:
|
595
582
|
shutil.unpack_archive(sample, path)
|
583
|
+
self._resetProject(path, data)
|
596
584
|
except Exception as exc:
|
597
585
|
SHARED.error(self.tr("Failed to create a new example project."), exc=exc)
|
598
586
|
return False
|
@@ -605,3 +593,22 @@ class ProjectBuilder:
|
|
605
593
|
return False
|
606
594
|
|
607
595
|
return True
|
596
|
+
|
597
|
+
def _resetProject(self, path: Path, data: dict) -> None:
|
598
|
+
"""Open a project and reset/update its settings."""
|
599
|
+
project = NWProject()
|
600
|
+
project.openProject(path)
|
601
|
+
project.data.setUuid("") # Creates a fresh uuid
|
602
|
+
if name := data.get("name", ""):
|
603
|
+
project.data.setName(name)
|
604
|
+
if author := data.get("author", ""):
|
605
|
+
project.data.setAuthor(author)
|
606
|
+
project.data.setSpellCheck(True)
|
607
|
+
project.data.setSpellLang(None)
|
608
|
+
project.data.setDoBackup(True)
|
609
|
+
project.data.setSaveCount(0)
|
610
|
+
project.data.setAutoCount(0)
|
611
|
+
project.data.setEditTime(0)
|
612
|
+
project.saveProject()
|
613
|
+
project.closeProject()
|
614
|
+
return
|
novelwriter/core/docbuild.py
CHANGED
@@ -317,6 +317,7 @@ class NWBuildDocument:
|
|
317
317
|
bldObj.setCommentType(nwComment.SYNOPSIS, self._build.getBool("text.includeSynopsis"))
|
318
318
|
bldObj.setCommentType(nwComment.SHORT, self._build.getBool("text.includeSynopsis"))
|
319
319
|
bldObj.setCommentType(nwComment.STORY, self._build.getBool("text.includeStory"))
|
320
|
+
bldObj.setCommentType(nwComment.NOTE, self._build.getBool("text.includeNotes"))
|
320
321
|
|
321
322
|
if isinstance(bldObj, ToHtml):
|
322
323
|
bldObj.setStyles(self._build.getBool("html.addStyles"))
|
novelwriter/core/index.py
CHANGED
@@ -131,6 +131,12 @@ class Index:
|
|
131
131
|
self._novelExtra = extra
|
132
132
|
return
|
133
133
|
|
134
|
+
def setItemClass(self, tHandle: str, itemClass: nwItemClass) -> None:
|
135
|
+
"""Update the class for all tags of a handle."""
|
136
|
+
logger.info("Updating class for '%s'", tHandle)
|
137
|
+
self._tagsIndex.updateClass(tHandle, itemClass.name)
|
138
|
+
return
|
139
|
+
|
134
140
|
##
|
135
141
|
# Public Methods
|
136
142
|
##
|
@@ -612,6 +618,10 @@ class Index:
|
|
612
618
|
"""Return all story structure keys."""
|
613
619
|
return self._itemIndex.allStoryKeys()
|
614
620
|
|
621
|
+
def getNoteKeys(self) -> set[str]:
|
622
|
+
"""Return all note comment keys."""
|
623
|
+
return self._itemIndex.allNoteKeys()
|
624
|
+
|
615
625
|
def novelStructure(
|
616
626
|
self, rootHandle: str | None = None, activeOnly: bool = True
|
617
627
|
) -> Iterable[tuple[str, str, str, IndexHeading]]:
|
@@ -854,6 +864,15 @@ class TagsIndex:
|
|
854
864
|
x.get("name", "") for x in self._tags.values() if x.get("class", "") == className
|
855
865
|
]
|
856
866
|
|
867
|
+
def updateClass(self, tHandle: str, className: str) -> None:
|
868
|
+
"""Update the class name of an item. This must be called when a
|
869
|
+
document moves to another class.
|
870
|
+
"""
|
871
|
+
for entry in self._tags.values():
|
872
|
+
if entry.get("handle") == tHandle:
|
873
|
+
entry["class"] = className
|
874
|
+
return
|
875
|
+
|
857
876
|
##
|
858
877
|
# Pack/Unpack
|
859
878
|
##
|
@@ -905,11 +924,12 @@ class IndexCache:
|
|
905
924
|
which provides lookup capabilities and caching for shared data.
|
906
925
|
"""
|
907
926
|
|
908
|
-
__slots__ = ("story", "tags")
|
927
|
+
__slots__ = ("note", "story", "tags")
|
909
928
|
|
910
929
|
def __init__(self, tagsIndex: TagsIndex) -> None:
|
911
930
|
self.tags: TagsIndex = tagsIndex
|
912
931
|
self.story: set[str] = set()
|
932
|
+
self.note: set[str] = set()
|
913
933
|
return
|
914
934
|
|
915
935
|
|
@@ -964,6 +984,10 @@ class ItemIndex:
|
|
964
984
|
"""Return all story structure keys."""
|
965
985
|
return self._cache.story.copy()
|
966
986
|
|
987
|
+
def allNoteKeys(self) -> set[str]:
|
988
|
+
"""Return all note comment keys."""
|
989
|
+
return self._cache.note.copy()
|
990
|
+
|
967
991
|
def allItemTags(self, tHandle: str) -> list[str]:
|
968
992
|
"""Get all tags set for headings of an item."""
|
969
993
|
if tHandle in self._items:
|
novelwriter/core/indexdata.py
CHANGED
@@ -316,6 +316,9 @@ class IndexHeading:
|
|
316
316
|
case "story" if key:
|
317
317
|
self._cache.story.add(key)
|
318
318
|
self._comments[f"story.{key}"] = str(text)
|
319
|
+
case "note" if key:
|
320
|
+
self._cache.note.add(key)
|
321
|
+
self._comments[f"note.{key}"] = str(text)
|
319
322
|
return
|
320
323
|
|
321
324
|
def setTag(self, tag: str) -> None:
|
@@ -395,7 +398,7 @@ class IndexHeading:
|
|
395
398
|
self.addReference(tag, keyword)
|
396
399
|
else:
|
397
400
|
raise ValueError("Heading reference contains an invalid keyword")
|
398
|
-
elif key == "summary" or key.startswith("story"):
|
401
|
+
elif key == "summary" or key.startswith(("story", "note")):
|
399
402
|
comment, _, kind = str(key).partition(".")
|
400
403
|
self.setComment(comment, compact(kind), str(entry))
|
401
404
|
else:
|
novelwriter/core/item.py
CHANGED
@@ -53,10 +53,10 @@ class NWItem:
|
|
53
53
|
"""
|
54
54
|
|
55
55
|
__slots__ = (
|
56
|
-
"_active", "_charCount", "
|
57
|
-
"
|
56
|
+
"_active", "_charCount", "_charInit", "_class", "_cursorPos",
|
57
|
+
"_expanded", "_handle", "_heading", "_import", "_layout", "_name",
|
58
58
|
"_order", "_paraCount", "_parent", "_project", "_root", "_status",
|
59
|
-
"_type", "_wordCount",
|
59
|
+
"_type", "_wordCount", "_wordInit",
|
60
60
|
)
|
61
61
|
|
62
62
|
def __init__(self, project: NWProject, handle: str) -> None:
|
@@ -81,7 +81,8 @@ class NWItem:
|
|
81
81
|
self._wordCount = 0 # Current word count
|
82
82
|
self._paraCount = 0 # Current paragraph count
|
83
83
|
self._cursorPos = 0 # Last cursor position
|
84
|
-
self.
|
84
|
+
self._wordInit = 0 # Initial character count
|
85
|
+
self._charInit = 0 # Initial word count
|
85
86
|
|
86
87
|
return
|
87
88
|
|
@@ -164,9 +165,13 @@ class NWItem:
|
|
164
165
|
def paraCount(self) -> int:
|
165
166
|
return self._paraCount
|
166
167
|
|
168
|
+
@property
|
169
|
+
def mainCount(self) -> int:
|
170
|
+
return self._charCount if CONFIG.useCharCount else self._wordCount
|
171
|
+
|
167
172
|
@property
|
168
173
|
def initCount(self) -> int:
|
169
|
-
return self.
|
174
|
+
return self._wordInit if CONFIG.useCharCount else self._charInit
|
170
175
|
|
171
176
|
@property
|
172
177
|
def cursorPos(self) -> int:
|
@@ -257,7 +262,8 @@ class NWItem:
|
|
257
262
|
self._paraCount = 0
|
258
263
|
self._cursorPos = 0
|
259
264
|
|
260
|
-
self.
|
265
|
+
self._wordInit = self._charCount
|
266
|
+
self._charInit = self._wordCount
|
261
267
|
|
262
268
|
return True
|
263
269
|
|
@@ -265,23 +271,24 @@ class NWItem:
|
|
265
271
|
def duplicate(cls, source: NWItem, handle: str) -> NWItem:
|
266
272
|
"""Make a copy of an item."""
|
267
273
|
new = cls(source._project, handle)
|
268
|
-
new._name
|
269
|
-
new._parent
|
270
|
-
new._root
|
271
|
-
new._order
|
272
|
-
new._type
|
273
|
-
new._class
|
274
|
-
new._layout
|
275
|
-
new._status
|
276
|
-
new._import
|
277
|
-
new._active
|
278
|
-
new._expanded
|
279
|
-
new._heading
|
280
|
-
new._charCount
|
281
|
-
new._wordCount
|
282
|
-
new._paraCount
|
283
|
-
new._cursorPos
|
284
|
-
new.
|
274
|
+
new._name = source._name
|
275
|
+
new._parent = source._parent
|
276
|
+
new._root = source._root
|
277
|
+
new._order = source._order
|
278
|
+
new._type = source._type
|
279
|
+
new._class = source._class
|
280
|
+
new._layout = source._layout
|
281
|
+
new._status = source._status
|
282
|
+
new._import = source._import
|
283
|
+
new._active = source._active
|
284
|
+
new._expanded = source._expanded
|
285
|
+
new._heading = source._heading
|
286
|
+
new._charCount = source._charCount
|
287
|
+
new._wordCount = source._wordCount
|
288
|
+
new._paraCount = source._paraCount
|
289
|
+
new._cursorPos = source._cursorPos
|
290
|
+
new._wordInit = source._wordInit
|
291
|
+
new._charInit = source._charInit
|
285
292
|
return new
|
286
293
|
|
287
294
|
##
|
@@ -428,7 +435,11 @@ class NWItem:
|
|
428
435
|
"""
|
429
436
|
if self._parent is not None:
|
430
437
|
# Only update for child items
|
431
|
-
self.
|
438
|
+
if itemClass != self._class:
|
439
|
+
self.setClass(itemClass)
|
440
|
+
if self._type == nwItemType.FILE:
|
441
|
+
# Notify the index of the class change
|
442
|
+
self._project.index.setItemClass(self._handle, itemClass)
|
432
443
|
|
433
444
|
if self._layout == nwItemLayout.NO_LAYOUT:
|
434
445
|
# If no layout is set, pick one
|
novelwriter/core/itemmodel.py
CHANGED
@@ -45,16 +45,18 @@ logger = logging.getLogger(__name__)
|
|
45
45
|
INV_ROOT = "invisibleRoot"
|
46
46
|
C_FACTOR = 0x0100
|
47
47
|
|
48
|
-
C_LABEL_TEXT
|
49
|
-
C_LABEL_ICON
|
50
|
-
C_LABEL_FONT
|
51
|
-
C_COUNT_TEXT
|
52
|
-
C_COUNT_ICON
|
53
|
-
C_COUNT_ALIGN
|
54
|
-
C_ACTIVE_ICON
|
55
|
-
C_ACTIVE_TIP
|
56
|
-
|
57
|
-
|
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_ACTIVE_ACCESS = 0x0200 | Qt.ItemDataRole.AccessibleTextRole
|
57
|
+
C_STATUS_ICON = 0x0300 | Qt.ItemDataRole.DecorationRole
|
58
|
+
C_STATUS_TIP = 0x0300 | Qt.ItemDataRole.ToolTipRole
|
59
|
+
C_STATUS_ACCESS = 0x0300 | Qt.ItemDataRole.AccessibleTextRole
|
58
60
|
|
59
61
|
NODE_FLAGS = Qt.ItemFlag.ItemIsEnabled
|
60
62
|
NODE_FLAGS |= Qt.ItemFlag.ItemIsSelectable
|
@@ -150,19 +152,21 @@ class ProjectNode:
|
|
150
152
|
|
151
153
|
# Active
|
152
154
|
aText, aIcon = self._item.getActiveStatus()
|
153
|
-
self._cache[C_ACTIVE_TIP] = aText
|
154
155
|
self._cache[C_ACTIVE_ICON] = aIcon
|
156
|
+
self._cache[C_ACTIVE_TIP] = aText
|
157
|
+
self._cache[C_ACTIVE_ACCESS] = aText
|
155
158
|
|
156
159
|
# Status
|
157
160
|
sText, sIcon = self._item.getImportStatus()
|
158
|
-
self._cache[C_STATUS_TIP] = sText
|
159
161
|
self._cache[C_STATUS_ICON] = sIcon
|
162
|
+
self._cache[C_STATUS_TIP] = sText
|
163
|
+
self._cache[C_STATUS_ACCESS] = sText
|
160
164
|
|
161
165
|
return
|
162
166
|
|
163
167
|
def updateCount(self, propagate: bool = True) -> None:
|
164
168
|
"""Update counts, and propagate upwards in the tree."""
|
165
|
-
self._count = self._item.
|
169
|
+
self._count = self._item.mainCount + sum(c._count for c in self._children) # noqa: SLF001
|
166
170
|
self._cache[C_COUNT_TEXT] = f"{self._count:n}"
|
167
171
|
if propagate and (parent := self._parent):
|
168
172
|
parent.updateCount()
|
novelwriter/core/novelmodel.py
CHANGED
@@ -46,6 +46,7 @@ R_TEXT = Qt.ItemDataRole.DisplayRole
|
|
46
46
|
R_ICON = Qt.ItemDataRole.DecorationRole
|
47
47
|
R_ALIGN = Qt.ItemDataRole.TextAlignmentRole
|
48
48
|
R_TIP = Qt.ItemDataRole.ToolTipRole
|
49
|
+
R_ACCESS = Qt.ItemDataRole.AccessibleTextRole
|
49
50
|
R_HANDLE = 0xff01
|
50
51
|
R_KEY = 0xff02
|
51
52
|
|
@@ -217,6 +218,7 @@ class NovelModel(QAbstractTableModel):
|
|
217
218
|
text = ", ".join(refs)
|
218
219
|
data[C_FACTOR*2 | R_TEXT] = text
|
219
220
|
data[C_FACTOR*2 | R_TIP] = f"<b>{self._extraLabel}:</b> {text}"
|
221
|
+
data[C_FACTOR*2 | R_ACCESS] = f"{self._extraLabel}: {text}"
|
220
222
|
data[C_FACTOR*3 | R_ICON] = self._more
|
221
223
|
data[R_HANDLE] = handle
|
222
224
|
data[R_KEY] = key
|
novelwriter/core/project.py
CHANGED
@@ -367,7 +367,7 @@ class NWProject:
|
|
367
367
|
# Often, the index needs to be rebuilt when updating format
|
368
368
|
self._index.rebuild()
|
369
369
|
|
370
|
-
self.
|
370
|
+
self.updateCounts()
|
371
371
|
self._session.startSession()
|
372
372
|
self.setProjectChanged(False)
|
373
373
|
self._valid = True
|
@@ -397,7 +397,7 @@ class NWProject:
|
|
397
397
|
else:
|
398
398
|
self._data.incSaveCount()
|
399
399
|
|
400
|
-
self.
|
400
|
+
self.updateCounts()
|
401
401
|
self.countStatus()
|
402
402
|
|
403
403
|
xmlWriter = self._storage.getXmlWriter()
|
@@ -483,14 +483,14 @@ class NWProject:
|
|
483
483
|
|
484
484
|
def setDefaultStatusImport(self) -> None:
|
485
485
|
"""Set the default status and importance values."""
|
486
|
-
self._data.itemStatus.add(None, self.tr("New"), (
|
487
|
-
self._data.itemStatus.add(None, self.tr("Note"), (
|
488
|
-
self._data.itemStatus.add(None, self.tr("Draft"), (
|
489
|
-
self._data.itemStatus.add(None, self.tr("Finished"), (
|
490
|
-
self._data.itemImport.add(None, self.tr("New"), (
|
491
|
-
self._data.itemImport.add(None, self.tr("Minor"), (
|
492
|
-
self._data.itemImport.add(None, self.tr("Major"), (
|
493
|
-
self._data.itemImport.add(None, self.tr("Main"), (
|
486
|
+
self._data.itemStatus.add(None, self.tr("New"), (120, 120, 120), "STAR", 0)
|
487
|
+
self._data.itemStatus.add(None, self.tr("Note"), (205, 171, 143), "TRIANGLE", 0)
|
488
|
+
self._data.itemStatus.add(None, self.tr("Draft"), (143, 240, 164), "CIRCLE_T", 0)
|
489
|
+
self._data.itemStatus.add(None, self.tr("Finished"), (249, 240, 107), "STAR", 0)
|
490
|
+
self._data.itemImport.add(None, self.tr("New"), (120, 120, 120), "SQUARE", 0)
|
491
|
+
self._data.itemImport.add(None, self.tr("Minor"), (220, 138, 221), "BLOCK_2", 0)
|
492
|
+
self._data.itemImport.add(None, self.tr("Major"), (220, 138, 221), "BLOCK_3", 0)
|
493
|
+
self._data.itemImport.add(None, self.tr("Main"), (220, 138, 221), "BLOCK_4", 0)
|
494
494
|
return
|
495
495
|
|
496
496
|
def setProjectLang(self, language: str | None) -> None:
|
@@ -515,10 +515,10 @@ class NWProject:
|
|
515
515
|
# Class Methods
|
516
516
|
##
|
517
517
|
|
518
|
-
def
|
519
|
-
"""Update the total word count values."""
|
520
|
-
|
521
|
-
self._data.setCurrCounts(
|
518
|
+
def updateCounts(self) -> None:
|
519
|
+
"""Update the total word and character count values."""
|
520
|
+
wNovel, wNotes, cNovel, cNotes = self._tree.sumCounts()
|
521
|
+
self._data.setCurrCounts(wNovel=wNovel, wNotes=wNotes, cNovel=cNovel, cNotes=cNotes)
|
522
522
|
return
|
523
523
|
|
524
524
|
def countStatus(self) -> None:
|
novelwriter/core/projectdata.py
CHANGED
@@ -66,8 +66,8 @@ class NWProjectData:
|
|
66
66
|
self._spellLang = None
|
67
67
|
|
68
68
|
# Project Dictionaries
|
69
|
-
self._initCounts = [0, 0]
|
70
|
-
self._currCounts = [0, 0]
|
69
|
+
self._initCounts = [0, 0, 0, 0]
|
70
|
+
self._currCounts = [0, 0, 0, 0]
|
71
71
|
self._lastHandle: dict[str, str | None] = {
|
72
72
|
"editor": None,
|
73
73
|
"viewer": None,
|
@@ -148,18 +148,18 @@ class NWProjectData:
|
|
148
148
|
return self._spellLang
|
149
149
|
|
150
150
|
@property
|
151
|
-
def initCounts(self) -> tuple[int, int]:
|
152
|
-
"""Return the initial count of words for novel
|
153
|
-
documents.
|
151
|
+
def initCounts(self) -> tuple[int, int, int, int]:
|
152
|
+
"""Return the initial count of words and characters for novel
|
153
|
+
and note documents.
|
154
154
|
"""
|
155
|
-
return self._initCounts[0], self._initCounts[1]
|
155
|
+
return self._initCounts[0], self._initCounts[1], self._initCounts[2], self._initCounts[3]
|
156
156
|
|
157
157
|
@property
|
158
|
-
def currCounts(self) -> tuple[int, int]:
|
159
|
-
"""Return the current count of words for novel
|
160
|
-
documents.
|
158
|
+
def currCounts(self) -> tuple[int, int, int, int]:
|
159
|
+
"""Return the current count of words and characters for novel
|
160
|
+
and note documents.
|
161
161
|
"""
|
162
|
-
return self._currCounts[0], self._currCounts[1]
|
162
|
+
return self._currCounts[0], self._currCounts[1], self._currCounts[2], self._currCounts[3]
|
163
163
|
|
164
164
|
@property
|
165
165
|
def lastHandle(self) -> dict[str, str | None]:
|
@@ -301,22 +301,40 @@ class NWProjectData:
|
|
301
301
|
self._project.setProjectChanged(True)
|
302
302
|
return
|
303
303
|
|
304
|
-
def setInitCounts(
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
self._initCounts[
|
311
|
-
self._currCounts[
|
304
|
+
def setInitCounts(
|
305
|
+
self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None
|
306
|
+
) -> None:
|
307
|
+
"""Set the count totals for novel and note files."""
|
308
|
+
if wNovel is not None:
|
309
|
+
count = checkInt(wNovel, 0)
|
310
|
+
self._initCounts[0] = count
|
311
|
+
self._currCounts[0] = count
|
312
|
+
if wNotes is not None:
|
313
|
+
count = checkInt(wNotes, 0)
|
314
|
+
self._initCounts[1] = count
|
315
|
+
self._currCounts[1] = count
|
316
|
+
if cNovel is not None:
|
317
|
+
count = checkInt(cNovel, 0)
|
318
|
+
self._initCounts[2] = count
|
319
|
+
self._currCounts[2] = count
|
320
|
+
if cNotes is not None:
|
321
|
+
count = checkInt(cNotes, 0)
|
322
|
+
self._initCounts[3] = count
|
323
|
+
self._currCounts[3] = count
|
312
324
|
return
|
313
325
|
|
314
|
-
def setCurrCounts(
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
if
|
319
|
-
self._currCounts[
|
326
|
+
def setCurrCounts(
|
327
|
+
self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None
|
328
|
+
) -> None:
|
329
|
+
"""Set the count totals for novel and note files."""
|
330
|
+
if wNovel is not None:
|
331
|
+
self._currCounts[0] = checkInt(wNovel, 0)
|
332
|
+
if wNotes is not None:
|
333
|
+
self._currCounts[1] = checkInt(wNotes, 0)
|
334
|
+
if cNovel is not None:
|
335
|
+
self._currCounts[2] = checkInt(cNovel, 0)
|
336
|
+
if cNotes is not None:
|
337
|
+
self._currCounts[3] = checkInt(cNotes, 0)
|
320
338
|
return
|
321
339
|
|
322
340
|
def setAutoReplace(self, value: dict) -> None:
|
novelwriter/core/projectxml.py
CHANGED
@@ -46,7 +46,7 @@ if TYPE_CHECKING:
|
|
46
46
|
logger = logging.getLogger(__name__)
|
47
47
|
|
48
48
|
FILE_VERSION = "1.5" # The current project file format version
|
49
|
-
FILE_REVISION = "
|
49
|
+
FILE_REVISION = "5" # The current project file format revision
|
50
50
|
HEX_VERSION = 0x0105
|
51
51
|
|
52
52
|
NUM_VERSION = {
|
@@ -109,6 +109,8 @@ class ProjectXMLReader:
|
|
109
109
|
Rev 3: Added TEMPLATE class. 2.3.
|
110
110
|
Rev 4: Added shape attribute to status and importance entry
|
111
111
|
nodes. 2.5.
|
112
|
+
Rev 5: Added novelChars and notesChars attributes to content
|
113
|
+
node. 2.7 RC 1.
|
112
114
|
"""
|
113
115
|
|
114
116
|
def __init__(self, path: str | Path) -> None:
|
@@ -286,9 +288,9 @@ class ProjectXMLReader:
|
|
286
288
|
elif xItem.tag == "spellLang": # Changed to spellChecking in 1.5
|
287
289
|
data.setSpellLang(xItem.text)
|
288
290
|
elif xItem.tag == "novelWordCount": # Moved to content attribute in 1.5
|
289
|
-
data.setInitCounts(
|
291
|
+
data.setInitCounts(wNovel=xItem.text)
|
290
292
|
elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
|
291
|
-
data.setInitCounts(
|
293
|
+
data.setInitCounts(wNotes=xItem.text)
|
292
294
|
|
293
295
|
return
|
294
296
|
|
@@ -298,8 +300,13 @@ class ProjectXMLReader:
|
|
298
300
|
"""Parse the content section of the XML file."""
|
299
301
|
logger.debug("Parsing <content> section")
|
300
302
|
|
301
|
-
|
302
|
-
data.setInitCounts(
|
303
|
+
# Moved in 1.5
|
304
|
+
data.setInitCounts(
|
305
|
+
wNovel=xSection.attrib.get("novelWords", None),
|
306
|
+
wNotes=xSection.attrib.get("notesWords", None),
|
307
|
+
cNovel=xSection.attrib.get("novelChars", None),
|
308
|
+
cNotes=xSection.attrib.get("notesChars", None),
|
309
|
+
)
|
303
310
|
|
304
311
|
for xItem in xSection:
|
305
312
|
if xItem.tag != "item":
|
@@ -527,10 +534,13 @@ class ProjectXMLWriter:
|
|
527
534
|
self._packSingleValue(xImport, "entry", label, attrib=attrib)
|
528
535
|
|
529
536
|
# Save Tree Content
|
537
|
+
counts = data.currCounts
|
530
538
|
contAttr = {
|
531
539
|
"items": str(len(content)),
|
532
|
-
"novelWords": str(
|
533
|
-
"notesWords": str(
|
540
|
+
"novelWords": str(counts[0]),
|
541
|
+
"notesWords": str(counts[1]),
|
542
|
+
"novelChars": str(counts[2]),
|
543
|
+
"notesChars": str(counts[3]),
|
534
544
|
}
|
535
545
|
|
536
546
|
xContent = ET.SubElement(xRoot, "content", attrib=contAttr)
|