Merge branch 'main' into restructure_docs

This commit is contained in:
Veronica Berglyd Olsen
2025-05-24 19:37:34 +02:00
committed by GitHub
62 changed files with 2254 additions and 1517 deletions
+34
View File
@@ -0,0 +1,34 @@
name: LockThreads
on:
schedule:
- cron: "14 3 */3 * *"
permissions:
issues: write
pull-requests: write
concurrency:
group: lock-threads
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: 120
issue-comment: >
This issue has been automatically locked since there has not been
any recent activity after it was closed. Please open a new issue
for related bugs or feature requests.
issue-lock-reason: "resolved"
pr-inactive-days: 120
# pr-comment: >
# This pull request thread has been automatically locked since there
# has not been any recent activity after it was closed. Please open
# an issue for any related bugs or feature requests.
pr-lock-reason: "resolved"
process-only: "issue, pr"
log-output: true
+79
View File
@@ -1,5 +1,84 @@
# novelWriter Changelog
## Version 2.7 RC 1 [2025-05-19]
### Release Notes
This is a release candidate of the next release version, and is intended for testing purposes.
Please be careful when using this version on live writing projects, and make sure you take frequent
backups.
### Detailed Changelog
**New Features**
* It is now possible to set character count as the main statistics displayed on the GUI instead of
word count. The setting is available under the "Appearance" section in Preferences. Issue #1657.
PR #2323.
* Comments can now be marked as "Manuscript Notes" with the `%Note.term:` syntax. The term is a
free form value that is displayed alongside the note when exported to the manuscript. Each term
is also added as a column in exports from the Outline View. Issue #1133. PR #2346.
* A completer drop down menu has been added to comments as well in the editor. It is triggered when
a new line starts with `%`. It will show the options of Synopsis, Short, Story and Note, and when
a period is added after the latter two, show a list of previously used Story and Note keys.
Issue #1784. PR #2290.
**Improvements**
* The dialogue colour of the Snazzy Light syntax theme has changed from yellow to blue, which
should make it easier to read. PR #2336.
* The Snazzy Light theme has also been added as a GUI theme. PR #2334.
* The auto-generated title page for new projects should now make a little more sense in terms of
default values filled in for the author. PR #2333.
* The new project form on the Welcome dialog now remembers the previous author name filled in, so
there should be no need to type it again every time. PR #2333.
* Project name and author name values are now also used for new example projects. PR #2333.
* The narrator break symbol settings for dialogue have been changed from a free text field to a
dropdown list of dashes. This helps to avoid misunderstandings where the field was populated with
other symbols, like quote symbols (see #2320). Issue #2324. PR #2327.
* A splash screen is now shown at startup, providing some information about the initialisation
steps the startup script is running before the GUI is launched. Since loading system fonts can be
quote heavy, this at least gives some user feedback to users running on systems where there is a
significant delay. In particular, there is a long delay when loading fonts with poor Unicode
support, in which case the Qt library will look for replacement glyphs when the symbols are first
encountered. Previously, the delay happened when the Welcome dialog was being drawn, since this
is the first time a non-Ascii Unicode character is used. Now these Unicode characters are probed
when the config is loaded, with progress output on the splash screen. Issue #2315. PR #2316.
**Bugfixes**
* Moving a note between root folders of a different type will now update the class the tags defined
within the note are associated with. This association is used to populate the tag auto-completion
in the editor. Issue #2330. PR #2343.
* The story structure comments now appear under the correct column when exported to CSV from the
Outline View. Issue #2313. PR #2343.
* A new unique ID is now assigned to new example projects. Creating multiple example projects would
previously retain the original ID, causing the Welcome dialog to override them rather than list
them as unique projects. PR #2333.
* In Qt6, or probably in PyQt6, the datetime type from Python was truncated to a date variable when
localising it. This has been fixed by explicitly converting the Python datetime to a QDateTime
before passing it to the PyQt wrapper. This is probably a bug in PyQt6. Issue #2325. PR #2326.
* A missing Remix icon for project copy has been added. Issue #2314. PR #2317.
**Accessibility**
* Trees and lists on the GUI have been assigned names so that screen readers can more easily
identify them. Switches on the GUI have also been associated with their labels so the screen
reader can pick up the label text. Icons on the project tree also have an accessibility label.
Issues #2106 and #2107. PR #2337.
* Switches should now show a highlighted border when they have focus, and the toggle animation also
now works properly when a switch is toggled using the keyboard. This should improve keyboard
navigation. PR #2337.
* The default status and importance labels for new projects now use shapes for icons, to make the
defaults more accessible for people with reduced colour vision. Issue #2332. PR #2333.
**Code Improvements**
* New tests have been added for the custom data models, using the Qt model tester framework that
ensures the models have all the virtual methods implemented. PR #2331.
----
## Version 2.7 Beta 1 [2025-04-20]
### Release Notes
+1291 -1144
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -49,9 +49,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "2.7b1"
__hexversion__ = "0x020700b1"
__date__ = "2025-04-20"
__version__ = "2.7rc1"
__hexversion__ = "0x020700c1"
__date__ = "2025-05-19"
__status__ = "Stable"
__domain__ = "novelwriter.io"
@@ -4,6 +4,7 @@
"Footnotes": "Footnotes",
"Comment": "Comment",
"Story Structure": "Story Structure",
"Note": "Note",
"Notes": "Notes",
"Tag": "Tag",
"Point of View": "Point of View",
+20 -4
View File
@@ -919,9 +919,10 @@ class RecentProjects:
puuid = str(entry.get("uuid", ""))
title = str(entry.get("title", ""))
words = checkInt(entry.get("words", 0), 0)
chars = checkInt(entry.get("chars", 0), 0)
saved = checkInt(entry.get("time", 0), 0)
if path and title:
self._setEntry(puuid, path, title, words, saved)
self._setEntry(puuid, path, title, words, chars, saved)
except Exception:
logger.error("Could not load recent project cache")
logException()
@@ -954,7 +955,14 @@ class RecentProjects:
try:
if (remove := self._map.get(data.uuid)) and (remove != str(path)):
self.remove(remove)
self._setEntry(data.uuid, str(path), data.name, sum(data.currCounts), int(saved))
self._setEntry(
data.uuid,
str(path),
data.name,
sum(data.currCounts[:2]),
sum(data.currCounts[2:]),
int(saved),
)
self.saveCache()
except Exception:
pass
@@ -967,9 +975,17 @@ class RecentProjects:
self.saveCache()
return
def _setEntry(self, puuid: str, path: str, title: str, words: int, saved: int) -> None:
def _setEntry(
self, puuid: str, path: str, title: str, words: int, chars: int, saved: int
) -> None:
"""Set an entry in the recent projects record."""
self._data[path] = {"uuid": puuid, "title": title, "words": words, "time": saved}
self._data[path] = {
"uuid": puuid,
"title": title,
"words": words,
"chars": chars,
"time": saved,
}
if puuid:
self._map[puuid] = path
return
+4
View File
@@ -352,6 +352,10 @@ class nwLabels:
nwStats.WORDS_TEXT: QT_TRANSLATE_NOOP("Stats", "Words in Text"),
nwStats.WORDS_TITLE: QT_TRANSLATE_NOOP("Stats", "Words in Headings"),
}
STATS_DISPLAY: Final[dict[str, str]] = {
nwStats.CHARS: QT_TRANSLATE_NOOP("Stats", "Characters: {0} ({1})"),
nwStats.WORDS: QT_TRANSLATE_NOOP("Stats", "Words: {0} ({1})"),
}
BUILD_FMT: Final[dict[nwBuildFmt, str]] = {
nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
+3 -1
View File
@@ -79,6 +79,7 @@ SETTINGS_TEMPLATE: dict[str, tuple[type, T_BuildValue]] = {
"text.includeSynopsis": (bool, False),
"text.includeComments": (bool, False),
"text.includeStory": (bool, False),
"text.includeNotes": (bool, False),
"text.includeKeywords": (bool, False),
"text.includeBodyText": (bool, True),
"text.ignoredKeywords": (str, ""),
@@ -146,6 +147,7 @@ SETTINGS_LABELS = {
"text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
"text.includeComments": QT_TRANSLATE_NOOP("Builds", "Include Comments"),
"text.includeStory": QT_TRANSLATE_NOOP("Builds", "Include Story Structure"),
"text.includeNotes": QT_TRANSLATE_NOOP("Builds", "Include Manuscript Notes"),
"text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
"text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
"text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
@@ -388,7 +390,7 @@ class BuildSettings:
def setValue(self, key: str, value: T_BuildValue) -> None:
"""Set a specific value for a build setting."""
if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]):
self._changed = value != self._settings[key]
self._changed |= (value != self._settings[key])
self._settings[key] = value
return
+1
View File
@@ -317,6 +317,7 @@ class NWBuildDocument:
bldObj.setCommentType(nwComment.SYNOPSIS, self._build.getBool("text.includeSynopsis"))
bldObj.setCommentType(nwComment.SHORT, self._build.getBool("text.includeSynopsis"))
bldObj.setCommentType(nwComment.STORY, self._build.getBool("text.includeStory"))
bldObj.setCommentType(nwComment.NOTE, self._build.getBool("text.includeNotes"))
if isinstance(bldObj, ToHtml):
bldObj.setStyles(self._build.getBool("html.addStyles"))
+25 -1
View File
@@ -131,6 +131,12 @@ class Index:
self._novelExtra = extra
return
def setItemClass(self, tHandle: str, itemClass: nwItemClass) -> None:
"""Update the class for all tags of a handle."""
logger.info("Updating class for '%s'", tHandle)
self._tagsIndex.updateClass(tHandle, itemClass.name)
return
##
# Public Methods
##
@@ -612,6 +618,10 @@ class Index:
"""Return all story structure keys."""
return self._itemIndex.allStoryKeys()
def getNoteKeys(self) -> set[str]:
"""Return all note comment keys."""
return self._itemIndex.allNoteKeys()
def novelStructure(
self, rootHandle: str | None = None, activeOnly: bool = True
) -> Iterable[tuple[str, str, str, IndexHeading]]:
@@ -854,6 +864,15 @@ class TagsIndex:
x.get("name", "") for x in self._tags.values() if x.get("class", "") == className
]
def updateClass(self, tHandle: str, className: str) -> None:
"""Update the class name of an item. This must be called when a
document moves to another class.
"""
for entry in self._tags.values():
if entry.get("handle") == tHandle:
entry["class"] = className
return
##
# Pack/Unpack
##
@@ -905,11 +924,12 @@ class IndexCache:
which provides lookup capabilities and caching for shared data.
"""
__slots__ = ("story", "tags")
__slots__ = ("note", "story", "tags")
def __init__(self, tagsIndex: TagsIndex) -> None:
self.tags: TagsIndex = tagsIndex
self.story: set[str] = set()
self.note: set[str] = set()
return
@@ -964,6 +984,10 @@ class ItemIndex:
"""Return all story structure keys."""
return self._cache.story.copy()
def allNoteKeys(self) -> set[str]:
"""Return all note comment keys."""
return self._cache.note.copy()
def allItemTags(self, tHandle: str) -> list[str]:
"""Get all tags set for headings of an item."""
if tHandle in self._items:
+4 -1
View File
@@ -316,6 +316,9 @@ class IndexHeading:
case "story" if key:
self._cache.story.add(key)
self._comments[f"story.{key}"] = str(text)
case "note" if key:
self._cache.note.add(key)
self._comments[f"note.{key}"] = str(text)
return
def setTag(self, tag: str) -> None:
@@ -395,7 +398,7 @@ class IndexHeading:
self.addReference(tag, keyword)
else:
raise ValueError("Heading reference contains an invalid keyword")
elif key == "summary" or key.startswith("story"):
elif key == "summary" or key.startswith(("story", "note")):
comment, _, kind = str(key).partition(".")
self.setComment(comment, compact(kind), str(entry))
else:
+35 -24
View File
@@ -53,10 +53,10 @@ class NWItem:
"""
__slots__ = (
"_active", "_charCount", "_class", "_cursorPos", "_expanded",
"_handle", "_heading", "_import", "_initCount", "_layout", "_name",
"_active", "_charCount", "_charInit", "_class", "_cursorPos",
"_expanded", "_handle", "_heading", "_import", "_layout", "_name",
"_order", "_paraCount", "_parent", "_project", "_root", "_status",
"_type", "_wordCount",
"_type", "_wordCount", "_wordInit",
)
def __init__(self, project: NWProject, handle: str) -> None:
@@ -81,7 +81,8 @@ class NWItem:
self._wordCount = 0 # Current word count
self._paraCount = 0 # Current paragraph count
self._cursorPos = 0 # Last cursor position
self._initCount = 0 # Initial word count
self._wordInit = 0 # Initial character count
self._charInit = 0 # Initial word count
return
@@ -164,9 +165,13 @@ class NWItem:
def paraCount(self) -> int:
return self._paraCount
@property
def mainCount(self) -> int:
return self._charCount if CONFIG.useCharCount else self._wordCount
@property
def initCount(self) -> int:
return self._initCount
return self._wordInit if CONFIG.useCharCount else self._charInit
@property
def cursorPos(self) -> int:
@@ -257,7 +262,8 @@ class NWItem:
self._paraCount = 0
self._cursorPos = 0
self._initCount = self._wordCount
self._wordInit = self._charCount
self._charInit = self._wordCount
return True
@@ -265,23 +271,24 @@ class NWItem:
def duplicate(cls, source: NWItem, handle: str) -> NWItem:
"""Make a copy of an item."""
new = cls(source._project, handle)
new._name = source._name
new._parent = source._parent
new._root = source._root
new._order = source._order
new._type = source._type
new._class = source._class
new._layout = source._layout
new._status = source._status
new._import = source._import
new._active = source._active
new._expanded = source._expanded
new._heading = source._heading
new._charCount = source._charCount
new._wordCount = source._wordCount
new._paraCount = source._paraCount
new._cursorPos = source._cursorPos
new._initCount = source._initCount
new._name = source._name
new._parent = source._parent
new._root = source._root
new._order = source._order
new._type = source._type
new._class = source._class
new._layout = source._layout
new._status = source._status
new._import = source._import
new._active = source._active
new._expanded = source._expanded
new._heading = source._heading
new._charCount = source._charCount
new._wordCount = source._wordCount
new._paraCount = source._paraCount
new._cursorPos = source._cursorPos
new._wordInit = source._wordInit
new._charInit = source._charInit
return new
##
@@ -428,7 +435,11 @@ class NWItem:
"""
if self._parent is not None:
# Only update for child items
self.setClass(itemClass)
if itemClass != self._class:
self.setClass(itemClass)
if self._type == nwItemType.FILE:
# Notify the index of the class change
self._project.index.setItemClass(self._handle, itemClass)
if self._layout == nwItemLayout.NO_LAYOUT:
# If no layout is set, pick one
+1 -1
View File
@@ -166,7 +166,7 @@ class ProjectNode:
def updateCount(self, propagate: bool = True) -> None:
"""Update counts, and propagate upwards in the tree."""
self._count = self._item.wordCount + sum(c._count for c in self._children) # noqa: SLF001
self._count = self._item.mainCount + sum(c._count for c in self._children) # noqa: SLF001
self._cache[C_COUNT_TEXT] = f"{self._count:n}"
if propagate and (parent := self._parent):
parent.updateCount()
+6 -6
View File
@@ -367,7 +367,7 @@ class NWProject:
# Often, the index needs to be rebuilt when updating format
self._index.rebuild()
self.updateWordCounts()
self.updateCounts()
self._session.startSession()
self.setProjectChanged(False)
self._valid = True
@@ -397,7 +397,7 @@ class NWProject:
else:
self._data.incSaveCount()
self.updateWordCounts()
self.updateCounts()
self.countStatus()
xmlWriter = self._storage.getXmlWriter()
@@ -515,10 +515,10 @@ class NWProject:
# Class Methods
##
def updateWordCounts(self) -> None:
"""Update the total word count values."""
novel, notes = self._tree.sumWords()
self._data.setCurrCounts(novel=novel, notes=notes)
def updateCounts(self) -> None:
"""Update the total word and character count values."""
wNovel, wNotes, cNovel, cNotes = self._tree.sumCounts()
self._data.setCurrCounts(wNovel=wNovel, wNotes=wNotes, cNovel=cNovel, cNotes=cNotes)
return
def countStatus(self) -> None:
+42 -24
View File
@@ -66,8 +66,8 @@ class NWProjectData:
self._spellLang = None
# Project Dictionaries
self._initCounts = [0, 0]
self._currCounts = [0, 0]
self._initCounts = [0, 0, 0, 0]
self._currCounts = [0, 0, 0, 0]
self._lastHandle: dict[str, str | None] = {
"editor": None,
"viewer": None,
@@ -148,18 +148,18 @@ class NWProjectData:
return self._spellLang
@property
def initCounts(self) -> tuple[int, int]:
"""Return the initial count of words for novel and note
documents.
def initCounts(self) -> tuple[int, int, int, int]:
"""Return the initial count of words and characters for novel
and note documents.
"""
return self._initCounts[0], self._initCounts[1]
return self._initCounts[0], self._initCounts[1], self._initCounts[2], self._initCounts[3]
@property
def currCounts(self) -> tuple[int, int]:
"""Return the current count of words for novel and note
documents.
def currCounts(self) -> tuple[int, int, int, int]:
"""Return the current count of words and characters for novel
and note documents.
"""
return self._currCounts[0], self._currCounts[1]
return self._currCounts[0], self._currCounts[1], self._currCounts[2], self._currCounts[3]
@property
def lastHandle(self) -> dict[str, str | None]:
@@ -301,22 +301,40 @@ class NWProjectData:
self._project.setProjectChanged(True)
return
def setInitCounts(self, novel: Any = None, notes: Any = None) -> None:
"""Set the word count totals for novel and note files."""
if novel is not None:
self._initCounts[0] = checkInt(novel, 0)
self._currCounts[0] = checkInt(novel, 0)
if notes is not None:
self._initCounts[1] = checkInt(notes, 0)
self._currCounts[1] = checkInt(notes, 0)
def setInitCounts(
self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None
) -> None:
"""Set the count totals for novel and note files."""
if wNovel is not None:
count = checkInt(wNovel, 0)
self._initCounts[0] = count
self._currCounts[0] = count
if wNotes is not None:
count = checkInt(wNotes, 0)
self._initCounts[1] = count
self._currCounts[1] = count
if cNovel is not None:
count = checkInt(cNovel, 0)
self._initCounts[2] = count
self._currCounts[2] = count
if cNotes is not None:
count = checkInt(cNotes, 0)
self._initCounts[3] = count
self._currCounts[3] = count
return
def setCurrCounts(self, novel: Any = None, notes: Any = None) -> None:
"""Set the word count totals for novel and note files."""
if novel is not None:
self._currCounts[0] = checkInt(novel, 0)
if notes is not None:
self._currCounts[1] = checkInt(notes, 0)
def setCurrCounts(
self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None
) -> None:
"""Set the count totals for novel and note files."""
if wNovel is not None:
self._currCounts[0] = checkInt(wNovel, 0)
if wNotes is not None:
self._currCounts[1] = checkInt(wNotes, 0)
if cNovel is not None:
self._currCounts[2] = checkInt(cNovel, 0)
if cNotes is not None:
self._currCounts[3] = checkInt(cNotes, 0)
return
def setAutoReplace(self, value: dict) -> None:
+17 -7
View File
@@ -46,7 +46,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
FILE_VERSION = "1.5" # The current project file format version
FILE_REVISION = "4" # The current project file format revision
FILE_REVISION = "5" # The current project file format revision
HEX_VERSION = 0x0105
NUM_VERSION = {
@@ -109,6 +109,8 @@ class ProjectXMLReader:
Rev 3: Added TEMPLATE class. 2.3.
Rev 4: Added shape attribute to status and importance entry
nodes. 2.5.
Rev 5: Added novelChars and notesChars attributes to content
node. 2.7 RC 1.
"""
def __init__(self, path: str | Path) -> None:
@@ -286,9 +288,9 @@ class ProjectXMLReader:
elif xItem.tag == "spellLang": # Changed to spellChecking in 1.5
data.setSpellLang(xItem.text)
elif xItem.tag == "novelWordCount": # Moved to content attribute in 1.5
data.setInitCounts(novel=xItem.text)
data.setInitCounts(wNovel=xItem.text)
elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
data.setInitCounts(notes=xItem.text)
data.setInitCounts(wNotes=xItem.text)
return
@@ -298,8 +300,13 @@ class ProjectXMLReader:
"""Parse the content section of the XML file."""
logger.debug("Parsing <content> section")
data.setInitCounts(novel=xSection.attrib.get("novelWords", None)) # Moved in 1.5
data.setInitCounts(notes=xSection.attrib.get("notesWords", None)) # Moved in 1.5
# Moved in 1.5
data.setInitCounts(
wNovel=xSection.attrib.get("novelWords", None),
wNotes=xSection.attrib.get("notesWords", None),
cNovel=xSection.attrib.get("novelChars", None),
cNotes=xSection.attrib.get("notesChars", None),
)
for xItem in xSection:
if xItem.tag != "item":
@@ -527,10 +534,13 @@ class ProjectXMLWriter:
self._packSingleValue(xImport, "entry", label, attrib=attrib)
# Save Tree Content
counts = data.currCounts
contAttr = {
"items": str(len(content)),
"novelWords": str(data.currCounts[0]),
"notesWords": str(data.currCounts[1]),
"novelWords": str(counts[0]),
"notesWords": str(counts[1]),
"novelChars": str(counts[2]),
"notesChars": str(counts[3]),
}
xContent = ET.SubElement(xRoot, "content", attrib=contAttr)
+29 -13
View File
@@ -79,29 +79,36 @@ class NWSessionLog:
return False
now = time()
iNovel, iNotes = self._project.data.initCounts
cNovel, cNotes = self._project.data.currCounts
iTotal = iNovel + iNotes
wDiff = cNovel + cNotes - iTotal
iWNovel, iWNotes, iCNovel, iCNotes = self._project.data.initCounts
cWNovel, cWNotes, cCNovel, cCNotes = self._project.data.currCounts
iWTotal = iWNovel + iWNotes
iCTotal = iCNovel + iCNotes
wDiff = cWNovel + cWNotes - iWTotal
cDiff = cCNovel + cCNotes - iCTotal
sTime = now - self._start
logger.info("The session lasted %d sec and added %d words", int(sTime), wDiff)
if sTime < 300 and wDiff == 0:
logger.info(
"The session lasted %d sec and added %d words abd %d characters",
int(sTime), wDiff, cDiff
)
if sTime < 300 and (wDiff == 0 or cDiff == 0):
logger.info("Session too short, skipping log entry")
return False
try:
if not sessFile.exists():
with open(sessFile, mode="w", encoding="utf-8") as fObj:
fObj.write(self.createInitial(iTotal))
fObj.write(self.createInitial(iWTotal))
with open(sessFile, mode="a+", encoding="utf-8") as fObj:
fObj.write(self.createRecord(
start=formatTimeStamp(self._start),
end=formatTimeStamp(now),
novel=cNovel,
notes=cNotes,
idle=round(idleTime)
novel=cWNovel,
notes=cWNotes,
idle=round(idleTime),
cnovel=cCNovel,
cnotes=cCNotes,
))
except Exception:
@@ -129,10 +136,19 @@ class NWSessionLog:
data = json.dumps({"type": "initial", "offset": total})
return f"{data}\n"
def createRecord(self, start: str, end: str, novel: int, notes: int, idle: int) -> str:
def createRecord(
self, start: str, end: str, novel: int, notes: int, idle: int,
cnovel: int = 0, cnotes: int = 0,
) -> str:
"""Low level function to create a log record."""
data = json.dumps({
"type": "record", "start": start, "end": end,
"novel": novel, "notes": notes, "idle": idle,
"type": "record",
"start": start,
"end": end,
"novel": novel,
"notes": notes,
"cnovel": cnovel,
"cnotes": cnotes,
"idle": idle,
})
return f"{data}\n"
+9 -5
View File
@@ -410,16 +410,20 @@ class NWTree:
return True
def sumWords(self) -> tuple[int, int]:
"""Loop over all entries and add up the word counts."""
noteWords = 0
def sumCounts(self) -> tuple[int, int, int, int]:
"""Loop over all entries and add up the word and char counts."""
novelWords = 0
notesWords = 0
novelChars = 0
notesChars = 0
for item in self._items.values():
if item.itemLayout == nwItemLayout.NOTE:
noteWords += item.wordCount
notesWords += item.wordCount
notesChars += item.charCount
elif item.itemLayout == nwItemLayout.DOCUMENT:
novelWords += item.wordCount
return novelWords, noteWords
novelChars += item.charCount
return novelWords, notesWords, novelChars, notesChars
##
# Tree Item Methods
+20 -9
View File
@@ -226,6 +226,14 @@ class GuiPreferences(NDialog):
self.tr("Turn off to use the Qt font dialog, which may have more options.")
)
# Use Character Count
self.useCharCount = NSwitch(self)
self.useCharCount.setChecked(CONFIG.useCharCount)
self.mainForm.addRow(
self.tr("Prefer character count over word count"), self.useCharCount,
self.tr("Display character count instead where available.")
)
# Document Style
# ==============
@@ -957,21 +965,24 @@ class GuiPreferences(NDialog):
refreshTree = False
# Appearance
guiLocale = self.guiLocale.currentData()
guiTheme = self.guiTheme.currentData()
iconTheme = self.iconTheme.currentData()
guiLocale = self.guiLocale.currentData()
guiTheme = self.guiTheme.currentData()
iconTheme = self.iconTheme.currentData()
useCharCount = self.useCharCount.isChecked()
updateTheme |= CONFIG.guiTheme != guiTheme
updateTheme |= CONFIG.iconTheme != iconTheme
needsRestart |= CONFIG.guiLocale != guiLocale
needsRestart |= CONFIG.guiFont != self._guiFont
refreshTree |= CONFIG.useCharCount != useCharCount
CONFIG.guiLocale = guiLocale
CONFIG.guiTheme = guiTheme
CONFIG.iconTheme = iconTheme
CONFIG.hideVScroll = self.hideVScroll.isChecked()
CONFIG.hideHScroll = self.hideHScroll.isChecked()
CONFIG.nativeFont = self.nativeFont.isChecked()
CONFIG.guiLocale = guiLocale
CONFIG.guiTheme = guiTheme
CONFIG.iconTheme = iconTheme
CONFIG.hideVScroll = self.hideVScroll.isChecked()
CONFIG.hideHScroll = self.hideHScroll.isChecked()
CONFIG.nativeFont = self.nativeFont.isChecked()
CONFIG.useCharCount = useCharCount
CONFIG.setGuiFont(self._guiFont)
# Document Style
+2 -1
View File
@@ -614,7 +614,8 @@ class Tokenizer(ABC):
tStyle |= BlockFmt.JUSTIFY
if cStyle in (
nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN, nwComment.STORY
nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN,
nwComment.STORY, nwComment.NOTE,
):
bStyle = COMMENT_STYLE[cStyle]
tLine, tFmt = self._formatComment(bStyle, cKey, cText)
+98 -40
View File
@@ -3,15 +3,16 @@ novelWriter GUI Document Editor
=================================
File History:
Created: 2018-09-29 [0.0.1] GuiDocEditor
Created: 2019-04-22 [0.0.1] BackgroundWordCounter
Created: 2019-09-29 [0.2.1] GuiDocEditSearch
Created: 2020-04-25 [0.4.5] GuiDocEditHeader
Rewritten: 2020-06-15 [0.9] GuiDocEditSearch
Created: 2020-06-27 [0.10] GuiDocEditFooter
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
Created: 2023-11-06 [2.2b1] MetaCompleter
Created: 2023-11-07 [2.2b1] GuiDocToolBar
Created: 2018-09-29 [0.0.1] GuiDocEditor
Created: 2019-04-22 [0.0.1] BackgroundWordCounter
Created: 2019-09-29 [0.2.1] GuiDocEditSearch
Created: 2020-04-25 [0.4.5] GuiDocEditHeader
Rewritten: 2020-06-15 [0.9] GuiDocEditSearch
Created: 2020-06-27 [0.10] GuiDocEditFooter
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
Created: 2023-11-06 [2.2b1] MetaCompleter
Created: 2023-11-07 [2.2b1] GuiDocToolBar
Extended: 2025-05-18 [2.7rc1] CommandCompleter
This file is a part of novelWriter
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
@@ -55,7 +56,9 @@ from novelwriter import CONFIG, SHARED
from novelwriter.common import (
decodeMimeHandles, fontMatcher, minmax, qtAddAction, qtLambda, transferCase
)
from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
from novelwriter.constants import (
nwConst, nwKeyWords, nwLabels, nwShortcode, nwStats, nwUnicode, trStats
)
from novelwriter.core.document import NWDocument
from novelwriter.enum import (
nwChange, nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass,
@@ -147,7 +150,7 @@ class GuiDocEditor(QPlainTextEdit):
self._autoReplace = TextAutoReplace()
# Completer
self._completer = MetaCompleter(self)
self._completer = CommandCompleter(self)
self._completer.complete.connect(self._insertCompletion)
# Create Custom Document
@@ -321,6 +324,7 @@ class GuiDocEditor(QPlainTextEdit):
"""
# Auto-Replace
self._autoReplace.initSettings()
self.docFooter.initSettings()
# Reload spell check and dictionaries
SHARED.updateSpellCheckLanguage()
@@ -1076,13 +1080,16 @@ class GuiDocEditor(QPlainTextEdit):
if (block := self._qDocument.findBlock(pos)).isValid():
text = block.text()
if text.startswith("@") and added + removed == 1:
if text and text[0] in "@%" and added + removed == 1:
# Only run on single character changes, or it will trigger
# at unwanted times when other changes are made to the document
cursor = self.textCursor()
bPos = cursor.positionInBlock()
if bPos > 0 and (viewport := self.viewport()):
show = self._completer.updateText(text, bPos)
if text[0] == "@":
show = self._completer.updateMetaText(text, bPos)
else:
show = self._completer.updateCommentText(text, bPos)
point = self.cursorRect().bottomRight()
self._completer.move(viewport.mapToGlobal(point))
self._completer.setVisible(show)
@@ -1233,7 +1240,8 @@ class GuiDocEditor(QPlainTextEdit):
"""Process the word counter's finished signal."""
if self._docHandle and self._nwItem:
logger.debug("Updating word count")
needsRefresh = wCount != self._nwItem.wordCount
mCount = cCount if CONFIG.useCharCount else wCount
needsRefresh = mCount != self._nwItem.mainCount
self._nwItem.setCharCount(cCount)
self._nwItem.setWordCount(wCount)
self._nwItem.setParaCount(pCount)
@@ -1241,7 +1249,7 @@ class GuiDocEditor(QPlainTextEdit):
self._nwItem.notifyToRefresh()
if not self.textCursor().hasSelection():
# Selection counter should take precedence (#2155)
self.docFooter.updateWordCount(wCount, False)
self.docFooter.updateMainCount(mCount, False)
return
@pyqtSlot()
@@ -1254,7 +1262,7 @@ class GuiDocEditor(QPlainTextEdit):
self._timerSel.start()
else:
self._timerSel.stop()
self.docFooter.updateWordCount(0, False)
self.docFooter.updateMainCount(0, False)
return
@pyqtSlot()
@@ -1271,8 +1279,7 @@ class GuiDocEditor(QPlainTextEdit):
def _updateSelCounts(self, cCount: int, wCount: int, pCount: int) -> None:
"""Update the counts on the counter's finished signal."""
if self._docHandle and self._nwItem:
logger.debug("User selected %d words", wCount)
self.docFooter.updateWordCount(wCount, True)
self.docFooter.updateMainCount(cCount if CONFIG.useCharCount else wCount, True)
self._timerSel.stop()
return
@@ -2070,13 +2077,13 @@ class GuiDocEditor(QPlainTextEdit):
return
class MetaCompleter(QMenu):
"""GuiWidget: Meta Completer Menu
class CommandCompleter(QMenu):
"""GuiWidget: Command Completer Menu
This is a context menu with options populated from the user's
defined tags. It also helps to type the meta data keyword on a new
line starting with an @. The updateText function should be called on
every keystroke on a line starting with @.
defined tags and keys. It also helps to type the meta data keyword
on a new line starting with @ or %. The update functions should be
called on every keystroke on a line starting with @ or %.
"""
complete = pyqtSignal(int, int, str)
@@ -2085,7 +2092,7 @@ class MetaCompleter(QMenu):
super().__init__(parent=parent)
return
def updateText(self, text: str, pos: int) -> bool:
def updateMetaText(self, text: str, pos: int) -> bool:
"""Update the menu options based on the line of text."""
self.clear()
kw, sep, _ = text.partition(":")
@@ -2093,7 +2100,7 @@ class MetaCompleter(QMenu):
offset = 0
length = len(kw.rstrip())
suffix = "" if sep else ":"
options = list(filter(
options = sorted(filter(
lambda x: x.startswith(kw.rstrip()), nwKeyWords.VALID_KEYS
))
else:
@@ -2105,7 +2112,7 @@ class MetaCompleter(QMenu):
offset = tPos[index] if lookup else pos
length = len(lookup)
suffix = ""
options = list(filter(
options = sorted(filter(
lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
nwKeyWords.KEY_CLASS.get(kw.strip())
)
@@ -2114,13 +2121,57 @@ class MetaCompleter(QMenu):
if not options:
return False
for value in sorted(options):
for value in options:
rep = value + suffix
action = qtAddAction(self, value)
action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
return True
def updateCommentText(self, text: str, pos: int) -> bool:
"""Update the menu options based on the line of text."""
self.clear()
cmd, sep, _ = text.partition(":")
if pos <= len(cmd):
clean = text[1:].lstrip()[:6].lower()
if clean[:6] == "story.":
pre, _, key = cmd.partition(".")
offset = len(pre) + 1
length = len(key)
suffix = "" if sep else ": "
options = sorted(filter(
lambda x: x.startswith(key.rstrip()),
SHARED.project.index.getStoryKeys(),
))
elif clean[:5] == "note.":
pre, _, key = cmd.partition(".")
offset = len(pre) + 1
length = len(key)
suffix = "" if sep else ": "
options = sorted(filter(
lambda x: x.startswith(key.rstrip()),
SHARED.project.index.getNoteKeys(),
))
elif pos < 12:
offset = 0
length = len(cmd.rstrip())
suffix = ""
options = list(filter(
lambda x: x.startswith(cmd.rstrip()),
["%Synopsis: ", "%Short: ", "%Story", "%Note"],
))
else:
return False
if options:
for value in options:
rep = value + suffix
action = qtAddAction(self, rep.rstrip(":. "))
action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
return True
return False
##
# Events
##
@@ -3045,9 +3096,9 @@ class GuiDocEditFooter(QWidget):
fPx = int(0.9*SHARED.theme.fontPixelSize)
# Cached Translations
self.initSettings()
self._trLineCount = self.tr("Line: {0} ({1})")
self._trWordCount = self.tr("Words: {0} ({1})")
self._trSelectCount = self.tr("Words: {0} selected")
self._trSelectCount = self.tr("Selected: {0}")
# Main Widget Settings
self.setContentsMargins(0, 0, 0, 0)
@@ -3108,7 +3159,7 @@ class GuiDocEditFooter(QWidget):
self.updateTheme()
# Initialise Info
self.updateWordCount(0, False)
self.updateMainCount(0, False)
logger.debug("Ready: GuiDocEditFooter")
@@ -3118,6 +3169,13 @@ class GuiDocEditFooter(QWidget):
# Methods
##
def initSettings(self) -> None:
"""Apply user settings."""
self._trMainCount = trStats(nwLabels.STATS_DISPLAY[
nwStats.CHARS if CONFIG.useCharCount else nwStats.WORDS
])
return
def updateFont(self) -> None:
"""Update the font settings."""
self.setFont(SHARED.theme.guiFont)
@@ -3162,7 +3220,7 @@ class GuiDocEditFooter(QWidget):
self._tItem = SHARED.project.tree[self._docHandle]
self.updateInfo()
self.updateWordCount(0, False)
self.updateMainCount(0, False)
return
@@ -3193,15 +3251,15 @@ class GuiDocEditFooter(QWidget):
)
return
def updateWordCount(self, wCount: int, selection: bool) -> None:
"""Update word counter information."""
if selection and wCount:
wText = self._trSelectCount.format(f"{wCount:n}")
def updateMainCount(self, count: int, selection: bool) -> None:
"""Update main counter information."""
if selection and count:
text = self._trSelectCount.format(f"{count:n}")
elif self._tItem:
wCount = self._tItem.wordCount
wDiff = wCount - self._tItem.initCount
wText = self._trWordCount.format(f"{wCount:n}", f"{wDiff:+n}")
count = self._tItem.mainCount
diff = count - self._tItem.initCount
text = self._trMainCount.format(f"{count:n}", f"{diff:+n}")
else:
wText = self._trWordCount.format("0", "+0")
self.wordsText.setText(wText)
text = self._trMainCount.format("0", "+0")
self.wordsText.setText(text)
return
+10 -5
View File
@@ -80,6 +80,7 @@ class GuiNovelView(QWidget):
# Function Mappings
self.setActive = self.novelBar.setActive
self.getSelectedHandle = self.novelTree.getSelectedHandle
self.refreshCurrentTree = self.novelBar.forceRefreshNovelTree
return
@@ -209,7 +210,7 @@ class GuiNovelToolBar(QWidget):
# Refresh Button
self.tbRefresh = NIconToolButton(self, iSz)
self.tbRefresh.setToolTip(self.tr("Refresh"))
self.tbRefresh.clicked.connect(self._forceRefreshNovelTree)
self.tbRefresh.clicked.connect(self.forceRefreshNovelTree)
# More Options Menu
self.mMore = QMenu(self)
@@ -274,7 +275,7 @@ class GuiNovelToolBar(QWidget):
self.novelValue.updateTheme()
self.tbNovel.setVisible(self.novelValue.count() > 1)
self._forceRefreshNovelTree()
self.forceRefreshNovelTree()
return
@@ -306,7 +307,7 @@ class GuiNovelToolBar(QWidget):
self.aLastCol[colType].setChecked(True)
self.novelView.novelTree.setLastColType(colType)
if doRefresh:
self._forceRefreshNovelTree()
self.forceRefreshNovelTree()
self.novelView.novelTree.resizeColumns()
return
@@ -324,11 +325,11 @@ class GuiNovelToolBar(QWidget):
return
##
# Private Slots
# Public Slots
##
@pyqtSlot()
def _forceRefreshNovelTree(self) -> None:
def forceRefreshNovelTree(self) -> None:
"""Rebuild the current tree."""
if tHandle := self.novelValue.handle:
self.novelView.setCurrentNovel(tHandle)
@@ -336,6 +337,10 @@ class GuiNovelToolBar(QWidget):
self._refresh[tHandle] = False
return
##
# Private Slots
##
@pyqtSlot(str)
def _refreshNovelTree(self, tHandle: str) -> None:
"""Refresh or schedule refresh of a novel tree."""
+9 -1
View File
@@ -751,9 +751,13 @@ class GuiOutlineTree(QTreeWidget):
def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]:
"""Dump all novel data into a table."""
sLabel = SHARED.project.localLookup("Story Structure")
nLabel = SHARED.project.localLookup("Note")
sKeys = sorted(SHARED.project.index.getStoryKeys())
nKeys = sorted(SHARED.project.index.getNoteKeys())
sMatch = [f"story.{k}" for k in sKeys]
nMatch = [f"note.{k}" for k in nKeys]
sHeaders = [f"{sLabel} ({k})" for k in sKeys]
nHeaders = [f"{nLabel} ({k})" for k in nKeys]
data: list[list[str | int]] = [[
"H",
@@ -777,6 +781,7 @@ class GuiOutlineTree(QTreeWidget):
trConst(nwLabels.OUTLINE_COLS[nwOutline.MENTION]),
trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP]),
*sHeaders,
*nHeaders,
]]
for _, tHandle, sTitle, novIdx in SHARED.project.index.novelStructure(
@@ -784,7 +789,9 @@ class GuiOutlineTree(QTreeWidget):
):
if novIdx.level != "H0" and (nwItem := SHARED.project.tree[tHandle]):
refs = SHARED.project.index.getReferences(tHandle, sTitle)
story = {k: v for k, v in novIdx.comments.items() if k in sMatch}.values()
comments = dict(novIdx.comments.items())
story = [comments.get(k, "") for k in sMatch]
notes = [comments.get(k, "") for k in nMatch]
data.append([
novIdx.level,
novIdx.title,
@@ -807,6 +814,7 @@ class GuiOutlineTree(QTreeWidget):
", ".join(refs[nwKeyWords.MENTION_KEY]),
novIdx.synopsis,
*story,
*notes,
])
return data
+14 -6
View File
@@ -33,7 +33,7 @@ from PyQt6.QtWidgets import QApplication, QLabel, QStatusBar, QWidget
from novelwriter import CONFIG, SHARED
from novelwriter.common import formatTime
from novelwriter.constants import nwConst
from novelwriter.constants import nwConst, nwLabels, nwStats, trStats
from novelwriter.extensions.modified import NClickableLabel
from novelwriter.extensions.statusled import StatusLED
@@ -108,11 +108,22 @@ class GuiMainStatus(QStatusBar):
logger.debug("Ready: GuiMainStatus")
self.initSettings()
self.updateTheme()
self.clearStatus()
return
def initSettings(self) -> None:
"""Apply user settings."""
if CONFIG.useCharCount:
self._trStatsCount = trStats(nwLabels.STATS_DISPLAY[nwStats.CHARS])
self._trStatsTip = self.tr("Total character count (session change)")
else:
self._trStatsCount = trStats(nwLabels.STATS_DISPLAY[nwStats.WORDS])
self._trStatsTip = self.tr("Total word count (session change)")
return
def clearStatus(self) -> None:
"""Reset all widgets on the status bar to default values."""
self.setRefTime(-1.0)
@@ -173,11 +184,8 @@ class GuiMainStatus(QStatusBar):
def setProjectStats(self, pWC: int, sWC: int) -> None:
"""Update the current project statistics."""
self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}"))
if CONFIG.incNotesWCount:
self.statsText.setToolTip(self.tr("Project word count (session change)"))
else:
self.statsText.setToolTip(self.tr("Novel word count (session change)"))
self.statsText.setText(self._trStatsCount.format(f"{pWC:n}", f"{sWC:+n}"))
self.statsText.setToolTip(self._trStatsTip)
return
def updateTime(self, idleTime: float = 0.0) -> None:
+19 -8
View File
@@ -1048,8 +1048,10 @@ class GuiMain(QMainWindow):
self.initMain()
self.saveDocument()
if tree:
if tree and not theme:
# These are also updated by a theme refresh
SHARED.project.tree.refreshAllItems()
self.novelView.refreshCurrentTree()
if theme:
SHARED.theme.loadTheme()
@@ -1075,6 +1077,7 @@ class GuiMain(QMainWindow):
self.projView.initSettings()
self.novelView.initSettings()
self.outlineView.initSettings()
self.mainStatus.initSettings()
# Force update of word count
self._lastTotalCount = 0
@@ -1261,15 +1264,23 @@ class GuiMain(QMainWindow):
if self._lastTotalCount != currentTotalCount:
self._lastTotalCount = currentTotalCount
SHARED.project.updateWordCounts()
SHARED.project.updateCounts()
if CONFIG.incNotesWCount:
iTotal = sum(SHARED.project.data.initCounts)
cTotal = sum(SHARED.project.data.currCounts)
self.mainStatus.setProjectStats(cTotal, cTotal - iTotal)
if CONFIG.useCharCount:
iTotal = sum(SHARED.project.data.initCounts[2:])
cTotal = sum(SHARED.project.data.currCounts[2:])
else:
iTotal = sum(SHARED.project.data.initCounts[:2])
cTotal = sum(SHARED.project.data.currCounts[:2])
else:
iNovel, _ = SHARED.project.data.initCounts
cNovel, _ = SHARED.project.data.currCounts
self.mainStatus.setProjectStats(cNovel, cNovel - iNovel)
if CONFIG.useCharCount:
iTotal = SHARED.project.data.initCounts[2]
cTotal = SHARED.project.data.currCounts[2]
else:
iTotal = SHARED.project.data.initCounts[0]
cTotal = SHARED.project.data.currCounts[0]
self.mainStatus.setProjectStats(cTotal, cTotal - iTotal)
return
+1 -1
View File
@@ -630,7 +630,7 @@ class _DetailsWidget(QWidget):
self.listView.addTopLevelItem(item)
for key in [
"text.includeSynopsis", "text.includeComments", "text.includeStory",
"text.includeKeywords", "text.includeBodyText",
"text.includeNotes", "text.includeKeywords", "text.includeBodyText",
]:
sub = QTreeWidgetItem()
sub.setText(0, build.getLabel(key))
+18 -7
View File
@@ -80,7 +80,8 @@ class GuiBuildSettings(NToolDialog):
logger.debug("Create: GuiBuildSettings")
self.setObjectName("GuiBuildSettings")
self._build = build
# Make a copy of the build object
self._build = BuildSettings.fromDict(build.pack())
self.setWindowTitle(self.tr("Manuscript Build Settings"))
self.setMinimumSize(700, 400)
@@ -184,6 +185,7 @@ class GuiBuildSettings(NToolDialog):
settings.
"""
logger.debug("Closing: GuiBuildSettings")
self._applyChanges()
self._askToSaveBuild()
self._saveSettings()
event.accept()
@@ -211,11 +213,14 @@ class GuiBuildSettings(NToolDialog):
"""Handle button clicks from the dialog button box."""
role = self.buttonBox.buttonRole(button)
if role == QtRoleApply:
self._applyChanges()
self._emitBuildData()
elif role == QtRoleAccept:
self._applyChanges()
self._emitBuildData()
self.close()
elif role == QtRoleReject:
self._build.resetChangedState()
self.close()
return
@@ -228,10 +233,9 @@ class GuiBuildSettings(NToolDialog):
whether the user wants to save them.
"""
if self._build.changed:
response = SHARED.question(self.tr(
if SHARED.question(self.tr(
"Do you want to save your changes to '{0}'?"
).format(self._build.name))
if response:
).format(self._build.name)):
self._emitBuildData()
self._build.resetChangedState()
return
@@ -246,14 +250,17 @@ class GuiBuildSettings(NToolDialog):
pOptions.setValue("GuiBuildSettings", "treeWidth", treeWidth)
pOptions.setValue("GuiBuildSettings", "filterWidth", filterWidth)
pOptions.saveSettings()
return
def _applyChanges(self) -> None:
"""Apply all settings changes to the build object."""
self._build.setName(self.editBuildName.text())
self.optTabHeadings.saveContent()
self.optTabFormatting.saveContent()
return
def _emitBuildData(self) -> None:
"""Assemble the build data and emit the signal."""
self._build.setName(self.editBuildName.text())
self.optTabHeadings.saveContent()
self.optTabFormatting.saveContent()
self.newSettingsReady.emit(self._build)
self._build.resetChangedState()
return
@@ -974,12 +981,14 @@ class _FormattingTab(NScrollableForm):
self.incSynopsis = NSwitch(self, height=iPx)
self.incComments = NSwitch(self, height=iPx)
self.incStory = NSwitch(self, height=iPx)
self.incNotes = NSwitch(self, height=iPx)
self.incKeywords = NSwitch(self, height=iPx)
self.addRow(self._build.getLabel("text.includeBodyText"), self.incBodyText)
self.addRow(self._build.getLabel("text.includeSynopsis"), self.incSynopsis)
self.addRow(self._build.getLabel("text.includeComments"), self.incComments)
self.addRow(self._build.getLabel("text.includeStory"), self.incStory)
self.addRow(self._build.getLabel("text.includeNotes"), self.incNotes)
self.addRow(self._build.getLabel("text.includeKeywords"), self.incKeywords)
# Ignored Keywords
@@ -1288,6 +1297,7 @@ class _FormattingTab(NScrollableForm):
self.incSynopsis.setChecked(self._build.getBool("text.includeSynopsis"))
self.incComments.setChecked(self._build.getBool("text.includeComments"))
self.incStory.setChecked(self._build.getBool("text.includeStory"))
self.incNotes.setChecked(self._build.getBool("text.includeNotes"))
self.incKeywords.setChecked(self._build.getBool("text.includeKeywords"))
self.ignoredKeywords.setText(self._build.getStr("text.ignoredKeywords"))
self.addNoteHead.setChecked(self._build.getBool("text.addNoteHeadings"))
@@ -1387,6 +1397,7 @@ class _FormattingTab(NScrollableForm):
self._build.setValue("text.includeSynopsis", self.incSynopsis.isChecked())
self._build.setValue("text.includeComments", self.incComments.isChecked())
self._build.setValue("text.includeStory", self.incStory.isChecked())
self._build.setValue("text.includeNotes", self.incNotes.isChecked())
self._build.setValue("text.includeKeywords", self.incKeywords.isChecked())
self._build.setValue("text.ignoredKeywords", self.ignoredKeywords.text())
self._build.setValue("text.addNoteHeadings", self.addNoteHead.isChecked())
+2 -2
View File
@@ -257,8 +257,8 @@ class _OverviewPage(NScrollablePage):
def updateProjectData(self) -> None:
"""Load information about the project."""
project = SHARED.project
project.updateWordCounts()
wcNovel, wcNotes = project.data.currCounts
project.updateCounts()
wcNovel, wcNotes, _, _ = project.data.currCounts
self.projName.setText(project.data.name)
self.projRevisions.setText(f"{project.data.saveCount:n}")
+1 -1
View File
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools"]
requires = ["setuptools >= 77.0.3"]
build-backend = "setuptools.build_meta"
[project]
+3 -2
View File
@@ -1,8 +1,8 @@
%%~name: Making a Scene
%%~path: 6a2d6d5f4f401/636b6aa9b697b
%%~kind: NOVEL/DOCUMENT
%%~hash: c057a5e9309b0e764c367b0fe9ab0607e3308622
%%~date: Unknown/2025-04-08 20:10:33
%%~hash: 1f3d98a6a27b4f9a7f2239fcd78f9e2263cd3b97
%%~date: Unknown/2025-05-18 22:36:59
### Making a Scene
@pov: Jane
@@ -11,6 +11,7 @@
@mention: Space
%Story.Resolution: You can describe the scene structure with story comments.
%Note.Consistency: You can also make notes about things like consistency of the story.
A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. The scene document can be sorted after the chapter document, or as a child of the chapter. Both result in the same output in the end, so it is a matter of preference.
+4 -4
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="4" timeStamp="2025-04-20 19:01:45">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2178" autoCount="285" editTime="96586">
<novelWriterXML appVersion="2.7rc1" hexVersion="0x020700c1" fileVersion="1.5" fileRevision="5" timeStamp="2025-05-19 12:51:02">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2191" autoCount="286" editTime="96880">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
@@ -36,7 +36,7 @@
<entry key="i56be10" count="1" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="31" novelWords="1016" notesWords="416">
<content items="31" novelWords="1016" notesWords="416" novelChars="5602" notesChars="2285">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name>
@@ -58,7 +58,7 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="159" />
<meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="718" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
+1 -1
View File
@@ -49,5 +49,5 @@ echo " Done!"
echo "================================================================================"
echo ""
echo " To upload packages to PyPi, run:"
echo " twine upload dist/*"
echo " python3 -m twine upload dist/*"
echo ""
+53 -36
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="4" timeStamp="2022-11-07 13:00:48">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="5" autoCount="10" editTime="1000">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:07:59">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2179" autoCount="285" editTime="1000">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
<settings>
<doBackup>yes</doBackup>
<doBackup>no</doBackup>
<language>en_GB</language>
<spellChecking auto="yes">en_GB</spellChecking>
<spellChecking auto="yes">None</spellChecking>
<lastHandle>
<entry key="editor">636b6aa9b697b</entry>
<entry key="viewer">636b6aa9b697b</entry>
@@ -20,32 +20,33 @@
<entry key="C">D</entry>
</autoReplace>
<status>
<entry key="sf12341" count="4" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="sf12341" count="8" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="sf24ce6" count="2" red="200" green="50" blue="0" shape="SQUARE">Notes</entry>
<entry key="sc24b8f" count="3" red="182" green="60" blue="0" shape="SQUARE">Started</entry>
<entry key="s90e6c9" count="7" red="193" green="129" blue="0" shape="SQUARE">1st Draft</entry>
<entry key="sd51c5b" count="0" red="193" green="129" blue="0" shape="SQUARE">2nd Draft</entry>
<entry key="s8ae72a" count="0" red="193" green="129" blue="0" shape="SQUARE">3rd Draft</entry>
<entry key="s78ea90" count="1" red="58" green="180" blue="58" shape="SQUARE">Finished</entry>
<entry key="sc24b8f" count="3" red="182" green="60" blue="0" shape="BARS_1">Started</entry>
<entry key="s90e6c9" count="5" red="193" green="129" blue="0" shape="BARS_2">1st Draft</entry>
<entry key="sd51c5b" count="1" red="193" green="129" blue="0" shape="BARS_3">2nd Draft</entry>
<entry key="s8ae72a" count="1" red="193" green="129" blue="0" shape="BARS_4">3rd Draft</entry>
<entry key="s78ea90" count="1" red="58" green="180" blue="58" shape="STAR">Finished</entry>
</status>
<importance>
<entry key="ia857f0" count="5" red="100" green="100" blue="100" shape="SQUARE">None</entry>
<entry key="icfb3a5" count="2" red="0" green="122" blue="188" shape="SQUARE">Minor</entry>
<entry key="i2d7a54" count="2" red="21" green="0" blue="180" shape="SQUARE">Major</entry>
<entry key="i56be10" count="1" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
<entry key="i4a1d39" count="1" red="220" green="138" blue="221" shape="BLOCK_1">Background</entry>
<entry key="icfb3a5" count="1" red="220" green="138" blue="221" shape="BLOCK_2">Minor</entry>
<entry key="i2d7a54" count="2" red="220" green="138" blue="221" shape="BLOCK_3">Major</entry>
<entry key="i56be10" count="1" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="27" novelWords="954" notesWords="409">
<content items="31" novelWords="1016" notesWords="416" novelChars="5602" notesChars="2285">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name>
</item>
<item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H1" charCount="93" wordCount="19" paraCount="2" cursorPos="119" />
<meta expanded="no" heading="H1" charCount="148" wordCount="29" paraCount="4" cursorPos="178" />
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H0" charCount="251" wordCount="50" paraCount="2" cursorPos="277" />
<meta expanded="no" heading="H0" charCount="233" wordCount="47" paraCount="2" cursorPos="194" />
<name status="sf12341" import="ia857f0" active="yes">Page</name>
</item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -57,28 +58,28 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="67" />
<meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="159" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="465" />
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="650" />
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="310" />
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="1182" />
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
</item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
<meta expanded="no" heading="H1" charCount="1909" wordCount="346" paraCount="7" cursorPos="0" />
<meta expanded="no" heading="H1" charCount="1909" wordCount="346" paraCount="7" cursorPos="1940" />
<name status="sf24ce6" import="ia857f0" active="no">A Note on Structure</name>
</item>
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="yes" heading="H2" charCount="139" wordCount="28" paraCount="1" cursorPos="188" />
<meta expanded="yes" heading="H2" charCount="139" wordCount="28" paraCount="1" cursorPos="356" />
<name status="s90e6c9" import="ia857f0" active="yes">Chapter Two</name>
</item>
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="0" />
<name status="s90e6c9" import="ia857f0" active="yes">We Found John!</name>
<meta expanded="no" heading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="237" />
<name status="sd51c5b" import="ia857f0" active="yes">We Found John!</name>
</item>
<item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
<meta expanded="yes" />
@@ -89,8 +90,8 @@
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item>
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="104" />
<name status="s90e6c9" import="ia857f0" active="yes">Chapter One</name>
<meta expanded="no" heading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="387" />
<name status="s8ae72a" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
<meta expanded="yes" />
@@ -101,28 +102,28 @@
<name status="sf12341" import="ia857f0">Main Characters</name>
</item>
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="24" />
<name status="sf12341" import="icfb3a5" active="yes">John Smith</name>
<meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="23" />
<name status="sf12341" import="i2d7a54" active="yes">John Smith</name>
</item>
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="25" />
<name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name>
<meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="31" />
<name status="sf12341" import="i56be10" active="yes">Jane Smith</name>
</item>
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
<meta expanded="yes" />
<name status="sf12341" import="ia857f0">Locations</name>
</item>
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="no" heading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="20" />
<name status="sf12341" import="i56be10" active="yes">Earth</name>
<meta expanded="no" heading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="111" />
<name status="sf12341" import="i2d7a54" active="yes">Earth</name>
</item>
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="133" />
<name status="sf12341" import="icfb3a5" active="yes">Space</name>
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="0" />
<name status="sf12341" import="i4a1d39" active="yes">Space</name>
</item>
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="no" heading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="45" />
<name status="sf12341" import="i2d7a54" active="yes">Mars</name>
<meta expanded="no" heading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="62" />
<name status="sf12341" import="icfb3a5" active="yes">Mars</name>
</item>
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">
<meta expanded="yes" />
@@ -136,7 +137,23 @@
<meta expanded="no" heading="H3" charCount="232" wordCount="42" paraCount="1" cursorPos="239" />
<name status="s90e6c9" import="ia857f0" active="yes">Old File</name>
</item>
<item handle="98acd8c76c93a" parent="None" root="98acd8c76c93a" order="5" type="ROOT" class="TRASH">
<item handle="f4ed1ae756a1f" parent="None" root="f4ed1ae756a1f" order="5" type="ROOT" class="TEMPLATE">
<meta expanded="yes" />
<name status="sf12341" import="ia857f0">Templates</name>
</item>
<item handle="5aec885635c85" parent="f4ed1ae756a1f" root="f4ed1ae756a1f" order="0" type="FILE" class="TEMPLATE" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="9" wordCount="2" paraCount="1" cursorPos="78" />
<name status="sf12341" import="ia857f0" active="yes">Scene</name>
</item>
<item handle="2a60782759c6f" parent="f4ed1ae756a1f" root="f4ed1ae756a1f" order="1" type="FILE" class="TEMPLATE" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="11" wordCount="2" paraCount="1" cursorPos="81" />
<name status="sf12341" import="ia857f0" active="yes">Chapter</name>
</item>
<item handle="5ee8aebcdebc9" parent="f4ed1ae756a1f" root="f4ed1ae756a1f" order="2" type="FILE" class="TEMPLATE" layout="NOTE">
<meta expanded="no" heading="H1" charCount="53" wordCount="7" paraCount="1" cursorPos="75" />
<name status="sf12341" import="ia857f0" active="yes">Character Note</name>
</item>
<item handle="98acd8c76c93a" parent="None" root="98acd8c76c93a" order="6" type="ROOT" class="TRASH">
<meta expanded="yes" />
<name status="sf12341" import="ia857f0">Trash</name>
</item>
+4 -4
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.6b1" hexVersion="0x020600b1" fileVersion="1.5" fileRevision="4" timeStamp="2024-11-20 19:22:22">
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="51" autoCount="29" editTime="2456">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:45:09">
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="52" autoCount="29" editTime="2465">
<name>Lorem Ipsum</name>
<author>lipsum.com</author>
</project>
@@ -31,7 +31,7 @@
<entry key="id6b1d0" count="0" red="50" green="200" blue="0" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="22" novelWords="3115" notesWords="738">
<content items="22" novelWords="3115" notesWords="738" novelChars="20774" notesChars="5003">
<item handle="b3643d0f92e32" parent="None" root="b3643d0f92e32" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="sbaa94f" import="i613591">Novel</name>
@@ -116,7 +116,7 @@
<meta expanded="no" heading="H1" charCount="1770" wordCount="259" paraCount="3" cursorPos="47" />
<name status="sbaa94f" import="i613591" active="yes">Ancient Europe</name>
</item>
<item handle="1ace7ab1a0fc6" parent="None" root="1ace7ab1a0fc6" order="0" type="ROOT" class="TRASH">
<item handle="1ace7ab1a0fc6" parent="None" root="1ace7ab1a0fc6" order="4" type="ROOT" class="TRASH">
<meta expanded="no" />
<name status="sbaa94f" import="i613591">Trash</name>
</item>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="4" timeStamp="2025-05-11 12:42:48">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:54">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name>
<author>Jane Doe</author>
@@ -28,7 +28,7 @@
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="12" novelWords="10" notesWords="6">
<content items="12" novelWords="10" notesWords="6" novelChars="45" notesChars="22">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="no" />
<name status="s000000" import="i000004">Novel</name>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="4" timeStamp="2025-05-11 12:42:48">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:54">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name>
<author>Jane Doe</author>
@@ -28,7 +28,7 @@
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="16" novelWords="9" notesWords="0">
<content items="16" novelWords="9" notesWords="0" novelChars="40" notesChars="0">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="no" />
<name status="s000000" import="i000004">Novel</name>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="4" timeStamp="2025-05-11 12:42:46">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:52">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name>
<author>Jane Doe</author>
@@ -28,7 +28,7 @@
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="19" novelWords="28" notesWords="0">
<content items="19" novelWords="28" notesWords="0" novelChars="129" notesChars="0">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="no" />
<name status="s000000" import="i000004">Novel</name>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="4" timeStamp="2025-05-11 12:42:46">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:52">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0">
<name>Test Project A</name>
<author>Jane Doe</author>
@@ -28,7 +28,7 @@
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="22" novelWords="0" notesWords="0">
<content items="22" novelWords="0" notesWords="0" novelChars="0" notesChars="0">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="no" />
<name status="s000000" import="i000004">Novel</name>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="4" timeStamp="2025-05-11 12:42:46">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:52">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0">
<name>Test Project B</name>
<author>Jane Doe</author>
@@ -28,7 +28,7 @@
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="16" novelWords="0" notesWords="0">
<content items="16" novelWords="0" notesWords="0" novelChars="0" notesChars="0">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="no" />
<name status="s000000" import="i000004">Novel</name>
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Properties xmlns:ns0="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<ns0:TotalTime>40</ns0:TotalTime>
<ns0:Application>novelWriter/2.6a3</ns0:Application>
<ns0:TotalTime>41</ns0:TotalTime>
<ns0:Application>novelWriter/2.7b1</ns0:Application>
<ns0:Words>4035</ns0:Words>
<ns0:Characters>21296</ns0:Characters>
<ns0:CharactersWithSpaces>24964</ns0:CharactersWithSpaces>
@@ -1,10 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dcterms:created xsi:type="dcterms:W3CDTF">2024-11-20T19:45:15</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2024-11-20T19:45:15</dcterms:modified>
<dcterms:created xsi:type="dcterms:W3CDTF">2025-04-29T22:46:36</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2025-04-29T22:46:36</dcterms:modified>
<dc:creator>lipsum.com</dc:creator>
<dc:title>Lorem Ipsum</dc:title>
<dc:language>en_GB</dc:language>
<cp:revision>51</cp:revision>
<cp:revision>52</cp:revision>
<cp:lastModifiedBy>lipsum.com</cp:lastModifiedBy>
</cp:coreProperties>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="4" timeStamp="2025-05-11 12:42:59">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:08:41">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="4">
<name>New Project</name>
<author>Jane Doe</author>
@@ -28,7 +28,7 @@
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="12" novelWords="173" notesWords="27">
<content items="12" novelWords="173" notesWords="27" novelChars="1007" notesChars="133">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="s000000" import="i000004">Novel</name>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="4" timeStamp="2025-05-11 12:39:41">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:01:24">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="2" autoCount="1" editTime="0">
<name>New Project</name>
<author>Jane Doe</author>
@@ -28,7 +28,7 @@
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="8" novelWords="9" notesWords="0">
<content items="8" novelWords="9" notesWords="0" novelChars="40" notesChars="0">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="no" />
<name status="s000000" import="i000004">Novel</name>
+130 -30
View File
@@ -38,10 +38,10 @@
"metaAttr": {
"expanded": false,
"heading": "H1",
"charCount": 93,
"wordCount": 19,
"paraCount": 2,
"cursorPos": 119
"charCount": 148,
"wordCount": 29,
"paraCount": 4,
"cursorPos": 178
},
"nameAttr": {
"status": "sc24b8f",
@@ -63,10 +63,10 @@
"metaAttr": {
"expanded": false,
"heading": "H0",
"charCount": 251,
"wordCount": 50,
"charCount": 233,
"wordCount": 47,
"paraCount": 2,
"cursorPos": 277
"cursorPos": 194
},
"nameAttr": {
"status": "sf12341",
@@ -138,10 +138,10 @@
"metaAttr": {
"expanded": false,
"heading": "H3",
"charCount": 2687,
"wordCount": 479,
"paraCount": 14,
"cursorPos": 67
"charCount": 2999,
"wordCount": 530,
"paraCount": 16,
"cursorPos": 159
},
"nameAttr": {
"status": "s90e6c9",
@@ -166,7 +166,7 @@
"charCount": 548,
"wordCount": 108,
"paraCount": 3,
"cursorPos": 465
"cursorPos": 650
},
"nameAttr": {
"status": "s90e6c9",
@@ -191,7 +191,7 @@
"charCount": 617,
"wordCount": 101,
"paraCount": 3,
"cursorPos": 310
"cursorPos": 1182
},
"nameAttr": {
"status": "s78ea90",
@@ -216,7 +216,7 @@
"charCount": 1909,
"wordCount": 346,
"paraCount": 7,
"cursorPos": 0
"cursorPos": 1940
},
"nameAttr": {
"status": "sf24ce6",
@@ -241,7 +241,7 @@
"charCount": 139,
"wordCount": 28,
"paraCount": 1,
"cursorPos": 188
"cursorPos": 356
},
"nameAttr": {
"status": "s90e6c9",
@@ -266,10 +266,10 @@
"charCount": 189,
"wordCount": 37,
"paraCount": 1,
"cursorPos": 0
"cursorPos": 237
},
"nameAttr": {
"status": "s90e6c9",
"status": "sd51c5b",
"import": "ia857f0",
"active": true
}
@@ -341,10 +341,10 @@
"charCount": 299,
"wordCount": 55,
"paraCount": 2,
"cursorPos": 104
"cursorPos": 387
},
"nameAttr": {
"status": "s90e6c9",
"status": "s8ae72a",
"import": "ia857f0",
"active": true
}
@@ -416,11 +416,11 @@
"charCount": 49,
"wordCount": 9,
"paraCount": 1,
"cursorPos": 24
"cursorPos": 23
},
"nameAttr": {
"status": "sf12341",
"import": "icfb3a5",
"import": "i2d7a54",
"active": true
}
},
@@ -441,11 +441,11 @@
"charCount": 55,
"wordCount": 9,
"paraCount": 1,
"cursorPos": 25
"cursorPos": 31
},
"nameAttr": {
"status": "sf12341",
"import": "i2d7a54",
"import": "i56be10",
"active": true
}
},
@@ -491,11 +491,11 @@
"charCount": 76,
"wordCount": 15,
"paraCount": 1,
"cursorPos": 20
"cursorPos": 111
},
"nameAttr": {
"status": "sf12341",
"import": "i56be10",
"import": "i2d7a54",
"active": true
}
},
@@ -516,11 +516,11 @@
"charCount": 115,
"wordCount": 24,
"paraCount": 1,
"cursorPos": 133
"cursorPos": 0
},
"nameAttr": {
"status": "sf12341",
"import": "icfb3a5",
"import": "i4a1d39",
"active": true
}
},
@@ -541,11 +541,11 @@
"charCount": 28,
"wordCount": 6,
"paraCount": 1,
"cursorPos": 45
"cursorPos": 62
},
"nameAttr": {
"status": "sf12341",
"import": "i2d7a54",
"import": "icfb3a5",
"active": true
}
},
@@ -624,13 +624,113 @@
"active": true
}
},
{
"name": "Templates",
"itemAttr": {
"handle": "f4ed1ae756a1f",
"parent": null,
"root": "f4ed1ae756a1f",
"order": 5,
"type": "ROOT",
"class": "TEMPLATE",
"layout": "NO_LAYOUT"
},
"metaAttr": {
"expanded": true,
"heading": "H0",
"charCount": 0,
"wordCount": 0,
"paraCount": 0,
"cursorPos": 0
},
"nameAttr": {
"status": "sf12341",
"import": "ia857f0",
"active": false
}
},
{
"name": "Scene",
"itemAttr": {
"handle": "5aec885635c85",
"parent": "f4ed1ae756a1f",
"root": "f4ed1ae756a1f",
"order": 0,
"type": "FILE",
"class": "TEMPLATE",
"layout": "DOCUMENT"
},
"metaAttr": {
"expanded": false,
"heading": "H3",
"charCount": 9,
"wordCount": 2,
"paraCount": 1,
"cursorPos": 78
},
"nameAttr": {
"status": "sf12341",
"import": "ia857f0",
"active": true
}
},
{
"name": "Chapter",
"itemAttr": {
"handle": "2a60782759c6f",
"parent": "f4ed1ae756a1f",
"root": "f4ed1ae756a1f",
"order": 1,
"type": "FILE",
"class": "TEMPLATE",
"layout": "DOCUMENT"
},
"metaAttr": {
"expanded": false,
"heading": "H2",
"charCount": 11,
"wordCount": 2,
"paraCount": 1,
"cursorPos": 81
},
"nameAttr": {
"status": "sf12341",
"import": "ia857f0",
"active": true
}
},
{
"name": "Character Note",
"itemAttr": {
"handle": "5ee8aebcdebc9",
"parent": "f4ed1ae756a1f",
"root": "f4ed1ae756a1f",
"order": 2,
"type": "FILE",
"class": "TEMPLATE",
"layout": "NOTE"
},
"metaAttr": {
"expanded": false,
"heading": "H1",
"charCount": 53,
"wordCount": 7,
"paraCount": 1,
"cursorPos": 75
},
"nameAttr": {
"status": "sf12341",
"import": "ia857f0",
"active": true
}
},
{
"name": "Trash",
"itemAttr": {
"handle": "98acd8c76c93a",
"parent": null,
"root": "98acd8c76c93a",
"order": 5,
"order": 6,
"type": "ROOT",
"class": "TRASH",
"layout": "NO_LAYOUT"
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="4" timeStamp="2020-05-28 09:59:15">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2020-05-28 09:59:15">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="0" autoCount="0" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -35,7 +35,7 @@
<entry key="i00000a" count="0" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="22" novelWords="0" notesWords="0">
<content items="22" novelWords="0" notesWords="0" novelChars="0" notesChars="0">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="s000002" import="i000007">Novel</name>
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="4" timeStamp="2020-06-26 21:20:24">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2020-06-26 21:20:24">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -35,7 +35,7 @@
<entry key="i00000a" count="0" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="22" novelWords="0" notesWords="0">
<content items="22" novelWords="0" notesWords="0" novelChars="0" notesChars="0">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="s000002" import="i000007">Novel</name>
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="4" timeStamp="2021-08-30 23:33:44">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2021-08-30 23:33:44">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -35,7 +35,7 @@
<entry key="i00000a" count="0" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="25" novelWords="840" notesWords="376">
<content items="25" novelWords="840" notesWords="376" novelChars="0" notesChars="0">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="s000002" import="i000007">Novel</name>
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="4" timeStamp="2022-10-25 18:26:15">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2022-10-25 18:26:15">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -35,7 +35,7 @@
<entry key="i00000a" count="0" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="25" novelWords="830" notesWords="376">
<content items="25" novelWords="830" notesWords="376" novelChars="0" notesChars="0">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="s000002" import="i000007">Novel</name>
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" fileRevision="4" timeStamp="2022-10-15 12:12:59">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2022-10-15 12:12:59">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -35,7 +35,7 @@
<entry key="i56be10" count="1" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="27" novelWords="954" notesWords="409">
<content items="27" novelWords="954" notesWords="409" novelChars="0" notesChars="0">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name>
+6
View File
@@ -1049,6 +1049,12 @@ def testCoreIndex_TagsIndex():
assert tagsIndex.tagClass("Tag3") == nwItemClass.PLOT.name
assert tagsIndex.tagClass("Tag4") is None
# Change class of item
tagsIndex.updateClass("0000000000003", nwItemClass.WORLD.name)
assert tagsIndex.tagClass("Tag3") == nwItemClass.WORLD.name
tagsIndex.updateClass("0000000000003", nwItemClass.PLOT.name)
assert tagsIndex.tagClass("Tag3") == nwItemClass.PLOT.name
# Pack Data
assert tagsIndex.packData() == content
+9
View File
@@ -224,6 +224,14 @@ def testCoreIndexData_IndexHeading():
"story.crisis": "It exploded!",
}
# Set Note Comment
head.setComment(nwComment.NOTE.name, "consitency", "Only explode once")
assert head.comments == {
"summary": "In the beginning ...",
"story.crisis": "It exploded!",
"note.consitency": "Only explode once",
}
# Set Tag
head.setTag("Stuff")
assert head.tag == "stuff" # Case insensitive
@@ -240,6 +248,7 @@ def testCoreIndexData_IndexHeading():
"refs": {"stuff": "@object"},
"summary": "In the beginning ...",
"story.crisis": "It exploded!",
"note.consitency": "Only explode once",
}
# Unpack KeyError
+10
View File
@@ -26,6 +26,7 @@ import pytest
from PyQt6.QtGui import QIcon
from novelwriter import CONFIG
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
@@ -168,6 +169,15 @@ def testCoreItem_Setters(mockGUI, mockRnd, fncPath):
item.setParaCount(1)
assert item.paraCount == 1
# MainCount
item.setWordCount(123)
item.setCharCount(1234)
CONFIG.useCharCount = False
assert item.mainCount == 123
CONFIG.useCharCount = True
assert item.mainCount == 1234
CONFIG.useCharCount = False
# CursorPos
item.setCursorPos(None)
assert item.cursorPos == 0
+40 -37
View File
@@ -52,8 +52,8 @@ class MockProject:
@pytest.fixture(scope="function", autouse=True)
def mockVersion(monkeypatch):
"""Mock the version info to prevent diff from failing."""
monkeypatch.setattr("novelwriter.core.projectxml.__version__", "2.0-rc1")
monkeypatch.setattr("novelwriter.core.projectxml.__hexversion__", "0x020000c1")
monkeypatch.setattr("novelwriter.core.projectxml.__version__", "2.7b1")
monkeypatch.setattr("novelwriter.core.projectxml.__hexversion__", "0x020700b1")
return
@@ -137,23 +137,23 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, mockGUI, tstPaths, fncPath):
assert xmlReader.state == XMLReadState.PARSED_OK
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0105
assert xmlReader.xmlRevision == 4
assert xmlReader.appVersion == "2.0-rc1"
assert xmlReader.hexVersion == 0x020000c1
assert xmlReader.xmlRevision == 5
assert xmlReader.appVersion == "2.7b1"
assert xmlReader.hexVersion == 0x020700b1
# Check loaded data
assert data.name == "Sample Project"
assert data.author == "Jane Smith"
assert data.saveCount == 5
assert data.autoCount == 10
assert data.saveCount == 2179
assert data.autoCount == 285
assert data.editTime == 1000
assert data.doBackup is True
assert data.doBackup is False
assert data.language == "en_GB"
assert data.spellCheck is True
assert data.spellLang == "en_GB"
assert data.initCounts == (954, 409)
assert data.currCounts == (954, 409)
assert data.spellLang is None
assert data.initCounts == (1016, 416, 5602, 2285)
assert data.currCounts == (1016, 416, 5602, 2285)
assert data.getLastHandle("editor") == "636b6aa9b697b"
assert data.getLastHandle("viewer") == "636b6aa9b697b"
@@ -182,33 +182,36 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, mockGUI, tstPaths, fncPath):
assert data.itemStatus["s78ea90"].color == QColor(58, 180, 58)
assert data.itemImport["ia857f0"].color == QColor(100, 100, 100)
assert data.itemImport["icfb3a5"].color == QColor(0, 122, 188)
assert data.itemImport["i2d7a54"].color == QColor(21, 0, 180)
assert data.itemImport["i56be10"].color == QColor(117, 0, 175)
assert data.itemImport["i4a1d39"].color == QColor(220, 138, 221)
assert data.itemImport["icfb3a5"].color == QColor(220, 138, 221)
assert data.itemImport["i2d7a54"].color == QColor(220, 138, 221)
assert data.itemImport["i56be10"].color == QColor(220, 138, 221)
assert data.itemStatus["sf12341"].shape == nwStatusShape.SQUARE
assert data.itemStatus["sf24ce6"].shape == nwStatusShape.SQUARE
assert data.itemStatus["sc24b8f"].shape == nwStatusShape.SQUARE
assert data.itemStatus["s90e6c9"].shape == nwStatusShape.SQUARE
assert data.itemStatus["sd51c5b"].shape == nwStatusShape.SQUARE
assert data.itemStatus["s8ae72a"].shape == nwStatusShape.SQUARE
assert data.itemStatus["s78ea90"].shape == nwStatusShape.SQUARE
assert data.itemStatus["sc24b8f"].shape == nwStatusShape.BARS_1
assert data.itemStatus["s90e6c9"].shape == nwStatusShape.BARS_2
assert data.itemStatus["sd51c5b"].shape == nwStatusShape.BARS_3
assert data.itemStatus["s8ae72a"].shape == nwStatusShape.BARS_4
assert data.itemStatus["s78ea90"].shape == nwStatusShape.STAR
assert data.itemImport["ia857f0"].shape == nwStatusShape.SQUARE
assert data.itemImport["icfb3a5"].shape == nwStatusShape.SQUARE
assert data.itemImport["i2d7a54"].shape == nwStatusShape.SQUARE
assert data.itemImport["i56be10"].shape == nwStatusShape.SQUARE
assert data.itemImport["i4a1d39"].shape == nwStatusShape.BLOCK_1
assert data.itemImport["icfb3a5"].shape == nwStatusShape.BLOCK_2
assert data.itemImport["i2d7a54"].shape == nwStatusShape.BLOCK_3
assert data.itemImport["i56be10"].shape == nwStatusShape.BLOCK_4
assert data.itemStatus["sf12341"].count == 4
assert data.itemStatus["sf12341"].count == 8
assert data.itemStatus["sf24ce6"].count == 2
assert data.itemStatus["sc24b8f"].count == 3
assert data.itemStatus["s90e6c9"].count == 7
assert data.itemStatus["sd51c5b"].count == 0
assert data.itemStatus["s8ae72a"].count == 0
assert data.itemStatus["s90e6c9"].count == 5
assert data.itemStatus["sd51c5b"].count == 1
assert data.itemStatus["s8ae72a"].count == 1
assert data.itemStatus["s78ea90"].count == 1
assert data.itemImport["ia857f0"].count == 5
assert data.itemImport["icfb3a5"].count == 2
assert data.itemImport["i4a1d39"].count == 1
assert data.itemImport["icfb3a5"].count == 1
assert data.itemImport["i2d7a54"].count == 2
assert data.itemImport["i56be10"].count == 1
@@ -281,8 +284,8 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockGUI, mockRnd):
assert data.language is None # Doesn't exist in 1.0
assert data.spellCheck is True
assert data.spellLang is None # Doesn't exist in 1.0
assert data.initCounts == (0, 0)
assert data.currCounts == (0, 0)
assert data.initCounts == (0, 0, 0, 0)
assert data.currCounts == (0, 0, 0, 0)
assert data.getLastHandle("editor") is None # Dropped by conversion
assert data.getLastHandle("viewer") is None # Dropped by conversion
@@ -426,8 +429,8 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockGUI, mockRnd):
assert data.language is None # Doesn't exist in 1.1
assert data.spellCheck is True
assert data.spellLang is None # Doesn't exist in 1.1
assert data.initCounts == (0, 0)
assert data.currCounts == (0, 0)
assert data.initCounts == (0, 0, 0, 0)
assert data.currCounts == (0, 0, 0, 0)
assert data.getLastHandle("editor") is None # Dropped by conversion
assert data.getLastHandle("viewer") is None # Dropped by conversion
@@ -571,8 +574,8 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockGUI, mockRnd):
assert data.language == "en_GB"
assert data.spellCheck is True
assert data.spellLang == "en_GB"
assert data.initCounts == (840, 376)
assert data.currCounts == (840, 376)
assert data.initCounts == (840, 376, 0, 0)
assert data.currCounts == (840, 376, 0, 0)
assert data.getLastHandle("editor") is None # Dropped by conversion
assert data.getLastHandle("viewer") is None # Dropped by conversion
@@ -719,8 +722,8 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockGUI, mockRnd):
assert data.language == "en_GB"
assert data.spellCheck is True
assert data.spellLang == "en_GB"
assert data.initCounts == (830, 376)
assert data.currCounts == (830, 376)
assert data.initCounts == (830, 376, 0, 0)
assert data.currCounts == (830, 376, 0, 0)
assert data.getLastHandle("editor") is None # Dropped by conversion
assert data.getLastHandle("viewer") is None # Dropped by conversion
@@ -867,8 +870,8 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockGUI, mockRnd):
assert data.language == "en_GB"
assert data.spellCheck is True
assert data.spellLang == "en_GB"
assert data.initCounts == (954, 409)
assert data.currCounts == (954, 409)
assert data.initCounts == (954, 409, 0, 0)
assert data.currCounts == (954, 409, 0, 0)
assert data.getLastHandle("editor") is None # Dropped by conversion
assert data.getLastHandle("viewer") is None # Dropped by conversion
+8 -6
View File
@@ -43,8 +43,8 @@ def testCoreSessions_Main(monkeypatch, mockGUI, fncPath):
assert isinstance(logFile, Path)
# Set some mock word counts
project.data.setInitCounts(50, 60)
project.data.setCurrCounts(160, 150)
project.data.setInitCounts(50, 60, 500, 600)
project.data.setCurrCounts(160, 150, 1600, 1500)
# The project init should already have created the session
sessLog = project.session
@@ -71,17 +71,19 @@ def testCoreSessions_Main(monkeypatch, mockGUI, fncPath):
assert records[1]["type"] == "record"
assert records[1]["novel"] == 160
assert records[1]["notes"] == 150
assert records[1]["cnovel"] == 1600
assert records[1]["cnotes"] == 1500
assert records[1]["idle"] == 1 # Should be rounded to full seconds
# Adding another record without changing word count should do nothing
project.data.setInitCounts(160, 150)
project.data.setCurrCounts(160, 150)
project.data.setInitCounts(160, 150, 1600, 1500)
project.data.setCurrCounts(160, 150, 1600, 1500)
assert sessLog.appendSession(1.6) is False
assert len(list(sessLog.iterRecords())) == 2
# But adding when count has changed should
project.data.setInitCounts(160, 150)
project.data.setCurrCounts(270, 240)
project.data.setInitCounts(160, 150, 1600, 1500)
project.data.setCurrCounts(270, 240, 2700, 2400)
sessLog._start -= 350.0 # Backdate the session start to allow logging
assert sessLog.appendSession(1.6) is True
records = list(sessLog.iterRecords())
+6 -2
View File
@@ -435,8 +435,8 @@ def testCoreStorage_OldFormatConvert(monkeypatch, mockGUI, fncPath):
sessLogOld.write_text((
"# Offset 150\n"
"# Start Time End Time Novel Notes Idle\n"
"2021-02-02 02:02:02 2021-02-02 03:03:03 200 200 10\n"
"2021-03-03 03:03:03 2021-03-03 04:04:04 300 300 20\n"
"2021-02-02 02:02:02 2021-02-02 03:03:03 200 200 10\n"
"2021-03-03 03:03:03 2021-03-03 04:04:04 300 300 20\n"
), encoding="utf-8")
assert sessLogOld.exists() is True
@@ -496,6 +496,8 @@ def testCoreStorage_OldFormatConvert(monkeypatch, mockGUI, fncPath):
"end": "2021-02-02 03:03:03",
"novel": 200,
"notes": 200,
"cnovel": 0,
"cnotes": 0,
"idle": 10,
}
assert data[2] == {
@@ -504,6 +506,8 @@ def testCoreStorage_OldFormatConvert(monkeypatch, mockGUI, fncPath):
"end": "2021-03-03 04:04:04",
"novel": 300,
"notes": 300,
"cnovel": 0,
"cnotes": 0,
"idle": 20,
}
+1 -1
View File
@@ -436,7 +436,7 @@ def testCoreTree_OtherMethods(qtbot, monkeypatch, mockGUI, fncPath, mockRnd):
]
# Refresh All
assert tree.sumWords() == (9, 0)
assert tree.sumCounts() == (9, 0, 40, 0)
assert tree.model.root.count == 9
for node in tree.nodes.values():
@@ -163,14 +163,17 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(True) # Use OS font dialog
prefs.guiFontButton.click()
prefs.hideVScroll.setChecked(True)
prefs.hideHScroll.setChecked(True)
prefs.useCharCount.setChecked(True)
assert CONFIG.guiLocale != "en_US"
assert CONFIG.guiTheme != "default_dark"
assert CONFIG.guiFont.family() != ""
assert CONFIG.hideVScroll is False
assert CONFIG.hideHScroll is False
assert CONFIG.useCharCount is False
# Document Style
prefs.guiSyntax.setCurrentIndex(prefs.guiSyntax.findData("default_dark"))
@@ -178,6 +181,7 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(False) # Use Qt font dialog
prefs.textFontButton.click()
prefs.showFullPath.setChecked(False)
prefs.incNotesWCount.setChecked(False)
@@ -344,6 +348,7 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
assert CONFIG.guiFont == QFont()
assert CONFIG.hideVScroll is True
assert CONFIG.hideHScroll is True
assert CONFIG.useCharCount is True
# Document Style
assert CONFIG.guiSyntax == "default_dark"
+61 -4
View File
@@ -1877,10 +1877,68 @@ def testGuiEditor_Completer(qtbot, nwGUI, projPath, mockRnd):
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Escape, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John"
"@focus: John\n"
)
# Send keypresses to the completer object for a comment
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert len(completer.actions()) == 4
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
"%Synopsis: \n"
)
# Auto-complete story comment
SHARED.project.index._itemIndex._cache.story.add("Resolution")
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert len(completer.actions()) == 4
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, ".", delay=KEY_DELAY)
assert len(completer.actions()) == 1
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
"%Synopsis: \n"
"%Story.Resolution: \n"
)
# Auto-complete note comment
SHARED.project.index._itemIndex._cache.note.add("Consistency")
qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
assert len(completer.actions()) == 4
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(completer, ".", delay=KEY_DELAY)
assert len(completer.actions()) == 1
qtbot.keyClick(completer, Qt.Key.Key_Down, delay=KEY_DELAY)
qtbot.keyClick(completer, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
assert docEditor.getText() == (
"### Scene One\n\n"
"@char: Jane\n"
"@focus: John\n"
"%Synopsis: \n"
"%Story.Resolution: \n"
"%Note.Consistency: \n"
)
# qtbot.stop()
@@ -1957,7 +2015,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
assert docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
# Open a document and populate it
SHARED.project.tree[C.hSceneDoc]._initCount = 0 # type: ignore
SHARED.project.tree[C.hSceneDoc]._wordInit = 0 # type: ignore
SHARED.project.tree[C.hSceneDoc]._wordCount = 0 # type: ignore
assert nwGUI.openDocument(C.hSceneDoc) is True
@@ -1981,7 +2039,6 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
assert threadPool.objectID() == id(docEditor._wCounterDoc)
docEditor._wCounterDoc.run()
# docEditor._updateDocCounts(cC, wC, pC)
assert SHARED.project.tree[C.hSceneDoc]._charCount == cC # type: ignore
assert SHARED.project.tree[C.hSceneDoc]._wordCount == wC # type: ignore
assert SHARED.project.tree[C.hSceneDoc]._paraCount == pC # type: ignore
@@ -1993,7 +2050,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
assert threadPool.objectID() == id(docEditor._wCounterSel)
docEditor._wCounterSel.run()
assert docEditor.docFooter.wordsText.text() == f"Words: {wC} selected"
assert docEditor.docFooter.wordsText.text() == f"Selected: {wC}"
# qtbot.stop()
+1
View File
@@ -184,6 +184,7 @@ def testGuiMain_UpdateTheme(qtbot, nwGUI):
CONFIG.guiSyntax = "default_dark"
mainTheme.loadTheme()
mainTheme.loadSyntax()
nwGUI._processConfigChanges(False, True, False, False)
nwGUI._processConfigChanges(True, True, True, True)
syntax = SHARED.theme.syntaxTheme
+1 -1
View File
@@ -93,7 +93,7 @@ def testGuiNovelView_Content(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
assert novelTree._getModel() is None
# Reload
novelBar._forceRefreshNovelTree()
novelView.refreshCurrentTree()
model = novelTree._getModel()
assert isinstance(model, NovelModel)
+14
View File
@@ -109,4 +109,18 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI._timeTick()
assert status.statsText.text() == "Words: 11 (+11)"
# Switch to character count
CONFIG.useCharCount = True
status.initSettings()
with monkeypatch.context() as mp:
mp.setattr("novelwriter.guimain.time", lambda *a: 50.0)
CONFIG.incNotesWCount = True
nwGUI._lastTotalCount = 0
nwGUI._timeTick()
assert status.statsText.text() == "Characters: 46 (+46)"
CONFIG.incNotesWCount = False
nwGUI._lastTotalCount = 0
nwGUI._timeTick()
assert status.statsText.text() == "Characters: 40 (+40)"
# qtbot.stop()
+70 -45
View File
@@ -72,7 +72,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
@pyqtSlot(BuildSettings)
def _testNewSettingsReady(new: BuildSettings):
nonlocal triggered
assert new is build
assert new.buildID == build.buildID
triggered = True
# Capture Apply button
@@ -103,6 +103,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
bSettings._build._changed = True
bSettings.close()
assert triggered
@@ -140,6 +141,9 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
bSettings.show()
bSettings.loadContent()
sBuild = bSettings._build
assert sBuild.buildID == build.buildID
filterTab = bSettings.optTabSelect
button = bSettings.sidebar._group.button(bSettings.OPT_FILTERS)
assert button is not None
@@ -153,15 +157,15 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
# Un-toggle note folders
filterTab.filterOpt._widgets[switchMap["worldRoot"]].setChecked(False) # World Root
assert filterTab.optTree.topLevelItemCount() == 3
assert C.hWorldRoot in build._skipRoot
assert C.hWorldRoot in sBuild._skipRoot
filterTab.filterOpt._widgets[switchMap["charRoot"]].setChecked(False) # Char Root
assert filterTab.optTree.topLevelItemCount() == 2
assert C.hCharRoot in build._skipRoot
assert C.hCharRoot in sBuild._skipRoot
filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(False) # Plot Root
assert filterTab.optTree.topLevelItemCount() == 1
assert C.hPlotRoot in build._skipRoot
assert C.hPlotRoot in sBuild._skipRoot
# Reset Plot and Char
filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(True)
@@ -170,7 +174,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
# Switch off novel docs
filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(False)
assert build.buildItemFilter(SHARED.project) == {
assert sBuild.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -186,7 +190,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
# Switch on note docs
filterTab.filterOpt._widgets[switchMap["incNotes"]].setChecked(True)
assert build.buildItemFilter(SHARED.project) == {
assert sBuild.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -202,7 +206,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
# Switch on inactive docs
filterTab.filterOpt._widgets[switchMap["incInactive"]].setChecked(True)
assert build.buildItemFilter(SHARED.project) == {
assert sBuild.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -220,7 +224,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
filterTab._treeMap[C.hChapterDoc].setSelected(True)
filterTab._treeMap[C.hSceneDoc].setSelected(True)
filterTab.includedButton.click()
assert build.buildItemFilter(SHARED.project) == {
assert sBuild.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -239,7 +243,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
filterTab._treeMap[hPlotDoc].setSelected(True) # type: ignore
filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore
filterTab.excludedButton.click()
assert build.buildItemFilter(SHARED.project) == {
assert sBuild.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -255,7 +259,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
# Switch on novel docs
filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(True)
assert build.buildItemFilter(SHARED.project) == {
assert sBuild.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED), # Now enabled
C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -273,7 +277,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
filterTab.optTree.clearSelection()
filterTab._treeMap[C.hNovelRoot].setSelected(True)
filterTab.resetButton.click()
assert build.buildItemFilter(SHARED.project) == {
assert sBuild.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -294,7 +298,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
filterTab._treeMap[hPlotDoc].setSelected(True) # type: ignore
filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore
filterTab.resetButton.click()
assert build.buildItemFilter(SHARED.project) == {
assert sBuild.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -429,6 +433,8 @@ def testToolBuildSettings_Headings(qtbot, nwGUI):
# Edit a Heading
# ==============
sBuild = bSettings._build
assert sBuild.buildID == build.buildID
# Create new format of all bits
headTab.btnChapter.click()
@@ -447,13 +453,13 @@ def testToolBuildSettings_Headings(qtbot, nwGUI):
headTab.aInsScAbs.trigger()
assert headTab.editTextBox.toPlainText() == allFmt
headTab.btnApply.click()
assert build.getStr("headings.fmtChapter") == allFmt
assert sBuild.getStr("headings.fmtChapter") == allFmt
# Check complex format
headTab.btnChapter.click()
headTab.editTextBox.setPlainText(f"Chapter {nwHeadFmt.CH_NUM}\n{nwHeadFmt.TITLE}\n")
headTab.btnApply.click()
assert build.getStr("headings.fmtChapter") == (
assert sBuild.getStr("headings.fmtChapter") == (
f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}"
)
@@ -461,39 +467,42 @@ def testToolBuildSettings_Headings(qtbot, nwGUI):
headTab.btnPart.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtPart") == nwHeadFmt.TITLE
assert sBuild.getStr("headings.fmtPart") == nwHeadFmt.TITLE
headTab.btnChapter.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtChapter") == nwHeadFmt.TITLE
assert sBuild.getStr("headings.fmtChapter") == nwHeadFmt.TITLE
headTab.btnUnnumbered.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtUnnumbered") == nwHeadFmt.TITLE
assert sBuild.getStr("headings.fmtUnnumbered") == nwHeadFmt.TITLE
headTab.btnScene.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtScene") == nwHeadFmt.TITLE
assert sBuild.getStr("headings.fmtScene") == nwHeadFmt.TITLE
headTab.btnAScene.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtAltScene") == nwHeadFmt.TITLE
assert sBuild.getStr("headings.fmtAltScene") == nwHeadFmt.TITLE
headTab.btnSection.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtSection") == nwHeadFmt.TITLE
assert sBuild.getStr("headings.fmtSection") == nwHeadFmt.TITLE
# Check hide switches
headTab.swtScene.setChecked(True)
headTab.swtSection.setChecked(True)
headTab.saveContent()
assert build.getBool("headings.hideScene") is True
assert build.getBool("headings.hideSection") is True
sBuild = bSettings._build
assert sBuild.buildID == build.buildID
assert sBuild.getBool("headings.hideScene") is True
assert sBuild.getBool("headings.hideSection") is True
# Finish
button = bSettings.buttonBox.button(QtDialogClose)
@@ -510,6 +519,8 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI):
build.setValue("text.includeBodyText", False)
build.setValue("text.includeSynopsis", False)
build.setValue("text.includeComments", False)
build.setValue("text.includeStory", False)
build.setValue("text.includeNotes", False)
build.setValue("text.includeKeywords", False)
build.setValue("text.ignoredKeywords", "")
@@ -530,6 +541,8 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI):
assert fmtTab.incBodyText.isChecked() is False
assert fmtTab.incSynopsis.isChecked() is False
assert fmtTab.incComments.isChecked() is False
assert fmtTab.incStory.isChecked() is False
assert fmtTab.incNotes.isChecked() is False
assert fmtTab.incKeywords.isChecked() is False
assert fmtTab.ignoredKeywords.text() == ""
@@ -539,6 +552,8 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI):
fmtTab.incBodyText.setChecked(True)
fmtTab.incSynopsis.setChecked(True)
fmtTab.incComments.setChecked(True)
fmtTab.incStory.setChecked(True)
fmtTab.incNotes.setChecked(True)
fmtTab.incKeywords.setChecked(True)
fmtTab.addNoteHead.setChecked(True)
@@ -550,14 +565,18 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI):
# Save values
fmtTab.saveContent()
sBuild = bSettings._build
assert sBuild.buildID == build.buildID
assert build.getBool("text.includeBodyText") is True
assert build.getBool("text.includeSynopsis") is True
assert build.getBool("text.includeComments") is True
assert build.getBool("text.includeKeywords") is True
assert build.getStr("text.ignoredKeywords") in ("@custom, @object", "@object, @custom")
assert sBuild.getBool("text.includeBodyText") is True
assert sBuild.getBool("text.includeSynopsis") is True
assert sBuild.getBool("text.includeComments") is True
assert sBuild.getBool("text.includeStory") is True
assert sBuild.getBool("text.includeNotes") is True
assert sBuild.getBool("text.includeKeywords") is True
assert sBuild.getStr("text.ignoredKeywords") in ("@custom, @object", "@object, @custom")
assert build.getBool("text.addNoteHeadings") is True
assert sBuild.getBool("text.addNoteHeadings") is True
# Finish
button = bSettings.buttonBox.button(QtDialogClose)
@@ -615,15 +634,17 @@ def testToolBuildSettings_FormatTextFormat(monkeypatch, qtbot, nwGUI):
# Save values
fmtTab.saveContent()
sBuild = bSettings._build
assert sBuild.buildID == build.buildID
assert build.getStr("format.textFont") == testFont.toString()
assert build.getFloat("format.lineHeight") == 1.15
assert sBuild.getStr("format.textFont") == testFont.toString()
assert sBuild.getFloat("format.lineHeight") == 1.15
assert build.getBool("format.justifyText") is True
assert build.getBool("format.stripUnicode") is True
assert build.getBool("format.replaceTabs") is True
assert build.getBool("format.keepBreaks") is False
assert build.getBool("format.showDialogue") is True
assert sBuild.getBool("format.justifyText") is True
assert sBuild.getBool("format.stripUnicode") is True
assert sBuild.getBool("format.replaceTabs") is True
assert sBuild.getBool("format.keepBreaks") is False
assert sBuild.getBool("format.showDialogue") is True
# Check that the font dialog doesn't fail
with monkeypatch.context() as mp:
@@ -674,10 +695,12 @@ def testToolBuildSettings_FormatFirstLineIndent(monkeypatch, qtbot, nwGUI):
# Save values
fmtTab.saveContent()
sBuild = bSettings._build
assert sBuild.buildID == build.buildID
assert build.getBool("format.firstLineIndent") is True
assert build.getFloat("format.firstIndentWidth") == 2.0
assert build.getBool("format.indentFirstPar") is True
assert sBuild.getBool("format.firstLineIndent") is True
assert sBuild.getFloat("format.firstIndentWidth") == 2.0
assert sBuild.getBool("format.indentFirstPar") is True
# Finish
button = bSettings.buttonBox.button(QtDialogClose)
@@ -792,15 +815,17 @@ def testToolBuildSettings_FormatOutput(qtbot, nwGUI):
# Save values
fmtTab.saveContent()
sBuild = bSettings._build
assert sBuild.buildID == build.buildID
assert build.getStr("doc.pageHeader") == "Stuff"
assert build.getInt("doc.pageCountOffset") == 1
assert build.getBool("doc.colorHeadings") is False
assert build.getBool("doc.scaleHeadings") is False
assert build.getBool("doc.boldHeadings") is False
assert sBuild.getStr("doc.pageHeader") == "Stuff"
assert sBuild.getInt("doc.pageCountOffset") == 1
assert sBuild.getBool("doc.colorHeadings") is False
assert sBuild.getBool("doc.scaleHeadings") is False
assert sBuild.getBool("doc.boldHeadings") is False
assert build.getBool("html.addStyles") is True
assert build.getBool("html.preserveTabs") is True
assert sBuild.getBool("html.addStyles") is True
assert sBuild.getBool("html.preserveTabs") is True
# Reset header format
fmtTab.btnPageHeader.click()