novelWriter 2.7b1__py3-none-any.whl → 2.7.1__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 (81) hide show
  1. novelwriter/__init__.py +17 -4
  2. novelwriter/assets/i18n/nw_cs_CZ.qm +0 -0
  3. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  4. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  5. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  6. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  7. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  8. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  9. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  10. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  11. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  12. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  13. novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
  14. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  15. novelwriter/assets/i18n/project_cs_CZ.json +2 -0
  16. novelwriter/assets/i18n/project_de_DE.json +3 -1
  17. novelwriter/assets/i18n/project_en_GB.json +1 -0
  18. novelwriter/assets/i18n/project_en_US.json +2 -0
  19. novelwriter/assets/i18n/project_it_IT.json +2 -0
  20. novelwriter/assets/i18n/project_ja_JP.json +2 -0
  21. novelwriter/assets/i18n/project_nb_NO.json +2 -0
  22. novelwriter/assets/i18n/project_nn_NO.json +5 -0
  23. novelwriter/assets/i18n/project_pl_PL.json +2 -0
  24. novelwriter/assets/i18n/project_pt_BR.json +2 -0
  25. novelwriter/assets/i18n/project_ru_RU.json +2 -0
  26. novelwriter/assets/i18n/project_zh_CN.json +2 -0
  27. novelwriter/assets/icons/remix_filled.icons +1 -0
  28. novelwriter/assets/icons/remix_outline.icons +1 -0
  29. novelwriter/assets/images/splash.png +0 -0
  30. novelwriter/assets/manual.pdf +0 -0
  31. novelwriter/assets/manual_fr.pdf +0 -0
  32. novelwriter/assets/sample.zip +0 -0
  33. novelwriter/assets/syntax/snazzy.conf +3 -3
  34. novelwriter/assets/text/credits_en.htm +6 -0
  35. novelwriter/assets/themes/snazzy.conf +48 -0
  36. novelwriter/common.py +10 -1
  37. novelwriter/config.py +96 -25
  38. novelwriter/constants.py +21 -0
  39. novelwriter/core/buildsettings.py +3 -1
  40. novelwriter/core/coretools.py +41 -34
  41. novelwriter/core/docbuild.py +1 -0
  42. novelwriter/core/index.py +35 -5
  43. novelwriter/core/indexdata.py +4 -1
  44. novelwriter/core/item.py +35 -24
  45. novelwriter/core/itemmodel.py +17 -13
  46. novelwriter/core/novelmodel.py +2 -0
  47. novelwriter/core/project.py +14 -14
  48. novelwriter/core/projectdata.py +42 -24
  49. novelwriter/core/projectxml.py +17 -7
  50. novelwriter/core/sessions.py +29 -13
  51. novelwriter/core/tree.py +9 -5
  52. novelwriter/dialogs/docmerge.py +2 -1
  53. novelwriter/dialogs/docsplit.py +6 -3
  54. novelwriter/dialogs/editlabel.py +11 -8
  55. novelwriter/dialogs/preferences.py +37 -26
  56. novelwriter/dialogs/projectsettings.py +3 -0
  57. novelwriter/extensions/configlayout.py +6 -2
  58. novelwriter/extensions/switch.py +16 -15
  59. novelwriter/extensions/switchbox.py +1 -0
  60. novelwriter/formats/tokenizer.py +2 -1
  61. novelwriter/gui/doceditor.py +106 -47
  62. novelwriter/gui/noveltree.py +11 -5
  63. novelwriter/gui/outline.py +9 -1
  64. novelwriter/gui/projtree.py +1 -0
  65. novelwriter/gui/search.py +1 -0
  66. novelwriter/gui/statusbar.py +14 -6
  67. novelwriter/gui/theme.py +25 -10
  68. novelwriter/guimain.py +29 -9
  69. novelwriter/splash.py +74 -0
  70. novelwriter/tools/lipsum.py +2 -1
  71. novelwriter/tools/manuscript.py +1 -1
  72. novelwriter/tools/manussettings.py +52 -20
  73. novelwriter/tools/noveldetails.py +9 -8
  74. novelwriter/tools/welcome.py +1 -0
  75. novelwriter/tools/writingstats.py +68 -45
  76. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/METADATA +2 -2
  77. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/RECORD +81 -78
  78. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/WHEEL +1 -1
  79. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/entry_points.txt +0 -0
  80. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/licenses/LICENSE.md +0 -0
  81. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/top_level.txt +0 -0
novelwriter/constants.py CHANGED
@@ -184,6 +184,10 @@ class nwKeyWords:
184
184
  POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
185
185
  OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY,
186
186
  ]
187
+ CAN_LOOKUP: Final[list[str]] = [
188
+ POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
189
+ OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
190
+ ]
187
191
 
188
192
  # Set of Valid Keys
189
193
  VALID_KEYS: Final[set[str]] = set(ALL_KEYS)
@@ -352,6 +356,10 @@ class nwLabels:
352
356
  nwStats.WORDS_TEXT: QT_TRANSLATE_NOOP("Stats", "Words in Text"),
353
357
  nwStats.WORDS_TITLE: QT_TRANSLATE_NOOP("Stats", "Words in Headings"),
354
358
  }
359
+ STATS_DISPLAY: Final[dict[str, str]] = {
360
+ nwStats.CHARS: QT_TRANSLATE_NOOP("Stats", "Characters: {0} ({1})"),
361
+ nwStats.WORDS: QT_TRANSLATE_NOOP("Stats", "Words: {0} ({1})"),
362
+ }
355
363
  BUILD_FMT: Final[dict[nwBuildFmt, str]] = {
356
364
  nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
357
365
  nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
@@ -505,6 +513,19 @@ class nwQuotes:
505
513
  "\u300f": QT_TRANSLATE_NOOP("Constant", "Right white corner bracket"),
506
514
  }
507
515
 
516
+ DASHES: Final[dict[str, str]] = {
517
+ "": QT_TRANSLATE_NOOP("Constant", "None"),
518
+ "\u2013": QT_TRANSLATE_NOOP("Constant", "Short dash"),
519
+ "\u2014": QT_TRANSLATE_NOOP("Constant", "Long dash"),
520
+ "\u2015": QT_TRANSLATE_NOOP("Constant", "Horizontal bar"),
521
+ }
522
+
523
+ ALLOWED: Final[list[str]] = [
524
+ "\u0027", "\u0022", "\u2018", "\u2019", "\u201a", "\u201b", "\u201c", "\u201d", "\u201e",
525
+ "\u201f", "\u2e42", "\u2039", "\u203a", "\u00ab", "\u00bb", "\u300c", "\u300d", "\u300e",
526
+ "\u300f", "\u2013", "\u2014", "\u2015",
527
+ ]
528
+
508
529
 
509
530
  class nwUnicode:
510
531
  """Supported unicode character constants and their HTML equivalents."""
@@ -79,6 +79,7 @@ SETTINGS_TEMPLATE: dict[str, tuple[type, T_BuildValue]] = {
79
79
  "text.includeSynopsis": (bool, False),
80
80
  "text.includeComments": (bool, False),
81
81
  "text.includeStory": (bool, False),
82
+ "text.includeNotes": (bool, False),
82
83
  "text.includeKeywords": (bool, False),
83
84
  "text.includeBodyText": (bool, True),
84
85
  "text.ignoredKeywords": (str, ""),
@@ -146,6 +147,7 @@ SETTINGS_LABELS = {
146
147
  "text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
147
148
  "text.includeComments": QT_TRANSLATE_NOOP("Builds", "Include Comments"),
148
149
  "text.includeStory": QT_TRANSLATE_NOOP("Builds", "Include Story Structure"),
150
+ "text.includeNotes": QT_TRANSLATE_NOOP("Builds", "Include Manuscript Notes"),
149
151
  "text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
150
152
  "text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
151
153
  "text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
@@ -388,7 +390,7 @@ class BuildSettings:
388
390
  def setValue(self, key: str, value: T_BuildValue) -> None:
389
391
  """Set a specific value for a build setting."""
390
392
  if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]):
391
- self._changed = value != self._settings[key]
393
+ self._changed |= (value != self._settings[key])
392
394
  self._settings[key] = value
393
395
  return
394
396
 
@@ -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
@@ -177,6 +177,16 @@ class Index:
177
177
  self.scanText(tHandle, self._project.storage.getDocumentText(tHandle))
178
178
  return
179
179
 
180
+ def refreshHandle(self, tHandle: str) -> None:
181
+ """Update the class for all tags of a handle."""
182
+ if item := self._project.tree[tHandle]:
183
+ logger.info("Updating class for '%s'", tHandle)
184
+ if item.isInactiveClass():
185
+ self.deleteHandle(tHandle)
186
+ else:
187
+ self._tagsIndex.updateClass(tHandle, item.itemClass.name)
188
+ return
189
+
180
190
  def indexChangedSince(self, checkTime: int | float) -> bool:
181
191
  """Check if the index has changed since a given time."""
182
192
  return self._indexChange > float(checkTime)
@@ -612,6 +622,10 @@ class Index:
612
622
  """Return all story structure keys."""
613
623
  return self._itemIndex.allStoryKeys()
614
624
 
625
+ def getNoteKeys(self) -> set[str]:
626
+ """Return all note comment keys."""
627
+ return self._itemIndex.allNoteKeys()
628
+
615
629
  def novelStructure(
616
630
  self, rootHandle: str | None = None, activeOnly: bool = True
617
631
  ) -> Iterable[tuple[str, str, str, IndexHeading]]:
@@ -743,10 +757,12 @@ class Index:
743
757
  """Return all tags used by a specific document."""
744
758
  return self._itemIndex.allItemTags(tHandle) if tHandle else []
745
759
 
746
- def getClassTags(self, itemClass: nwItemClass | None) -> list[str]:
747
- """Return all tags based on itemClass."""
748
- name = None if itemClass is None else itemClass.name
749
- return self._tagsIndex.filterTagNames(name)
760
+ def getKeyWordTags(self, keyWord: str) -> list[str]:
761
+ """Return all tags usable for a specific keyword."""
762
+ if keyWord in nwKeyWords.CAN_LOOKUP:
763
+ itemClass = nwKeyWords.KEY_CLASS.get(keyWord)
764
+ return self._tagsIndex.filterTagNames(itemClass.name if itemClass else None)
765
+ return []
750
766
 
751
767
  def getTagsData(
752
768
  self, activeOnly: bool = True
@@ -854,6 +870,15 @@ class TagsIndex:
854
870
  x.get("name", "") for x in self._tags.values() if x.get("class", "") == className
855
871
  ]
856
872
 
873
+ def updateClass(self, tHandle: str, className: str) -> None:
874
+ """Update the class name of an item. This must be called when a
875
+ document moves to another class.
876
+ """
877
+ for entry in self._tags.values():
878
+ if entry.get("handle") == tHandle:
879
+ entry["class"] = className
880
+ return
881
+
857
882
  ##
858
883
  # Pack/Unpack
859
884
  ##
@@ -905,11 +930,12 @@ class IndexCache:
905
930
  which provides lookup capabilities and caching for shared data.
906
931
  """
907
932
 
908
- __slots__ = ("story", "tags")
933
+ __slots__ = ("note", "story", "tags")
909
934
 
910
935
  def __init__(self, tagsIndex: TagsIndex) -> None:
911
936
  self.tags: TagsIndex = tagsIndex
912
937
  self.story: set[str] = set()
938
+ self.note: set[str] = set()
913
939
  return
914
940
 
915
941
 
@@ -964,6 +990,10 @@ class ItemIndex:
964
990
  """Return all story structure keys."""
965
991
  return self._cache.story.copy()
966
992
 
993
+ def allNoteKeys(self) -> set[str]:
994
+ """Return all note comment keys."""
995
+ return self._cache.note.copy()
996
+
967
997
  def allItemTags(self, tHandle: str) -> list[str]:
968
998
  """Get all tags set for headings of an item."""
969
999
  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.refreshHandle(self._handle)
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: