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.
Files changed (56) hide show
  1. novelwriter/__init__.py +17 -4
  2. novelwriter/assets/i18n/project_en_GB.json +1 -0
  3. novelwriter/assets/icons/remix_filled.icons +1 -0
  4. novelwriter/assets/icons/remix_outline.icons +1 -0
  5. novelwriter/assets/images/splash.png +0 -0
  6. novelwriter/assets/manual.pdf +0 -0
  7. novelwriter/assets/manual_fr.pdf +0 -0
  8. novelwriter/assets/sample.zip +0 -0
  9. novelwriter/assets/syntax/snazzy.conf +3 -3
  10. novelwriter/assets/themes/snazzy.conf +48 -0
  11. novelwriter/common.py +10 -1
  12. novelwriter/config.py +96 -25
  13. novelwriter/constants.py +17 -0
  14. novelwriter/core/buildsettings.py +2 -0
  15. novelwriter/core/coretools.py +41 -34
  16. novelwriter/core/docbuild.py +1 -0
  17. novelwriter/core/index.py +25 -1
  18. novelwriter/core/indexdata.py +4 -1
  19. novelwriter/core/item.py +35 -24
  20. novelwriter/core/itemmodel.py +17 -13
  21. novelwriter/core/novelmodel.py +2 -0
  22. novelwriter/core/project.py +14 -14
  23. novelwriter/core/projectdata.py +42 -24
  24. novelwriter/core/projectxml.py +17 -7
  25. novelwriter/core/sessions.py +29 -13
  26. novelwriter/core/tree.py +9 -5
  27. novelwriter/dialogs/docmerge.py +2 -1
  28. novelwriter/dialogs/docsplit.py +6 -3
  29. novelwriter/dialogs/editlabel.py +11 -8
  30. novelwriter/dialogs/preferences.py +37 -26
  31. novelwriter/dialogs/projectsettings.py +3 -0
  32. novelwriter/extensions/configlayout.py +6 -2
  33. novelwriter/extensions/switch.py +16 -15
  34. novelwriter/extensions/switchbox.py +1 -0
  35. novelwriter/formats/tokenizer.py +2 -1
  36. novelwriter/gui/doceditor.py +98 -40
  37. novelwriter/gui/noveltree.py +11 -5
  38. novelwriter/gui/outline.py +9 -1
  39. novelwriter/gui/projtree.py +1 -0
  40. novelwriter/gui/search.py +1 -0
  41. novelwriter/gui/statusbar.py +14 -6
  42. novelwriter/gui/theme.py +25 -10
  43. novelwriter/guimain.py +29 -9
  44. novelwriter/splash.py +74 -0
  45. novelwriter/tools/lipsum.py +2 -1
  46. novelwriter/tools/manuscript.py +1 -1
  47. novelwriter/tools/manussettings.py +38 -13
  48. novelwriter/tools/noveldetails.py +9 -8
  49. novelwriter/tools/welcome.py +1 -0
  50. novelwriter/tools/writingstats.py +68 -45
  51. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/METADATA +1 -1
  52. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/RECORD +56 -53
  53. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/WHEEL +1 -1
  54. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/entry_points.txt +0 -0
  55. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/licenses/LICENSE.md +0 -0
  56. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/top_level.txt +0 -0
@@ -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
- lblNewProject = self.tr("New Project")
420
- lblTitlePage = self.tr("Title Page")
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", lblNewProject))
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(lblTitlePage, hNovelRoot)
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 "None",
450
- address=self.tr("Address"),
451
- title=project.data.name or "None",
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
- chSynop = self.tr("Summary of the chapter.")
466
- scSynop = self.tr("Summary of the scene.")
467
- bfNote = self.tr("A short description.")
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: {chSynop}\n\n")
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: {scSynop}\n\n")
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: {scSynop}\n\n")
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: {bfNote}\n\n"
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
- project = NWProject()
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
@@ -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:
@@ -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", "_class", "_cursorPos", "_expanded",
57
- "_handle", "_heading", "_import", "_initCount", "_layout", "_name",
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._initCount = 0 # Initial word count
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._initCount
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._initCount = self._wordCount
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 = source._name
269
- new._parent = source._parent
270
- new._root = source._root
271
- new._order = source._order
272
- new._type = source._type
273
- new._class = source._class
274
- new._layout = source._layout
275
- new._status = source._status
276
- new._import = source._import
277
- new._active = source._active
278
- new._expanded = source._expanded
279
- new._heading = source._heading
280
- new._charCount = source._charCount
281
- new._wordCount = source._wordCount
282
- new._paraCount = source._paraCount
283
- new._cursorPos = source._cursorPos
284
- new._initCount = source._initCount
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.setClass(itemClass)
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
@@ -45,16 +45,18 @@ logger = logging.getLogger(__name__)
45
45
  INV_ROOT = "invisibleRoot"
46
46
  C_FACTOR = 0x0100
47
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
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.wordCount + sum(c._count for c in self._children) # noqa: SLF001
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()
@@ -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
@@ -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.updateWordCounts()
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.updateWordCounts()
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"), (100, 100, 100), "SQUARE", 0)
487
- self._data.itemStatus.add(None, self.tr("Note"), (200, 50, 0), "SQUARE", 0)
488
- self._data.itemStatus.add(None, self.tr("Draft"), (200, 150, 0), "SQUARE", 0)
489
- self._data.itemStatus.add(None, self.tr("Finished"), (50, 200, 0), "SQUARE", 0)
490
- self._data.itemImport.add(None, self.tr("New"), (100, 100, 100), "SQUARE", 0)
491
- self._data.itemImport.add(None, self.tr("Minor"), (200, 50, 0), "SQUARE", 0)
492
- self._data.itemImport.add(None, self.tr("Major"), (200, 150, 0), "SQUARE", 0)
493
- self._data.itemImport.add(None, self.tr("Main"), (50, 200, 0), "SQUARE", 0)
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 updateWordCounts(self) -> None:
519
- """Update the total word count values."""
520
- novel, notes = self._tree.sumWords()
521
- self._data.setCurrCounts(novel=novel, notes=notes)
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:
@@ -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 and note
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 and note
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(self, novel: Any = None, notes: Any = None) -> None:
305
- """Set the word count totals for novel and note files."""
306
- if novel is not None:
307
- self._initCounts[0] = checkInt(novel, 0)
308
- self._currCounts[0] = checkInt(novel, 0)
309
- if notes is not None:
310
- self._initCounts[1] = checkInt(notes, 0)
311
- self._currCounts[1] = checkInt(notes, 0)
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(self, novel: Any = None, notes: Any = None) -> None:
315
- """Set the word count totals for novel and note files."""
316
- if novel is not None:
317
- self._currCounts[0] = checkInt(novel, 0)
318
- if notes is not None:
319
- self._currCounts[1] = checkInt(notes, 0)
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:
@@ -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 = "4" # The current project file format 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(novel=xItem.text)
291
+ data.setInitCounts(wNovel=xItem.text)
290
292
  elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
291
- data.setInitCounts(notes=xItem.text)
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
- data.setInitCounts(novel=xSection.attrib.get("novelWords", None)) # Moved in 1.5
302
- data.setInitCounts(notes=xSection.attrib.get("notesWords", None)) # Moved in 1.5
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(data.currCounts[0]),
533
- "notesWords": str(data.currCounts[1]),
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)