Skip to content

Commit ab328d0

Browse files
authored
Add a setting for using character count as main count (#2323)
2 parents 4c16e17 + 28c70df commit ab328d0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+560
-285
lines changed

novelwriter/config.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -919,9 +919,10 @@ def loadCache(self) -> bool:
919919
puuid = str(entry.get("uuid", ""))
920920
title = str(entry.get("title", ""))
921921
words = checkInt(entry.get("words", 0), 0)
922+
chars = checkInt(entry.get("chars", 0), 0)
922923
saved = checkInt(entry.get("time", 0), 0)
923924
if path and title:
924-
self._setEntry(puuid, path, title, words, saved)
925+
self._setEntry(puuid, path, title, words, chars, saved)
925926
except Exception:
926927
logger.error("Could not load recent project cache")
927928
logException()
@@ -954,7 +955,14 @@ def update(self, path: str | Path, data: NWProjectData, saved: float | int) -> N
954955
try:
955956
if (remove := self._map.get(data.uuid)) and (remove != str(path)):
956957
self.remove(remove)
957-
self._setEntry(data.uuid, str(path), data.name, sum(data.currCounts), int(saved))
958+
self._setEntry(
959+
data.uuid,
960+
str(path),
961+
data.name,
962+
sum(data.currCounts[:2]),
963+
sum(data.currCounts[2:]),
964+
int(saved),
965+
)
958966
self.saveCache()
959967
except Exception:
960968
pass
@@ -967,9 +975,17 @@ def remove(self, path: str | Path) -> None:
967975
self.saveCache()
968976
return
969977

970-
def _setEntry(self, puuid: str, path: str, title: str, words: int, saved: int) -> None:
978+
def _setEntry(
979+
self, puuid: str, path: str, title: str, words: int, chars: int, saved: int
980+
) -> None:
971981
"""Set an entry in the recent projects record."""
972-
self._data[path] = {"uuid": puuid, "title": title, "words": words, "time": saved}
982+
self._data[path] = {
983+
"uuid": puuid,
984+
"title": title,
985+
"words": words,
986+
"chars": chars,
987+
"time": saved,
988+
}
973989
if puuid:
974990
self._map[puuid] = path
975991
return

novelwriter/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,10 @@ class nwLabels:
352352
nwStats.WORDS_TEXT: QT_TRANSLATE_NOOP("Stats", "Words in Text"),
353353
nwStats.WORDS_TITLE: QT_TRANSLATE_NOOP("Stats", "Words in Headings"),
354354
}
355+
STATS_DISPLAY: Final[dict[str, str]] = {
356+
nwStats.CHARS: QT_TRANSLATE_NOOP("Stats", "Characters: {0} ({1})"),
357+
nwStats.WORDS: QT_TRANSLATE_NOOP("Stats", "Words: {0} ({1})"),
358+
}
355359
BUILD_FMT: Final[dict[nwBuildFmt, str]] = {
356360
nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
357361
nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),

novelwriter/core/item.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ class NWItem:
5353
"""
5454

5555
__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",
5858
"_order", "_paraCount", "_parent", "_project", "_root", "_status",
59-
"_type", "_wordCount",
59+
"_type", "_wordCount", "_wordInit",
6060
)
6161

6262
def __init__(self, project: NWProject, handle: str) -> None:
@@ -81,7 +81,8 @@ def __init__(self, project: NWProject, handle: str) -> None:
8181
self._wordCount = 0 # Current word count
8282
self._paraCount = 0 # Current paragraph count
8383
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
8586

8687
return
8788

@@ -164,9 +165,13 @@ def wordCount(self) -> int:
164165
def paraCount(self) -> int:
165166
return self._paraCount
166167

168+
@property
169+
def mainCount(self) -> int:
170+
return self._charCount if CONFIG.useCharCount else self._wordCount
171+
167172
@property
168173
def initCount(self) -> int:
169-
return self._initCount
174+
return self._wordInit if CONFIG.useCharCount else self._charInit
170175

171176
@property
172177
def cursorPos(self) -> int:
@@ -257,31 +262,33 @@ def unpack(self, data: dict) -> bool:
257262
self._paraCount = 0
258263
self._cursorPos = 0
259264

260-
self._initCount = self._wordCount
265+
self._wordInit = self._charCount
266+
self._charInit = self._wordCount
261267

262268
return True
263269

264270
@classmethod
265271
def duplicate(cls, source: NWItem, handle: str) -> NWItem:
266272
"""Make a copy of an item."""
267273
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
285292
return new
286293

287294
##

novelwriter/core/itemmodel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def refresh(self) -> None:
166166

167167
def updateCount(self, propagate: bool = True) -> None:
168168
"""Update counts, and propagate upwards in the tree."""
169-
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
170170
self._cache[C_COUNT_TEXT] = f"{self._count:n}"
171171
if propagate and (parent := self._parent):
172172
parent.updateCount()

novelwriter/core/project.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def openProject(self, projPath: str | Path, clearLock: bool = False) -> bool:
367367
# Often, the index needs to be rebuilt when updating format
368368
self._index.rebuild()
369369

370-
self.updateWordCounts()
370+
self.updateCounts()
371371
self._session.startSession()
372372
self.setProjectChanged(False)
373373
self._valid = True
@@ -397,7 +397,7 @@ def saveProject(self, autoSave: bool = False) -> bool:
397397
else:
398398
self._data.incSaveCount()
399399

400-
self.updateWordCounts()
400+
self.updateCounts()
401401
self.countStatus()
402402

403403
xmlWriter = self._storage.getXmlWriter()
@@ -515,10 +515,10 @@ def setProjectChanged(self, status: bool) -> bool:
515515
# Class Methods
516516
##
517517

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)
522522
return
523523

524524
def countStatus(self) -> None:

novelwriter/core/projectdata.py

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ def __init__(self, project: NWProject) -> None:
6666
self._spellLang = None
6767

6868
# 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]
7171
self._lastHandle: dict[str, str | None] = {
7272
"editor": None,
7373
"viewer": None,
@@ -148,18 +148,18 @@ def spellLang(self) -> str | None:
148148
return self._spellLang
149149

150150
@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.
154154
"""
155-
return self._initCounts[0], self._initCounts[1]
155+
return self._initCounts[0], self._initCounts[1], self._initCounts[2], self._initCounts[3]
156156

157157
@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.
161161
"""
162-
return self._currCounts[0], self._currCounts[1]
162+
return self._currCounts[0], self._currCounts[1], self._currCounts[2], self._currCounts[3]
163163

164164
@property
165165
def lastHandle(self) -> dict[str, str | None]:
@@ -301,22 +301,40 @@ def setLastHandles(self, value: dict) -> None:
301301
self._project.setProjectChanged(True)
302302
return
303303

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
312324
return
313325

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)
320338
return
321339

322340
def setAutoReplace(self, value: dict) -> None:

novelwriter/core/projectxml.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
logger = logging.getLogger(__name__)
4747

4848
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
5050
HEX_VERSION = 0x0105
5151

5252
NUM_VERSION = {
@@ -109,6 +109,8 @@ class for items. 2.3 Beta 1.
109109
Rev 3: Added TEMPLATE class. 2.3.
110110
Rev 4: Added shape attribute to status and importance entry
111111
nodes. 2.5.
112+
Rev 5: Added novelChars and notesChars attributes to content
113+
node. 2.7 RC 1.
112114
"""
113115

114116
def __init__(self, path: str | Path) -> None:
@@ -286,9 +288,9 @@ def _parseProjectSettings(self, xSection: ET.Element, data: NWProjectData) -> No
286288
elif xItem.tag == "spellLang": # Changed to spellChecking in 1.5
287289
data.setSpellLang(xItem.text)
288290
elif xItem.tag == "novelWordCount": # Moved to content attribute in 1.5
289-
data.setInitCounts(novel=xItem.text)
291+
data.setInitCounts(wNovel=xItem.text)
290292
elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
291-
data.setInitCounts(notes=xItem.text)
293+
data.setInitCounts(wNotes=xItem.text)
292294

293295
return
294296

@@ -298,8 +300,13 @@ def _parseProjectContent(
298300
"""Parse the content section of the XML file."""
299301
logger.debug("Parsing <content> section")
300302

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+
)
303310

304311
for xItem in xSection:
305312
if xItem.tag != "item":
@@ -527,10 +534,13 @@ def write(self, data: NWProjectData, content: list, saveTime: float, editTime: i
527534
self._packSingleValue(xImport, "entry", label, attrib=attrib)
528535

529536
# Save Tree Content
537+
counts = data.currCounts
530538
contAttr = {
531539
"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]),
534544
}
535545

536546
xContent = ET.SubElement(xRoot, "content", attrib=contAttr)

0 commit comments

Comments
 (0)