Add a custom text document in the editor

This commit is contained in:
Veronica Berglyd Olsen
2023-09-07 19:20:00 +02:00
parent e08790e889
commit 16e2e0cc83
6 changed files with 154 additions and 95 deletions
+48 -51
View File
@@ -56,6 +56,7 @@ from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.core.index import countWords from novelwriter.core.index import countWords
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
from novelwriter.gui.editordocument import GuiTextDocument
from novelwriter.extensions.wheeleventfilter import WheelEventFilter from novelwriter.extensions.wheeleventfilter import WheelEventFilter
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@@ -120,9 +121,12 @@ class GuiDocEditor(QPlainTextEdit):
self._typPadBefore = "" self._typPadBefore = ""
self._typPadAfter = "" self._typPadAfter = ""
# Core Elements and Signals # Create Custom Document
qDoc = self.document() self._qDocument = GuiTextDocument(self)
qDoc.contentsChange.connect(self._docChange) self.setDocument(self._qDocument)
# Connect Signals
self._qDocument.contentsChange.connect(self._docChange)
self.selectionChanged.connect(self._updateSelectedStatus) self.selectionChanged.connect(self._updateSelectedStatus)
# Document Title # Document Title
@@ -130,9 +134,6 @@ class GuiDocEditor(QPlainTextEdit):
self.docFooter = GuiDocEditFooter(self) self.docFooter = GuiDocEditFooter(self)
self.docSearch = GuiDocEditSearch(self) self.docSearch = GuiDocEditSearch(self)
# Syntax
self.highLight = GuiDocHighlighter(qDoc)
# Context Menu # Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu) self.customContextMenuRequested.connect(self._openContextMenu)
@@ -212,7 +213,7 @@ class GuiDocEditor(QPlainTextEdit):
@property @property
def isEmpty(self) -> bool: def isEmpty(self) -> bool:
"""Check if the current document is empty.""" """Check if the current document is empty."""
return self.document().isEmpty() return self._qDocument.isEmpty()
## ##
# Methods # Methods
@@ -266,7 +267,7 @@ class GuiDocEditor(QPlainTextEdit):
self.docHeader.matchColours() self.docHeader.matchColours()
self.docFooter.matchColours() self.docFooter.matchColours()
self.highLight.initHighlighter() self._qDocument.syntaxHighlighter.initHighlighter()
return return
@@ -312,8 +313,7 @@ class GuiDocEditor(QPlainTextEdit):
# Due to cursor visibility, a part of the margin must be # Due to cursor visibility, a part of the margin must be
# allocated to the document itself. See issue #1112. # allocated to the document itself. See issue #1112.
cW = 2*self.cursorWidth() cW = 2*self.cursorWidth()
qDoc = self.document() self._qDocument.setDocumentMargin(cW)
qDoc.setDocumentMargin(cW)
self._vpMargin = max(CONFIG.getTextMargin() - cW, 0) self._vpMargin = max(CONFIG.getTextMargin() - cW, 0)
self.setViewportMargins(self._vpMargin, self._vpMargin, self._vpMargin, self._vpMargin) self.setViewportMargins(self._vpMargin, self._vpMargin, self._vpMargin, self._vpMargin)
@@ -327,7 +327,7 @@ class GuiDocEditor(QPlainTextEdit):
if CONFIG.showLineEndings: if CONFIG.showLineEndings:
theOpt.setFlags(theOpt.flags() | QTextOption.ShowLineAndParagraphSeparators) theOpt.setFlags(theOpt.flags() | QTextOption.ShowLineAndParagraphSeparators)
qDoc.setDefaultTextOption(theOpt) self._qDocument.setDefaultTextOption(theOpt)
# Scroll bars # Scroll bars
if CONFIG.hideVScroll: if CONFIG.hideVScroll:
@@ -377,13 +377,12 @@ class GuiDocEditor(QPlainTextEdit):
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self._docHandle = tHandle self._docHandle = tHandle
self.highLight.setHandle(tHandle)
tStart = time() tStart = time()
self._allowAutoReplace(False) self._allowAutoReplace(False)
self.setPlainText(docText) self._qDocument.setTextContent(docText, tHandle)
self._allowAutoReplace(True) self._allowAutoReplace(True)
logger.debug("Document text loaded in %.3f ms", 1000*(time() - tStart)) logger.debug("Document text set in %.3f ms", 1000*(time() - tStart))
qApp.processEvents() qApp.processEvents()
self._lastEdit = time() self._lastEdit = time()
@@ -404,14 +403,15 @@ class GuiDocEditor(QPlainTextEdit):
self.docFooter.updateLineCount() self.docFooter.updateLineCount()
# This is a hack to fix invisible cursor on an empty document # This is a hack to fix invisible cursor on an empty document
if self.document().characterCount() <= 1: if self._qDocument.characterCount() <= 1:
self.setPlainText("\n") self.setPlainText("\n")
self.setPlainText("") self.setPlainText("")
self.setCursorPosition(0) self.setCursorPosition(0)
qApp.processEvents() qApp.processEvents()
self.document().clearUndoRedoStacks()
self.setDocumentChanged(False) self.setDocumentChanged(False)
self._qDocument.clearUndoRedoStacks()
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
# Update the status bar # Update the status bar
@@ -422,12 +422,12 @@ class GuiDocEditor(QPlainTextEdit):
def updateTagHighLighting(self) -> None: def updateTagHighLighting(self) -> None:
"""Rerun the syntax highlighter on all meta data lines.""" """Rerun the syntax highlighter on all meta data lines."""
self.highLight.rehighlightByType(GuiDocHighlighter.BLOCK_META) self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
return return
def redrawText(self) -> None: def redrawText(self) -> None:
"""Redraw the text by marking the document content as dirty.""" """Redraw the text by marking the document content as dirty."""
self.document().markContentsDirty(0, self.document().characterCount()) self._qDocument.markContentsDirty(0, self._qDocument.characterCount())
self.updateDocMargins() self.updateDocMargins()
return return
@@ -561,7 +561,7 @@ class GuiDocEditor(QPlainTextEdit):
paragraph and line separators though. paragraph and line separators though.
See: https://doc.qt.io/qt-5/qtextdocument.html#toPlainText See: https://doc.qt.io/qt-5/qtextdocument.html#toPlainText
""" """
text = self.document().toRawText() text = self._qDocument.toRawText()
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
return text return text
@@ -586,7 +586,7 @@ class GuiDocEditor(QPlainTextEdit):
def setCursorPosition(self, position: int) -> None: def setCursorPosition(self, position: int) -> None:
"""Move the cursor to a given position in the document.""" """Move the cursor to a given position in the document."""
nChars = self.document().characterCount() nChars = self._qDocument.characterCount()
if nChars > 1 and isinstance(position, int): if nChars > 1 and isinstance(position, int):
cursor = self.textCursor() cursor = self.textCursor()
cursor.setPosition(minmax(position, 0, nChars-1)) cursor.setPosition(minmax(position, 0, nChars-1))
@@ -605,7 +605,7 @@ class GuiDocEditor(QPlainTextEdit):
def setCursorLine(self, line: int | None) -> None: def setCursorLine(self, line: int | None) -> None:
"""Move the cursor to a given line in the document.""" """Move the cursor to a given line in the document."""
if isinstance(line, int) and line > 0: if isinstance(line, int) and line > 0:
block = self.document().findBlockByNumber(line - 1) block = self._qDocument.findBlockByNumber(line - 1)
if block: if block:
self.setCursorPosition(block.position()) self.setCursorPosition(block.position())
logger.debug("Cursor moved to line %d", line) logger.debug("Cursor moved to line %d", line)
@@ -638,7 +638,7 @@ class GuiDocEditor(QPlainTextEdit):
self._spellCheck = state self._spellCheck = state
self.mainGui.mainMenu.setSpellCheck(state) self.mainGui.mainMenu.setSpellCheck(state)
SHARED.project.data.setSpellCheck(state) SHARED.project.data.setSpellCheck(state)
self.highLight.setSpellCheck(state) self._qDocument.syntaxHighlighter.setSpellCheck(state)
if state is False: if state is False:
self.spellCheckDocument() self.spellCheckDocument()
@@ -655,7 +655,7 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Running spell checker") logger.debug("Running spell checker")
start = time() start = time()
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self.highLight.rehighlight() self._qDocument.syntaxHighlighter.rehighlight()
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
logger.debug("Document highlighted in %.3f ms", 1000*(time() - start)) logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
self.statusMessage.emit(self.tr("Spell check complete")) self.statusMessage.emit(self.tr("Spell check complete"))
@@ -994,7 +994,7 @@ class GuiDocEditor(QPlainTextEdit):
self.wcTimerDoc.start() self.wcTimerDoc.start()
if self._doReplace and added == 1: if self._doReplace and added == 1:
self._docAutoReplace(self.document().findBlock(pos)) self._docAutoReplace(self._qDocument.findBlock(pos))
return return
@@ -1122,7 +1122,7 @@ class GuiDocEditor(QPlainTextEdit):
theWord = cursor.selectedText().strip().strip(self._nonWord) theWord = cursor.selectedText().strip().strip(self._nonWord)
logger.debug("Added '%s' to project dictionary", theWord) logger.debug("Added '%s' to project dictionary", theWord)
SHARED.spelling.addWord(theWord) SHARED.spelling.addWord(theWord)
self.highLight.rehighlightBlock(cursor.block()) self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
return return
@pyqtSlot() @pyqtSlot()
@@ -1418,8 +1418,8 @@ class GuiDocEditor(QPlainTextEdit):
posS = theCursor.selectionStart() posS = theCursor.selectionStart()
posE = theCursor.selectionEnd() posE = theCursor.selectionEnd()
blockS = self.document().findBlock(posS) blockS = self._qDocument.findBlock(posS)
blockE = self.document().findBlock(posE) blockE = self._qDocument.findBlock(posE)
if blockS != blockE: if blockS != blockE:
posE = blockS.position() + blockS.length() - 1 posE = blockS.position() + blockS.length() - 1
@@ -1430,14 +1430,14 @@ class GuiDocEditor(QPlainTextEdit):
numB = 0 numB = 0
for n in range(fLen): for n in range(fLen):
if self.document().characterAt(posS-n-1) == fChar: if self._qDocument.characterAt(posS-n-1) == fChar:
numB += 1 numB += 1
else: else:
break break
numA = 0 numA = 0
for n in range(fLen): for n in range(fLen):
if self.document().characterAt(posE+n) == fChar: if self._qDocument.characterAt(posE+n) == fChar:
numA += 1 numA += 1
else: else:
break break
@@ -1487,9 +1487,8 @@ class GuiDocEditor(QPlainTextEdit):
posS = theCursor.selectionStart() posS = theCursor.selectionStart()
posE = theCursor.selectionEnd() posE = theCursor.selectionEnd()
qDoc = self.document() blockS = self._qDocument.findBlock(posS)
blockS = qDoc.findBlock(posS) blockE = self._qDocument.findBlock(posE)
blockE = qDoc.findBlock(posE)
if blockS != blockE: if blockS != blockE:
posE = blockS.position() + blockS.length() - 1 posE = blockS.position() + blockS.length() - 1
@@ -1690,16 +1689,15 @@ class GuiDocEditor(QPlainTextEdit):
def _removeInParLineBreaks(self) -> None: def _removeInParLineBreaks(self) -> None:
"""Strip line breaks within paragraphs in the selected text.""" """Strip line breaks within paragraphs in the selected text."""
theCursor = self.textCursor() cursor = self.textCursor()
theDoc = self.document()
iS = 0 iS = 0
iE = theDoc.blockCount() - 1 iE = self._qDocument.blockCount() - 1
rS = 0 rS = 0
rE = theDoc.characterCount() rE = self._qDocument.characterCount()
if theCursor.hasSelection(): if cursor.hasSelection():
sBlock = theDoc.findBlock(theCursor.selectionStart()) sBlock = self._qDocument.findBlock(cursor.selectionStart())
eBlock = theDoc.findBlock(theCursor.selectionEnd()) eBlock = self._qDocument.findBlock(cursor.selectionEnd())
iS = sBlock.blockNumber() iS = sBlock.blockNumber()
iE = eBlock.blockNumber() iE = eBlock.blockNumber()
rS = sBlock.position() rS = sBlock.position()
@@ -1709,7 +1707,7 @@ class GuiDocEditor(QPlainTextEdit):
currPar = [] currPar = []
cleanText = "" cleanText = ""
for i in range(iS, iE+1): for i in range(iS, iE+1):
cBlock = theDoc.findBlockByNumber(i) cBlock = self._qDocument.findBlockByNumber(i)
cText = cBlock.text() cText = cBlock.text()
if cText.strip() == "": if cText.strip() == "":
if currPar: if currPar:
@@ -1726,12 +1724,12 @@ class GuiDocEditor(QPlainTextEdit):
cleanText += " ".join(currPar) + "\n\n" cleanText += " ".join(currPar) + "\n\n"
# Replace the text with the cleaned up text # Replace the text with the cleaned up text
theCursor.beginEditBlock() cursor.beginEditBlock()
theCursor.clearSelection() cursor.clearSelection()
theCursor.setPosition(rS) cursor.setPosition(rS)
theCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS) cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS)
theCursor.insertText(cleanText.rstrip() + "\n") cursor.insertText(cleanText.rstrip() + "\n")
theCursor.endEditBlock() cursor.endEditBlock()
return return
@@ -1913,11 +1911,10 @@ class GuiDocEditor(QPlainTextEdit):
# Underscore counts as a part of the word, so check that the # Underscore counts as a part of the word, so check that the
# selection isn't wrapped in italics markers. # selection isn't wrapped in italics markers.
reSelect = False reSelect = False
qDoc = self.document() if self._qDocument.characterAt(posS) == "_":
if qDoc.characterAt(posS) == "_":
posS += 1 posS += 1
reSelect = True reSelect = True
if qDoc.characterAt(posE) == "_": if self._qDocument.characterAt(posE) == "_":
posE -= 1 posE -= 1
reSelect = True reSelect = True
if reSelect: if reSelect:
@@ -2838,7 +2835,7 @@ class GuiDocEditFooter(QWidget):
else: else:
theCursor = self.docEditor.textCursor() theCursor = self.docEditor.textCursor()
iLine = theCursor.blockNumber() + 1 iLine = theCursor.blockNumber() + 1
iDist = 100*iLine/self.docEditor.document().blockCount() iDist = 100*iLine/self.docEditor._qDocument.blockCount()
self.linesText.setText( self.linesText.setText(
self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %") self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
) )
@@ -2869,7 +2866,7 @@ class GuiDocEditFooter(QWidget):
self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}") self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
) )
byteSize = self.docEditor.document().characterCount() byteSize = self.docEditor._qDocument.characterCount()
self.wordsText.setToolTip( self.wordsText.setToolTip(
self.tr("Document size is {0} bytes").format(f"{byteSize:n}") self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
) )
+23 -25
View File
@@ -51,6 +51,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
logger.debug("Create: GuiDocHighlighter") logger.debug("Create: GuiDocHighlighter")
self._tItem = None
self._tHandle = None self._tHandle = None
self._spellCheck = False self._spellCheck = False
self._spellRx = QRegularExpression() self._spellRx = QRegularExpression()
@@ -79,11 +80,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return return
@property
def spellCheck(self) -> bool:
"""Check if spell checking is enabled."""
return self._spellCheck
def initHighlighter(self) -> None: def initHighlighter(self) -> None:
"""Initialise the syntax highlighter, setting all the colour """Initialise the syntax highlighter, setting all the colour
rules and building the RegExes. rules and building the RegExes.
@@ -248,6 +244,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
def setHandle(self, tHandle: str) -> None: def setHandle(self, tHandle: str) -> None:
"""Set the handle of the currently highlighted document.""" """Set the handle of the currently highlighted document."""
self._tHandle = tHandle self._tHandle = tHandle
self._tItem = SHARED.project.tree[tHandle]
logger.debug(
"Syntax highlighter %s for item '%s'",
"enabled" if self._tItem else "disabled", tHandle
)
return return
## ##
@@ -284,27 +285,24 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if text.startswith("@"): # Keywords and commands if text.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META) self.setCurrentBlockState(self.BLOCK_META)
pIndex = SHARED.project.index if self._tItem:
tItem = SHARED.project.tree[self._tHandle] pIndex = SHARED.project.index
if tItem is None: isValid, theBits, thePos = pIndex.scanThis(text)
return isGood = pIndex.checkThese(theBits, self._tItem)
if isValid:
isValid, theBits, thePos = pIndex.scanThis(text) for n, theBit in enumerate(theBits):
isGood = pIndex.checkThese(theBits, tItem) xPos = thePos[n]
if isValid: xLen = len(theBit)
for n, theBit in enumerate(theBits): if isGood[n]:
xPos = thePos[n] if n == 0:
xLen = len(theBit) self.setFormat(xPos, xLen, self._hStyles["keyword"])
if isGood[n]: else:
if n == 0: self.setFormat(xPos, xLen, self._hStyles["value"])
self.setFormat(xPos, xLen, self._hStyles["keyword"])
else: else:
self.setFormat(xPos, xLen, self._hStyles["value"]) kwFmt = self.format(xPos)
else: kwFmt.setUnderlineColor(self._colError)
kwFmt = self.format(xPos) kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
kwFmt.setUnderlineColor(self._colError) self.setFormat(xPos, xLen, kwFmt)
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
self.setFormat(xPos, xLen, kwFmt)
# We never want to run the spell checker on keyword/values, # We never want to run the spell checker on keyword/values,
# so we force a return here # so we force a return here
+64
View File
@@ -0,0 +1,64 @@
"""
novelWriter GUI Text Document
===============================
File History:
Created: 2023-09-07 [2.2b1]
This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import logging
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QTextDocument
from PyQt5.QtWidgets import QPlainTextDocumentLayout
from novelwriter.gui.dochighlight import GuiDocHighlighter
logger = logging.getLogger(__name__)
class GuiTextDocument(QTextDocument):
def __init__(self, parent: QObject) -> None:
super().__init__(parent=parent)
self._handle = None
self._syntax = GuiDocHighlighter(self)
self.setDocumentLayout(QPlainTextDocumentLayout(self))
logger.debug("Ready: GuiTextDocument")
return
def __del__(self): # pragma: no cover
logger.debug("Delete: GuiTextDocument")
return
@property
def syntaxHighlighter(self) -> GuiDocHighlighter:
return self._syntax
def setTextContent(self, text: str, tHandle: str) -> None:
"""Set the text content of the document."""
self._syntax.setHandle(tHandle)
self.setPlainText(text)
return
# END Class GuiTextDocument
+2 -2
View File
@@ -621,10 +621,10 @@ class GuiMain(QMainWindow):
break break
if nHandle is not None: if nHandle is not None:
self.openDocument(nHandle, tLine=0, doScroll=True) self.openDocument(nHandle, tLine=1, doScroll=True)
return True return True
elif wrapAround: elif wrapAround:
self.openDocument(fHandle, tLine=0, doScroll=True) self.openDocument(fHandle, tLine=1, doScroll=True)
return False return False
return False return False
+3 -3
View File
@@ -1,8 +1,8 @@
%%~name: Making a Scene %%~name: Making a Scene
%%~path: 6a2d6d5f4f401/636b6aa9b697b %%~path: 6a2d6d5f4f401/636b6aa9b697b
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
%%~hash: 053cc65631403c15ddc112849dc7fcae44eb9d63 %%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5
%%~date: Unknown/2023-08-25 16:56:11 %%~date: Unknown/2023-09-07 19:00:10
### Making a Scene ### Making a Scene
@pov: Jane @pov: Jane
@@ -15,7 +15,7 @@ Each paragraph in the scene is separated by a blank line. The text supports mini
In addition, the editor supports automatic formatting of “quotes”, both double and single. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.” In addition, the editor supports automatic formatting of “quotes”, both double and single. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is sett in Project Settings. If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is set in Project Settings.
The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported and can be automatically inserted when typing two hyphens. The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported and can be automatically inserted when typing two hyphens.
+14 -14
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-09-01 20:48:55"> <novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-09-07 19:14:50">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1522" autoCount="237" editTime="75353"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1531" autoCount="238" editTime="75619">
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
@@ -58,27 +58,27 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name> <name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item> </item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <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="66" /> <meta expanded="no" heading="H3" charCount="2686" wordCount="479" paraCount="14" cursorPos="19" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name> <name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item> </item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <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="531" />
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name> <name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
</item> </item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT"> <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="0" />
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name> <name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
</item> </item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE"> <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> <name status="sf24ce6" import="ia857f0" active="no">A Note on Structure</name>
</item> </item>
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT"> <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> <name status="s90e6c9" import="ia857f0" active="yes">Chapter Two</name>
</item> </item>
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <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" /> <meta expanded="no" heading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="237" />
<name status="s90e6c9" import="ia857f0" active="yes">We Found John!</name> <name status="s90e6c9" import="ia857f0" active="yes">We Found John!</name>
</item> </item>
<item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL"> <item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
@@ -90,7 +90,7 @@
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name> <name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item> </item>
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <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" /> <meta expanded="no" heading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="387" />
<name status="s90e6c9" import="ia857f0" active="yes">Chapter One</name> <name status="s90e6c9" import="ia857f0" active="yes">Chapter One</name>
</item> </item>
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER"> <item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
@@ -102,11 +102,11 @@
<name status="sf12341" import="ia857f0">Main Characters</name> <name status="sf12341" import="ia857f0">Main Characters</name>
</item> </item>
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE"> <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" /> <meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="65" />
<name status="sf12341" import="icfb3a5" active="yes">John Smith</name> <name status="sf12341" import="icfb3a5" active="yes">John Smith</name>
</item> </item>
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE"> <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" /> <meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="71" />
<name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name> <name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name>
</item> </item>
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD"> <item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
@@ -114,15 +114,15 @@
<name status="sf12341" import="ia857f0">Locations</name> <name status="sf12341" import="ia857f0">Locations</name>
</item> </item>
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE"> <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" /> <meta expanded="no" heading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="111" />
<name status="sf12341" import="i56be10" active="yes">Earth</name> <name status="sf12341" import="i56be10" active="yes">Earth</name>
</item> </item>
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE"> <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" /> <meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="135" />
<name status="sf12341" import="icfb3a5" active="yes">Space</name> <name status="sf12341" import="icfb3a5" active="yes">Space</name>
</item> </item>
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE"> <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" /> <meta expanded="no" heading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="62" />
<name status="sf12341" import="i2d7a54" active="yes">Mars</name> <name status="sf12341" import="i2d7a54" active="yes">Mars</name>
</item> </item>
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE"> <item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">