From e52d8d9ff7dbdd7982dbd2124dcbda6f1425a143 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Nov 2022 16:59:04 +0100 Subject: [PATCH 01/20] Add novel tree method to refresh metadata for item --- novelwriter/core/index.py | 5 +++ novelwriter/gui/doceditor.py | 14 +++++---- novelwriter/gui/noveltree.py | 60 ++++++++++++++++++++++++++++-------- novelwriter/guimain.py | 2 ++ 4 files changed, 62 insertions(+), 19 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index bdd49925..cf9cc002 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -501,6 +501,11 @@ class NWIndex: yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem return + def getItemData(self, tHandle): + """Get the index data for a given item. + """ + return self._itemIndex[tHandle] + def getNovelWordCount(self, skipExcl=True): """Count the number of words in the novel project. """ diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 1e2704f2..caa1b925 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -51,7 +51,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.core import NWSpellEnchant, countWords -from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode +from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass from novelwriter.common import transferCase from novelwriter.constants import nwConst, nwFiles, nwKeyWords, nwUnicode from novelwriter.gui.dochighlight import GuiDocHighlighter @@ -71,6 +71,8 @@ class GuiDocEditor(QTextEdit): docEditedStatusChanged = pyqtSignal(bool) docCountsChanged = pyqtSignal(str, int, int, int) loadDocumentTagRequest = pyqtSignal(str, Enum) + novelStructureChanged = pyqtSignal() + novelItemMetaChanged = pyqtSignal(str) def __init__(self, mainGui): super().__init__(parent=mainGui) @@ -534,11 +536,11 @@ class GuiDocEditor(QTextEdit): self.theProject.index.scanText(tHandle, docText) newHeader = self._nwItem.mainHeading - # ToDo: This should be a signal - if self._updateHeaders(): - self.mainGui.requestNovelTreeRefresh() - else: - self.mainGui.novelView.updateWordCounts(tHandle) + if self._nwItem.itemClass == nwItemClass.NOVEL: + if self._updateHeaders(): + self.novelStructureChanged.emit() + else: + self.novelItemMetaChanged.emit(tHandle) # ToDo: This should be a signal if oldHeader != newHeader: diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 02a7f567..e3134c8c 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -83,7 +83,6 @@ class GuiNovelView(QWidget): self.setLayout(self.outerBox) # Function Mappings - self.updateWordCounts = self.novelTree.updateWordCounts self.getSelectedHandle = self.novelTree.getSelectedHandle self.setActiveHandle = self.novelTree.setActiveHandle @@ -107,12 +106,6 @@ class GuiNovelView(QWidget): self.novelTree.initSettings() return - def refreshTree(self): - """Refresh the current tree. - """ - self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree")) - return - def clearProject(self): """Clear project-related GUI content. """ @@ -164,6 +157,13 @@ class GuiNovelView(QWidget): # Public Slots ## + @pyqtSlot() + def refreshTree(self): + """Refresh the current tree. + """ + self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree")) + return + @pyqtSlot(str) def updateRootItem(self, tHandle): """If any root item changes, rebuild the novel root menu. @@ -171,6 +171,14 @@ class GuiNovelView(QWidget): self.novelBar.buildNovelRootMenu() return + @pyqtSlot(str) + def updateNovelItemMeta(self, tHandle): + """The meta data of a novel item has changed, and the tree item + needs to be refreshed. + """ + self.novelTree.refreshHandle(tHandle) + return + # END Class GuiNovelView @@ -495,13 +503,39 @@ class GuiNovelTree(QTreeWidget): return - def updateWordCounts(self, tHandle): - """Update the word count for a given handle. + def refreshHandle(self, tHandle): + """Refresh the data for a given handle. """ - tHeaders = self.theProject.index.getHandleWordCounts(tHandle) - for titleKey, wCount in tHeaders: - if titleKey in self._treeMap: - self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}") + idxData = self.theProject.index.getItemData(tHandle) + if idxData is None: + return + + for sTitle, tHeading in idxData.items(): + sKey = f"{tHandle}:{sTitle}" + trItem = self._treeMap.get(sKey, None) + if trItem is None: + logger.debug("Heading '%s' not in novel tree", sKey) + continue + + iLevel = nwHeaders.H_LEVEL.get(tHeading.level, 0) + if iLevel == 0: + continue + + hDec = self.mainTheme.getHeaderDecoration(iLevel) + + trItem.setData(self.C_TITLE, Qt.DecorationRole, hDec) + trItem.setText(self.C_TITLE, tHeading.title) + trItem.setFont(self.C_TITLE, self._hFonts[iLevel]) + trItem.setText(self.C_WORDS, f"{tHeading.wordCount:n}") + trItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) + trItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore) + + # Custom column + lastText, toolTip = self._getLastColumnText(tHandle, sTitle) + trItem.setText(self.C_EXTRA, lastText) + if lastText: + trItem.setToolTip(self.C_EXTRA, toolTip) + return def getSelectedHandle(self): diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6a25e528..c4b569e6 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -224,6 +224,8 @@ class GuiMain(QMainWindow): self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) self.docEditor.docCountsChanged.connect(self.projView.updateCounts) self.docEditor.loadDocumentTagRequest.connect(self._followTag) + self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree) + self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta) self.docViewer.loadDocumentTagRequest.connect(self._followTag) From b0a0ca089f605ab2a5763b9c768f099ce79b7f52 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Nov 2022 16:59:51 +0100 Subject: [PATCH 02/20] Rewrite index class to use sequential title keys rather than line numbers --- novelwriter/common.py | 4 +- novelwriter/constants.py | 1 - novelwriter/core/index.py | 135 +++++++++++++++++++++-------------- novelwriter/gui/noveltree.py | 18 ++--- novelwriter/gui/outline.py | 33 ++++----- novelwriter/gui/projtree.py | 10 +-- novelwriter/guimain.py | 28 +++++--- 7 files changed, 133 insertions(+), 96 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index c712015a..5db4752d 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -154,11 +154,11 @@ def isHandle(value): def isTitleTag(value): - """Check if a string is a valid title string. + """Check if a string is a valid title tag string. """ if not isinstance(value, str): return False - if len(value) != 7: + if len(value) != 5: return False if not value.startswith("T"): return False diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 87293a52..3d13d1f2 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -61,7 +61,6 @@ class nwHeaders: H_VALID = ("H0", "H1", "H2", "H3", "H4") H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} - TT_NONE = "T000000" # END Class nwHeaders diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index cf9cc002..e2152ba3 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -41,6 +41,8 @@ from novelwriter.common import ( logger = logging.getLogger(__name__) +TT_NONE = "T0000" + class NWIndex: """This class holds the entire index for a given project. The index @@ -281,50 +283,58 @@ class NWIndex: def _scanActive(self, tHandle, theItem, theText, itemTags): """Scan an active document for meta data. """ - nTitle = 0 - findHeader = True - theLines = theText.splitlines() + nTitle = 0 # Line Number of the previous title + cTitle = TT_NONE # Tag of the current title + pTitle = TT_NONE # Tag of the previous title + firstHeader = True # First header has been seen + theLines = theText.splitlines() for nLine, aLine in enumerate(theLines, start=1): - if len(aLine.strip()) == 0: + if aLine.strip() == "": continue if aLine.startswith("#"): - if findHeader: - hDepth, _ = self._splitHeading(aLine) - if hDepth != "H0": - theItem.setMainHeading(hDepth) - findHeader = False + hDepth, hText = self._splitHeading(aLine) + if hDepth == "H0": + continue - isTitle = self._indexTitle(tHandle, aLine, nLine) - if isTitle and nLine > 0: + if firstHeader: + theItem.setMainHeading(hDepth) + firstHeader = False + + cTitle = self._itemIndex.addItemHeading(tHandle, nLine, hDepth, hText) + if cTitle != TT_NONE: if nTitle > 0: + # We have a new title, so we need to count the words of the previous one lastText = "\n".join(theLines[nTitle-1:nLine-1]) - self._indexWordCounts(tHandle, lastText, nTitle) + self._indexWordCounts(tHandle, lastText, pTitle) nTitle = nLine + pTitle = cTitle elif aLine.startswith("@"): - self._indexKeyword(tHandle, aLine, nTitle, theItem.itemClass, itemTags) + if cTitle != TT_NONE: + self._indexKeyword(tHandle, aLine, cTitle, theItem.itemClass, itemTags) elif aLine.startswith("%"): - if nTitle > 0: + if cTitle != TT_NONE: toCheck = aLine[1:].lstrip() synTag = toCheck[:9].lower() tLen = len(aLine) cLen = len(toCheck) cOff = tLen - cLen if synTag == "synopsis:": - self._indexSynopsis(tHandle, aLine[cOff+9:].strip(), nTitle) + sText = aLine[cOff+9:].strip() + self._itemIndex.setHeadingSynopsis(tHandle, cTitle, sText) # Count words for remaining text after last heading - if nTitle > 0: + if pTitle != TT_NONE: lastText = "\n".join(theLines[nTitle-1:]) - self._indexWordCounts(tHandle, lastText, nTitle) + self._indexWordCounts(tHandle, lastText, pTitle) # Also count words on a page with no titles - if nTitle == 0: - self._indexWordCounts(tHandle, theText, nTitle) + if cTitle == TT_NONE: + self._indexWordCounts(tHandle, theText, cTitle) # Prune no longer used tags for tTag, isActive in itemTags.items(): @@ -362,34 +372,14 @@ class NWIndex: return "H2", aLine[4:].strip() return "H0", "" - def _indexTitle(self, tHandle, aLine, nTitle): - """Save information about the title and its location in the - file to the index. - """ - hDepth, hText = self._splitHeading(aLine) - if hDepth == "H0": - return False - - sTitle = f"T{nTitle:06d}" - self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText) - return True - - def _indexWordCounts(self, tHandle, theText, nTitle): + def _indexWordCounts(self, tHandle, theText, sTitle): """Count text stats and save the counts to the index. """ - sTitle = f"T{nTitle:06d}" cC, wC, pC = countWords(theText) - self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC) + self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC) return - def _indexSynopsis(self, tHandle, theText, nTitle): - """Save the synopsis to the index. - """ - sTitle = f"T{nTitle:06d}" - self._itemIndex.setHeadingSynopsis(tHandle, sTitle, theText) - return - - def _indexKeyword(self, tHandle, aLine, nTitle, itemClass, itemTags): + def _indexKeyword(self, tHandle, aLine, sTitle, itemClass, itemTags): """Validate and save the information about a reference to a tag in another file, or the setting of a tag in the file. A record of active tags is updated so that no longer used tags can be @@ -404,7 +394,6 @@ class NWIndex: logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle) return - sTitle = f"T{nTitle:06d}" if theBits[0] == nwKeyWords.TAG_KEY: tagName = theBits[1] self._tagsIndex.add(tagName, tHandle, sTitle, itemClass) @@ -506,6 +495,14 @@ class NWIndex: """ return self._itemIndex[tHandle] + def getItemHeader(self, tHandle, sTitle): + """Get the header entry for a specific item and heading. + """ + tItem = self._itemIndex[tHandle] + if isinstance(tItem, IndexItem): + return tItem[sTitle] + return None + def getNovelWordCount(self, skipExcl=True): """Count the number of words in the novel project. """ @@ -649,6 +646,8 @@ class TagsIndex: control of the keys. """ + __slots__ = ("_tags") + def __init__(self): self._tags = {} return @@ -695,7 +694,7 @@ class TagsIndex: def tagHeading(self, tagKey): """Get the heading of a given tag. """ - return self._tags.get(tagKey, {}).get("heading", nwHeaders.TT_NONE) + return self._tags.get(tagKey, {}).get("heading", TT_NONE) def tagClass(self, tagKey): """Get the class of a given tag. @@ -754,6 +753,8 @@ class ItemIndex: IndexHeading object for each header of the text. """ + __slots__ = ("_project", "_items") + def __init__(self, project): self._project = project self._items = {} @@ -844,13 +845,15 @@ class ItemIndex: # Setters ## - def addItemHeading(self, tHandle, sTitle, hDepth, hText): - """Set the main heading level of an item. + def addItemHeading(self, tHandle, lineNo, hDepth, hText): + """Add a heading to an item. """ if tHandle in self._items: tItem = self._items[tHandle] - tItem.addHeading(IndexHeading(sTitle, hDepth, hText)) - return + sTitle = tItem.nextHeading() + tItem.addHeading(IndexHeading(sTitle, lineNo, hDepth, hText)) + return sTitle + return TT_NONE def setHeadingCounts(self, tHandle, sTitle, cC, wC, pC): """Set the character, word and paragraph counts of a heading @@ -921,14 +924,16 @@ class IndexItem: must be reset each time the item is re-indexed. """ + __slots__ = ("_handle", "_item", "_headings", "_headings", "_count") + def __init__(self, tHandle, tItem): self._handle = tHandle self._item = tItem self._headings = {} - self._index = 0 + self._count = 0 # Add a placeholder heading - self._headings[nwHeaders.TT_NONE] = IndexHeading(nwHeaders.TT_NONE) + self._headings[TT_NONE] = IndexHeading(TT_NONE) return @@ -951,8 +956,8 @@ class IndexItem: """Add a heading to the item. Also remove the placeholder entry if it exists. """ - if nwHeaders.TT_NONE in self._headings: - self._headings.pop(nwHeaders.TT_NONE) + if TT_NONE in self._headings: + self._headings.pop(TT_NONE) self._headings[tHeading.key] = tHeading return @@ -1011,6 +1016,12 @@ class IndexItem: tags.append(tag) return tags + def nextHeading(self): + """Return the next heading key to be used. + """ + self._count += 1 + return f"T{self._count:04d}" + ## # Pack/Unpack ## @@ -1056,8 +1067,14 @@ class IndexHeading: of all references made under each heading. """ - def __init__(self, key, level="H0", title=""): + __slots__ = ( + "_key", "_line", "_level", "_title", "_charCount", "_wordCount", + "_paraCount", "_synopsis", "_tag", "_refs", + ) + + def __init__(self, key, line=0, level="H0", title=""): self._key = key + self._line = line self._level = level self._title = title @@ -1082,6 +1099,10 @@ class IndexHeading: def key(self): return self._key + @property + def line(self): + return self._line + @property def level(self): return self._level @@ -1125,6 +1146,12 @@ class IndexHeading: self._level = level return + def setLine(self, line): + """Set the line number of a heading. + """ + self._line = max(0, checkInt(line, 0)) + return + def setCounts(self, charCount, wordCount, paraCount): """Set the character, word and paragraph count. Make sure the value is an integer and is not smaller than 0. @@ -1166,6 +1193,7 @@ class IndexHeading: return { "level": self._level, "title": self._title, + "line": self._line, "tag": self._tag, "cCount": self._charCount, "wCount": self._wordCount, @@ -1187,6 +1215,7 @@ class IndexHeading: self.setLevel(data.get("level", "H0")) self._title = str(data.get("title", "")) self._tag = str(data.get("tag", "")) + self.setLine(data.get("line", 0)) self.setCounts( data.get("cCount", 0), data.get("wCount", 0), diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index e3134c8c..bd796db7 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -40,7 +40,6 @@ from PyQt5.QtWidgets import ( ) from novelwriter.enum import nwDocMode, nwItemClass, nwOutline -from novelwriter.common import checkInt from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst logger = logging.getLogger(__name__) @@ -60,7 +59,7 @@ class GuiNovelView(QWidget): # Signals for user interaction with the novel tree selectedItemChanged = pyqtSignal(str) - openDocumentRequest = pyqtSignal(str, Enum, int, str) + openDocumentRequest = pyqtSignal(str, Enum, str) def __init__(self, mainGui): super().__init__(parent=mainGui) @@ -543,14 +542,11 @@ class GuiNovelTree(QTreeWidget): selected, return the first. """ selItem = self.selectedItems() - tHandle = None - tLine = 0 if selItem: tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE) sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE) - tLine = checkInt(sTitle[1:], 1) - 1 - - return tHandle, tLine + return tHandle, sTitle + return None, None def setLastColType(self, colType, doRefresh=True): """Change the content type of the last column and rebuild. @@ -609,11 +605,11 @@ class GuiNovelTree(QTreeWidget): if not isinstance(selItem, QTreeWidgetItem): return - tHandle, _ = self.getSelectedHandle() + tHandle, sTitle = self.getSelectedHandle() if tHandle is None: return - self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "") return @@ -655,8 +651,8 @@ class GuiNovelTree(QTreeWidget): clicked, and send it to the main gui class for opening in the document editor. """ - tHandle, tLine = self.getSelectedHandle() - self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, tLine, "") + tHandle, sTitle = self.getSelectedHandle() + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "") return ## diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 3ccba658..dd0bb3d2 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -55,6 +55,7 @@ logger = logging.getLogger(__name__) class GuiOutlineView(QWidget): loadDocumentTagRequest = pyqtSignal(str, Enum) + openDocumentRequest = pyqtSignal(str, Enum, str) def __init__(self, mainGui): super().__init__(parent=mainGui) @@ -375,15 +376,16 @@ class GuiOutlineTree(QTreeWidget): hiddenStateChanged = pyqtSignal() activeItemChanged = pyqtSignal(str, str) - def __init__(self, theOutline): - super().__init__(parent=theOutline) + def __init__(self, outlineView): + super().__init__(parent=outlineView) logger.debug("Initialising GuiOutlineTree ...") - self.mainConf = novelwriter.CONFIG - self.mainGui = theOutline.mainGui - self.theProject = theOutline.mainGui.theProject - self.mainTheme = theOutline.mainGui.mainTheme + self.mainConf = novelwriter.CONFIG + self.outlineView = outlineView + self.mainGui = outlineView.mainGui + self.theProject = outlineView.mainGui.theProject + self.mainTheme = outlineView.mainGui.mainTheme self.setUniformRowHeights(True) self.setFrameStyle(QFrame.NoFrame) @@ -524,13 +526,11 @@ class GuiOutlineTree(QTreeWidget): selected, return the first. """ selItem = self.selectedItems() - tHandle = None - tLine = 0 if selItem: tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) - tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1 - - return tHandle, tLine + sTitle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE) + return tHandle, sTitle + return None, None ## # Slots @@ -542,8 +542,10 @@ class GuiOutlineTree(QTreeWidget): clicked, and send it to the main gui class for opening in the document editor. """ - tHandle, tLine = self.getSelectedHandle() - self.mainGui.openDocument(tHandle, tLine=tLine - 1, doScroll=True) + tHandle, sTitle = self.getSelectedHandle() + if tHandle is None: + return + self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "") return @pyqtSlot() @@ -554,9 +556,8 @@ class GuiOutlineTree(QTreeWidget): selItems = self.selectedItems() if selItems: tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) - sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE) + sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE) self.activeItemChanged.emit(tHandle, sTitle) - return @pyqtSlot(int, int, int) @@ -718,7 +719,7 @@ class GuiOutlineTree(QTreeWidget): trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level) trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[nwItem.mainHeading]) trItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName) - trItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0")) + trItem.setText(self._colIdx[nwOutline.LINE], f"{novIdx.line:n}") trItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis) trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}") trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}") diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 5ddeacba..92bdea27 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -60,7 +60,7 @@ class GuiProjectView(QWidget): # Signals for user interaction with the project tree selectedItemChanged = pyqtSignal(str) - openDocumentRequest = pyqtSignal(str, Enum, int, str) + openDocumentRequest = pyqtSignal(str, Enum, str) # Requests for the main GUI projectSettingsRequest = pyqtSignal(int) @@ -1144,7 +1144,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.isFileType(): - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "") else: trItem.setExpanded(not trItem.isExpanded()) @@ -1190,11 +1190,11 @@ class GuiProjectTree(QTreeWidget): if isFile: aOpenDoc = ctxMenu.addAction(self.tr("Open Document")) aOpenDoc.triggered.connect( - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "") ) aViewDoc = ctxMenu.addAction(self.tr("View Document")) aViewDoc.triggered.connect( - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "") ) ctxMenu.addSeparator() @@ -1324,7 +1324,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.isFileType(): - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "") return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index c4b569e6..b32c3830 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -230,6 +230,7 @@ class GuiMain(QMainWindow): self.docViewer.loadDocumentTagRequest.connect(self._followTag) self.outlineView.loadDocumentTagRequest.connect(self._followTag) + self.outlineView.openDocumentRequest.connect(self._openDocument) # Finalise Initialisation # ======================= @@ -650,7 +651,7 @@ class GuiMain(QMainWindow): return True - def viewDocument(self, tHandle=None, tAnchor=None): + def viewDocument(self, tHandle=None, sTitle=None): """Load a document for viewing in the view panel. """ if not self.hasProject: @@ -689,7 +690,8 @@ class GuiMain(QMainWindow): self.splitDocs.setSizes(vPos) self.viewMeta.setVisible(self.mainConf.showRefPanel) - self.docViewer.navigateTo(tAnchor) + if sTitle: + self.docViewer.navigateTo(f"#{sTitle}") return True @@ -777,17 +779,23 @@ class GuiMain(QMainWindow): return False tHandle = None + sTitle = None tLine = None if self.projView.treeHasFocus(): tHandle = self.projView.getSelectedHandle() elif self.novelView.treeHasFocus(): - tHandle, tLine = self.novelView.getSelectedHandle() + tHandle, sTitle = self.novelView.getSelectedHandle() elif self.outlineView.treeHasFocus(): - tHandle, tLine = self.outlineView.getSelectedHandle() + tHandle, sTitle = self.outlineView.getSelectedHandle() else: logger.warning("No item selected") return False + if tHandle is not None and sTitle is not None: + hItem = self.theProject.index.getItemHeader(tHandle, sTitle) + if hItem is not None: + tLine = hItem.line + if tHandle is not None: self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False) @@ -1473,18 +1481,22 @@ class GuiMain(QMainWindow): if tMode == nwDocMode.EDIT: self.openDocument(tHandle) elif tMode == nwDocMode.VIEW: - self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}") + self.viewDocument(tHandle=tHandle, sTitle=sTitle) return - @pyqtSlot(str, Enum, int, str) - def _openDocument(self, tHandle, tMode, tLine, tAnchor): + @pyqtSlot(str, Enum, str) + def _openDocument(self, tHandle, tMode, sTitle): """Handle an open document request from one of the tree views. """ if tHandle is not None: if tMode == nwDocMode.EDIT: + tLine = None + hItem = self.theProject.index.getItemHeader(tHandle, sTitle) + if hItem is not None: + tLine = hItem.line self.openDocument(tHandle, tLine=tLine, changeFocus=False) elif tMode == nwDocMode.VIEW: - self.viewDocument(tHandle=tHandle, tAnchor=(tAnchor or None)) + self.viewDocument(tHandle=tHandle, sTitle=sTitle) return @pyqtSlot(nwView) From 992ed4f36c9c925ab2be79cc9c3d1415924c749a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Nov 2022 17:56:31 +0100 Subject: [PATCH 03/20] Update tests --- novelwriter/core/index.py | 27 +- novelwriter/gui/noveltree.py | 2 +- novelwriter/gui/outline.py | 2 +- .../coreIndex_LoadSave_tagsIndex.json | 56 +-- tests/test_base/test_base_common.py | 10 +- tests/test_core/test_core_index.py | 419 +++++++++--------- tests/test_gui/test_gui_noveltree.py | 12 +- tests/test_gui/test_gui_outline.py | 8 +- 8 files changed, 275 insertions(+), 261 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index e2152ba3..6005b280 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -480,16 +480,6 @@ class NWIndex: # Extract Data ## - def novelStructure(self, rootHandle=None, skipExcl=True): - """Iterate over all titles in the novel, in the correct order as - they appear in the tree view and in the respective document - files, but skipping all note files. - """ - novStruct = self._itemIndex.iterNovelStructure(rootHandle=rootHandle, skipExcl=skipExcl) - for tHandle, sTitle, hItem in novStruct: - yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem - return - def getItemData(self, tHandle): """Get the index data for a given item. """ @@ -503,6 +493,16 @@ class NWIndex: return tItem[sTitle] return None + def novelStructure(self, rootHandle=None, skipExcl=True): + """Iterate over all titles in the novel, in the correct order as + they appear in the tree view and in the respective document + files, but skipping all note files. + """ + novStruct = self._itemIndex.iterNovelStructure(rootHandle=rootHandle, skipExcl=skipExcl) + for tHandle, sTitle, hItem in novStruct: + yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem + return + def getNovelWordCount(self, skipExcl=True): """Count the number of words in the novel project. """ @@ -600,13 +600,6 @@ class NWIndex: return theRefs - def getNovelData(self, tHandle, sTitle): - """Return the novel data of a given handle and title. - """ - if tHandle in self._itemIndex: - return self._itemIndex[tHandle][sTitle] - return None - def getBackReferenceList(self, tHandle): """Build a list of files referring back to our file, specified by tHandle. diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index bd796db7..654dec2c 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -729,7 +729,7 @@ class GuiNovelTree(QTreeWidget): logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle) pIndex = self.theProject.index - novIdx = pIndex.getNovelData(tHandle, sTitle) + novIdx = pIndex.getItemHeader(tHandle, sTitle) refTags = pIndex.getReferences(tHandle, sTitle) synopText = novIdx.synopsis diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index dd0bb3d2..bc2763d7 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -1049,7 +1049,7 @@ class GuiOutlineDetails(QScrollArea): """ pIndex = self.theProject.index nwItem = self.theProject.tree[tHandle] - novIdx = pIndex.getNovelData(tHandle, sTitle) + novIdx = pIndex.getItemHeader(tHandle, sTitle) theRefs = pIndex.getReferences(tHandle, sTitle) if nwItem is None or novIdx is None: return False diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index cba693dc..738e7916 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -1,109 +1,109 @@ { "tagsIndex": { - "Bod": {"handle": "4c4f28287af27", "heading": "T000001", "class": "CHARACTER"}, - "Main": {"handle": "2426c6f0ca922", "heading": "T000001", "class": "PLOT"}, - "Europe": {"handle": "04468803b92e1", "heading": "T000001", "class": "WORLD"} + "Bod": {"handle": "4c4f28287af27", "heading": "T0001", "class": "CHARACTER"}, + "Main": {"handle": "2426c6f0ca922", "heading": "T0001", "class": "PLOT"}, + "Europe": {"handle": "04468803b92e1", "heading": "T0001", "class": "WORLD"} }, "itemIndex": { "7a992350f3eb6": { "headings": { - "T000001": {"level": "H1", "title": "Lorem Ipsum", "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""} + "T0001": {"level": "H1", "title": "Lorem Ipsum", "line": 1, "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""} } }, "8c58a65414c23": { "headings": { - "T000000": {"level": "H0", "title": "", "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""} + "T0000": {"level": "H0", "title": "", "line": 0, "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""} } }, "88d59a277361b": { "headings": { - "T000001": {"level": "H2", "title": "Prologue", "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."} + "T0001": {"level": "H2", "title": "Prologue", "line": 1, "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."} } }, "db7e733775d4d": { "headings": { - "T000001": {"level": "H1", "title": "Act One", "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""} + "T0001": {"level": "H1", "title": "Act One", "line": 1, "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""} } }, "fb609cd8319dc": { "headings": { - "T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."} + "T0001": {"level": "H2", "title": "Chapter One", "line": 1, "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."} }, "references": { - "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} + "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "88243afbe5ed8": { "headings": { - "T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."}, - "T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} + "T0001": {"level": "H3", "title": "Scene One", "line": 1, "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."}, + "T0002": {"level": "H4", "title": "Scene One, Section Two", "line": 13, "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} }, "references": { - "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} + "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "f96ec11c6a3da": { "headings": { - "T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."}, - "T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} + "T0001": {"level": "H3", "title": "Scene Two", "line": 1, "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."}, + "T0002": {"level": "H4", "title": "Scene Two, Section Two", "line": 15, "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} }, "references": { - "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} + "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "846352075de7d": { "headings": { - "T000001": {"level": "H2", "title": "Why do we use it?", "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""} + "T0001": {"level": "H2", "title": "Why do we use it?", "line": 1, "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""} } }, "441420a886d82": { "headings": { - "T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."} + "T0001": {"level": "H2", "title": "Chapter Two", "line": 1, "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."} }, "references": { - "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} + "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "eb103bc70c90c": { "headings": { - "T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."} + "T0001": {"level": "H3", "title": "Scene Three", "line": 1, "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."} }, "references": { - "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} + "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "f8c0562e50f1b": { "headings": { - "T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."} + "T0001": {"level": "H3", "title": "Scene Four", "line": 1, "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."} }, "references": { - "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} + "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "47666c91c7ccf": { "headings": { - "T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."} + "T0001": {"level": "H3", "title": "Scene Five", "line": 1, "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."} }, "references": { - "T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} + "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} } }, "4c4f28287af27": { "headings": { - "T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""} + "T0001": {"level": "H1", "title": "Nobody Owens", "line": 1, "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""} }, "references": { - "T000001": {"Main": "@plot"} + "T0001": {"Main": "@plot"} } }, "2426c6f0ca922": { "headings": { - "T000001": {"level": "H1", "title": "Main Plot", "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""} + "T0001": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""} } }, "04468803b92e1": { "headings": { - "T000001": {"level": "H1", "title": "Ancient Europe", "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""} + "T0001": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""} } } } diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index e11895dc..a3d9d28e 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -208,12 +208,12 @@ def testBaseCommon_IsHandle(): def testBaseCommon_IsTitleTag(): """Test the isItemClass function. """ - assert isTitleTag("T123456") is True + assert isTitleTag("T1234") is True - assert isTitleTag("t123456") is False - assert isTitleTag("S123456") is False - assert isTitleTag("T12345A") is False - assert isTitleTag("T1234567") is False + assert isTitleTag("t1234") is False + assert isTitleTag("S1234") is False + assert isTitleTag("T123A") is False + assert isTitleTag("T12345") is False assert isTitleTag("None") is False assert isTitleTag(None) is False diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 6d85dfb0..644c77cd 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -29,7 +29,7 @@ from tools import C, buildTestProject, cmpFiles, writeFile from novelwriter.enum import nwItemClass, nwItemLayout from novelwriter.constants import nwFiles -from novelwriter.core.index import NWIndex, countWords, TagsIndex +from novelwriter.core.index import IndexItem, NWIndex, countWords, TagsIndex from novelwriter.core.project import NWProject @@ -231,10 +231,10 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd): "@invalid: John\n" # Checks for issue #688 )) assert theIndex._tagsIndex.tagHandle("Jane") == cHandle - assert theIndex._tagsIndex.tagHeading("Jane") == "T000001" + assert theIndex._tagsIndex.tagHeading("Jane") == "T0001" assert theIndex._tagsIndex.tagClass("Jane") == "CHARACTER" - assert theIndex.getNovelData(nHandle, "T000001").title == "Hello World!" - assert theIndex.getReferences(nHandle, "T000001") == { + assert theIndex.getItemHeader(nHandle, "T0001").title == "Hello World!" + assert theIndex.getReferences(nHandle, "T0001") == { "@char": [], "@custom": [], "@entity": [], @@ -345,9 +345,9 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): "Well, not really.\n" )) assert theIndex._tagsIndex.tagHandle("Jane") == cHandle - assert theIndex._tagsIndex.tagHeading("Jane") == "T000001" + assert theIndex._tagsIndex.tagHeading("Jane") == "T0001" assert theIndex._tagsIndex.tagClass("Jane") == "CHARACTER" - assert theIndex.getNovelData(nHandle, "T000001").title == "Hello World!" + assert theIndex.getItemHeader(nHandle, "T0001").title == "Hello World!" # Title Indexing # ============== @@ -369,40 +369,45 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): "##### Title Five\n\n" # Not interpreted as a title, the hashes are counted as a word "Paragraph Five.\n\n" )) - assert theIndex._itemIndex[nHandle]["T000001"].references == {} - assert theIndex._itemIndex[nHandle]["T000007"].references == {} - assert theIndex._itemIndex[nHandle]["T000013"].references == {} - assert theIndex._itemIndex[nHandle]["T000019"].references == {} + assert theIndex._itemIndex[nHandle]["T0001"].references == {} + assert theIndex._itemIndex[nHandle]["T0002"].references == {} + assert theIndex._itemIndex[nHandle]["T0003"].references == {} + assert theIndex._itemIndex[nHandle]["T0004"].references == {} - assert theIndex._itemIndex[nHandle]["T000001"].level == "H1" - assert theIndex._itemIndex[nHandle]["T000007"].level == "H2" - assert theIndex._itemIndex[nHandle]["T000013"].level == "H3" - assert theIndex._itemIndex[nHandle]["T000019"].level == "H4" + assert theIndex._itemIndex[nHandle]["T0001"].level == "H1" + assert theIndex._itemIndex[nHandle]["T0002"].level == "H2" + assert theIndex._itemIndex[nHandle]["T0003"].level == "H3" + assert theIndex._itemIndex[nHandle]["T0004"].level == "H4" - assert theIndex._itemIndex[nHandle]["T000001"].title == "Title One" - assert theIndex._itemIndex[nHandle]["T000007"].title == "Title Two" - assert theIndex._itemIndex[nHandle]["T000013"].title == "Title Three" - assert theIndex._itemIndex[nHandle]["T000019"].title == "Title Four" + assert theIndex._itemIndex[nHandle]["T0001"].line == 1 + assert theIndex._itemIndex[nHandle]["T0002"].line == 7 + assert theIndex._itemIndex[nHandle]["T0003"].line == 13 + assert theIndex._itemIndex[nHandle]["T0004"].line == 19 - assert theIndex._itemIndex[nHandle]["T000001"].charCount == 23 - assert theIndex._itemIndex[nHandle]["T000007"].charCount == 23 - assert theIndex._itemIndex[nHandle]["T000013"].charCount == 27 - assert theIndex._itemIndex[nHandle]["T000019"].charCount == 56 + assert theIndex._itemIndex[nHandle]["T0001"].title == "Title One" + assert theIndex._itemIndex[nHandle]["T0002"].title == "Title Two" + assert theIndex._itemIndex[nHandle]["T0003"].title == "Title Three" + assert theIndex._itemIndex[nHandle]["T0004"].title == "Title Four" - assert theIndex._itemIndex[nHandle]["T000001"].wordCount == 4 - assert theIndex._itemIndex[nHandle]["T000007"].wordCount == 4 - assert theIndex._itemIndex[nHandle]["T000013"].wordCount == 4 - assert theIndex._itemIndex[nHandle]["T000019"].wordCount == 9 + assert theIndex._itemIndex[nHandle]["T0001"].charCount == 23 + assert theIndex._itemIndex[nHandle]["T0002"].charCount == 23 + assert theIndex._itemIndex[nHandle]["T0003"].charCount == 27 + assert theIndex._itemIndex[nHandle]["T0004"].charCount == 56 - assert theIndex._itemIndex[nHandle]["T000001"].paraCount == 1 - assert theIndex._itemIndex[nHandle]["T000007"].paraCount == 1 - assert theIndex._itemIndex[nHandle]["T000013"].paraCount == 1 - assert theIndex._itemIndex[nHandle]["T000019"].paraCount == 3 + assert theIndex._itemIndex[nHandle]["T0001"].wordCount == 4 + assert theIndex._itemIndex[nHandle]["T0002"].wordCount == 4 + assert theIndex._itemIndex[nHandle]["T0003"].wordCount == 4 + assert theIndex._itemIndex[nHandle]["T0004"].wordCount == 9 - assert theIndex._itemIndex[nHandle]["T000001"].synopsis == "Synopsis One." - assert theIndex._itemIndex[nHandle]["T000007"].synopsis == "Synopsis Two." - assert theIndex._itemIndex[nHandle]["T000013"].synopsis == "Synopsis Three." - assert theIndex._itemIndex[nHandle]["T000019"].synopsis == "Synopsis Four." + assert theIndex._itemIndex[nHandle]["T0001"].paraCount == 1 + assert theIndex._itemIndex[nHandle]["T0002"].paraCount == 1 + assert theIndex._itemIndex[nHandle]["T0003"].paraCount == 1 + assert theIndex._itemIndex[nHandle]["T0004"].paraCount == 3 + + assert theIndex._itemIndex[nHandle]["T0001"].synopsis == "Synopsis One." + assert theIndex._itemIndex[nHandle]["T0002"].synopsis == "Synopsis Two." + assert theIndex._itemIndex[nHandle]["T0003"].synopsis == "Synopsis Three." + assert theIndex._itemIndex[nHandle]["T0004"].synopsis == "Synopsis Four." # Note File assert theIndex.scanText(cHandle, ( @@ -411,13 +416,14 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): "% synopsis: Synopsis One.\n\n" "Paragraph One.\n\n" )) - assert theIndex._itemIndex[cHandle]["T000001"].references == {} - assert theIndex._itemIndex[cHandle]["T000001"].level == "H1" - assert theIndex._itemIndex[cHandle]["T000001"].title == "Title One" - assert theIndex._itemIndex[cHandle]["T000001"].charCount == 23 - assert theIndex._itemIndex[cHandle]["T000001"].wordCount == 4 - assert theIndex._itemIndex[cHandle]["T000001"].paraCount == 1 - assert theIndex._itemIndex[cHandle]["T000001"].synopsis == "Synopsis One." + assert theIndex._itemIndex[cHandle]["T0001"].references == {} + assert theIndex._itemIndex[cHandle]["T0001"].level == "H1" + assert theIndex._itemIndex[cHandle]["T0001"].line == 1 + assert theIndex._itemIndex[cHandle]["T0001"].title == "Title One" + assert theIndex._itemIndex[cHandle]["T0001"].charCount == 23 + assert theIndex._itemIndex[cHandle]["T0001"].wordCount == 4 + assert theIndex._itemIndex[cHandle]["T0001"].paraCount == 1 + assert theIndex._itemIndex[cHandle]["T0001"].synopsis == "Synopsis One." # Valid and Invalid References assert theIndex.scanText(sHandle, ( @@ -428,7 +434,7 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): "% synopsis: Synopsis One.\n\n" "Paragraph One.\n\n" )) - assert theIndex._itemIndex[sHandle]["T000001"].references == { + assert theIndex._itemIndex[sHandle]["T0001"].references == { "One": {"@pov"}, "Two": {"@char"} } @@ -439,25 +445,27 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): "#! My Project\n\n" ">> By Jane Doe <<\n\n" )) - assert theIndex._itemIndex[cHandle]["T000001"].references == {} - assert theIndex._itemIndex[tHandle]["T000001"].level == "H1" - assert theIndex._itemIndex[tHandle]["T000001"].title == "My Project" - assert theIndex._itemIndex[tHandle]["T000001"].charCount == 21 - assert theIndex._itemIndex[tHandle]["T000001"].wordCount == 5 - assert theIndex._itemIndex[tHandle]["T000001"].paraCount == 1 - assert theIndex._itemIndex[tHandle]["T000001"].synopsis == "" + assert theIndex._itemIndex[cHandle]["T0001"].references == {} + assert theIndex._itemIndex[tHandle]["T0001"].level == "H1" + assert theIndex._itemIndex[tHandle]["T0001"].line == 1 + assert theIndex._itemIndex[tHandle]["T0001"].title == "My Project" + assert theIndex._itemIndex[tHandle]["T0001"].charCount == 21 + assert theIndex._itemIndex[tHandle]["T0001"].wordCount == 5 + assert theIndex._itemIndex[tHandle]["T0001"].paraCount == 1 + assert theIndex._itemIndex[tHandle]["T0001"].synopsis == "" assert theIndex.scanText(tHandle, ( "##! Prologue\n\n" "In the beginning there was time ...\n\n" )) - assert theIndex._itemIndex[cHandle]["T000001"].references == {} - assert theIndex._itemIndex[tHandle]["T000001"].level == "H2" - assert theIndex._itemIndex[tHandle]["T000001"].title == "Prologue" - assert theIndex._itemIndex[tHandle]["T000001"].charCount == 43 - assert theIndex._itemIndex[tHandle]["T000001"].wordCount == 8 - assert theIndex._itemIndex[tHandle]["T000001"].paraCount == 1 - assert theIndex._itemIndex[tHandle]["T000001"].synopsis == "" + assert theIndex._itemIndex[cHandle]["T0001"].references == {} + assert theIndex._itemIndex[tHandle]["T0001"].level == "H2" + assert theIndex._itemIndex[tHandle]["T0001"].line == 1 + assert theIndex._itemIndex[tHandle]["T0001"].title == "Prologue" + assert theIndex._itemIndex[tHandle]["T0001"].charCount == 43 + assert theIndex._itemIndex[tHandle]["T0001"].wordCount == 8 + assert theIndex._itemIndex[tHandle]["T0001"].paraCount == 1 + assert theIndex._itemIndex[tHandle]["T0001"].synopsis == "" # Page wo/Title # ============= @@ -466,25 +474,27 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): assert theIndex.scanText(pHandle, ( "This is a page with some text on it.\n\n" )) - assert theIndex._itemIndex[pHandle]["T000000"].references == {} - assert theIndex._itemIndex[pHandle]["T000000"].level == "H0" - assert theIndex._itemIndex[pHandle]["T000000"].title == "" - assert theIndex._itemIndex[pHandle]["T000000"].charCount == 36 - assert theIndex._itemIndex[pHandle]["T000000"].wordCount == 9 - assert theIndex._itemIndex[pHandle]["T000000"].paraCount == 1 - assert theIndex._itemIndex[pHandle]["T000000"].synopsis == "" + assert theIndex._itemIndex[pHandle]["T0000"].references == {} + assert theIndex._itemIndex[pHandle]["T0000"].level == "H0" + assert theIndex._itemIndex[pHandle]["T0000"].line == 0 + assert theIndex._itemIndex[pHandle]["T0000"].title == "" + assert theIndex._itemIndex[pHandle]["T0000"].charCount == 36 + assert theIndex._itemIndex[pHandle]["T0000"].wordCount == 9 + assert theIndex._itemIndex[pHandle]["T0000"].paraCount == 1 + assert theIndex._itemIndex[pHandle]["T0000"].synopsis == "" theProject.tree[pHandle]._layout = nwItemLayout.NOTE assert theIndex.scanText(pHandle, ( "This is a page with some text on it.\n\n" )) - assert theIndex._itemIndex[pHandle]["T000000"].references == {} - assert theIndex._itemIndex[pHandle]["T000000"].level == "H0" - assert theIndex._itemIndex[pHandle]["T000000"].title == "" - assert theIndex._itemIndex[pHandle]["T000000"].charCount == 36 - assert theIndex._itemIndex[pHandle]["T000000"].wordCount == 9 - assert theIndex._itemIndex[pHandle]["T000000"].paraCount == 1 - assert theIndex._itemIndex[pHandle]["T000000"].synopsis == "" + assert theIndex._itemIndex[pHandle]["T0000"].references == {} + assert theIndex._itemIndex[pHandle]["T0000"].level == "H0" + assert theIndex._itemIndex[pHandle]["T0000"].line == 0 + assert theIndex._itemIndex[pHandle]["T0000"].title == "" + assert theIndex._itemIndex[pHandle]["T0000"].charCount == 36 + assert theIndex._itemIndex[pHandle]["T0000"].wordCount == 9 + assert theIndex._itemIndex[pHandle]["T0000"].paraCount == 1 + assert theIndex._itemIndex[pHandle]["T0000"].synopsis == "" assert theProject.closeProject() is True @@ -512,8 +522,8 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): nHandle = theProject.newFile("Hello", C.hNovelRoot) cHandle = theProject.newFile("Jane", C.hCharRoot) - assert theIndex.getNovelData("", "") is None - assert theIndex.getNovelData(C.hNovelRoot, "") is None + assert theIndex.getItemHeader("", "") is None + assert theIndex.getItemHeader(C.hNovelRoot, "") is None assert theIndex.scanText(cHandle, ( "# Jane Smith\n" @@ -534,10 +544,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): theKeys.append(aKey) assert theKeys == [ - f"{C.hTitlePage}:T000001", - f"{C.hChapterDoc}:T000001", - f"{C.hSceneDoc}:T000001", - f"{nHandle}:T000001", + f"{C.hTitlePage}:T0001", + f"{C.hChapterDoc}:T0001", + f"{C.hSceneDoc}:T0001", + f"{nHandle}:T0001", ] # Check that excluded files can be skipped @@ -548,10 +558,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): theKeys.append(aKey) assert theKeys == [ - f"{C.hTitlePage}:T000001", - f"{C.hChapterDoc}:T000001", - f"{C.hSceneDoc}:T000001", - f"{nHandle}:T000001", + f"{C.hTitlePage}:T0001", + f"{C.hChapterDoc}:T0001", + f"{C.hSceneDoc}:T0001", + f"{nHandle}:T0001", ] theKeys = [] @@ -559,9 +569,9 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): theKeys.append(aKey) assert theKeys == [ - f"{C.hTitlePage}:T000001", - f"{C.hChapterDoc}:T000001", - f"{C.hSceneDoc}:T000001", + f"{C.hTitlePage}:T0001", + f"{C.hChapterDoc}:T0001", + f"{C.hSceneDoc}:T0001", ] # The novel file should have the correct counts @@ -570,6 +580,13 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): assert wC == 12 # Words in text and title only assert pC == 2 # Paragraphs in text only + # getItemData + # =========== + + theItem = theIndex.getItemData(nHandle) + assert isinstance(theItem, IndexItem) + assert theItem.headings() == ["T0001"] + # getReferences # ============= @@ -594,13 +611,13 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): # The character file should have a record of the reference from the novel file theRefs = theIndex.getBackReferenceList(cHandle) - assert theRefs == {nHandle: "T000001"} + assert theRefs == {nHandle: "T0001"} # getTagSource # ============ - assert theIndex.getTagSource("Jane") == (cHandle, "T000001") - assert theIndex.getTagSource("John") == (None, "T000000") + assert theIndex.getTagSource("Jane") == (cHandle, "T0001") + assert theIndex.getTagSource("John") == (None, "T0000") # getCounts # ========= @@ -632,13 +649,13 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): assert pC == 4 # First part - cC, wC, pC = theIndex.getCounts(nHandle, "T000001") + cC, wC, pC = theIndex.getCounts(nHandle, "T0001") assert cC == 62 assert wC == 12 assert pC == 2 # Second part - cC, wC, pC = theIndex.getCounts(nHandle, "T000011") + cC, wC, pC = theIndex.getCounts(nHandle, "T0002") assert cC == 90 assert wC == 16 assert pC == 2 @@ -665,13 +682,13 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): assert pC == 4 # First part - cC, wC, pC = theIndex.getCounts(cHandle, "T000001") + cC, wC, pC = theIndex.getCounts(cHandle, "T0001") assert cC == 62 assert wC == 12 assert pC == 2 # Second part - cC, wC, pC = theIndex.getCounts(cHandle, "T000011") + cC, wC, pC = theIndex.getCounts(cHandle, "T0002") assert cC == 90 assert wC == 16 assert pC == 2 @@ -692,36 +709,36 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): assert theIndex.scanText(tHandle, "### Scene Two\n\n") assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ - (C.hTitlePage, "T000001"), - (C.hChapterDoc, "T000001"), - (C.hSceneDoc, "T000001"), - (nHandle, "T000001"), - (nHandle, "T000011"), - (hHandle, "T000001"), - (sHandle, "T000001"), - (tHandle, "T000001"), + (C.hTitlePage, "T0001"), + (C.hChapterDoc, "T0001"), + (C.hSceneDoc, "T0001"), + (nHandle, "T0001"), + (nHandle, "T0002"), + (hHandle, "T0001"), + (sHandle, "T0001"), + (tHandle, "T0001"), ] assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=True)] == [ - (C.hTitlePage, "T000001"), - (C.hChapterDoc, "T000001"), - (C.hSceneDoc, "T000001"), - (hHandle, "T000001"), - (sHandle, "T000001"), - (tHandle, "T000001"), + (C.hTitlePage, "T0001"), + (C.hChapterDoc, "T0001"), + (C.hSceneDoc, "T0001"), + (hHandle, "T0001"), + (sHandle, "T0001"), + (tHandle, "T0001"), ] # Add a fake handle to the tree and check that it's ignored theProject.tree._treeOrder.append("0000000000000") assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ - (C.hTitlePage, "T000001"), - (C.hChapterDoc, "T000001"), - (C.hSceneDoc, "T000001"), - (nHandle, "T000001"), - (nHandle, "T000011"), - (hHandle, "T000001"), - (sHandle, "T000001"), - (tHandle, "T000001"), + (C.hTitlePage, "T0001"), + (C.hChapterDoc, "T0001"), + (C.hSceneDoc, "T0001"), + (nHandle, "T0001"), + (nHandle, "T0002"), + (hHandle, "T0001"), + (sHandle, "T0001"), + (tHandle, "T0001"), ] theProject.tree._treeOrder.remove("0000000000000") @@ -734,37 +751,37 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): # Table of Contents assert theIndex.getTableOfContents(C.hNovelRoot, 0, skipExcl=True) == [] assert theIndex.getTableOfContents(C.hNovelRoot, 1, skipExcl=True) == [ - (f"{C.hTitlePage}:T000001", 1, "New Novel", 15), + (f"{C.hTitlePage}:T0001", 1, "New Novel", 15), ] assert theIndex.getTableOfContents(C.hNovelRoot, 2, skipExcl=True) == [ - (f"{C.hTitlePage}:T000001", 1, "New Novel", 5), - (f"{C.hChapterDoc}:T000001", 2, "New Chapter", 4), - (f"{hHandle}:T000001", 2, "Chapter One", 6), + (f"{C.hTitlePage}:T0001", 1, "New Novel", 5), + (f"{C.hChapterDoc}:T0001", 2, "New Chapter", 4), + (f"{hHandle}:T0001", 2, "Chapter One", 6), ] assert theIndex.getTableOfContents(C.hNovelRoot, 3, skipExcl=True) == [ - (f"{C.hTitlePage}:T000001", 1, "New Novel", 5), - (f"{C.hChapterDoc}:T000001", 2, "New Chapter", 2), - (f"{C.hSceneDoc}:T000001", 3, "New Scene", 2), - (f"{hHandle}:T000001", 2, "Chapter One", 2), - (f"{sHandle}:T000001", 3, "Scene One", 2), - (f"{tHandle}:T000001", 3, "Scene Two", 2), + (f"{C.hTitlePage}:T0001", 1, "New Novel", 5), + (f"{C.hChapterDoc}:T0001", 2, "New Chapter", 2), + (f"{C.hSceneDoc}:T0001", 3, "New Scene", 2), + (f"{hHandle}:T0001", 2, "Chapter One", 2), + (f"{sHandle}:T0001", 3, "Scene One", 2), + (f"{tHandle}:T0001", 3, "Scene Two", 2), ] assert theIndex.getTableOfContents(C.hNovelRoot, 0, skipExcl=False) == [] assert theIndex.getTableOfContents(C.hNovelRoot, 1, skipExcl=False) == [ - (f"{C.hTitlePage}:T000001", 1, "New Novel", 9), - (f"{nHandle}:T000001", 1, "Hello World!", 12), - (f"{nHandle}:T000011", 1, "Hello World!", 22), + (f"{C.hTitlePage}:T0001", 1, "New Novel", 9), + (f"{nHandle}:T0001", 1, "Hello World!", 12), + (f"{nHandle}:T0002", 1, "Hello World!", 22), ] # Header Word Counts bHandle = "0000000000000" assert theIndex.getHandleWordCounts(bHandle) == [] - assert theIndex.getHandleWordCounts(hHandle) == [("%s:T000001" % hHandle, 2)] - assert theIndex.getHandleWordCounts(sHandle) == [("%s:T000001" % sHandle, 2)] - assert theIndex.getHandleWordCounts(tHandle) == [("%s:T000001" % tHandle, 2)] + assert theIndex.getHandleWordCounts(hHandle) == [("%s:T0001" % hHandle, 2)] + assert theIndex.getHandleWordCounts(sHandle) == [("%s:T0001" % sHandle, 2)] + assert theIndex.getHandleWordCounts(tHandle) == [("%s:T0001" % tHandle, 2)] assert theIndex.getHandleWordCounts(nHandle) == [ - (f"{nHandle}:T000001", 12), (f"{nHandle}:T000011", 16) + (f"{nHandle}:T0001", 12), (f"{nHandle}:T0002", 16) ] assert theIndex.saveIndex() is True @@ -773,11 +790,11 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): # Header Record bHandle = "0000000000000" assert theIndex.getHandleHeaders(bHandle) == [] - assert theIndex.getHandleHeaders(hHandle) == [("T000001", "H2", "Chapter One")] - assert theIndex.getHandleHeaders(sHandle) == [("T000001", "H3", "Scene One")] - assert theIndex.getHandleHeaders(tHandle) == [("T000001", "H3", "Scene Two")] + assert theIndex.getHandleHeaders(hHandle) == [("T0001", "H2", "Chapter One")] + assert theIndex.getHandleHeaders(sHandle) == [("T0001", "H3", "Scene One")] + assert theIndex.getHandleHeaders(tHandle) == [("T0001", "H3", "Scene Two")] assert theIndex.getHandleHeaders(nHandle) == [ - ("T000001", "H1", "Hello World!"), ("T000011", "H1", "Hello World!") + ("T0001", "H1", "Hello World!"), ("T0002", "H1", "Hello World!") ] assert theProject.closeProject() is True @@ -796,25 +813,25 @@ def testCoreIndex_TagsIndex(): content = { "Tag1": { "handle": "0000000000001", - "heading": "T000001", + "heading": "T0001", "class": nwItemClass.NOVEL.name, }, "Tag2": { "handle": "0000000000002", - "heading": "T000002", + "heading": "T0002", "class": nwItemClass.CHARACTER.name, }, "Tag3": { "handle": "0000000000003", - "heading": "T000003", + "heading": "T0003", "class": nwItemClass.PLOT.name, }, } # Add data - tagsIndex.add("Tag1", "0000000000001", "T000001", nwItemClass.NOVEL) - tagsIndex.add("Tag2", "0000000000002", "T000002", nwItemClass.CHARACTER) - tagsIndex.add("Tag3", "0000000000003", "T000003", nwItemClass.PLOT) + tagsIndex.add("Tag1", "0000000000001", "T0001", nwItemClass.NOVEL) + tagsIndex.add("Tag2", "0000000000002", "T0002", nwItemClass.CHARACTER) + tagsIndex.add("Tag3", "0000000000003", "T0003", nwItemClass.PLOT) assert tagsIndex._tags == content # Get items @@ -836,10 +853,10 @@ def testCoreIndex_TagsIndex(): assert tagsIndex.tagHandle("Tag4") is None # Read back headings - assert tagsIndex.tagHeading("Tag1") == "T000001" - assert tagsIndex.tagHeading("Tag2") == "T000002" - assert tagsIndex.tagHeading("Tag3") == "T000003" - assert tagsIndex.tagHeading("Tag4") == "T000000" + assert tagsIndex.tagHeading("Tag1") == "T0001" + assert tagsIndex.tagHeading("Tag2") == "T0002" + assert tagsIndex.tagHeading("Tag3") == "T0003" + assert tagsIndex.tagHeading("Tag4") == "T0000" # Read back classes assert tagsIndex.tagClass("Tag1") == nwItemClass.NOVEL.name @@ -880,7 +897,7 @@ def testCoreIndex_TagsIndex(): tagsIndex.unpackData({ 1234: { "handle": "0000000000001", - "heading": "T000001", + "heading": "T0001", "class": "NOVEL", } }) @@ -889,7 +906,7 @@ def testCoreIndex_TagsIndex(): with pytest.raises(KeyError): tagsIndex.unpackData({ "Tag1": { - "heading": "T000001", + "heading": "T0001", "class": "NOVEL", } }) @@ -908,7 +925,7 @@ def testCoreIndex_TagsIndex(): tagsIndex.unpackData({ "Tag1": { "handle": "0000000000001", - "heading": "T000001", + "heading": "T0001", } }) @@ -917,7 +934,7 @@ def testCoreIndex_TagsIndex(): tagsIndex.unpackData({ "Tag1": { "handle": "blablabla", - "heading": "T000001", + "heading": "T0001", "class": "NOVEL", } }) @@ -937,7 +954,7 @@ def testCoreIndex_TagsIndex(): tagsIndex.unpackData({ "Tag1": { "handle": "0000000000001", - "heading": "T000001", + "heading": "T0001", "class": "blabla", } }) @@ -975,66 +992,70 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): assert cHandle in itemIndex assert itemIndex[cHandle].item == theProject.tree[cHandle] assert itemIndex.allItemTags(cHandle) == [] - assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T000000" + assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T0000" # Add a heading to the item, which should replace the T000000 heading - itemIndex.addItemHeading(cHandle, "T000001", "H2", "Chapter One") - assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T000001" + assert itemIndex.addItemHeading(cHandle, 1, "H2", "Chapter One") == "T0001" + assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T0001" + + # Add a heading to an invalid item + assert itemIndex.addItemHeading(C.hInvalid, 1, "H1", "Stuff") == "T0000" # Set the remainig data values - itemIndex.setHeadingCounts(cHandle, "T000001", 60, 10, 2) - itemIndex.setHeadingSynopsis(cHandle, "T000001", "In the beginning ...") - itemIndex.setHeadingTag(cHandle, "T000001", "One") - itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@pov") - itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@focus") - itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane", "John"], "@char") + itemIndex.setHeadingCounts(cHandle, "T0001", 60, 10, 2) + itemIndex.setHeadingSynopsis(cHandle, "T0001", "In the beginning ...") + itemIndex.setHeadingTag(cHandle, "T0001", "One") + itemIndex.addHeadingReferences(cHandle, "T0001", ["Jane"], "@pov") + itemIndex.addHeadingReferences(cHandle, "T0001", ["Jane"], "@focus") + itemIndex.addHeadingReferences(cHandle, "T0001", ["Jane", "John"], "@char") idxData = itemIndex.packData() - assert idxData[cHandle]["headings"]["T000001"] == { - "level": "H2", "title": "Chapter One", "tag": "One", + assert idxData[cHandle]["headings"]["T0001"] == { + "level": "H2", "line": 1, "title": "Chapter One", "tag": "One", "cCount": 60, "wCount": 10, "pCount": 2, "synopsis": "In the beginning ...", } - assert "@pov" in idxData[cHandle]["references"]["T000001"]["Jane"] - assert "@focus" in idxData[cHandle]["references"]["T000001"]["Jane"] - assert "@char" in idxData[cHandle]["references"]["T000001"]["Jane"] - assert "@char" in idxData[cHandle]["references"]["T000001"]["John"] + assert "@pov" in idxData[cHandle]["references"]["T0001"]["Jane"] + assert "@focus" in idxData[cHandle]["references"]["T0001"]["Jane"] + assert "@char" in idxData[cHandle]["references"]["T0001"]["Jane"] + assert "@char" in idxData[cHandle]["references"]["T0001"]["John"] # Add the other two files itemIndex.add(nHandle, theProject.tree[nHandle]) itemIndex.add(sHandle, theProject.tree[sHandle]) - itemIndex.addItemHeading(nHandle, "T000001", "H1", "Novel") - itemIndex.addItemHeading(sHandle, "T000001", "H3", "Scene One") + itemIndex.addItemHeading(nHandle, 1, "H1", "Novel") + itemIndex.addItemHeading(sHandle, 1, "H3", "Scene One") # Check Item and Heading Direct Access # ==================================== # Check repr strings assert repr(itemIndex[nHandle]) == f"" - assert repr(itemIndex[nHandle]["T000001"]) == "" + assert repr(itemIndex[nHandle]["T0001"]) == "" # Check content of a single item - assert "T000001" in itemIndex[nHandle] + assert "T0001" in itemIndex[nHandle] assert itemIndex[cHandle].allTags() == ["One"] # Check the content of a single heading - assert itemIndex[cHandle]["T000001"].key == "T000001" - assert itemIndex[cHandle]["T000001"].level == "H2" - assert itemIndex[cHandle]["T000001"].title == "Chapter One" - assert itemIndex[cHandle]["T000001"].tag == "One" - assert itemIndex[cHandle]["T000001"].charCount == 60 - assert itemIndex[cHandle]["T000001"].wordCount == 10 - assert itemIndex[cHandle]["T000001"].paraCount == 2 - assert itemIndex[cHandle]["T000001"].synopsis == "In the beginning ..." - assert "Jane" in itemIndex[cHandle]["T000001"].references - assert "John" in itemIndex[cHandle]["T000001"].references + assert itemIndex[cHandle]["T0001"].key == "T0001" + assert itemIndex[cHandle]["T0001"].level == "H2" + assert itemIndex[cHandle]["T0001"].line == 1 + assert itemIndex[cHandle]["T0001"].title == "Chapter One" + assert itemIndex[cHandle]["T0001"].tag == "One" + assert itemIndex[cHandle]["T0001"].charCount == 60 + assert itemIndex[cHandle]["T0001"].wordCount == 10 + assert itemIndex[cHandle]["T0001"].paraCount == 2 + assert itemIndex[cHandle]["T0001"].synopsis == "In the beginning ..." + assert "Jane" in itemIndex[cHandle]["T0001"].references + assert "John" in itemIndex[cHandle]["T0001"].references # Check heading level setter - itemIndex[cHandle]["T000001"].setLevel("H3") # Change it - assert itemIndex[cHandle]["T000001"].level == "H3" - itemIndex[cHandle]["T000001"].setLevel("H2") # Set it back - assert itemIndex[cHandle]["T000001"].level == "H2" - itemIndex[cHandle]["T000001"].setLevel("H5") # Invalid level - assert itemIndex[cHandle]["T000001"].level == "H2" + itemIndex[cHandle]["T0001"].setLevel("H3") # Change it + assert itemIndex[cHandle]["T0001"].level == "H3" + itemIndex[cHandle]["T0001"].setLevel("H2") # Set it back + assert itemIndex[cHandle]["T0001"].level == "H2" + itemIndex[cHandle]["T0001"].setLevel("H5") # Invalid level + assert itemIndex[cHandle]["T0001"].level == "H2" # Data Extraction # =============== @@ -1044,9 +1065,9 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): assert allHeads[0][0] == cHandle assert allHeads[1][0] == nHandle assert allHeads[2][0] == sHandle - assert allHeads[0][1] == "T000001" - assert allHeads[1][1] == "T000001" - assert allHeads[2][1] == "T000001" + assert allHeads[0][1] == "T0001" + assert allHeads[1][1] == "T0001" + assert allHeads[2][1] == "T0001" # Ask for stuff that doesn't exist assert itemIndex.allItemTags("blablabla") == [] @@ -1058,7 +1079,7 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): mHandle = theProject.newRoot(nwItemClass.NOVEL) uHandle = theProject.newFile("Title Page", mHandle) itemIndex.add(uHandle, theProject.tree[uHandle]) - itemIndex.addItemHeading(uHandle, "T000001", "H1", "Novel 2") + itemIndex.addItemHeading(uHandle, "T0001", "H1", "Novel 2") assert uHandle in itemIndex # Structure of all novels @@ -1134,20 +1155,20 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): # Reference without a heading should be rejected itemIndex.unpackData({ cHandle: { - "headings": {"T000001": {}}, - "references": {"T000001": {}, "T000002": {}}, + "headings": {"T0001": {}}, + "references": {"T0001": {}, "T0002": {}}, } }) - assert "T000001" in itemIndex[cHandle] - assert "T000002" not in itemIndex[cHandle] + assert "T0001" in itemIndex[cHandle] + assert "T0002" not in itemIndex[cHandle] itemIndex.clear() # Tag keys must be strings with pytest.raises(ValueError): itemIndex.unpackData({ cHandle: { - "headings": {"T000001": {}}, - "references": {"T000001": {1234: "@pov"}}, + "headings": {"T0001": {}}, + "references": {"T0001": {1234: "@pov"}}, } }) @@ -1155,8 +1176,8 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): with pytest.raises(ValueError): itemIndex.unpackData({ cHandle: { - "headings": {"T000001": {}}, - "references": {"T000001": {"John": []}}, + "headings": {"T0001": {}}, + "references": {"T0001": {"John": []}}, } }) @@ -1164,16 +1185,16 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): with pytest.raises(ValueError): itemIndex.unpackData({ cHandle: { - "headings": {"T000001": {}}, - "references": {"T000001": {"John": "@pov,@char,@stuff"}}, + "headings": {"T0001": {}}, + "references": {"T0001": {"John": "@pov,@char,@stuff"}}, } }) # This should pass itemIndex.unpackData({ cHandle: { - "headings": {"T000001": {}}, - "references": {"T000001": {"John": "@pov,@char"}}, + "headings": {"T0001": {}}, + "references": {"T0001": {"John": "@pov,@char"}}, } }) diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 20c38d03..15101972 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -92,7 +92,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): assert not topItem.isSelected() topItem.setSelected(True) assert novelTree.selectedItems()[0] == topItem - assert novelView.getSelectedHandle() == (C.hTitlePage, 0) + assert novelView.getSelectedHandle() == (C.hTitlePage, "T0001") # Refresh using the slot for the butoom novelBar._refreshNovelTree() @@ -142,31 +142,31 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): novelBar.setLastColType(NovelTreeColumn.HIDDEN) assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True assert novelTree.lastColType == NovelTreeColumn.HIDDEN - assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ("", "") + assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ("", "") novelBar.setLastColType(NovelTreeColumn.POV) assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False assert novelTree.lastColType == NovelTreeColumn.POV - assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ( + assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ( "Jane", "Point of View: Jane" ) novelBar.setLastColType(NovelTreeColumn.FOCUS) assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False assert novelTree.lastColType == NovelTreeColumn.FOCUS - assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ( + assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ( "Jane", "Focus: Jane" ) novelBar.setLastColType(NovelTreeColumn.PLOT) assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False assert novelTree.lastColType == NovelTreeColumn.PLOT - assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ( + assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ( "", "Plot: " ) novelTree._lastCol = None - assert novelTree._getLastColumnText("0000000000000", "T000000") == ("", "") + assert novelTree._getLastColumnText("0000000000000", "T0000") == ("", "") # Item Meta # ========= diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 23f61c5b..707f497d 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -225,9 +225,9 @@ def testGuiOutline_Content(qtbot, nwGUI, nwLipsum): selItem = outlineTree.topLevelItem(4) outlineTree.setCurrentItem(selItem) - tHandle, tLine = outlineTree.getSelectedHandle() + tHandle, sTitle = outlineTree.getSelectedHandle() assert tHandle == "88243afbe5ed8" - assert tLine == 0 + assert sTitle == "T0001" assert outlineData.titleLabel.text() == "Scene" assert outlineData.titleValue.text() == "Scene One" @@ -243,9 +243,9 @@ def testGuiOutline_Content(qtbot, nwGUI, nwLipsum): selItem = outlineTree.topLevelItem(5) outlineTree.setCurrentItem(selItem) - tHandle, tLine = outlineTree.getSelectedHandle() + tHandle, sTitle = outlineTree.getSelectedHandle() assert tHandle == "88243afbe5ed8" - assert tLine == 12 + assert sTitle == "T0002" assert outlineData.titleLabel.text() == "Section" assert outlineData.titleValue.text() == "Scene One, Section Two" From ebc66540692195cb226d29ab992a6b73ee6ace42 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Nov 2022 18:45:36 +0100 Subject: [PATCH 04/20] Improve how the novel tree is refreshed --- novelwriter/core/index.py | 79 ++++++++++++---------------- novelwriter/gui/doceditor.py | 29 +++------- novelwriter/gui/noveltree.py | 63 ++++++++++------------ novelwriter/guimain.py | 19 +------ tests/test_core/test_core_index.py | 16 ++---- tests/test_gui/test_gui_doceditor.py | 4 +- tests/test_gui/test_gui_guimain.py | 1 - 7 files changed, 76 insertions(+), 135 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 6005b280..fb7c5f28 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -528,13 +528,13 @@ class NWIndex: for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle) ] - def getHandleHeaders(self, tHandle): - """Get all headers for a specific handle. + def getHandleHeaderCount(self, tHandle): + """Get the number of headers in an item. """ - return [ - (sTitle, hItem.level, hItem.title) - for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle) - ] + tItem = self._itemIndex[tHandle] + if isinstance(tItem, IndexItem): + return len(tItem) + return 0 def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True): """Generate a table of contents up to a maximum depth. @@ -645,6 +645,16 @@ class TagsIndex: self._tags = {} return + def __contains__(self, tagKey): + return tagKey in self._tags + + def __delitem__(self, tagKey): + self._tags.pop(tagKey, None) + return + + def __getitem__(self, tagKey): + return self._tags.get(tagKey, None) + ## # Methods ## @@ -655,22 +665,6 @@ class TagsIndex: self._tags = {} return - def __contains__(self, tagKey): - """Check if a tag exists in the index, - """ - return tagKey in self._tags - - def __delitem__(self, tagKey): - """Delete an entry in the index. - """ - self._tags.pop(tagKey, None) - return - - def __getitem__(self, tagKey): - """Return a tag, or return None if it isn't found. - """ - return self._tags.get(tagKey, None) - def add(self, tagKey, tHandle, sTitle, itemClass): """Add a key to the index and set all values. """ @@ -753,6 +747,16 @@ class ItemIndex: self._items = {} return + def __contains__(self, tHandle): + return tHandle in self._items + + def __delitem__(self, tHandle): + self._items.pop(tHandle, None) + return + + def __getitem__(self, tHandle): + return self._items.get(tHandle, None) + ## # Methods ## @@ -763,22 +767,6 @@ class ItemIndex: self._items = {} return - def __contains__(self, tHandle): - """Check if an item exists in the index, - """ - return tHandle in self._items - - def __delitem__(self, tHandle): - """Delete an entry in the index. - """ - self._items.pop(tHandle, None) - return - - def __getitem__(self, tHandle): - """Return an item, or return None if it isn't found. - """ - return self._items.get(tHandle, None) - def add(self, tHandle, tItem): """Add a new item to the index. This will overwrite the item if it already exists. @@ -933,6 +921,15 @@ class IndexItem: def __repr__(self): return f"" + def __len__(self): + return len(self._headings) + + def __getitem__(self, sTitle): + return self._headings.get(sTitle, None) + + def __contains__(self, sTitle): + return sTitle in self._headings + ## # Properties ## @@ -987,12 +984,6 @@ class IndexItem: # Data Methods ## - def __getitem__(self, sTitle): - return self._headings.get(sTitle, None) - - def __contains__(self, sTitle): - return sTitle in self._headings - def items(self): return self._headings.items() diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index caa1b925..3b40b05a 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -90,7 +90,6 @@ class GuiDocEditor(QTextEdit): self._docChanged = False # Flag for changed status of document self._docHandle = None # The handle of the open file - self._docHeaders = [] # Record of headers in the file self._spellCheck = False # Flag for spell checking enabled self._nonWord = "\"'" # Characters to not include in spell checking @@ -417,8 +416,8 @@ class GuiDocEditor(QTextEdit): self._queuePos = self._nwItem.cursorPos else: self.setCursorPosition(self._nwItem.cursorPos) - else: - self.setCursorLine(tLine) + elif isinstance(tLine, int): + self.setCursorLine(tLine - 1) if self.mainConf.scrollPastEnd > 0: fSize = QFontMetrics(self.font()).lineSpacing() @@ -427,7 +426,6 @@ class GuiDocEditor(QTextEdit): self.document().rootFrame().setFrameFormat(docFrame) self.docFooter.updateLineCount() - self._docHeaders = self.theProject.index.getHandleHeaders(self._docHandle) qApp.processEvents() self.document().clearUndoRedoStacks() @@ -533,14 +531,16 @@ class GuiDocEditor(QTextEdit): self.setDocumentChanged(False) oldHeader = self._nwItem.mainHeading + oldCount = self.theProject.index.getHandleHeaderCount(tHandle) self.theProject.index.scanText(tHandle, docText) newHeader = self._nwItem.mainHeading + newCount = self.theProject.index.getHandleHeaderCount(tHandle) if self._nwItem.itemClass == nwItemClass.NOVEL: - if self._updateHeaders(): - self.novelStructureChanged.emit() - else: + if oldCount == newCount: self.novelItemMetaChanged.emit(tHandle) + else: + self.novelStructureChanged.emit() # ToDo: This should be a signal if oldHeader != newHeader: @@ -2067,21 +2067,6 @@ class GuiDocEditor(QTextEdit): return False return True - def _updateHeaders(self): - """Update the headers record and return True if anything - changed, if a check flag was provided. - """ - if self._docHandle is None: - return False - - newHeaders = self.theProject.index.getHandleHeaders(self._docHandle) - newLev = [x[1] for x in newHeaders] - oldLev = [x[1] for x in self._docHeaders] - - self._docHeaders = newHeaders - - return newLev != oldLev - def _checkDocSize(self, theSize): """Check if document size crosses the big document limit set in config. If so, we will set the big document flag to True. diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 654dec2c..2a022bb0 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -477,7 +477,7 @@ class GuiNovelTree(QTreeWidget): return def refreshTree(self, rootHandle=None, overRide=False): - """Called whenever the Novel tab is activated. + """Refresh the tree if it has been changed. """ logger.debug("Requesting refresh of the novel tree") if rootHandle is None: @@ -509,31 +509,16 @@ class GuiNovelTree(QTreeWidget): if idxData is None: return + logger.debug("Refreshing meta data for item '%s'", tHandle) for sTitle, tHeading in idxData.items(): sKey = f"{tHandle}:{sTitle}" trItem = self._treeMap.get(sKey, None) if trItem is None: logger.debug("Heading '%s' not in novel tree", sKey) - continue + self.refreshTree() + return - iLevel = nwHeaders.H_LEVEL.get(tHeading.level, 0) - if iLevel == 0: - continue - - hDec = self.mainTheme.getHeaderDecoration(iLevel) - - trItem.setData(self.C_TITLE, Qt.DecorationRole, hDec) - trItem.setText(self.C_TITLE, tHeading.title) - trItem.setFont(self.C_TITLE, self._hFonts[iLevel]) - trItem.setText(self.C_WORDS, f"{tHeading.wordCount:n}") - trItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - trItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore) - - # Custom column - lastText, toolTip = self._getLastColumnText(tHandle, sTitle) - trItem.setText(self.C_EXTRA, lastText) - if lastText: - trItem.setToolTip(self.C_EXTRA, toolTip) + self._updateTreeItemValues(trItem, tHeading, tHandle, sTitle) return @@ -668,30 +653,16 @@ class GuiNovelTree(QTreeWidget): novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) for tKey, tHandle, sTitle, novIdx in novStruct: - - iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0) - if iLevel == 0: + if novIdx.level == "H0": continue - hDec = self.mainTheme.getHeaderDecoration(iLevel) - newItem = QTreeWidgetItem() - newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec) - newItem.setText(self.C_TITLE, novIdx.title) newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle) newItem.setData(self.C_TITLE, self.D_TITLE, sTitle) newItem.setData(self.C_TITLE, self.D_KEY, tKey) - newItem.setFont(self.C_TITLE, self._hFonts[iLevel]) - newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}") newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore) - - # Custom column - lastText, toolTip = self._getLastColumnText(tHandle, sTitle) - newItem.setText(self.C_EXTRA, lastText) - if lastText: - newItem.setToolTip(self.C_EXTRA, toolTip) + self._updateTreeItemValues(newItem, novIdx, tHandle, sTitle) self._treeMap[tKey] = newItem self.addTopLevelItem(newItem) @@ -702,6 +673,26 @@ class GuiNovelTree(QTreeWidget): return + def _updateTreeItemValues(self, trItem, idxItem, tHandle, sTitle): + """Set the tree item values from the index entry. + """ + iLevel = nwHeaders.H_LEVEL.get(idxItem.level, 0) + hDec = self.mainTheme.getHeaderDecoration(iLevel) + + trItem.setData(self.C_TITLE, Qt.DecorationRole, hDec) + trItem.setText(self.C_TITLE, idxItem.title) + trItem.setFont(self.C_TITLE, self._hFonts[iLevel]) + trItem.setText(self.C_WORDS, f"{idxItem.wordCount:n}") + trItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore) + + # Custom column + lastText, toolTip = self._getLastColumnText(tHandle, sTitle) + trItem.setText(self.C_EXTRA, lastText) + if lastText: + trItem.setToolTip(self.C_EXTRA, toolTip) + + return + def _getLastColumnText(self, tHandle, sTitle): """Generate the text for the last column based on user settings. """ diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index b32c3830..92b7c9cf 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -818,17 +818,8 @@ class GuiMain(QMainWindow): """Rebuild the project tree. """ self.projView.populateTree() - # self.novelView.refreshTree() return - def requestNovelTreeRefresh(self): - """Update the novel tree, but only if it is visible. - """ - if self.projStack.currentIndex() == self.idxNovelView and self.hasProject: - self.novelView.refreshTree() - return True - return False - def rebuildIndex(self, beQuiet=False): """Rebuild the entire index. """ @@ -843,6 +834,7 @@ class GuiMain(QMainWindow): self.projView.saveProjectTasks() self.theProject.index.rebuildIndex() self.projView.populateTree() + self.novelView.refreshTree() tEnd = time() self.setStatus( @@ -1578,7 +1570,6 @@ class GuiMain(QMainWindow): self.docEditor.closeSearch() elif self.isFocusMode: self.toggleFocusMode() - return @pyqtSlot(int) @@ -1595,17 +1586,11 @@ class GuiMain(QMainWindow): """Activated when the project view tab is changed. """ sHandle = None - if stIndex == self.idxProjView: sHandle = self.projView.getSelectedHandle() - elif stIndex == self.idxNovelView: - if self.hasProject: - self.novelView.refreshTree() - sHandle, _ = self.novelView.getSelectedHandle() - + sHandle, _ = self.novelView.getSelectedHandle() self.itemDetails.updateViewBox(sHandle) - return # END Class GuiMain diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 644c77cd..7ea81ed2 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -580,12 +580,13 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): assert wC == 12 # Words in text and title only assert pC == 2 # Paragraphs in text only - # getItemData - # =========== + # getItemData + getHandleHeaderCount + # ================================== theItem = theIndex.getItemData(nHandle) assert isinstance(theItem, IndexItem) assert theItem.headings() == ["T0001"] + assert theIndex.getHandleHeaderCount(nHandle) == 1 # getReferences # ============= @@ -786,17 +787,6 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): assert theIndex.saveIndex() is True assert theProject.saveProject() is True - - # Header Record - bHandle = "0000000000000" - assert theIndex.getHandleHeaders(bHandle) == [] - assert theIndex.getHandleHeaders(hHandle) == [("T0001", "H2", "Chapter One")] - assert theIndex.getHandleHeaders(sHandle) == [("T0001", "H3", "Scene One")] - assert theIndex.getHandleHeaders(tHandle) == [("T0001", "H3", "Scene Two")] - assert theIndex.getHandleHeaders(nHandle) == [ - ("T0001", "H1", "Hello World!"), ("T0002", "H1", "Hello World!") - ] - assert theProject.closeProject() is True # END Test testCoreIndex_ExtractData diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 7f47fd60..017c955c 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -118,10 +118,10 @@ def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumTex assert nwGUI.docEditor.loadText(C.hSceneDoc) is True assert nwGUI.docEditor._bigDoc is True - # Regular open, with line number + # Regular open, with line number (1 indexed) assert nwGUI.docEditor.loadText(C.hSceneDoc, tLine=4) is True cursPos = nwGUI.docEditor.getCursorPosition() - assert nwGUI.docEditor.document().findBlock(cursPos).blockNumber() == 4 + assert nwGUI.docEditor.document().findBlock(cursPos).blockNumber() == 3 # Load empty document nwGUI.docEditor.replaceText("") diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 9d4f2d02..43ac328f 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -57,7 +57,6 @@ def testGuiMain_ProjectBlocker(nwGUI): assert nwGUI.importDocument() is False assert nwGUI.openSelectedItem() is False assert nwGUI.editItemLabel() is False - assert nwGUI.requestNovelTreeRefresh() is False assert nwGUI.rebuildIndex() is False assert nwGUI.showProjectSettingsDialog() is False assert nwGUI.showProjectDetailsDialog() is False From 8dbaa6bbd92af032872c00a26fdc94afb9bb066c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Nov 2022 18:59:47 +0100 Subject: [PATCH 05/20] Remove no longer needed index function --- novelwriter/core/index.py | 8 -------- tests/test_core/test_core_index.py | 10 ---------- 2 files changed, 18 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index fb7c5f28..fdedb526 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -520,14 +520,6 @@ class NWIndex: hCount[iLevel] += 1 return hCount - def getHandleWordCounts(self, tHandle): - """Get all header word counts for a specific handle. - """ - return [ - (f"{tHandle}:{sTitle}", hItem.wordCount) - for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle) - ] - def getHandleHeaderCount(self, tHandle): """Get the number of headers in an item. """ diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 7ea81ed2..dd94155f 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -775,16 +775,6 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): (f"{nHandle}:T0002", 1, "Hello World!", 22), ] - # Header Word Counts - bHandle = "0000000000000" - assert theIndex.getHandleWordCounts(bHandle) == [] - assert theIndex.getHandleWordCounts(hHandle) == [("%s:T0001" % hHandle, 2)] - assert theIndex.getHandleWordCounts(sHandle) == [("%s:T0001" % sHandle, 2)] - assert theIndex.getHandleWordCounts(tHandle) == [("%s:T0001" % tHandle, 2)] - assert theIndex.getHandleWordCounts(nHandle) == [ - (f"{nHandle}:T0001", 12), (f"{nHandle}:T0002", 16) - ] - assert theIndex.saveIndex() is True assert theProject.saveProject() is True assert theProject.closeProject() is True From 6560594c4941d256dd555508b276d2a76541e6d2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 16:48:46 +0100 Subject: [PATCH 06/20] Fix scrolling when moving to a specific line in editor (#1239) --- novelwriter/gui/doceditor.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 3b40b05a..8eded68e 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -52,7 +52,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWSpellEnchant, countWords from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass -from novelwriter.common import transferCase +from novelwriter.common import minmax, transferCase from novelwriter.constants import nwConst, nwFiles, nwKeyWords, nwUnicode from novelwriter.gui.dochighlight import GuiDocHighlighter @@ -654,17 +654,25 @@ class GuiDocEditor(QTextEdit): self.docEditedStatusChanged.emit(self._docChanged) return self._docChanged - def setCursorPosition(self, thePosition): + def setCursorPosition(self, position): """Move the cursor to a given position in the document. """ - if not isinstance(thePosition, int): + if not isinstance(position, int): return False nChars = self.document().characterCount() if nChars > 1: theCursor = self.textCursor() - theCursor.setPosition(min(max(thePosition, 0), nChars-1)) + theCursor.setPosition(minmax(position, 0, nChars-1)) self.setTextCursor(theCursor) + + # The editor scrolls so the cursor is on the last line, so we must correct + vPos = self.verticalScrollBar().value() # Current scrollbar position + cPos = self.cursorRect().topLeft().y() # Cursor position to scroll to + dMrg = int(self.document().documentMargin()) # Document margin to subtract + mPos = int(self.viewport().height()*0.1) # Distance from top to adjust for (10%) + self.verticalScrollBar().setValue(max(0, vPos + cPos - dMrg - mPos)) + self.docFooter.updateLineCount() return True @@ -677,18 +685,17 @@ class GuiDocEditor(QTextEdit): self._nwItem.setCursorPos(cursPos) return - def setCursorLine(self, theLine): + def setCursorLine(self, lineNo): """Move the cursor to a given line in the document. """ - if not isinstance(theLine, int): + if not isinstance(lineNo, int): return False - if theLine >= 0: - theBlock = self.document().findBlockByLineNumber(theLine) + if lineNo >= 0: + theBlock = self.document().findBlockByLineNumber(lineNo) if theBlock: self.setCursorPosition(theBlock.position()) - self.docFooter.updateLineCount() - logger.debug("Cursor moved to line %d", theLine) + logger.debug("Cursor moved to line %d", lineNo) return True From a2ea7b40e23f9a28a97f6c9011b7f236c297b8bf Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 16:49:59 +0100 Subject: [PATCH 07/20] Don't reload document if opening the same (#1242) --- novelwriter/gui/doceditor.py | 7 ++++--- novelwriter/gui/noveltree.py | 6 +++--- novelwriter/gui/outline.py | 4 ++-- novelwriter/gui/projtree.py | 10 +++++----- novelwriter/guimain.py | 17 ++++++++++++----- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 8eded68e..b2641ce6 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -417,7 +417,7 @@ class GuiDocEditor(QTextEdit): else: self.setCursorPosition(self._nwItem.cursorPos) elif isinstance(tLine, int): - self.setCursorLine(tLine - 1) + self.setCursorLine(tLine) if self.mainConf.scrollPastEnd > 0: fSize = QFontMetrics(self.font()).lineSpacing() @@ -691,8 +691,9 @@ class GuiDocEditor(QTextEdit): if not isinstance(lineNo, int): return False - if lineNo >= 0: - theBlock = self.document().findBlockByLineNumber(lineNo) + lineIdx = lineNo - 1 # Block index is 0 offset, lineNo is 1 offset + if lineIdx >= 0: + theBlock = self.document().findBlockByLineNumber(lineIdx) if theBlock: self.setCursorPosition(theBlock.position()) logger.debug("Cursor moved to line %d", lineNo) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 2a022bb0..12ff53ad 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -59,7 +59,7 @@ class GuiNovelView(QWidget): # Signals for user interaction with the novel tree selectedItemChanged = pyqtSignal(str) - openDocumentRequest = pyqtSignal(str, Enum, str) + openDocumentRequest = pyqtSignal(str, Enum, str, bool) def __init__(self, mainGui): super().__init__(parent=mainGui) @@ -594,7 +594,7 @@ class GuiNovelTree(QTreeWidget): if tHandle is None: return - self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "") + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False) return @@ -637,7 +637,7 @@ class GuiNovelTree(QTreeWidget): document editor. """ tHandle, sTitle = self.getSelectedHandle() - self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "") + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True) return ## diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index bc2763d7..98d3b36e 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -55,7 +55,7 @@ logger = logging.getLogger(__name__) class GuiOutlineView(QWidget): loadDocumentTagRequest = pyqtSignal(str, Enum) - openDocumentRequest = pyqtSignal(str, Enum, str) + openDocumentRequest = pyqtSignal(str, Enum, str, bool) def __init__(self, mainGui): super().__init__(parent=mainGui) @@ -545,7 +545,7 @@ class GuiOutlineTree(QTreeWidget): tHandle, sTitle = self.getSelectedHandle() if tHandle is None: return - self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "") + self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True) return @pyqtSlot() diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 92bdea27..3b6a38f0 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -60,7 +60,7 @@ class GuiProjectView(QWidget): # Signals for user interaction with the project tree selectedItemChanged = pyqtSignal(str) - openDocumentRequest = pyqtSignal(str, Enum, str) + openDocumentRequest = pyqtSignal(str, Enum, str, bool) # Requests for the main GUI projectSettingsRequest = pyqtSignal(int) @@ -1144,7 +1144,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.isFileType(): - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "") + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True) else: trItem.setExpanded(not trItem.isExpanded()) @@ -1190,11 +1190,11 @@ class GuiProjectTree(QTreeWidget): if isFile: aOpenDoc = ctxMenu.addAction(self.tr("Open Document")) aOpenDoc.triggered.connect( - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "") + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True) ) aViewDoc = ctxMenu.addAction(self.tr("View Document")) aViewDoc.triggered.connect( - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "") + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False) ) ctxMenu.addSeparator() @@ -1324,7 +1324,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.isFileType(): - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "") + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 92b7c9cf..3a5d278d 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -596,14 +596,21 @@ class GuiMain(QMainWindow): logger.debug("Requested item '%s' is not a document", tHandle) return False + cHandle = self.docEditor.docHandle() + if cHandle == tHandle: + self.docEditor.setCursorLine(tLine) + if changeFocus: + self.docEditor.setFocus() + return True + self.closeDocument(beforeOpen=True) self._changeView(nwView.EDITOR) if self.docEditor.loadText(tHandle, tLine): - if changeFocus: - self.docEditor.setFocus() self.theProject.data.setLastHandle(tHandle, "editor") self.projView.setSelectedHandle(tHandle, doScroll=doScroll) self.novelView.setActiveHandle(tHandle) + if changeFocus: + self.docEditor.setFocus() else: return False @@ -1476,8 +1483,8 @@ class GuiMain(QMainWindow): self.viewDocument(tHandle=tHandle, sTitle=sTitle) return - @pyqtSlot(str, Enum, str) - def _openDocument(self, tHandle, tMode, sTitle): + @pyqtSlot(str, Enum, str, bool) + def _openDocument(self, tHandle, tMode, sTitle, setFocus): """Handle an open document request from one of the tree views. """ if tHandle is not None: @@ -1486,7 +1493,7 @@ class GuiMain(QMainWindow): hItem = self.theProject.index.getItemHeader(tHandle, sTitle) if hItem is not None: tLine = hItem.line - self.openDocument(tHandle, tLine=tLine, changeFocus=False) + self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus) elif tMode == nwDocMode.VIEW: self.viewDocument(tHandle=tHandle, sTitle=sTitle) return From e0ca2e7fb5cd742fd4dcebffbbbcf74db77fbd2e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 16:50:41 +0100 Subject: [PATCH 08/20] Update tests and remove unused functions in viewer --- novelwriter/gui/docviewer.py | 39 +++---------------- .../guiEditor_Main_Final_nwProject.nwx | 16 +++----- tests/test_gui/test_gui_doceditor.py | 12 +++--- tests/test_gui/test_gui_docviewer.py | 17 +++----- tests/test_gui/test_gui_guimain.py | 11 ------ 5 files changed, 24 insertions(+), 71 deletions(-) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index e498a100..a25fad6d 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -323,43 +323,10 @@ class GuiDocViewer(QTextBrowser): return - ## - # Properties - ## - - def docHandle(self): - """Return the handle of the currently open document. Returns - None if no document is open. - """ - return self._docHandle - ## # Setters ## - def setCursorPosition(self, thePosition): - """Move the cursor to a given position in the document. - """ - if not isinstance(thePosition, int): - return False - if thePosition >= 0: - theCursor = self.textCursor() - theCursor.setPosition(thePosition) - self.setTextCursor(theCursor) - return True - - def setCursorLine(self, theLine): - """Move the cursor to a given line in the document. - """ - if not isinstance(theLine, int): - return False - if theLine >= 0: - theBlock = self.document().findBlockByLineNumber(theLine) - if theBlock: - self.setCursorPosition(theBlock.position()) - logger.debug("Cursor moved to line %d", theLine) - return True - def setScrollPosition(self, thePos): """Set the scrollbar position. """ @@ -372,6 +339,12 @@ class GuiDocViewer(QTextBrowser): # Getters ## + def docHandle(self): + """Return the handle of the currently open document. Returns + None if no document is open. + """ + return self._docHandle + def getScrollPosition(self): """Get the scrollbar position. Returns 0 if no scrollbar. """ diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index e384ef9e..dc0456a7 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,6 +1,6 @@ - - + + New Project New Novel Jane Doe @@ -11,9 +11,9 @@ None 000000000000f - None + 000000000000f 0000000000008 - 0000000000008 + None @@ -30,13 +30,13 @@ Finished - New + New Minor Major Main - + Novel @@ -81,9 +81,5 @@ New Note - - - Trash - diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 017c955c..3de027df 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -211,7 +211,7 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): assert nwGUI.theProject.tree[C.hSceneDoc].cursorPos == 10 assert nwGUI.docEditor.setCursorLine(None) is False - assert nwGUI.docEditor.setCursorLine(2) is True + assert nwGUI.docEditor.setCursorLine(3) is True assert nwGUI.docEditor.getCursorPosition() == 15 # Document Changed Signal @@ -510,7 +510,7 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd theText = "### A Scene\n\n\n%s" % ipsumText[0] assert nwGUI.docEditor.replaceText(theText) is True - assert nwGUI.docEditor.setCursorLine(2) + assert nwGUI.docEditor.setCursorLine(3) # Invalid Keyword assert nwGUI.docEditor.insertKeyWord("stuff") is False @@ -1049,10 +1049,10 @@ def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, projPath, ipsumText assert nwGUI.docEditor.getText() == "Title\n\n" assert nwGUI.docEditor.getCursorPosition() == 5 - # Second Line + # Third Line # This also needs to add a new block assert nwGUI.docEditor.replaceText("#### Title\n\nThe Text\n\n") is True - assert nwGUI.docEditor.setCursorLine(2) is True + assert nwGUI.docEditor.setCursorLine(3) is True assert nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_COM) is True assert nwGUI.docEditor.getText() == "#### Title\n\n% The Text\n\n" @@ -1086,11 +1086,11 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert nwGUI.openDocument(C.hSceneDoc) is True # Empty Block - assert nwGUI.docEditor.setCursorLine(1) is True + assert nwGUI.docEditor.setCursorLine(2) is True assert nwGUI.docEditor._followTag() is False # Not On Tag - assert nwGUI.docEditor.setCursorLine(0) is True + assert nwGUI.docEditor.setCursorLine(1) is True assert nwGUI.docEditor._followTag() is False # On Tag Keyword diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 22057e15..f7c2d766 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -57,17 +57,10 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.docViewer.docHeader._refreshDocument() assert nwGUI.docViewer.toPlainText() == origText - # Cursor line - assert nwGUI.docViewer.setCursorLine("not a number") is False - assert nwGUI.docViewer.setCursorLine(3) is True - theCursor = nwGUI.docViewer.textCursor() - assert theCursor.position() == 40 - - # Cursor position - assert nwGUI.docViewer.setCursorPosition("not a number") is False - assert nwGUI.docViewer.setCursorPosition(100) is True - # Select word + theCursor = nwGUI.docViewer.textCursor() + theCursor.setPosition(100) + nwGUI.docViewer.setTextCursor(theCursor) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) qClip = qApp.clipboard() @@ -113,7 +106,9 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger) # Select "Bod" link - assert nwGUI.docViewer.setCursorPosition(27) is True + theCursor = nwGUI.docViewer.textCursor() + theCursor.setPosition(27) + nwGUI.docViewer.setTextCursor(theCursor) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) theRect = nwGUI.docViewer.cursorRect() # qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 43ac328f..da1902da 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -515,17 +515,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert nwGUI.saveProject() assert nwGUI.closeDocViewer() - # Check a Quick Create and Delete - assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None) - newHandle = nwGUI.projView.getSelectedHandle() - assert newHandle == "0000000000013" - assert nwGUI.theProject.tree[newHandle] is not None - assert nwGUI.projView.requestDeleteItem() - assert nwGUI.projView.setSelectedHandle(newHandle) - assert nwGUI.projView.requestDeleteItem() - assert nwGUI.theProject.tree["0000000000014"] is not None # Trash - assert nwGUI.saveProject() - # Check the files projFile = projPath / "nwProject.nwx" testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx" From a15d25370e27b2589400d28b1913463abcbf20fb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 21:49:15 +0100 Subject: [PATCH 09/20] Improve project tree iterator --- novelwriter/core/index.py | 4 +--- novelwriter/core/tree.py | 49 +++++++++++++-------------------------- novelwriter/guimain.py | 2 +- 3 files changed, 18 insertions(+), 37 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index fdedb526..39c70c81 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -104,7 +104,7 @@ class NWIndex: """ self.clearIndex() for nwItem in self._project.tree: - if nwItem is not None and nwItem.isFileType(): + if nwItem.isFileType(): tHandle = nwItem.itemHandle theDoc = self._project.storage.getDocument(tHandle) self.scanText(tHandle, theDoc.readDocument() or "") @@ -794,8 +794,6 @@ class ItemIndex: a given root handle, or for all if root handle is None. """ for tItem in self._project.tree: - if tItem is None: - continue if tItem.isNoteLayout(): continue if skipExcl and not tItem.isActive: diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 1f9960de..c71b1804 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -50,7 +50,6 @@ class NWTree: self._treeRoots = {} # The root items of the tree self._trashRoot = None # The handle of the trash root folder self._archRoot = None # The handle of the archive root folder - self._theIndex = 0 # The current iterator index self._treeChanged = False # True if tree structure has changed return @@ -62,12 +61,11 @@ class NWTree: def clear(self): """Clear the item tree entirely. """ - self._projTree = {} - self._treeOrder = [] - self._treeRoots = {} - self._trashRoot = None - self._archRoot = None - self._theIndex = 0 + self._projTree = {} + self._treeOrder = [] + self._treeRoots = {} + self._trashRoot = None + self._archRoot = None self._treeChanged = False return @@ -278,7 +276,7 @@ class NWTree: """ for tHandle in self._treeOrder: nwItem = self.__getitem__(tHandle) - if nwItem is not None and nwItem.isRootType(): + if isinstance(nwItem, NWItem) and nwItem.isRootType(): if itemClass is None or nwItem.itemClass == itemClass: yield tHandle, nwItem return @@ -365,22 +363,18 @@ class NWTree: return True ## - # Meta Methods + # Special Methods ## def __len__(self): - """Return the length counter. Does not check that it is correct! + """The number of items in the project. """ return len(self._treeOrder) def __bool__(self): - """Returns True if the tree has any entries. + """True if there are any items in the project. """ - return len(self._treeOrder) > 0 - - ## - # Item Access Methods - ## + return bool(self._treeOrder) def __getitem__(self, tHandle): """Return a project item based on its handle. Returns None if @@ -417,25 +411,14 @@ class NWTree: """ return tHandle in self._treeOrder - ## - # Iterator Methods - ## - def __iter__(self): - """Initiates the iterator. + """Iterate through project items. """ - self._theIndex = 0 - return self - - def __next__(self): - """Returns the item from the next entry in the _treeOrder list. - """ - if self._theIndex < len(self._treeOrder): - theItem = self.__getitem__(self._treeOrder[self._theIndex]) - self._theIndex += 1 - return theItem - else: - raise StopIteration + for tHandle in self._treeOrder: + tItem = self._projTree.get(tHandle) + if isinstance(tItem, NWItem): + yield tItem + return ## # Internal Functions diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 3a5d278d..9409c270 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -628,7 +628,7 @@ class GuiMain(QMainWindow): fHandle = None # The first file handle we encounter foundIt = False # We've found tHandle, pick the next we see for tItem in self.theProject.tree: - if tItem is None or not tItem.isFileType(): + if not tItem.isFileType(): continue if fHandle is None: fHandle = tItem.itemHandle From 1e2920641d0af59d4f95f4972144e5ae48648845 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 23:27:54 +0100 Subject: [PATCH 10/20] Force a max width of extra column in novel view (#1238) --- novelwriter/gui/noveltree.py | 42 ++++++++++++++++++++++------ novelwriter/gui/outline.py | 1 + novelwriter/guimain.py | 3 ++ tests/test_gui/test_gui_noveltree.py | 16 +++++++---- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 12ff53ad..cc809e17 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -605,6 +605,22 @@ class GuiNovelTree(QTreeWidget): self.clearSelection() return + def resizeEvent(self, event): + """Elide labels in the extra column. + """ + super().resizeEvent(event) + newW = event.size().width() + oldW = event.oldSize().width() + if newW != oldW: + eliW = int(0.25 * newW) + fMetric = self.fontMetrics() + for i in range(self.topLevelItemCount()): + trItem = self.topLevelItem(i) + if isinstance(trItem, QTreeWidgetItem): + lastText = trItem.data(self.C_EXTRA, Qt.UserRole) + trItem.setText(self.C_EXTRA, fMetric.elidedText(lastText, Qt.ElideRight, eliW)) + return + ## # Private Slots ## @@ -686,10 +702,12 @@ class GuiNovelTree(QTreeWidget): trItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore) # Custom column + mW = int(0.25 * self.viewport().width()) lastText, toolTip = self._getLastColumnText(tHandle, sTitle) - trItem.setText(self.C_EXTRA, lastText) - if lastText: - trItem.setToolTip(self.C_EXTRA, toolTip) + elideText = self.fontMetrics().elidedText(lastText, Qt.ElideRight, mW) + trItem.setText(self.C_EXTRA, elideText) + trItem.setData(self.C_EXTRA, Qt.UserRole, lastText) + trItem.setToolTip(self.C_EXTRA, toolTip) return @@ -699,18 +717,24 @@ class GuiNovelTree(QTreeWidget): if self._lastCol == NovelTreeColumn.HIDDEN: return "", "" + refData = [] + refName = "" theRefs = self.theProject.index.getReferences(tHandle, sTitle) if self._lastCol == NovelTreeColumn.POV: - newText = ", ".join(theRefs[nwKeyWords.POV_KEY]) - return newText, f"{self._povLabel}: {newText}" + refData = theRefs[nwKeyWords.POV_KEY] + refName = self._povLabel elif self._lastCol == NovelTreeColumn.FOCUS: - newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY]) - return newText, f"{self._focLabel}: {newText}" + refData = theRefs[nwKeyWords.FOCUS_KEY] + refName = self._focLabel elif self._lastCol == NovelTreeColumn.PLOT: - newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY]) - return newText, f"{self._pltLabel}: {newText}" + refData = theRefs[nwKeyWords.PLOT_KEY] + refName = self._pltLabel + + if refData: + toolText = ", ".join(refData) + return refData[0], f"{refName}: {toolText}" return "", "" diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 98d3b36e..ef898237 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -73,6 +73,7 @@ class GuiOutlineView(QWidget): self.splitOutline = QSplitter(Qt.Vertical) self.splitOutline.addWidget(self.outlineTree) self.splitOutline.addWidget(self.outlineData) + self.splitOutline.setOpaqueResize(False) self.splitOutline.setSizes(self.mainConf.outlinePanePos) # Assemble diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 9409c270..7e55ecf4 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -140,12 +140,14 @@ class GuiMain(QMainWindow): self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.viewMeta) self.splitView.setHandleWidth(hWd) + self.splitView.setOpaqueResize(False) self.splitView.setSizes(self.mainConf.viewPanePos) # Splitter : Document Editor / Document Viewer self.splitDocs = QSplitter(Qt.Horizontal) self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.splitView) + self.splitDocs.setOpaqueResize(False) self.splitDocs.setHandleWidth(hWd) # Splitter : Project Tree / Main Tabs @@ -153,6 +155,7 @@ class GuiMain(QMainWindow): self.splitMain.setContentsMargins(0, 0, 0, 0) self.splitMain.addWidget(self.treePane) self.splitMain.addWidget(self.splitDocs) + self.splitMain.setOpaqueResize(False) self.splitMain.setHandleWidth(hWd) self.splitMain.setSizes(self.mainConf.mainPanePos) diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 15101972..c75a29f3 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -144,11 +144,11 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): assert novelTree.lastColType == NovelTreeColumn.HIDDEN assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ("", "") - novelBar.setLastColType(NovelTreeColumn.POV) + novelBar.setLastColType(NovelTreeColumn.PLOT) assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False - assert novelTree.lastColType == NovelTreeColumn.POV + assert novelTree.lastColType == NovelTreeColumn.PLOT assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ( - "Jane", "Point of View: Jane" + "", "" ) novelBar.setLastColType(NovelTreeColumn.FOCUS) @@ -158,16 +158,20 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): "Jane", "Focus: Jane" ) - novelBar.setLastColType(NovelTreeColumn.PLOT) + novelBar.setLastColType(NovelTreeColumn.POV) assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False - assert novelTree.lastColType == NovelTreeColumn.PLOT + assert novelTree.lastColType == NovelTreeColumn.POV assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ( - "", "Plot: " + "Jane", "Point of View: Jane" ) novelTree._lastCol = None assert novelTree._getLastColumnText("0000000000000", "T0000") == ("", "") + # This forces the resizeEvent function to process labels + spSize = nwGUI.splitMain.sizes() + nwGUI.splitMain.setSizes([spSize[0] + 10, spSize[1] - 10]) + # Item Meta # ========= From b7f99ec95f993d52bb31c4c6819a37e76b8b703e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 23:41:04 +0100 Subject: [PATCH 11/20] Update workflow actions --- .github/workflows/test_linux.yml | 6 +++--- .github/workflows/test_mac.yml | 6 +++--- .github/workflows/test_win.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml index 6dcf160f..9535e435 100644 --- a/.github/workflows/test_linux.yml +++ b/.github/workflows/test_linux.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Python Setup - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: x64 @@ -27,7 +27,7 @@ jobs: sudo apt update sudo apt install libenchant-dev qttools5-dev-tools aspell-en - name: Checkout Source - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install Dependencies (pip) run: | pip install --upgrade pip @@ -40,4 +40,4 @@ jobs: export QT_QPA_PLATFORM=offscreen pytest -v --cov=novelwriter --timeout=60 - name: Upload to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 diff --git a/.github/workflows/test_mac.yml b/.github/workflows/test_mac.yml index ccb1c0c1..5b964c8d 100644 --- a/.github/workflows/test_mac.yml +++ b/.github/workflows/test_mac.yml @@ -15,7 +15,7 @@ jobs: runs-on: macos-latest steps: - name: Python Setup - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.10" architecture: x64 @@ -23,7 +23,7 @@ jobs: run: | brew install enchant - name: Checkout Source - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install Dependencies run: | pip install --upgrade pip @@ -35,4 +35,4 @@ jobs: export QT_QPA_PLATFORM=offscreen pytest -v --cov=novelwriter --timeout=60 - name: Upload to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 diff --git a/.github/workflows/test_win.yml b/.github/workflows/test_win.yml index a976026a..94598d16 100644 --- a/.github/workflows/test_win.yml +++ b/.github/workflows/test_win.yml @@ -15,12 +15,12 @@ jobs: runs-on: windows-latest steps: - name: Python Setup - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.10" architecture: x64 - name: Checkout Source - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install Dependencies run: | pip install --upgrade pip @@ -30,4 +30,4 @@ jobs: run: | pytest -v --cov=novelwriter --timeout=60 - name: Upload to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 From af991eb31ba28f449256c060e253991ace804c7b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 23:43:43 +0100 Subject: [PATCH 12/20] Update actions for syntax workflow --- .github/workflows/syntax.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index 68eac6bc..c770fa85 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -15,12 +15,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Python Setup - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 architecture: x64 - name: Checkout Source - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install flake8 run: pip install flake8 - name: Syntax Error Check From 62cf9b618489b11e27ddd4b4320f782646d6e4ae Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 16 Nov 2022 11:20:40 +0100 Subject: [PATCH 13/20] Sort out status leds and project closing (#1237) --- novelwriter/enum.py | 9 --------- novelwriter/gui/statusbar.py | 25 ++++++++++++++----------- novelwriter/guimain.py | 11 ++++++++--- tests/test_gui/test_gui_statusbar.py | 14 +++++++------- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index ecb4ea4a..b5038da3 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -130,15 +130,6 @@ class nwAlert(Enum): # END Enum nwAlert -class nwState(Enum): - - NONE = 0 - BAD = 1 - GOOD = 2 - -# END Enum nwState - - class nwView(Enum): EDITOR = 0 diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 26830e2d..9dcb97af 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -34,7 +34,6 @@ from PyQt5.QtGui import QColor, QPainter from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton from novelwriter.common import formatTime -from novelwriter.enum import nwState logger = logging.getLogger(__name__) @@ -53,8 +52,8 @@ class GuiMainStatus(QStatusBar): self.userIdle = False colNone = QColor(*self.mainTheme.statNone) - colTrue = QColor(*self.mainTheme.statUnsaved) - colFalse = QColor(*self.mainTheme.statSaved) + colSaved = QColor(*self.mainTheme.statSaved) + colUnsaved = QColor(*self.mainTheme.statUnsaved) iPx = self.mainTheme.baseIconSize @@ -72,7 +71,7 @@ class GuiMainStatus(QStatusBar): self.addPermanentWidget(self.langText) # The Editor Status - self.docIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self) + self.docIcon = StatusLED(colNone, colSaved, colUnsaved, iPx, iPx, self) self.docText = QLabel(self.tr("Editor")) self.docIcon.setContentsMargins(0, 0, 0, 0) self.docText.setContentsMargins(0, 0, xM, 0) @@ -80,7 +79,7 @@ class GuiMainStatus(QStatusBar): self.addPermanentWidget(self.docText) # The Project Status - self.projIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self) + self.projIcon = StatusLED(colNone, colSaved, colUnsaved, iPx, iPx, self) self.projText = QLabel(self.tr("Project")) self.projIcon.setContentsMargins(0, 0, 0, 0) self.projText.setContentsMargins(0, 0, xM, 0) @@ -122,8 +121,8 @@ class GuiMainStatus(QStatusBar): self.setRefTime(None) self.setLanguage(None, "") self.setProjectStats(0, 0) - self.setProjectStatus(nwState.NONE) - self.setDocumentStatus(nwState.NONE) + self.setProjectStatus(StatusLED.S_NONE) + self.setDocumentStatus(StatusLED.S_NONE) self.updateTime() return True @@ -236,14 +235,14 @@ class GuiMainStatus(QStatusBar): def doUpdateProjectStatus(self, isChanged): """Slot for updating the project status. """ - self.setProjectStatus(nwState.GOOD if isChanged else nwState.BAD) + self.setProjectStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD) return @pyqtSlot(bool) def doUpdateDocumentStatus(self, isChanged): """Slot for updating the document status. """ - self.setDocumentStatus(nwState.GOOD if isChanged else nwState.BAD) + self.setDocumentStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD) return # END Class GuiMainStatus @@ -251,6 +250,10 @@ class GuiMainStatus(QStatusBar): class StatusLED(QAbstractButton): + S_NONE = 0 + S_BAD = 1 + S_GOOD = 2 + def __init__(self, colNone, colGood, colBad, sW, sH, parent=None): super().__init__(parent=parent) @@ -271,9 +274,9 @@ class StatusLED(QAbstractButton): def setState(self, theState): """Set the colour state. """ - if theState == nwState.GOOD: + if theState == self.S_GOOD: self._theCol = self._colGood - elif theState == nwState.BAD: + elif theState == self.S_BAD: self._theCol = self._colBad else: self._theCol = self._colNone diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 7e55ecf4..3c9fbe56 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -312,7 +312,7 @@ class GuiMain(QMainWindow): # Work Area self.docEditor.clearEditor() self.docEditor.setDictionaries() - self.closeDocViewer() + self.closeDocViewer(byUser=False) self.outlineView.clearProject() # General @@ -1220,14 +1220,19 @@ class GuiMain(QMainWindow): self.theProject.data.setLastHandle(None, "editor") return - def closeDocViewer(self): + def closeDocViewer(self, byUser=True): """Close the document view panel. """ self.docViewer.clearViewer() - self.theProject.data.setLastHandle(None, "viewer") + if byUser: + # Only reset the last handle if the user called this + self.theProject.data.setLastHandle(None, "viewer") + + # Hide the panel bPos = self.splitMain.sizes() self.splitView.setVisible(False) self.splitDocs.setSizes([bPos[1], 0]) + return not self.splitView.isVisible() def toggleFocusMode(self): diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index a057fb36..58ea144e 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -24,7 +24,7 @@ import pytest from tools import C, buildTestProject -from novelwriter.enum import nwState +from novelwriter.gui.statusbar import StatusLED @pytest.mark.gui @@ -44,19 +44,19 @@ def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): assert nwGUI.mainStatus.refTime == refTime # Project Status - nwGUI.mainStatus.setProjectStatus(nwState.NONE) + nwGUI.mainStatus.setProjectStatus(StatusLED.S_NONE) assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colNone - nwGUI.mainStatus.setProjectStatus(nwState.BAD) + nwGUI.mainStatus.setProjectStatus(StatusLED.S_BAD) assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colBad - nwGUI.mainStatus.setProjectStatus(nwState.GOOD) + nwGUI.mainStatus.setProjectStatus(StatusLED.S_GOOD) assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colGood # Document Status - nwGUI.mainStatus.setDocumentStatus(nwState.NONE) + nwGUI.mainStatus.setDocumentStatus(StatusLED.S_NONE) assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colNone - nwGUI.mainStatus.setDocumentStatus(nwState.BAD) + nwGUI.mainStatus.setDocumentStatus(StatusLED.S_BAD) assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colBad - nwGUI.mainStatus.setDocumentStatus(nwState.GOOD) + nwGUI.mainStatus.setDocumentStatus(StatusLED.S_GOOD) assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colGood # Idle Status From 61ab5974990cb61f70ea297e5ebf6c3613317984 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 16 Nov 2022 11:21:16 +0100 Subject: [PATCH 14/20] Make go to line in editor work consistently with auto-scrolling --- novelwriter/gui/doceditor.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index b2641ce6..af150b85 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -666,12 +666,17 @@ class GuiDocEditor(QTextEdit): theCursor.setPosition(minmax(position, 0, nChars-1)) self.setTextCursor(theCursor) - # The editor scrolls so the cursor is on the last line, so we must correct - vPos = self.verticalScrollBar().value() # Current scrollbar position - cPos = self.cursorRect().topLeft().y() # Cursor position to scroll to - dMrg = int(self.document().documentMargin()) # Document margin to subtract - mPos = int(self.viewport().height()*0.1) # Distance from top to adjust for (10%) - self.verticalScrollBar().setValue(max(0, vPos + cPos - dMrg - mPos)) + # By default, the editor scrolls so the cursor is on the + # last line, so we must correct it. The user setting for + # auto-scroll is used to determine the scroll distance. This + # makes it compatible with the typewriter scrolling feature + # when it is enabled. By default, it's 30% of viewport. + vPos = self.verticalScrollBar().value() + cPos = self.cursorRect().topLeft().y() + mPos = int(self.mainConf.autoScrollPos*0.01 * self.viewport().height()) + if cPos > mPos: + # Only scroll if the cursor is past the auto-scroll limit + self.verticalScrollBar().setValue(max(0, vPos + cPos - mPos)) self.docFooter.updateLineCount() From 9a20eab1b2949212e6fc7f52364c22b179b588c6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 16 Nov 2022 15:58:52 +0100 Subject: [PATCH 15/20] Update docs, index and build, and rename viewsbar to sidebar --- docs/source/conf.py | 2 +- docs/source/index.rst | 4 +- docs/source/requirements.txt | 4 +- i18n/nw_base.ts | 596 ++++++++++---------- novelwriter/gui/__init__.py | 4 +- novelwriter/gui/{viewsbar.py => sidebar.py} | 12 +- novelwriter/guimain.py | 4 +- 7 files changed, 313 insertions(+), 313 deletions(-) rename novelwriter/gui/{viewsbar.py => sidebar.py} (95%) diff --git a/docs/source/conf.py b/docs/source/conf.py index b8dbf3f6..19ed3014 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -42,7 +42,7 @@ templates_path = ["_templates"] source_suffix = ".rst" master_doc = "index" today_fmt = "%A, %d %B %Y at %H:%M" -language = None +language = "en" exclude_patterns = [] pygments_style = "sphinx" pygments_dark_style = "monokai" diff --git a/docs/source/index.rst b/docs/source/index.rst index 20c39895..f35ac64c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -27,8 +27,8 @@ JSON files. See the :ref:`a_breakdown_storage` section for more details. Any operating system that can run Python 3 and has the Qt 5 libraries should be able to run novelWriter. It runs fine on Linux, Windows and macOS, and users have tested it on other platforms -too. novelWriter can be run directly from the Python source, installed from the pip tool. See -:ref:`a_started` for more details. +too. novelWriter can be run directly from the Python source, or installed from packages or the pip +tool. See :ref:`a_started` for more details. .. note:: Version 1.5 introduced a few changes that will require you to make a few minor modifications to diff --git a/docs/source/requirements.txt b/docs/source/requirements.txt index 465c82d2..777d09c0 100644 --- a/docs/source/requirements.txt +++ b/docs/source/requirements.txt @@ -1,4 +1,4 @@ furo -sphinx~=4.0 -pygments~=2.7 +sphinx>=4.0 +pygments>=2.7 docutils==0.17.1 diff --git a/i18n/nw_base.ts b/i18n/nw_base.ts index d4241a2b..22f5923d 100644 --- a/i18n/nw_base.ts +++ b/i18n/nw_base.ts @@ -77,264 +77,264 @@ Constant - - - + + + None - + Novel - - + + Plot - - + + Characters - - + + Locations - - + + Timeline + + + + Objects + + - Objects + Entities - Entities - - - - - Custom - + Archive - + Trash - - + + Novel Document - - + + Project Note - + Root Folder - + Folder - + Novel Title Page - + Novel Chapter - + Novel Scene - + Novel Section - + Tag - + Point of View - - + + Focus - + Title - + Level - + Document - + Line - + Chars - + Words - + Pars - + POV - + Synopsis - + Straight single quotation mark - + Straight double quotation mark - + Left single quotation mark - + Right single quotation mark - + Single low-9 quotation mark - + Single high-reversed-9 quotation mark - + Left double quotation mark - + Right double quotation mark - + Double low-9 quotation mark - + Double high-reversed-9 quotation mark - + Double low-reversed-9 quotation mark - + Single left-pointing angle quotation mark - + Single right-pointing angle quotation mark - + Double left-pointing angle quotation mark - + Double right-pointing angle quotation mark - + Left corner bracket - + Right corner bracket - + Left white corner bracket - + Right white corner bracket @@ -809,32 +809,32 @@ GuiDocEditFooter - + Status - + Line: {0} ({1}) - + Words: {0} ({1}) - + Document size is {0} bytes - + Words: {0} selected - + Character count: {0} @@ -842,22 +842,22 @@ GuiDocEditHeader - + Edit document label - + Search document - + Toggle Focus Mode - + Close the document @@ -865,58 +865,58 @@ GuiDocEditSearch - - + + Search - + Replace - + Case Sensitive - + Whole Words Only - + RegEx Mode - + Loop Search - + Search Next File - + Preserve Case - + Close Search - + Find in current document - + Find and replace in current document @@ -924,117 +924,117 @@ GuiDocEditor - + The document you are trying to open is too big. The document size is {0} MB. The maximum size allowed is {1} MB. - + Opened Document: {0} - + The text you are trying to add is too big. The text size is {0} MB. The maximum size allowed is {1} MB. - + File Changed on Disk - + This document has been changed outside of novelWriter while it was open. Overwrite the file on disk? - + Could not save document. - + Saved Document: {0} - + Spell checking requires the package PyEnchant. It does not appear to be installed. - + Spell check complete - + File Location - + The currently open file is saved in: - + The document has grown too big and you cannot add more text to it. The maximum size of a single novelWriter document is {0} MB. - + Follow Tag - + Cut - + Copy - + Paste - + Select All - + Select Word - + Select Paragraph - + Spelling Suggestion(s) - + No Suggestions - + Add Word to Dictionary - + Please select some text before calling replace quotes. @@ -1118,42 +1118,42 @@ GuiDocViewFooter - + Show/hide the references panel - + Activate to freeze the content of the references panel when changing document - + Show comments - + Show synopsis comments - + References - + Sticky - + Comments - + Synopsis @@ -1161,22 +1161,22 @@ GuiDocViewHeader - + Go backward - + Go forward - + Reload the document - + Close the document @@ -1189,22 +1189,22 @@ - + Copy - + Select All - + Select Word - + Select Paragraph @@ -1291,173 +1291,173 @@ GuiMain - + You are running an untested development version of novelWriter. Please be careful when working on a live project and make sure you take regular backups. - + novelWriter is ready ... - + Cannot create a new project when another project is open. - + A project already exists in that location. Please choose another folder. - + Close Project - + Close the current project? - - + + Changes are saved automatically. - + Backup Project - + Backup the current project? - + The project was locked by the computer '{0}' ({1} {2}), last active on {3}. - + Project Locked - + The project is already open by another instance of novelWriter, and is therefore locked. Override lock and continue anyway? - + Note: If the program or the computer previously crashed, the lock can safely be overridden. However, overriding it is not recommended if the project is open in another instance of novelWriter. Doing so may corrupt the project. - + The project index is outdated or broken. Rebuilding index. - + Text files ({0}) - + Markdown files ({0}) - + novelWriter files ({0}) - + All files ({0}) - + Import File - + Could not read file. The file must be an existing text file. - + Please open a document to import the text file into. - + Import Document - + Importing the file will overwrite the current content of the document. Do you want to proceed? - + Indexing completed in {0} ms - + The project index has been successfully rebuilt. - + Some changes will not be applied until novelWriter has been restarted. - + Information - + Warning - + Error - + This is a bug! - + Internal Error - + Exit - + Do you want to exit novelWriter? - + Could not find the reference for tag '{0}'. It either doesn't exist, or the index is out of date. The index can be updated from the Tools menu, or by pressing {1}. @@ -2063,38 +2063,38 @@ GuiMainStatus - - + + None - + Editor - + Project - + Session Time - + Words: {0} ({1}) - + Project word count (session change) - + Novel word count (session change) @@ -2102,47 +2102,47 @@ GuiNovelToolBar - + Novel Outline - + Refresh - + Novel Root - + Last Column - + Hidden - + Point of View Character - + Focus Character - + Novel Plot - + More Options @@ -2150,7 +2150,7 @@ GuiNovelTree - + No meta data @@ -2158,65 +2158,65 @@ GuiOutlineDetails - - - - + + + + Title - + Chapter - + Scene - + Section - + Document - + Status - + Characters - + Words - + Paragraphs - + Synopsis - + Title Details - + Reference Tags @@ -2224,7 +2224,7 @@ GuiOutlineHeaderMenu - + Select Columns @@ -2232,17 +2232,17 @@ GuiOutlineToolBar - + Outline of - + Refresh - + All Novel Folders @@ -3556,6 +3556,74 @@ + + GuiSideBar + + + Project + + + + + Project Tree View + + + + + Novel + + + + + Novel Tree View + + + + + Outline + + + + + Novel Outline View + + + + + Build + + + + + Build Novel Project + + + + + Details + + + + + Project Details + + + + + Stats + + + + + Writing Statistics + + + + + Settings + + + GuiUpdates @@ -3590,74 +3658,6 @@ - - GuiViewsBar - - - Project - - - - - Project Tree View - - - - - Novel - - - - - Novel Tree View - - - - - Outline - - - - - Novel Outline View - - - - - Build - - - - - Build Novel Project - - - - - Details - - - - - Project Details - - - - - Stats - - - - - Writing Statistics - - - - - Settings - - - GuiWordList @@ -3833,173 +3833,173 @@ NWProject - + Could not delete document file. - + Unknown - + Project file does not appear to be a novelWriterXML file. - + Unknown or unsupported novelWriter project file format. The project cannot be opened by this version of novelWriter. The file was saved with novelWriter version {0}. - + Failed to parse project xml. - + File Version - + The file format of your project is about to be updated. If you proceed, older versions of novelWriter will no longer be able to open this project. Continue? - + Version Conflict - + This project was saved by a newer version of novelWriter, version {0}. This is version {1}. If you continue to open the project, some attributes and settings may not be preserved, but the overall project should be fine. Continue opening the project? - + Opened Project: {0} - + There is no project open. - + Failed to save project. - + Saved Project: {0} - + Backing up project ... - + Cannot backup project because no valid backup path is set. Please set a valid backup location in Preferences. - + Cannot backup project because no project name is set. Please set a Project Name in Project Settings. - + Could not create backup folder. - + Backup from {0} - + Backup archive file written to: {0} - + Could not write backup archive. - + Project backed up to '{0}' - + New - + Note - + Draft - + Finished - + Minor - + Major - + Main - + and - + Found {0} orphaned file(s) in project folder. - + Recovered - + [{0}] {1} - + Recovered File {0} - + One or more orphaned files could not be added back into the project. Make sure at least a Novel root folder exists. diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index 17699942..bbe814d2 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -28,7 +28,7 @@ from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.theme import GuiTheme -from novelwriter.gui.viewsbar import GuiViewsBar +from novelwriter.gui.sidebar import GuiSideBar __all__ = [ "GuiDocEditor", @@ -41,5 +41,5 @@ __all__ = [ "GuiOutlineView", "GuiProjectView", "GuiTheme", - "GuiViewsBar", + "GuiSideBar", ] diff --git a/novelwriter/gui/viewsbar.py b/novelwriter/gui/sidebar.py similarity index 95% rename from novelwriter/gui/viewsbar.py rename to novelwriter/gui/sidebar.py index da85275c..59d827b7 100644 --- a/novelwriter/gui/viewsbar.py +++ b/novelwriter/gui/sidebar.py @@ -1,7 +1,7 @@ """ -novelWriter – GUI Main Window Views ToolBar +novelWriter – GUI Main Window SideBar =========================================== -GUI class for the main window "Views" toolbar +GUI class for the main window side bar File History: Created: 2022-05-10 [1.7b1] @@ -36,14 +36,14 @@ from novelwriter.enum import nwView logger = logging.getLogger(__name__) -class GuiViewsBar(QToolBar): +class GuiSideBar(QToolBar): viewChangeRequested = pyqtSignal(nwView) def __init__(self, mainGui): super().__init__(parent=mainGui) - logger.debug("Initialising GuiViewsBar ...") + logger.debug("Initialising GuiSideBar ...") self.mainConf = novelwriter.CONFIG self.mainGui = mainGui @@ -123,7 +123,7 @@ class GuiViewsBar(QToolBar): self.updateTheme() - logger.debug("GuiViewsBar initialisation complete") + logger.debug("GuiSideBar initialisation complete") return @@ -142,4 +142,4 @@ class GuiViewsBar(QToolBar): return -# END Class GuiViewsBar +# END Class GuiSideBar diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 3c9fbe56..1aab1e88 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -41,7 +41,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, GuiMainStatus, GuiNovelView, GuiOutlineView, GuiProjectView, GuiTheme, - GuiViewsBar + GuiSideBar ) from novelwriter.dialogs import ( GuiAbout, GuiPreferences, GuiProjectDetails, GuiProjectLoad, @@ -118,7 +118,7 @@ class GuiMain(QMainWindow): self.itemDetails = GuiItemDetails(self) self.outlineView = GuiOutlineView(self) self.mainMenu = GuiMainMenu(self) - self.viewsBar = GuiViewsBar(self) + self.viewsBar = GuiSideBar(self) # Project Tree Stack self.projStack = QStackedWidget() From af7b46f714af834af9ccb4d0e6564ed2c2c68e10 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 16 Nov 2022 15:59:21 +0100 Subject: [PATCH 16/20] Update docs introduction section --- docs/source/int_customise.rst | 15 --------------- docs/source/int_introduction.rst | 17 ++++++++++------- docs/source/int_overview.rst | 4 ++-- docs/source/int_source.rst | 13 +++++++++++++ docs/source/int_started.rst | 27 ++++++++++++++++++--------- 5 files changed, 43 insertions(+), 33 deletions(-) diff --git a/docs/source/int_customise.rst b/docs/source/int_customise.rst index f77cc9cc..ef71544a 100644 --- a/docs/source/int_customise.rst +++ b/docs/source/int_customise.rst @@ -58,18 +58,3 @@ For novelWriter to be able to locate the custom theme files, you must copy them Once the files are copied there, they should show up in :guilabel:`Preferences` with the label you set as ``name`` inside the file. - - -Theme CSS Files ---------------- - -If you wish, you can also modify the CSS styles of the GUI in addition to change colour settings. -This is only available for GUI themes, and you do this by creating a file with the exact same file -name as the ``.conf`` file with colour settings and give it the ``.qss`` extension. - -On Windows, file extensions may not be visible by default, so make sure you only have one file -extension, and don't end up with two. - -The QSS files are Qt Style Sheet files. See Qt's -`The Style Sheet Syntax `_ documentation for more -details. diff --git a/docs/source/int_introduction.rst b/docs/source/int_introduction.rst index 3089ece0..64763498 100644 --- a/docs/source/int_introduction.rst +++ b/docs/source/int_introduction.rst @@ -4,10 +4,10 @@ Key Features ************ -novelWriter is a multi-document plain text editor using a markup syntax inspired by markdown to +novelWriter is a multi-document plain text editor using a markup syntax inspired by Markdown to apply simple formatting to the text. It is designed for writing novels, so the formatting features -are limited. Your novel project is organised as a collection of separate plain text documents -instead of a single, large document. +are limited to those relevant for this purpose. Your novel project is organised as a collection of +separate plain text documents instead of a single, large document. Below are some key features of novelWriter. @@ -27,7 +27,9 @@ Below are some key features of novelWriter. You can split your novel project up into as many individual documents as you want to. When you build the project, they are all glued together in the top-to-bottom order in which they appear in the project tree. You can use as few text documents as you like, but splitting the project up - into chapters and scenes means you can easily reorder them using the drag and drop feature. + into chapters and scenes means you can easily reorder them using the drag and drop feature. You + can start out with a few documents and then later split the document into multiple documents + based on its headers. **Keep track of your plot elements** All notes in your project can be assigned a *tag* you can *reference* from any other document or @@ -36,11 +38,12 @@ Below are some key features of novelWriter. keywords. **Get an overview of your plot elements** - In the :guilabel:`Outline` tab on the main window you can see an outline of all the chapter and - scene sections of your project. If they have any references in them, these are listed in + In the :guilabel:`Outline View` on the main window you can see an outline of all the chapters, + scenes, and sections of your project. If they have any references in them, these are listed in columns. You can also add a synopsis to each document, which can be listed here. You have the option to add or remove columns of information from the outline. A subset of the outline - information is also available in the :guilabel:`Novel` tab under the main project tree. + information is also available in the :guilabel:`Novel View` as a replacement for the main + project tree. **Building your manuscript** Whether you want to compile a manuscript, or export all your notes, or generate an outline of diff --git a/docs/source/int_overview.rst b/docs/source/int_overview.rst index d773f493..372bfd57 100644 --- a/docs/source/int_overview.rst +++ b/docs/source/int_overview.rst @@ -15,7 +15,7 @@ language that doesn't require a compiler to build and run. That means that the c computer right out of the box, or from a zip file. While it is developed for Linux primarily, it runs just fine on Windows as well. It also works fine -on macOS, but the author is not a mac user so less attention is paid to that platform. +on macOS, but the author is not a mac user, so less attention is paid to that platform. In order to run novelWriter, you also need a few additional packages. The user interface is built with `Qt 5 `_, a cross platform library for building graphical user interface @@ -86,5 +86,5 @@ meta data for it to extract. them for tags you've set so that it knows which file to open when you click on a reference. :ref:`a_export` - Recommended Reading - This section explains in more detail how the export tool works. In particular how you can + This section explains in more detail how the build tool works. In particular how you can control the way chapter titles are formatted, and how scene and section breaks are handled. diff --git a/docs/source/int_source.rst b/docs/source/int_source.rst index 879700a5..b364cea4 100644 --- a/docs/source/int_source.rst +++ b/docs/source/int_source.rst @@ -110,6 +110,19 @@ needed package is called `qttools5-dev-tools`. the ``i18n`` folder of the source code. +.. _a_source_sample: + +Building the Example Project +============================ + +In order to be able to create new projects from example files, you need a ``sample.zip`` file in +the ``assets`` folder of the source. This file can be built from setup script by running: + +.. code-block:: bash + + python setup.py sample + + .. _a_source_docs: Building the Documentation diff --git a/docs/source/int_started.rst b/docs/source/int_started.rst index 5b5fff70..2bd5afb3 100644 --- a/docs/source/int_started.rst +++ b/docs/source/int_started.rst @@ -15,6 +15,7 @@ Getting Started .. _python.org: https://www.python.org/downloads/windows .. _Releases: https://github.com/vkbo/novelWriter/releases .. _RPM: https://github.com/vkbo/novelWriter/issues/907 +.. _AppImage: https://appimage.org/ If you are using Windows or a Debian-based Linux distribtuion, you can install novelWriter from package installers. If you are on macOS, you have the option to run novelWriter from a standalone @@ -46,10 +47,10 @@ If you have any issues, try uninstalling the previous version and making a fresh already had a version installed via a different method, you should uninstall that first. -.. _a_started_debian: +.. _a_started_linux: -Install on Debian/Ubuntu/Mint -============================= +Install on Linux +================ A Debian package can be downloaded from the `main website`_, or from the Releases_ page on GitHub. This package should work on both Debian, Ubuntu and Linux Mint. @@ -57,8 +58,8 @@ This package should work on both Debian, Ubuntu and Linux Mint. If you prefer, you can also add the novelWriter repository on Launchpad to your package manager. -Ubuntu and Mint ---------------- +Ubuntu +------ You can add the Ubuntu PPA_ and install novelWriter with the following commands. @@ -71,11 +72,11 @@ You can add the Ubuntu PPA_ and install novelWriter with the following commands. If you want pre-releases, add the ``ppa:vkbo/novelwriter-pre`` repository instead. -Debian ------- +Debian and Mint +--------------- -Since this is a pure Python package, the Launchpad PPA can in principle also be used on Debian. -However, the above command will fail to add the signing key. +Since this is a pure Python package, the Launchpad PPA can in principle also be used on Debian or +Mint. However, the above command will fail to add the signing key. Instead, run the following commands to add the repository and key: @@ -97,6 +98,14 @@ Then run the update and install commands as for Ubuntu: different compression algorithm that Debian doesn't currently support. +Other Distros +------------- + +For other Linux distros than the ones mentioned above, the primary option is AppImage_. These are +completely standalone images for the app that include the necessary environment to run novelWriter. +They can be run on any Linux distro. + + .. _a_started_minimal: Minimal Package Install From 09c414c9912d1d0bc063ffe11fd96b829eab92b8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 16 Nov 2022 16:17:22 +0100 Subject: [PATCH 17/20] Update docs usage section --- docs/source/usage_breakdown.rst | 119 ++++++++++++++++++---------- docs/source/usage_format.rst | 43 +++++----- docs/source/usage_interface.rst | 107 ++++++++++++++----------- docs/source/usage_projectformat.rst | 27 ++++--- 4 files changed, 178 insertions(+), 118 deletions(-) diff --git a/docs/source/usage_breakdown.rst b/docs/source/usage_breakdown.rst index dead83d3..541b2705 100644 --- a/docs/source/usage_breakdown.rst +++ b/docs/source/usage_breakdown.rst @@ -8,7 +8,7 @@ How it Works The main features of novelWriter are listed in the :ref:`a_intro` section. Here, we go into some more details on how they are implemented. Later on in this documentation, these features will be -covered in more detail. +covered in even more detail. .. _a_breakdown_design: @@ -19,52 +19,80 @@ GUI Layout and Design The user interface of novelWriter is intended to be as minimalistic as practically possible, while at the same time provide a complete set of features needed for writing a novel. -The main window does not have a toolbar like many other applications do. This reduces clutter, and -since the documents are formatted with style tags, is more or less redundant. However, most -formatting features supported are available through convenient keyboard shortcuts. They are also -available in the main menu so you don't have to look up formatting codes every time you need them. -However, a list of all shortcuts can be found in the :ref:`a_kb` section. +The main window does not have an editor toolbar like many other applications do. This reduces +clutter, and since the documents are formatted with style tags, is more or less redundant. However, +most formatting features supported are available through convenient keyboard shortcuts. They are +alsoavailable in the main menu so you don't have to look up formatting codes every time you need +them. However, a list of all shortcuts can be found in the :ref:`a_kb` section. .. note:: novelWriter is not intended to be a full office type word processor. It doesn't support images, links, tables, and other complex structures and objects often needed for such documents. Formatting is limited to headers, emphasis, text alignment, and a few other simple features. +On the left edge of the main window, you will find a sidebar. This bar has buttons for the standard +views you can switch between, a quick link to the :guilabel:`Build Novel Project` tool, and a set +of project-related tools as well as quick access to settings at the bottom. -Window Tabs and Areas ---------------------- -The main window is split in two, or optionally three, panels. The left-most panel contains the -project tree and all the documents in your project. The second panel is the document editor. An -optional third panel is a document viewer which can view any document in your project independently -of what is open in the document editor. It is not intended as a preview window, although you can -use it for this as well as it will apply the formatting tags you have specified. The main purpose -of the viewer is for viewing your notes next to your editor while you're writing. +Project Tree View +----------------- + +When in :guilabel:`Project Tree View` mode, the main work area of the main window is split in two, +or optionally three, panels. The left-most panel contains the project tree and all the documents in +your project. The second panel is the document editor. An optional third panel is a document viewer +which can view any document in your project independently of what is open in the document editor. +It is not intended as a preview window, although you can use it for this as well as it will apply +the formatting tags you have specified. The main purpose of the viewer is for viewing your notes +next to your editor while you're writing. The editor also has a :guilabel:`Focus Mode` you can toggle either from the menu, or from the icon in the editor header. When :guilabel:`Focus Mode` is enabled, all the user interface elements other than the document editor itself are hidden away. -A second tab is also available on the main window. This is the :guilabel:`Outline` tab where the -entire novel structure can be displayed, with all the tags and references listed. Depending on how -you structure your novel documents, this outline can be quite different from your project tree. -Your project tree lists individual documents, your Outline tree lists the structure of the novel -itself in terms of partitions, chapters and scenes as it appears in the text of those documents. + +Novel Tree View +--------------- + +When in :guilabel:`Novel Tree View` mode, the project tree is replaces by an overview of your novel +structure. Instead of showing individual documents, the tree shows all headings of your novel text. +Each heading is indented according to the heading level. You can open and edit your novel documents +from this view as well. All headings contained in the currently open document should be highlighted +in the view. + +If you have multiple Novel root folders, you can switch between them from the dropdown menu from +the buttons at the top of the tree view. You can also select to view an extra column of data. To +select its content, see the menu icon button. + +If you click the arrow to the right of each item, a tooltip will pop up showing you all the meta +data collected for that heading entry. + + +Novel Outline View +------------------ + +When in :guilabel:`Novel Outline View` mode, the tree, editor and viewer will be replaced by a +large table that shows the entire novel structure with all the tags and references listed. Pretty +much all collected meta data is available here in different columns. + +You can select which novel root folder to display from the dropdown box, and you can select which +columns to show or hide from the menu button. You can also rearrange the columns by drag and drop. Colour Themes ------------- -The colour theme of the user interface defaults to that of the host operating system. Some other -light and dark colour themes are provided, and can be enabled in :guilabel:`Preferences` from the -:guilabel:`Tools` menu. A number of syntax highlighting themes are also available in -:guilabel:`Preferences`. Icon themes for light and dark GUIs are also available. The icons are -based on the Typicons_ icon set designed by Stephen Hutchings. +The default colour theme of the user interface is the default theme from the Qt library. There is a +standard dark theme provided as well, which is similar to the default Qt theme. Some other light +and dark colour themes are also provided. You can select which one you prefer from in +:guilabel:`Preferences` from :guilabel:`Settings` or the :guilabel:`Tools` menu. + +A number of syntax highlighting themes are also available in :guilabel:`Preferences`. These are +separate settings because there are a lot more options for syntax highlighting. .. note:: - The GUI colour theme and the syntax highlighting theme are separate settings in - :guilabel:`Preferences`. If you switch to dark mode on the GUI, you should also switch the icon - theme and syntax highlighting theme. + If you switch to dark mode on the GUI, you should also switch the icon theme and syntax + highlighting theme, otherwise icons may be hard to see. .. _a_breakdown_project: @@ -75,10 +103,11 @@ Project Layout This is a brief introduction to how you structure your writing projects. All of this will be covered in more detail later. -The main point is that you are free to organise your project documents as you wish into subfolders, -and split the text between documents in whatever way suits you. All that matters to novelWriter is -the linear order the documents appear at in the project tree (top to bottom). The chapters, scenes -and sections of the novel are determined by the headings within those documents. +The main point of novelWriter is that you are free to organise your project documents as you wish +into subfolders or subdocuments, and split the text between these documents in whatever way suits +you. All that matters to novelWriter is the linear order the documents appear at in the project +tree (top to bottom). The chapters, scenes and sections of the novel are determined by the headings +within those documents. The four heading levels (**H1** to **H4**) are treated as follows: @@ -87,33 +116,40 @@ The four heading levels (**H1** to **H4**) are treated as follows: * **H3** is used for scene titles – optionally replaced by separators. * **H4** is for section titles within scenes, if such granularity is needed. +The project tree will select an icon for the document based on the first heading in it. + This header level structure is only taken into account for novel documents. For the project notes, the header levels have no structural meaning, and the user is free to do whatever they want. See :ref:`a_struct` and :ref:`a_notes` for more details. +.. note:: + You can add documents as child items of other documents if you wish. This is often more useful + than adding folders, since you may want to have the chapter heading in a separate document from + your individual scene documents. + .. _a_breakdown_export: -Project Export -============== +Building the Manuscript +======================= -The project can at any time be exported to a range of different formats through the -:guilabel:`Build Novel Project` tool. Natively, novelWriter supports export to Open Document, -HTML5, and various flavours of Markdown. +The project can at any time be assembled into a range of different formats through the +:guilabel:`Build Novel Project` tool. Natively, novelWriter supports Open Document, HTML5, and +various flavours of Markdown. -The HTML5 export format is suitable for conversion by a number of other tools like Pandoc, or for +The HTML5 format is suitable for conversion by a number of other tools like Pandoc, or for importing into word processors if the Open Document format isn't suitable. In addition, printing and printing to PDF is also possible. You can also export the content of the project to a JSON file. This is useful if you want to write -your own processing script in for instance Python as the entire novel can be read into a Python +your own processing script in for instance Python, as the entire novel can be read into a Python dictionary with a couple of lines of code. The JSON file can be populated either with HTML formatted text, or with the raw text as typed into the novel documents. See :ref:`a_export_options` for more details. A number of filter options can be applied to the :guilabel:`Build Novel Project` tool, allowing you -to export a draft manuscript, a reference document of notes, an outline based on chapter and scene -titles with a synopsis each, and so on. See :ref:`a_export` for more details on export features and +to make a draft manuscript, a reference document of notes, an outline based on chapter and scene +titles with a synopsis each, and so on. See :ref:`a_export` for more details on build features and formats. @@ -132,7 +168,8 @@ project is saved directly to your project folder in separate files. Only the pro the text you are currently editing is stored in memory at any given time. Secondly, having multiple small files means it is very easy to sync them between computers with standard file synchronisation tools. Thirdly, if you use version control software to track the changes to your project, the file -formats used for the files are well suited. Also the JSON documents have line breaks and indents. +formats used for the files are well suited. Also the JSON documents have line breaks and indents, +which makes it easier to track them with version control software. .. note:: diff --git a/docs/source/usage_format.rst b/docs/source/usage_format.rst index 77ca1d3b..678401e4 100644 --- a/docs/source/usage_format.rst +++ b/docs/source/usage_format.rst @@ -37,34 +37,34 @@ can select them for :guilabel:`Preferences`. Headings ======== -Four levels of headings are allowed. For project notes they are free to be used as you see fit. -That is, novelWriter doesn't assign the different headings any meaning. However, for novel -documents they indicate the structural level of the novel and must be used correctly to produce the -intended result. See :ref:`a_struct_heads` for more details. +Four levels of headings are allowed. For project notes, they are free to be used as you see fit. +That is, novelWriter doesn't assign the different headings any particular meaning. However, for +novel documents they indicate the structural level of the novel and must be used correctly to +produce the intended result. See :ref:`a_struct_heads` for more details. ``# Title Text`` Heading level one. For novel documents, the header level indicates the start of a new partition. ``## Title Text`` Heading level two. For novel documents, the header level indicates the start of a new chapter. - Chapter numbers can be inserted automatically when exporting the manuscript. + Chapter numbers can be inserted automatically when building the manuscript. ``### Title Text`` Heading level three. For novel documents, the header level indicates the start of a new scene. - Scene numbers or scene separators can be inserted automatically when exporting the manuscript, + Scene numbers or scene separators can be inserted automatically when building the manuscript, so you can use the title field as a working title for your scenes if you wish. ``#### Title Text`` Heading level four. For novel documents, the header level indicates the start of a new section. - Section titles can be replaced by separators or removed completely when exporting the - manuscript. + Section titles can be replaced by separators or removed completely when building the manuscript. For headers level one and two, adding a ``!`` modifies the behaviour of the heading: ``#! Title Text`` This tells the build tool that the level one heading is intended to be used for the novel's - main title, like for instance on the front page. When exporting, this will use a different - styling and will exclude the title from for instance a Table of Contents in Libre Office. + main title, like for instance on the front page. When building the manuscript, this will use a + different styling and will exclude the title from for instance a Table of Contents in Libre + Office. ``##! Title Text`` This tells the build tool to not assign a chapter number to this chapter title if automatic @@ -121,7 +121,7 @@ A minimal set of text emphasis styles are supported. Strikethrough text. In markdown guides it is often recommended to differentiate between strong importance and emphasis -by using ``**`` for strong and ``_`` for emphasis, although markdown generally also supports ``__`` +by using ``**`` for strong and ``_`` for emphasis, although Markdown generally also supports ``__`` for strong and ``*`` for emphasis. However, since the differentiation makes the highlighting and conversion significantly simpler and faster, in novelWriter this is a rule, not just a recommendation. @@ -144,9 +144,9 @@ In addition, the following rules apply: Comments and Synopsis ===================== -In addition to these standard markdown features, novelWriter also allows for comments in documents. +In addition to these standard Markdown features, novelWriter also allows for comments in documents. The text of a comment is ignored by the word counter. The text can also be filtered out when -exporting or viewing the document. +building the manuscript or viewing the document. If the first word of a comment is ``Synopsis:`` (with the colon included), the comment is treated specially and will show up in the :ref:`a_ui_outline` in a dedicated column. The word ``synopsis`` @@ -154,13 +154,13 @@ is not case sensitive. If it is correctly formatted, the syntax highlighter will altering the colour of the word. ``% text...`` - This is a comment. The text is not exported by default (this can be overridden), seen in the + This is a comment. The text is not renderred by default (this can be overridden), seen in the document viewer, or counted towards word counts. ``% Synopsis: text...`` This is a synopsis comment. It is generally treated in the same way as a regular comment, except that it is also captured by the indexing algorithm and displayed in the :ref:`a_ui_outline`. It - can also be filtered separately when exporting the project to for instance generate an outline + can also be filtered separately when building the project to for instance generate an outline document of the whole project. .. note:: @@ -174,8 +174,9 @@ Tags and References =================== The document editor supports a minimal set of keywords used for setting tags, and making references -between documents. The tags and references can be set once per section defined by a heading. Using -them multiple times under the same heading will just override the previous setting. +between documents. The tag can be set once per section defined by a heading. Setting it multiple +times under the same heading will just override the previous setting. References can be set +anywhere within a section, and are collected according to their category. ``@keyword: value`` A keyword argument followed by a value, or a comma separated list of values. @@ -226,7 +227,7 @@ Vertical Space and Page Breaks ============================== Adding more than one line break between paragraphs will *not* increase the space between those -paragraphs when exporting the project. To add additional space between paragraphs, add the text +paragraphs when building the project. To add additional space between paragraphs, add the text ``[VSPACE]`` on a line of its own, and the build tool will insert a blank paragraph in its place. If you need multiple blank paragraphs just add a colon and a number to the above code. For @@ -242,6 +243,6 @@ Page breaks are automatically added to partition, chapter and unnumbered chapter documents. If you want such breaks for scenes and sections, you must add them manually. .. note:: - The page break code is applied to the text that follows. It adds a "page break before" mark to - the text when exporting to HTML or Open Document. This means that a ``[NEW PAGE]`` which has no - text following it will not result in a page break. + The page break code is applied to the text that follows it. It adds a "page break before" mark + to the text when exporting to HTML or Open Document. This means that a ``[NEW PAGE]`` which has + no text following it, it will not result in a page break. diff --git a/docs/source/usage_interface.rst b/docs/source/usage_interface.rst index 15b29ef6..c1ca2f70 100644 --- a/docs/source/usage_interface.rst +++ b/docs/source/usage_interface.rst @@ -22,7 +22,8 @@ the project, and has four columns: The first column shows the icon and label of each folder, document, or note in your project. The label is not the same as the title you set inside the document. However, the document's label will appear in the header above the document text itself so you know where in the project an - open document belongs. + open document belongs. The icon is selected based on the type of item, and for novel documents, + the level of the first header in the document text. **Column 2** The second column shows the word count of the document, or the sum of words of the child items @@ -30,20 +31,34 @@ the project, and has four columns: from the :guilabel:`Tools` menu, or by pressing :kbd:`F9`. **Column 3** - The third column indicates whether the document is included in the final project build or not. - You may want to filter out documents that you no longer want to keep in the final manuscript, - but want to keep in the project tree for reference. + The third column indicates whether the document is considered active or inactive in the project. + You can use this flag to indicate that a document is still in the project, but should not be + considered an active part of it. When you run the :guilabel:`Build Novel Project` tool, you can + filter based on this flag. You can change this value from the context menu. **Column 4** The fourth column shows the user-defined status or importance labels you've assigned to each - project item. See :ref:`a_ui_tree_status` for more details. + project item. See :ref:`a_ui_tree_status` for more details. You can change these labels from the + context menu. Right-clicking an item in the project tree will open a context menu under the cursor, displaying a selection of actions that can be performed on the selected item. -The label, status or importance setting, the layout, and the include flag can all be edited using -the :guilabel:`Item Settings` dialog box. The dialog can be opened from the :guilabel:`Project` -menu, or by pressing :kbd:`F2` with the item selected. +At the top of the tree, you will find a set of buttons. + +* The first button is a quick links button that will show you a dropdown menu of all the root + folders in your project. Selecting one will scroll to that position in the tree. You can also + activate this menu by pressing :kbd:`Ctrl`:kbd:`L`. +* The next buttons can be used to move items up and down in the project tree. This is the only way + to move root folders. +* The next button opens a dropdown menu for adding new items to the tree. This includes root + folders. You can also activate this dropdown menu by pressing :kbd:`Ctrl`:kbd:`N`. +* The last button is a menu of further actions on the entire project tree. + +.. tip:: + Under the :guilabel:`Transform` submenu in the context menu of an item, you will find several + options on how to change a document or folder. This includes changing between document and note, + splitting them into multiple documents, or merging child items into a single document. Below the project tree you will find a small details panel showing the full information of the currently selected item. This panel also includes the latest paragraph and character counts in @@ -56,13 +71,15 @@ The Novel Tree -------------- An alternative way to view the project structure is the novel tree. You can switch to this view by -selecting the :guilabel:`Novel` tab under the project tree. This view is a simplified version of -the view in the :guilabel:`Outline`. It is convenient when you want to browse the structure of the -story itself rather than the document files. +selecting the :guilabel:`Novel Tree View` button in the sidebar. This view is a simplified version +of the view in the :guilabel:`Outline`. It is convenient when you want to browse the structure of +the story itself rather than the document files. .. note:: - You cannot reorganise the entries in the novel tree, or add any new ones, as that would imply - restructuring the content of the document files. Any editing must be done in the project tree. + You cannot reorganise the entries in the novel tree, or add any new documents, as that would + imply restructuring the content of the document files. Any editing must be done in the project + tree. However, you can add new headings to existing documents, which will be updated in this + view. .. _a_ui_tree_status: @@ -96,19 +113,15 @@ Drag & drop has only limited support for moving documents. In general, bulk acti allowed. This is deliberate to avoid accidentally messing up your project. If you make a mistake, the last move action can be undone by pressing :kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`. -Documents and their folders can be rearranged freely within their root folders. Novel documents -cannot be moved out of the :guilabel:`Novel` folder, except to :guilabel:`Trash` and the -:guilabel:`Archive` folders. Notes can be moved freely between all root folders, but keep in mind -that if you move a note into a :guilabel:`Novel`, its "Importance" setting will be reset to the -default "Status" setting. See :ref:`a_ui_tree_status`. - -Folders cannot be moved at all outside their root tree. Neither can a folder containing documents -be deleted. You must first delete the containing documents. +Documents and their folders can be rearranged freely within their root folders. If you move a Novel +documents out of a Novel folder, it will be converted to a project note. Notes can be moved freely +between all root folders, but keep in mind that if you move a note into a :guilabel:`Novel`, its +"Importance" setting will be switched with a "Status" setting. See :ref:`a_ui_tree_status`. The old +value will not be overwritten though, and should be restored if you move it back. Root folders in the project tree cannot be dragged & dropped at all. If you want to reorder them, -you can move them up or down with respect to eachother from the :guilabel:`Project` menu, the -right-click context menu, or by pressing :kbd:`Ctrl`:kbd:`Shift` and the :kbd:`Up` or :kbd:`Down` -key. +you can move them up or down with respect to eachother from the arrow buttons at the top of the +project tree, or by pressing :kbd:`Ctrl`:kbd:`Shift` and the :kbd:`Up` or :kbd:`Down` key. .. _a_ui_edit: @@ -118,19 +131,23 @@ Editing and Viewing Documents To edit a document, double-click it in the project tree, or press the :kbd:`Return` key while having it selected. This will open the document in the document editor. The editor uses a -markdown-like syntax for some features, and a novelWriter-specific syntax for others. The syntax -format is described in the :ref:`a_fmt` section. The editor has a maximise button (toggles the -:guilabel:`Focus Mode`) and a close button in the top–right corner. On the top–left side you will -find an edit button that opens the :guilabel:`Item Settings` dialog for the currently open -document, and a search button to open the search dialog. +Markdown-like syntax for some features, and a novelWriter-specific syntax for others. The syntax +format is described in the :ref:`a_fmt` section. + +The editor has a maximise button (toggles the :guilabel:`Focus Mode`) and a close button in the +top–right corner. On the top–left side you will find an edit button that opens the +:guilabel:`Item Label` dialog for the currently open document, and a search button to open the +search dialog. Any document in the project tree can also be viewed in parallel in a right hand side document viewer. To view a document, press :kbd:`Ctrl`:kbd:`R`, or select :guilabel:`View Document` in the -menu. If you have a middle mouse button, middle-clicking on the document will also open it in the -viewer. The document viewed does not have to be the same document as currently being edited. -However, If you *are* viewing the same document, pressing :kbd:`Ctrl`:kbd:`R` again will update the -document with your latest changes. You can also press the reload button in the top–right corner of -the view panel, next to the close button, to achieve the same thing. +menu or context menu. If you have a middle mouse button, middle-clicking on the document will also +open it in the viewer. + +The document viewed does not have to be the same document as currently being edited. However, If +you *are* viewing the same document, pressing :kbd:`Ctrl`:kbd:`R` again will update the document +with your latest changes. You can also press the reload button in the top–right corner of the view +panel, next to the close button, to achieve the same thing. Both the document editor and viewer will show the label of the document in the header at the top of the edit or view panel. Optionally, the full project path to the document can be shown. This can be @@ -218,24 +235,26 @@ tricky for languages that use the same symbol for these, like English does. Project Outline View ==================== -The project's Outline view is available as the second tab on the right hand side of the main window -labelled :guilabel:`Outline`. The outline provides an overview of the novel structure, displaying a -tree hierarchy of the elements of the novel, that is, the level 1 to 4 headings representing -partitions, chapters, scenes and sections. +The project's Outline view is available as another view option from the views bar. The outline +provides an overview of the novel structure, displaying a tree hierarchy of the elements of the +novel, that is, the level 1 to 4 headings representing partitions, chapters, scenes and sections. The document containing the heading can also be displayed as a separate column, as well as the line number where it occurs. Double-clicking an entry will open the corresponding document in the editor. +You can select which novel folder to display from the dropdown menu. You can optionally also choose +to show a combination of all novel folders. + .. note:: Since the internal structure of the novel does not depend directly on the folder and document structure of the project tree, these will not necessarily look the same, depending on how you choose to organise your documents. See the :ref:`a_struct` page for more details. Various meta data and information extracted from tags can be displayed in columns in the outline. -A default set of such columns is visible, but you can turn on or off more columns by right clicking -the header and selecting the columns you want to show. The order of the columns can also be -rearranged by dragging them to a different position. +A default set of such columns is visible, but you can turn on or off more columns from the menu +button in the toolbar. The order of the columns can also be rearranged by dragging them to a +different position. .. note:: The :guilabel:`Title` column cannot be disabled or moved. @@ -244,10 +263,8 @@ The information viewed in the outline is based on the project's main index. Whil its best to keep the index up to date when contents change, you can always rebuild it manually by pressing :kbd:`F9` if something isn't right. -The outline view itself can be regenerated by pressing :kbd:`F10`. You can also enable automatic -updating in the :guilabel:`Tools` menu, which will trigger an update whenever the index is updated -and the :guilabel:`Outline` tab is active. You may want to disable this feature if your project is -very large, +The outline view itself can be regenerated by pressing the refresh button. By default, the content +is refreshed each time you switch to this view. The :guilabel:`Synopsis` column of the outline view takes its information from a specially formatted comment. See :ref:`a_fmt_comm`. diff --git a/docs/source/usage_projectformat.rst b/docs/source/usage_projectformat.rst index 6a7c4c8b..3bc594a0 100644 --- a/docs/source/usage_projectformat.rst +++ b/docs/source/usage_projectformat.rst @@ -19,12 +19,27 @@ applicable. have the option to decline the upgrade. +.. _a_prjfmt_1_5: + +Format 1.5 Changes +================== + +This project format was introduced in novelWriter version 2.0. + +This is a modification of the 1.4 format. It makes the XML more consistent in that meta data have +been moved to the section nodes, and key/value settings now have a consistent format. Logical flags +are saved as yes/no instead of Python True/False, and the main heading of the document is now saved +to the item rather than in the index. + + .. _a_prjfmt_1_4: Format 1.4 Changes ================== -This project format was introduced in novelWriter version 1.7. +This project format was introduced in novelWriter version 2.0 RC 1. Since this was a release +candidate, it is unlikely that your project uses it, but it may be the case if you've installed a +pre-release. This format changes the way project items (folders, documents and notes) are stored. It is a more compact format that is simpler and faster to parse, and easier to extend. The conversion is done @@ -77,10 +92,6 @@ Format 1.2 Changes This project format was introduced in novelWriter version 0.10. With this format, the way auto-replace entries were stored in the main project XML file changed. -Opening an old project automatically converts the storage format up to and including version 1.1.1. - -Format 1.2 projects can be opened without loss of information up until version 1.1.1, and if the -auto-replace is not being used, can still be opened in novelWriter as of version |release|. .. _a_prjfmt_1_1: @@ -96,9 +107,6 @@ novelWriter documents were saved in a series of folders numbered from ``data_0`` It also reduces the number of meta data and cache files. These files are automatically deleted if an old project is opened. This was also when the Table of Contents file was introduced. -Format 1.1 projects can be opened without loss of information up until version 1.1.1, and if the -auto-replace is not being used, can still be opened in novelWriter as of version |release|. - .. _a_prjfmt_1_0: @@ -106,6 +114,3 @@ Format 1.0 Changes ================== This is the original file format and project structure. It was in use up to version 0.6.3. - -Format 1.0 projects can be opened without loss of information up until version 1.1.1, and if the -auto-replace is not being used, can still be opened in novelWriter as of version |release|. From 308a1649a546348740d6efe6e4604cb47a63a265 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 16 Nov 2022 23:04:35 +0100 Subject: [PATCH 18/20] Update docs project section --- docs/source/project_export.rst | 61 ++++++++-------- docs/source/project_notes.rst | 36 +++++++-- docs/source/project_overview.rst | 117 +++++++++++++++++------------- docs/source/project_structure.rst | 73 +++++++++++-------- 4 files changed, 168 insertions(+), 119 deletions(-) diff --git a/docs/source/project_export.rst b/docs/source/project_export.rst index 5fc22cf3..6055d991 100644 --- a/docs/source/project_export.rst +++ b/docs/source/project_export.rst @@ -1,11 +1,12 @@ .. _a_export: -****************** -Exporting Projects -****************** +*********************** +Building the Manuscript +*********************** -The novelWriter project can be exported in various formats using the build tool available from -:guilabel:`Build Novel Project` in the :guilabel:`Tools` menu, or by pressing :kbd:`F5`. +You can at any time build a manuscript, an outline of your notes, or any other type of document +from the text in your project. All of this is handled by the :guilabel:`Build Novel Project` tool. +You can activate it from the sidebar, the :guilabel:`Tools` menu, or by pressing :kbd:`F5`. .. _a_export_headers: @@ -66,7 +67,7 @@ Scene Separators If you don't want any titles for your scenes (or for your sections if you have them), you can leave the formatting boxes empty. If so, an empty paragraph will be inserted between the scenes or -sections instead resulting in a gap in the text. +sections instead, resulting in a gap in the text. Alternatively, if you want a separator between them, like the common ``* * *``, you can enter the desired separator text in the formatting box. In fact, if the format is a piece of static text, it @@ -78,13 +79,9 @@ will always be treated as a separator. File Selection ============== -Which documents and notes are selected for export can be controlled from the options on the left -side of the dialog window. The switch for :guilabel:`Include novel files` will enable or disable -inclusion of novel documents, and the switch for :guilabel:`Include note files` will do the same -for project notes. This allows for exporting just the novel, just your notes, or both, as you wish. - -In addition, you can select to export the synopsis comments, regular comments, keywords, and even -exclude the body text itself. +Which documents and notes are selected for the build can be controlled from the options on the left +side of the dialog window. In addition, you can select to include the synopsis comments, regular +comments, keywords, and even exclude the body text itself if you just want an outline. .. tip:: If you for instance want to export a document with an outline of the novel, you can enable @@ -92,11 +89,9 @@ exclude the body text itself. followed by the tags and references and the synopsis. If you need to exclude specific documents from your exports, like draft documents or documents you -want to take out of your manuscript, but don't want to delete, you can un-check the -:guilabel:`Include when building project` option for each such document in the project tree. An -included document has a checkmark after in the third column of the project tree. The -:guilabel:`Build Novel Project` tool has a switch to ignore this flag if you need to collectively -override these settings. +want to take out of your manuscript, but don't want to delete, you can set the documents as +"inactive" in the project tree. :guilabel:`Build Novel Project` tool has a switch to collectively +exclude inactive documents. .. _a_export_print: @@ -108,13 +103,17 @@ The print button allows you to print the content in the preview window. You can of your system's printers, or print directly to a file as PDF. You can also print to file from the regular print dialog. The direct to file option is just a shortcut. +.. note:: + The paper format should in all cases default to whatever your system default is. Of you want to + change it, you have to select it from the :guilabel:`Print Preview`` dialog. + .. _a_export_formats: Export Formats ============== -Currently, six formats are supported for exporting. +Currently, six formats are supported. Open Document Format The Build tool can produce either an ``.odt`` file, or an ``.fodt`` file. The latter is just a @@ -122,9 +121,9 @@ Open Document Format former, and a few the latter. novelWriter HTML - The HTML export format writes a single ``.htm`` file with minimal style formatting. The exported - HTML document is suitable for further processing by document conversion tools like Pandoc, for - importing in word processors, or for printing from browser. + The HTML format writes a single ``.htm`` file with minimal style formatting. The HTML document + is suitable for further processing by document conversion tools like Pandoc, for importing in + word processors, or for printing from browser. novelWriter Markdown This is simply a concatenation of the project documents selected by the filters. The documents @@ -133,23 +132,23 @@ novelWriter Markdown import back into novelWriter. Standard/GitHub Markdown - The Markdown export format comes in both Standard and GitHub flavour. The *only* difference in - terms of novelWriter functionality is the support for strikethrough text, which is not supported - by the Standard flavour, but *is* supported by the GitHub flavour. + The Markdown format comes in both Standard and GitHub flavour. The *only* difference in terms of + novelWriter functionality is the support for strikethrough text, which is not supported by the + Standard flavour, but *is* supported by the GitHub flavour. .. _a_export_options: -Additional Export Options -========================= +Additional Formats +================== In addition to the above document formats, the novelWriter HTML and Markdown formats can also be wrapped in a JSON file. These files will have a meta data entry and a body entry. For HTML, also -the accompanying css styles are exported. +the accompanying css styles are included. -The text body is saved in a two-level list. The outer list contains one entry per exported -document, in the order they appear in the project tree. Each document is then split up into a list -as well, with one entry per paragraph it contains. +The text body is saved in a two-level list. The outer list contains one entry per document, in the +order they appear in the project tree. Each document is then split up into a list as well, with one +entry per paragraph it contains. These files are mainly intended for scripted post-processing for those who want that option. A JSON file can be imported directly into a Python dict object or a PHP array, to mentions a few options. diff --git a/docs/source/project_notes.rst b/docs/source/project_notes.rst index 09064530..992f812e 100644 --- a/docs/source/project_notes.rst +++ b/docs/source/project_notes.rst @@ -25,9 +25,9 @@ Tags in Notes Each new heading in a note can have a tag associated with it. The format of a tag is ``@tag: tagname``, where tagname is a unique identifier. Tags can then be referenced in the novel -documents, or cross-referenced in other notes, and will show up in the outline view and in the -back-reference panel when a document is being viewed. See :ref:`a_struct_tags` for how to reference -notes. +documents, or cross-referenced in other notes, and will show up in the Outline View and in the +back-reference panel when a document is opened in the viewer. See :ref:`a_struct_tags` for how to +reference notes. The syntax highlighter will alert the user that the keyword is correctly used and that the tag is allowed, that is, the tag is unique. Duplicate tags should be detected as long as the index is up @@ -36,15 +36,37 @@ colour that valid tags do. The tag is the only part of these notes that the application uses. The rest of the document content is there for the writer to use in whatever way they wish. Of course, the content of the documents -can be exported if you want to compile a single document of all your notes, or include them in an -outline. +can be added to the manuscript, or an outline document. If you want to compile a single document of +all your notes, you can do this from the :guilabel:`Build Novel Project` tool. A note can also reference other notes in the same way novel documents do. When the note is opened in the view panel, the references become clickable links, making it easier to follow connections in -the plot. Notes don't show up in the outline view though, so referencing between notes is only -meaningful if you want to be able to click-navigate between them. +the plot. Notes don't show up in the Outline View though, so referencing between notes is only +meaningful if you want to be able to click-navigate between them, or of course if you just want to +highlight that two notes are related. .. tip:: If you cross-reference between notes and export your project as an HTML document using the :guilabel:`Build Novel Project` tool, the cross-references become clickable links in the exported HTML document. + +Example of a project note with two headers, with separate tags, and with references to other notes: + +.. code-block:: none + :linenos: + + # Main Characters + + ## Jane Doe + + @tag: Jane + @location: Earth + + Something about Jane ... + + ## John Doh + + @tag: John + @location: Mars + + Something about John ... diff --git a/docs/source/project_overview.rst b/docs/source/project_overview.rst index 3dfa3737..5f7cc103 100644 --- a/docs/source/project_overview.rst +++ b/docs/source/project_overview.rst @@ -33,12 +33,14 @@ other root folders. These other root folder types are intended for your notes on elements of your story. Using them is of course entirely optional. A new project may not have all of the root folders present, but you can add the ones you want from -:guilabel:`Create Root Folder` in the :guilabel:`Project` menu. +the project tree. Each root folder has one or more reference keyword associated with it that can be used to reference -content in your notes from other documents and notes. The intended usage of each type of root -folder is listed below. However, aside from the :guilabel:`Novel` folder, no restrictions are -applied by the application. You can use them however you want. +tags in your notes from other documents and notes. The intended usage of each type of root folder +is listed below. However, aside from the :guilabel:`Novel` folder, no restrictions are applied by +the application. You can use them however you want. + +You can make multiple root folders of each kind. :guilabel:`Novel` This is the root folder of all text that goes into the final novel. This class of documents have @@ -51,9 +53,9 @@ applied by the application. You can use them however you want. folder can be references using the ``@plot`` keyword. :guilabel:`Characters` - Character notes go in this root folder. These are especially important if one wants to use the - Outline view to see which character appears where, and which part of the story is told from a - specific character's point-of-view or focusing on a particular character's storyline. Tags in + Character notes go in this root folder. These are especially important if you want to use the + Outline View to see which character appears where, and which part of the story is told from a + specific character's point-of-view, or focusing on a particular character's storyline. Tags in this folder can be referenced using the ``@pov`` keyword for point-of-view characters, ``@focus`` for a focus character, or the ``@char`` keyword for any other characters. @@ -86,6 +88,28 @@ information about the tags listed, see :ref:`a_struct_tags`. You can rename root folders to whatever you want. However, this doesn't change the reference keyword. +Example of a character note: + +.. code-block:: none + :linenos: + + # Jane Doe + + @tag: Jane + + Some information about the character Jane Doe. + +Example of a novel scene referencing the above character: + +.. code-block:: none + :linenos: + + ### Chapter 1, Scene 1 + + @pov: Jane + + When Jane woke up that morning ... + .. _a_proj_roots_del: @@ -97,9 +121,8 @@ trash folder can then be deleted permanently, either individually, or by emptyin the menu. Documents in the trash folder are removed from the project index and cannot be referenced. -Folders and root folders can only be deleted when they are empty. Recursive deletion is not -supported. A document or a folder can be deleted from the :guilabel:`Project` menu, or by pressing -:kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`. +A document or a folder can be deleted from the :guilabel:`Project` menu, or by pressing +:kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`. Root folders can only be deleted when they are empty. .. _a_proj_roots_out: @@ -109,9 +132,7 @@ Archived Documents If you don't want to delete a document, or put it in the :guilabel:`Trash` folder where it may be deleted, but still want it out of your main project tree, you can create an :guilabel:`Archive` -root folder from the :guilabel:`Project` menu. You are not allowed to move entire folders to this -root folder, only documents. If you need folders in it to organise your documents, you can of -course create new ones there. +root folder. You can drag any document to this folder and preserve its settings. The document will always be excluded from the :guilabel:`Build Novel Project` builds. It is also removed from the project @@ -179,9 +200,10 @@ and to be able to collapse and hide them in the project tree when you're not wor documents. .. tip:: - You can use folders to sort your scene documents into chapters. You will still need to add a - chapter document as the first item of your chapter folder, and the scene documents as the - following items. Other ways to use folders is to make a folder for each act or part. + You can add child documents to other documents. This is particularly useful when you create + chapters and scenes. If you add separate scene documents, you should also add separate chapter + documents, even if they only contain a chapter heading. You can then add scene documents as + child items to the chapters. .. _a_proj_files: @@ -189,21 +211,15 @@ documents. Project Documents ================= -New documents can be created from the :guilabel:`Document` menu, or by pressing :kbd:`Ctrl`:kbd:`N` -while in the project tree. This will create a new, empty document, and open the :guilabel:`Item -Settings` dialog where the document label and various other settings can be changed. This dialog -can also be opened again later from either the :guilabel:`Project` menu, selecting :guilabel:`Edit -Project Item`, or by pressing :kbd:`F2` with the item selected. +New documents can be created from tool bar in the Project Tree, or by pressing :kbd:`Ctrl`:kbd:`N`. +This will open the create new item menu and let you choose between a number of pre-defined +documents and folders. You will be prompted for a label for the new item. You can always rename an +item by selecting :guilabel:`Rename Item` from the :guilabel:`Project` menu, or by pressing +:kbd:`F2`. -The layout of the document is also defined here. The two options available are :guilabel:`Novel -Document` and :guilabel:`Project Note`. These behave differently when the project is built. A -project note is never treated as part of the novel, no matter where in the project it is located. -See :ref:`a_struct_layout` for more details. - -You can also select whether the document is by default included when building the project. This -setting can be overridden in the :guilabel:`Build Novel Project` tool if you wish to include them -anyway. This is covered in the :ref:`a_export_files` section. You can also toggle the included -state of a document from the right-click context menu. +Other settings for project items are available from the context menu that you can activate by +right-clicking on them inb the Project Tree. The :guilabel:`Transform` submenu includes options for +converting, splitting, or merging items. .. _a_proj_files_counts: @@ -237,17 +253,16 @@ The :guilabel:`Project Settings` can be accessed from the :guilabel:`Project` me Settings Tab ------------ -The :guilabel:`Settings` tab holds the project title and author settings. +The :guilabel:`Settings` tab holds the project name, title, and author settings. -The :guilabel:`Working Title` can be set to a different title than the :guilabel:`Book Title`. The -difference between them is simply that the :guilabel:`Working Title` is used for the GUI (main -window title) and for generating the backup files. The intention is that the :guilabel:`Working -Title` should remain unchanged throughout the project, otherwise the name of exported files and +The :guilabel:`Project Name` can be set to a different value than the :guilabel:`Novel Title`. The +difference between them is simply that the :guilabel:`Project Name` is used for the GUI (main +window title) and for generating the backup files. The intention is that the :guilabel:`Project +Name` should remain unchanged throughout the project, otherwise the name of exported files and backup files may change too. -The :guilabel:`Book Title` and :guilabel:`Book Authors` settings are currently not used for -anything, so setting them is just for the benefit of the author. Future features may use them, and -they are exported on some export formats in the :guilabel:`Build Novel Project` tool. +The :guilabel:`Novel Title` and :guilabel:`Authors` settings are used when building the manuscript, +for some formats. If your project is in a different language than your main spell checking is set to, you can override the default spell checking language here. You can also override the automatic backup @@ -257,14 +272,14 @@ setting. Status and Importance Tabs -------------------------- -Each document or folder of type :guilabel:`Novel` can be given a status level, signified by a -coloured icon, and each document or folder of the remaining types can be given an importance level. -These are colour coded icons and labels that can be applied to each document or folder. +Each document or folder of type :guilabel:`Novel` can be given a _Status_ label accompanied by a +coloured icon, and each document or folder of the remaining types can be given an _Importance_ +label. These are purely there for the user's convenience, and you are not required to use them for any other features to work. No other part of novelWriter accesses this information. The intention is to use these to indicate at what stage of completion each novel document is, or how important the -content of a note is to the plot. You don't have to use them this way, that's just what they were +content of a note is to the story. You don't have to use them this way, that's just what they were intended for, but you can make them whatever you want. See also :ref:`a_ui_tree_status`. @@ -279,7 +294,7 @@ Auto-Replace Tab A set of automatically replaced keywords can be added in this tab. The keywords in the left column will be replaced by the text in the right column when documents are opened in the viewer. They will -also be applied to exports. +also be applied to manuscript builds. The auto-replace feature will replace text in angle brackets that are in this list. The syntax highlighter will add an alternate colour to text marching the syntax, but it doesn't check if the @@ -300,16 +315,17 @@ An automatic backup system is built into novelWriter. In order to use it, a back the backup files are to be stored must be provided in :guilabel:`Preferences`. Backups can be run automatically when a project is closed, which also implies it is run when the -application itself is closed. Backups are date stamped zip files of the entire project folder, and -are stored in a subfolder of the backup path. The subfolder will have the same name as the project -:guilabel:`Working Title` set in :ref:`a_proj_settings`. +application itself is closed. Backups are date stamped zip files of the project files in the +project folder (files not strictly a part of the project are ignored). The zip archives are stored +in a subfolder of the backup path. The subfolder will have the same name as the +:guilabel:`Project Name` as defined in :ref:`a_proj_settings`. The backup feature, when configured, can also be run manually from the :guilabel:`Tools` menu. -It is also possible to disable automated backups for a given project in :guilabel:`Project -Settings`. +It is also possible to disable automated backups for a given project in +:guilabel:`Project Settings`. .. note:: - For the backup to be able to run, the :guilabel:`Working Title` must be set in + For the backup to be able to run, the :guilabel:`Project Name` must be set in :guilabel:`Project Settings`. This value is used to generate the folder name for the zip files. Without it, the backup will not run at all, but it will produce a warning message. @@ -320,7 +336,8 @@ Writing Statistics ================== When you work on a project, a log file records when you opened it, when you closed it, and the -total word counts of your novel documents and notes at the end of the session. You can view this +total word counts of your novel documents and notes at the end of the session provided that the +session lasted either more than 5 minutes, or that the total word count changed. You can view this file in the ``meta`` folder in the directory where you saved your project. The file is named ``sessionStats.log``. diff --git a/docs/source/project_structure.rst b/docs/source/project_structure.rst index 78e04b63..445c22c7 100644 --- a/docs/source/project_structure.rst +++ b/docs/source/project_structure.rst @@ -22,10 +22,9 @@ Four levels of headings are supported, signified by the number of hashes (``#``) title. See also the :ref:`a_fmt` section for more details about the markdown syntax. .. note:: - The header levels are not only important when generating the exported novel file, they are also - used by the indexer when building the outline tree in the :guilabel:`Outline` tab as well as the - :guilabel:`Novel` tab of the project tree. Each heading also starts a new region where new - references and tags can be defined. + The header levels are not only important when generating the manuscript, they are also used by + the indexer when building the outline tree in the Outline as well as the Novel Tree. Each + heading also starts a new region where new references and tags can be defined. The syntax for the four basic header types, and the two special header types, is listed in section :ref:`a_fmt_head`. The meaning of the four levels for the structure of your novel is as follows: @@ -33,8 +32,7 @@ The syntax for the four basic header types, and the two special header types, is **Header Level 1: Partition** This header level signifies that the text refers to a top level partition. This is useful when you want to split the manuscript up into books, parts, or acts. These headings are not required. - The novel title itself should use the special header level one code explained in - :ref:`a_fmt_head`. + The novel title itself should use the special header level explained in :ref:`a_fmt_head`. **Header Level 2: Chapter** This header level signifies a chapter level partition. Each time you want to start a new @@ -46,7 +44,7 @@ The syntax for the four basic header types, and the two special header types, is **Header Level 3: Scene** This header level signifies a scene level partition. You must provide a title text, but the - title text can be replaced with a scene separator or just skipped entirely when you export your + title text can be replaced with a scene separator or just skipped entirely when you build your manuscript. **Header Level 4: Section** @@ -55,12 +53,12 @@ The syntax for the four basic header types, and the two special header types, is mid-scene, like if you change the point-of-view character. You are free to use sections as you wish, and can filter them out of the final manuscript just like with scene titles. -Page breaks are automatically added before level 1 and 2 headers when you export your project to a +Page breaks are automatically added before level 1 and 2 headers when you build your project to a format that supports page breaks, or when you print the document directly from the build tool. If you want page breaks in other places, you have to specify them manually. See :ref:`a_fmt_break`. .. tip:: - There are multiple options of how to process novel titles when exporting the manuscript. For + There are multiple options of how to process novel titles when building the manuscript. For instance, chapter numbers can be applied automatically, and so can scene numbers if you want them in a draft manuscript. See the :ref:`a_export` page for more details. @@ -70,19 +68,19 @@ you want page breaks in other places, you have to specify them manually. See :re Novel Title and Front Matter ---------------------------- -It is recommended that you add a document at the very top of your project with the novel title as -the first line. You should modify the level 1 header format code with an ``!`` in order to render -it as a document title that is excluded from any automatic Table of Content in an exported -document, like so: +It is recommended that you add a document at the very top of each Novel root folder with the novel +title as the first line. You should modify the level 1 header format code with an ``!`` in order to +render it as a document title that is excluded from any automatic Table of Content in a manuscript +build document, like so: ``#! My Novel`` -The title is by default centred on the page when exported. You can add more text to the page as you -wish, like for instance the author's name and details. +The title is by default centred on the page. You can add more text to the page as you wish, like +for instance the author's name and details. If you want an additional page of text after the title page, starting on a fresh page, you can add ``[NEW PAGE]`` on a line by itself, and continue the text after it. This will insert a page break -when the project is exported. +before the text. .. _a_struct_heads_unnum: @@ -97,9 +95,9 @@ build tool to skip these chapters. ``##! Unnumbered Chapter Title`` There is a separate formatting feature for such chapters in the :guilabel:`Build Novel Project` -tool as well. See the :ref:`a_export` page for more details. When exporting to a format that -supports page breaks, also unnumbered chapters will have a page break added just like for normal -chapters. +tool as well. See the :ref:`a_export` page for more details. When building a document of a format +that supports page breaks, also unnumbered chapters will have a page break added just like for +normal chapters. .. Note:: Previously, you could also disable the automatic numbering of a chapter by adding an ``*`` as @@ -110,14 +108,13 @@ chapters. .. _a_struct_tags: -Tag References -============== +Note References +=============== Each text partition, indicated by a heading of any level, can contain references to tags set in the -supporting notes of the project. The references are gathered by the indexer and used to generate an -outline view on the :guilabel:`Outline` tab of how the different parts of the novel are connected. -This section covers how to set references to tags. See :ref:`a_notes_tags` for how to define tags -the references can point to. +project notes of the project. The references are gathered by the indexer and used to generate the +Outline View. This section covers how to make references to tags. See :ref:`a_notes_tags` for how +to define tags the references can point to. References and tags are also clickable in the document editor and viewer, making it easy to navigate between reference notes while writing. Clicked links are always opened in the view panel. @@ -171,6 +168,22 @@ The highlighter may be mistaken if the index of defined tags is out of date. If to regenerate it, or select :guilabel:`Rebuild Index` from the :guilabel:`Tools` menu. In general, the index for a document is regenerated when it is saved, so this shouldn't normally be necessary. +Example of a novel document with references to characters and plots: + +.. code-block:: none + :linenos: + + ## Chapter 1 + + @pov: Jane + + ### Scene 1 + + @char: John, Sam + @plot: Main + + Once upon a time ... + .. _a_struct_layout: @@ -181,15 +194,13 @@ All documents in the project can have a layout format set. Previously, there wer available to change how the documents where formatted on export. These have now been reduced to just two layouts: :guilabel:`Novel Document` and :guilabel:`Project Note`. -Novel documents can only live in the :guilabel:`Novel` root folder. You can also move them to +Novel documents can only live in a :guilabel:`Novel` type root folder. You can also move them to :guilabel:`Archive` and :guilabel:`Trash` of course. Project notes can be added anywhere in the project. -Depending on which icon theme you're using, the project tree can distinguish between the different -layouts and header levels of the documents to help indicate which are project notes and which are -novel documents containing a partition, chapter, or scene. If the icon theme you've selected -doesn't show a difference, you can still see the layout description in the details panel below the -project tree. +The project tree can distinguish between the different layouts and header levels of the documents +using coloured icons, and optionally add emphasis on the label (See the :guilabel:`Preferences`.) +For novel documents, the heading level of the first heading is recorded, and indicated by the icon. .. tip:: You can always start writing with a coarse setup with one or a few documents, and then later use From 47d2c20bfda72b69d134ab2ec5abe19e32cd1225 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 17 Nov 2022 10:57:19 +0100 Subject: [PATCH 19/20] Move the file format spec document into the source code --- .gitignore | 1 + docs/FileFormatSpec-1.5.fodt | 2096 ++++++++++++++++++++++++++++++++++ 2 files changed, 2097 insertions(+) create mode 100644 docs/FileFormatSpec-1.5.fodt diff --git a/.gitignore b/.gitignore index e7099146..e12c3f96 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ i18n/*.qph /novelwriter/assets/manual.pdf *.qch *.qhc +.~lock.* # Python Temp __pycache__ diff --git a/docs/FileFormatSpec-1.5.fodt b/docs/FileFormatSpec-1.5.fodt new file mode 100644 index 00000000..892c8659 --- /dev/null +++ b/docs/FileFormatSpec-1.5.fodt @@ -0,0 +1,2096 @@ + + + + Veronica Olsen2022-11-05T18:25:44.8874731592022-11-17T10:07:56.114449354Veronica OlsenPT3H36M22S34LibreOffice/7.0.4.2$Linux_X86_64 LibreOffice_project/00$Build-2 + + + 166007 + 0 + 26555 + 20394 + true + false + + + view2 + 13430 + 174449 + 0 + 166007 + 26554 + 186399 + 0 + 1 + false + 140 + false + false + + + + + false + false + false + true + true + true + true + true + false + 0 + false + false + false + true + false + false + true + false + false + false + true + true + true + false + false + false + false + false + false + false + true + false + false + true + false + false + false + true + 0 + 1 + true + + high-resolution + true + + + false + false + true + false + true + true + false + true + + true + 327557 + + true + false + true + 0 + + false + false + false + true + false + true + false + false + false + false + true + false + + false + false + true + false + false + false + false + false + false + false + false + false + 189070 + false + false + false + false + false + true + false + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + novelWriter + Project File Format 1.5 Specification + + This document covers the file format specification for the 1.5 file format for novelWriter project files. See the documentation1 + See: https://novelwriter.readthedocs.io/ for a full description of the app’s functionality. + The target audience of this document is developers who intend to write a tool or script that generates valid novelWriter project files from data from other applications, or for templating purposes. + Note that flags are generally written out as yes/no values, but also true/false and on/off are understood by the application. The None value that occasionally occurs is the text representation of the Python None value, and must be used in those cases. + XML Root + Tag: novelWriterXML + The root tag of a novelWriter project file must be named “novelWriterXML”. This value is case sensitive. The fileVersion attribute must also be set to the appropriate format version in order to be parsed correctly. + Attributes + + + + + + + Name + + + Required + + + Description + + + + + appVersion + + + No + + + A string representation of the novelWriter version used to write the file. The value is not required, but is used to report to the user when the file format is converted. + + + + + hexVersion + + + No + + + A hex representation of the novelWriter version used to write the file. The value is used to check if the project is opened by a lower version than was used to write it, which issues a warning. Defaults to 0x0. + + + + + fileVersion + + + Yes + + + The file format version used when writing the project file. This determines how the file is parsed, so it is important that it is set correctly. + + + + + timeStamp + + + No + + + The ISO 8601 timestamp of when the file was saved. This information is not used by the parser, and is purely for debugging. + + + + Example + <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" + fileVersion="1.5" timeStamp="2022-11-05 17:46:51"> + + Project Node + Tag: project + The project section is a level one node under the root. It stores the primary settings for the project. The only required values are the id attribute and the name value. All other entries are set to default values during parsing and can be ommitted from the XML. + Attributes + + + + + + + Name + + + Required + + + Description + + + + + id + + + Yes + + + A UUID that is unique for the project and does not change during the lifetime of the project. It is used by novelWriter for to keep track of temporary files and associate cross-project settings with different projects without relying on the project name. + + + + + saveCount + + + No + + + A number representing the number of times the project file has been written as a result of user interaction with the application. + + + + + autoCount + + + No + + + A number representing the number of times the project file has been written as a result of internal timed automatic operations of the application. + + + + + editTime + + + No + + + A number representing the accumulated time in seconds the project has been open. + + + + Values + + + + + + + Name + + + Required + + + Description + + + + + name + + + Yes + + + The name the user has given to the project. + + + + + title + + + No + + + The title the user has given to the project, which may differ from the project name. The value is used in some places in the user interface, and can be added to manuscript builds. + + + + + author + + + No + + + The name of a single author of the project. This value can be repeated for multiple authors. + + + + Example + <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" + saveCount="5" autoCount="10" editTime="1000"> + <name>Sample Project</name> + <title>Sample Project</title> + <author>Jane Smith</author> + <author>Jay Doh</author> + </project> + + Settings Node + Tag: settings + The settings section is a level one node under the root. It stores the various runtime values for the project, and the settings available from the Project Settings panel. None of these settings are required, and if missing, will be set to default values. The entire settings section can thus be ommitted. + Attributes + None + Values + + + + + + + Name + + + Required + + + Description + + + + + doBackup + + + No + + + User-controlled setting for whether or not to run backup when the project is closed. + + + + + language + + + No + + + The language used when building the manuscript. The settings is controlled from the Build Novel Project tool. + + + + + spellChecking + + + No + + + The spell checking language to use for this project if it differs from the default spell checking language for the application. + + Attribute auto: A flag determining whether the spell checking is automatic or not. + + + + + lastHandle + + + No + + + An auto-generated list of the last documents open in the editor and viewer, and which novel root folder was last viewed in the Novel Tree and Outline View. These are saved as key/value pairs. See separate section for how they are stored as XML. + + + + + autoReplace + + + No + + + The entries of the auto-replace feature available from the Project Settings. These are saved as key/value pairs. See separate section for how they are stored as XML. + + + + + titleFormat + + + No + + + The formatting specifications set by the user in the Build Novel Project tool for formatting headers in the manuscript. These are saved as key/value pairs. See separate section for how they are stored as XML. + + + + + status + + + No + + + The status labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML. + + + + + importance + + + No + + + The importance labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML. + + + + Example + <settings> + <doBackup>yes</doBackup> + <language>en_GB</language> + <spellChecking auto="yes">en_GB</spellChecking> + <lastHandle /> + <autoReplace /> + <titleFormat /> + <status /> + <importance /> + </settings> + + Key/Value Nodes + The key/value nodes are used for lastHandle, autoReplace and titleFormat settings. The lookup key is stored as an attribute, and the value is the text of the node. + Attributes + + + + + + + Name + + + Required + + + Description + + + + + key + + + Yes + + + The lookup key. + + + + Example + <lastHandle> + <entry key="editor">636b6aa9b697b</entry> + <entry key="viewer">636b6aa9b697b</entry> + <entry key="novelTree">7031beac91f75</entry> + <entry key="outline">7031beac91f75</entry> + </lastHandle> + Status/Importance Key/Value Nodes + The status and importance settings anre stored as key/value nodes with additional data attributes. + Attributes + + + + + + + Name + + + Required + + + Description + + + + + key + + + Yes + + + The lookup key. For status labels, it must consist of an “s” followed by six hexadecimal numbers. These must be unique for each entry. For importance labels it must consist of an “i” followed by six hexadecimal numbers. + + + + + count + + + No + + + A record of the number of times each label is used in the project content. + + + + + red + + + Yes + + + A number between 0 and 255 representing the red component of the label colour. + + + + + green + + + Yes + + + A number between 0 and 255 representing the green component of the label colour. + + + + + blue + + + Yes + + + A number between 0 and 255 representing the blue component of the label colour. + + + + Example + <status> + <entry key="sf12341" count="4" + red="100" green="100" blue="100">New</entry> + <entry key="sd51c5b" count="0" + red="193" green="129" blue="0">Draft</entry> + <entry key="s78ea90" count="1" + red="58" green="180" blue="58">Finished</entry> + </status> + <importance> + <entry key="ia857f0" count="5" + red="100" green="100" blue="100">None</entry> + <entry key="icfb3a5" count="2" + red="0" green="122" blue="188">Minor</entry> + <entry key="i2d7a54" count="2" + red="21" green="0" blue="180">Major</entry> + </importance> + + Content Node + Tag: content + The content section is a level one node under the root. It stores all the project items of the project. The items are stored in the order in which they appear in the project tree, so altering the order will affect the structure. The attribute with the order number is not used when processing the data. It is mainly written to the XML for debugging purposes. + Attributes + + + + + + + Name + + + Required + + + Description + + + + + items + + + No + + + The number of project items in the content section. + + + + + novelWords + + + No + + + The number of words in total for all document nodes layout DOCUMENT. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set in order to ensure correct statistics. + + + + + notesWords + + + No + + + The number of words in total for all document nodes layout NOTE. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set in order to ensure correct statistics. + + + + Values + + + + + + + Name + + + Required + + + Description + + + + + item + + + No + + + A node representing a project item. + + + + Example + <content items="27" novelWords="954" notesWords="409"> + <item handle="7031beac91f75" parent="None" root="7031beac91f75" + order="0" type="ROOT" class="NOVEL"> + <meta expanded="yes"/> + <name status="sc24b8f" import="ia857f0">Novel</name> + </item> + ... + </content> + Item Nodes + The item nodes make up the actual project content of the project. Each node represents either a root folder, a regular folder, or a document. Each node has a type, class and layout setting that determine its category. Each item is given a handle that is a random hexadecimal string of length 13. For the items that are document files, this handle corresponds to its filename. It is therefore important that these match. Each document node is expected to correspond to a file in the contents folder of the project named “content/7031beac91f75.nwd” for the item handle “7031beac91f75”. + Each item node has a meta data node and a name node. The meta data node contains only collected information, and is thus not strictly required. However, an accurate word count and the correct icon in the project tree depend on these values. These values are set by the indexer class, so rebuilding the index should restore the data. + The name node contains the primary user defined settings for an item, and is required. + Attributes + + + + + + + Name + + + Required + + + Description + + + + + handle + + + Yes + + + The 13 value hexadecimal string that represents the item in the project. This is the primary identifier of a project item, and is required. + + + + + parent + + + Yes + + + The handle of the parent item in the tree. This value should only be None for root folders. All other items must have a parent handle set. If the parent handle is None for an item that isn’t a root folder, the item will be treated as orphaned during project loading. + + + + + root + + + No + + + The handle of the top of its hierarchy of parent items. That is, the root folder which it ultimately sits under. If the attribute is not set, it will be computed during loading. It is primarily saved to the XML file for efficiency reasons. + + + + + order + + + No + + + The numerical order of the item under its parent item. This value is not used during the loading process as the physical item order in the content node is used instead. It is primarily saved to the XML for debugging purposes. + + + + + type + + + Yes + + + The item type of the item node. Allowed values are ROOT, FOLDER and FILE. + + + + + class + + + Yes + + + The item class of the item node. There are a number of item classes available in the app, all corresponding to a specific type of root folder. Each item in a root folder should have the same class set as the root folder itself. The attribute is not strictly required for items that aren’t root folders as it will be automatically set to match during loading. + + + + + layout + + + Yes + + + The item layout of the item node. Allowed values are DOCUMENT and NOTE. This is the attribute that determines if a FILE type item is a Novel Document or a Project Note. + + + + Values + + + + + + + Name + + + Required + + + Description + + + + + meta + + + No + + + A node of meta data attributes for the item. + + + + + name + + + Yes + + + A node of user settings for the item. + + + + Meta Nodes + The meta data collected for the current item. This data is collected primarily by the indexer class. It can be restored by rebuilding the index, so the data is not essential. + Attributes + + + + + + + Name + + + Required + + + Description + + + + + expanded + + + No + + + Whether the tree node in the project tree was expanded or collapsed during the last session. Applies to all item types. + + + + + heading + + + No + + + The heading level of the first heading of the text of the item. Only applies to FILE item types. Allowed values are H0, H1, H2, H3, and H4. Other values are reset to H0. + + + + + charCount + + + No + + + The number of characters in the text of the item. Only applies to FILE item types. + + + + + wordCount + + + No + + + The number of words in the text of the item. Only applies to FILE item types. + + + + + paraCount + + + No + + + The number of paragraphs in the text of the item. Only applies to FILE item types. + + + + + cursorPos + + + No + + + The last cursor position in the text of the item from the last session. Only applies to FILE item types. The value is used to restore the cursor position when the document is opened in the editor. + + + + Name Nodes + The name node contains information about an item that is set by the user. Including its label, which is the text value of the node. This is the label that is displayed in the project tree. + Attributes + + + + + + + Name + + + Required + + + Description + + + + + status + + + No + + + The ID of the status label that has been set for this item. Defaults to the first status item defined in the status section of the settings node. + + + + + import + + + No + + + The ID of the importance label that has been set for this item. Defaults to the first importance item defined in the importance section of the settings node. + + + + + + active + + + No + + + The active/inactive status of the document. Only applies to FILE item types. + + + + Example + <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"/> + <name status="sc24b8f" import="ia857f0" active="yes">Title Page</name> + </item> + + + \ No newline at end of file From d13e2531e8ab8aa776b6e8bc88495a7b3519769e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 17 Nov 2022 10:58:04 +0100 Subject: [PATCH 20/20] Add a link to the file format spec in the docs (#1012) --- docs/source/_static/fileformatspec15.pdf | Bin 0 -> 302571 bytes docs/source/_static/novelwriter-dark.png | Bin 33051 -> 0 bytes docs/source/_static/novelwriter-light.png | Bin 23559 -> 0 bytes docs/source/usage_projectformat.rst | 20 ++++++++++++++++++-- 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 docs/source/_static/fileformatspec15.pdf delete mode 100644 docs/source/_static/novelwriter-dark.png delete mode 100644 docs/source/_static/novelwriter-light.png diff --git a/docs/source/_static/fileformatspec15.pdf b/docs/source/_static/fileformatspec15.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1c36fd700b9bbc5953d2d7c9bca6e472c345196b GIT binary patch literal 302571 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 Bx=J0FMNV( zqR@$Lc^fee+})&Xj})T{t!?2u+nMdxrlSWPy(mq+gZyiqA1AKs(uEyRja&m?&3m5N z>O5jmw?>MQ8Mq<1Ij^?o3WlPQyq)A&4}#kCdGgolUgjw>t)JaRbxvNW7Uz7=3pj43 z-8=2_Wb&OIaeFNDWDIJHFI=e?&A(G0dO=vZEu^G48>&)l3Mz`ZI$7E!pEDm~mTbpouuW0H7P8NfeJb)WY0#d;;d=dDX#H9V4A(Du zymVjN9s!S6*a0U^O6(t>y)9Nz(Zm}u_#Wev?HbTC0*u{8I}zf$YXa`4WNoh=l6PU( zjYm-aZbI&xVsFn(Hg2i;_zqi%xbpBUkL1Tq&2rPf`F(RU-sk%KZEAE4dnC(G=6nO5 zv9~k^iXMzIW{an0l*rkj+=cEwn{X+zYR}|l3S5|QCD5t=ZT0U)sisz*$jRKpW5COZ zVG48);6a3lwQkylyX2qtV*~uJCkp{BtZtNLDoN`?tmQQB@5nCTbe(xV`t2J>c6{DT^~02 zA!d(B#toT}Y1M_FPhgmBEYdkH`^$VA-E&ZLHO(jT0{1s(TpRzU$A zA=)-A|{h?jf-W3TZU-8qcenm#1aF`5Tg`!Zdsg4 zFPrI@F#?t*S}yeTrJQ}WL)WuFK|rlzKjcKy;BCdRQ5;6=tpE*8yHd$jH`nS8s$_d8 zkul%aA0(`SV7){A-@HlYqT>)xw@n!aP7*5TUu_hj7j7Qc@gK)g$a}*gGaiRI+B~e{ zKZqrnefW@rk+=Mf&%IDWNf*S}gW+ExHQS~d#5(*dAZ0m0(c<~PK1||Jg*IHJ=*9l! z{honr)RyVLpc^g!?dCfN*;c~8{5N{fAO|LHaIyT|I3+Hp4RYX4`%nMUieAU`leRhI z4SL=fC=2Xz6-pM=v#g;%KrDjwka9$f-_R^SDbJA!b&0{^oS5c=J~13(7|hgG5u@yIOcdiCC(oPEi{QUh^r9o>LS zJx>vfxFHYKIAug|bRVDB=d*_tUoW?p6xxBekBNaEaQZ)iu?u>c>|&N7;sg4a`mdkg zd+<9jIk+~jiw==&Y{5Sv&o3uE+^KhZq4IB>Hnsb7>42B~=$Y29x3KhRVxF@P5ZpM2 z#}Jkj=oU)gtC)x?MWl{$#>xkM)>RZSVio(MLCQzJ+?6j8{Edcv1wT0Bf31|F7z8(T z@t`x~NVVPBkFpYhxVaHVo7Ft{h4%11k|l&nGv_)L006}ijQqNC#;pY;gQfHv=ei4V zSkSK|v8g!$eZ}~2X4>T_kG6&zD&sjGlubJpK-SCr$kBLehHtF?l*_m2MjR&>)z1-UW`J{q5SAV z0S^?y^&^C6o~#^A^(%MgfS?^@%-KSYPPb=k7uRR44!|Q??kyuNA1pqQ8z3oVG`{RP z1e(?aAwK>9KrADGQLPiEZy*M#?H9mhDUui|d^xy-fL?z%bpG?7+3D|j-ADg&9+goz zK|Nxk=?}L1Rx=BY3ixW)t1n7cWC(wAjfz*f|9L7 z?}ee{l_N~Hx)8(NuTUtPHiW?T(kUq67&az{j!{57a4UMG^HF{sRwVq2h{;$OMX_dk z)kG&9InH;k1Tv|gsQka5E(HWE%0met=`M%3_AA00jng9d1|j=PEP?=u2yWjGqo3}z z)&6ddQJOg$kwZ;32~_D3&t=cNigZawY@Z5|*E3q@-#to!#z+Js@8c4K{?w+)V6y4RcSs=`1f)rmx;;D-bRjZwMW^Rw9CE#&&=La^u^uEaA_^kn zA6ZFTN5VddR6q$LMNjzyTl%<#1fCe_(`!Jr4(yTbuaocDCbFf9@km{HT{HtVv9iDIC z8};cgd*LIc<5K+BV*_i`mi56zLLaQvRFydgj?j+s!6@?(x;2cR>Fd=|tZF6kAN{W8 z3bXemW<+`uF_XP3LRcGTs)J^4me>%U_|4pv5auz(P9zA#t^&qGtH&*h`4#Q!M80+t1WUc5)`8yiU!{X@@d-=V4qN(q z#5n9Az>d7scHg~W#C#MS6$gY28NH?Y^F(NCP0=j$dSc1u|% zo0}$Ln&~H->n1Ygn{>LrzQ_D9<|AI}o%*HD63y;-fJFI!>j&CK@z_OBVStIr(~A zTq<_qSgWe?f3G{cO!O9Ln-wy$e>H2WI8|ehiA|HwkS@G)DPBRFZdGON^=np7ZELdj zsdp(9@u6m#PT?g=MJeSZG zpYB>SRDa}(ng;?MAxZOgoy9}Xmgak|z#z0Bfp3ta3;En?oyo?Rm2~K!##;!lN5#VeV?!V?O$xvCL!A!t+4Z*XMC*$c*9@6++D!CMO0I2&! ze!S*28ouWB{+4C)xm>h2?$9GbqCP*vjeuZSbYR*X#v45jKkC|86yEdDqht3(Rq)Uq z&NUp=MvVg*yxr{md(G4-;BLH!%sxECVPHG_LDIUrj!FO~ys4*QDoH(CD%KI{5ZEV* zWmRX=y;Ylu?rKqZIgUO7*)arJ^iGv+_Z5EBFB7bK zY>m0^qA}FX?_LoSnNiKtkGvE;*8gXTx342es{!7&qzC-SLyuh|n)Az>!T9hIlS;BK zs~cBji4>dhtGFh#P7?F3kZ2uccsYjii?j9mT>bs#@n-k^a_L-rYf<{%p<`h{a>+|2&CegE~M8K`!$V_ovr zS$|-ff{a|8^E+ThB*cu=a+|)3nKc&PnHii$)CqZ z4fh-x_3fL-b2fQ+7!f$^7uRz4!|p7Jy^-_}^n5OG$lcbSvX`(AWU^TP*6hQi1+NZP zSJr0eKYO2ziVp>z`6VL|z}Vt;q1*UZVGT&OGO99Y z&;cckzD!npWsj>xHXuE?8xuPkW=97C*;+6rz8$$)Bfm>{OE%IfMaeW48a1v*9N1cY za{T*+3VHhZBll_AB|>rX0{d=^W3=>tw>`wX6<+|{dLDfp#e06s;5oUrl^yl7YV{t0 z+@S~86kXc(NJeIaam2s3=Fq$o&a9o(t2!sHcwz6w`aap4b15fEw`Ek!ti5Ck+&(!S znYpXuHmZlyMz$BHKeS1BujR>ER4Ho*)TBtxzPUBf5XMh9I;PE7DlN^@<&?~X*0oh@ zeFQwsYs*s~#!tYX4+C8D4bO$n@&6{_NB*|0ETdT+1x1uf8pdUq(4wVVAcx~V8TQje z3{c{T$IAhK<%#z>8o8hJ&^}5+SIMC~DUVTh>{aAWpU28dp-36nQ^2t=(22>9C7CFa zGQH$x&YU1WHn`C&b4l5VpS1;yJC*%osKB1!dVMpNE)rXL1sPsC4O7pEE6-}g$Lhno zY=I|F`D{!BZqY$#L6RI^SdKL2%iO}Ow`>PZ#QULWOUc14n$>>-H}5g5tb;Su;l&c~jPdO97=k9LdGZNH528aUayxf*Ck zMW`V}s_)!SN|BvSM@cFwLQkqY2T?q&4vSUWr^h6E_=PO&WeW@{GsKTPC`}8Ng+pH0 zo>x_z&5dc0Ii#^YjQFU6Yev;}O~F!J=F90&elBn9V+6{fTMh!!@Y6cXmVjuF@M+$6 ziwR51-mjQwW5I^t^~ga2B3qgiz{TTBWf7y1jV7Z?^-?Ee18y=AzP7c)){Z$SbVRAN z6=<9|MH$rFjg95X{eJJ*dQ3S?S*O{VC@#^qq29P)uaVv!Gc$Q$t+5gM9ou#GJ1<jTs&ztN}~Vj>?1 zQ{0J+UOMNo;caJSQV#x6Idw4F`wlFDv5L2x?0F03?S-Oq5Cw=549 zV-yoU)NE&LY!jy8s?pBxZag7FSVz( zRpJII8z~z1_!z#nh=Mp5L~j5d}S ztjp7D4IV~6=L=beY-13TozTE(3GRMwe6a?>LvrE{|2Z+7xUFA0e0$ycI^JmC{mu6I zr=c?Bumvo7f^{UR1th)zLC*b{S+_*oP0U!XodBDaVmdS1-5UQPc0BjF%imk4@|-1xXFJ~kRouL@_Q0h=IkFNd>M?z4$df_#J_;< z?)bbGnYv`?1j;vPMyM&p-qxsCBcF7iXfExgQU8l%{&yYxk7QDqoJ_v81dpYPY379!f{5Q~IGacsooJYVkz)Sr+7wBQpKyhYK6TWKr@ zlhBYx;w56(s*hLdiDw1tfGkp~k-tjRCWnse;KiM2zAhI}YE&@D+) zog{Y39)~V$PUFFyiNv^9&1ualN^&$87Kc{E`1=H}+TpYQA`W(>@_!FKcv;?l9g;R~ zj94H>HT1p~nO*ui6{^ffn*0q?fJ7qR@$th6ISYFnY!G6#

luREFKAct(VC6tbSK z46Lt6BDRv<39vG*jbgr9DCeL&EvYANBjT;kH5+*OzCHWJem#CgecjI(+r=o z%!Ng2*}Mpwr;q^`jda+8G>upXZ{kt<)X=hmY`vADhbWnmx)jTXMkQMy4v^t5uT@lPu9sly0wlMu2^HszDdY4YH_v6v!@{jD%ST zd|JT2IzY0G1i$9a1~F}4KDt%c@l|`Sgq0CNwO39M$|;rlyn-5*9pn=GlN;@`^@%GX zknWG(dqR}8!4*|+HR5qnkZZk40<9D89%BaPu9lUf)`mLHbvcq7H=?EOwl2o z*^KP?mr<42bTbDP2_AA9$m@wnv??Zw7D{Ih#GMLLo>MRm%baHbMWaIy!7RfBE= z;aQ!&E^POJq1lZttsHE5tOz}YFxN!U^z)`382P%^l6W4@XaMC97gMNb5vfZ zSZZRAOjCvw&AGKH6A0=No@Pk&Hc)54Pr(7rEeS@8kr*QphR|QNDPT&1Tp&aJvx3^~ z;miYxlRcoU`49S&5v%HCDAvgYn!)+a zz~Ipvfw1q>r`;b=tA(-XB3~7?OsR~vF0bcE> zXU=B<#PHugI zOKpGXUcExiP&-8|L2>PMy+(_u%ZGQ0iiuH&i{12@Cx7`eC?KjJW|ur>EVR@qbul;= z$?yNFAz43)ZRG%CFbU+4DxV~?5vWk3Ctk5H(q?36BIsDsPg9RBOW$e2gB?h``zAl- zaX8}CnIWgGo`Qr_)GF)1%YGJKaEqTP1EgxpYTFtA>%sguY&iw%^Ou2k-m$8#WLz!$8zqZW%AJ5zr?SZgP&q~ndAT(DGRejY+v_mQ(1o0%h=Qt~-h%;NZK70UQYc+eLv0 zv3f7b{>TujuawMzx=pS7in#I;V2FpOaZp8lF0}^O_M<#xOnv|Y_QU?KU?aEEA>1qX zr%rkvt?69H%vE3Faq(TL4_y?Z`UC~Cw z?Opv;S{QUCk3XEm!;vi3l24@VYpPa<5znPo$O-M@l`7y3%EMCQCpF6**9Vf9J-z5IAOD zd1z1~aH}=IR{>$cKTCGb2aa)W`YI#T!`D{i56`LVm1;u$9My52dgmz#aF#}snkgRK5wxp1$6JS>1>PK-h8&$6sx%tYbTvT{OW3_sxvt6?LH0w zn>`(Qov`I;SXDU19Ee3n$6NW^wJ;+wQc2`GGv{fPT`-jmm7=J(rZc|w7#tL&e|4e> zm(Im-zOu^4DW$?$FY~6I%eroz!kL`mK`8zNgWl-6QH{sCBgx0gvdR{TbL1}@A!2l| zwCpr)@URalf=tT=bxDPcu<4LFid?~a_FX!1LS!zK4uVuf4Lj!Nscyjxz{zQKNf|@R z4<>t`H{VnmBj55tL({%|I)9DAi>+Kv^r)9VT*eh(Bay?MUKX!VU8bysRLb{Vr~NKwr8K|Z2vtPG2L3>|!MWv3E42DAc}U6lU(Hv4eN<}T!5+^~bv;o? zRi9a%T^504vCXZ<4gS#G8B~+{QeY8V(h1V4Cv>0t{dn9|&{?h>lb9hyJAoQubdGG)OP}lz=L#!3oar79g(s_a zDmW1{(Fx;3o~+`&K4LeSm%KP_k^I?Pg5?D zH>$`_ni$;a6|T*XH+fm>Z!w07!{17gbG2>v83={e)wuXX{vns$LXYlEbmA+A@)X%c zyCv#)ve3gqM^{!wF6GRvdu8PF5d%re?|+bX`L;8V_)l`7ij(=_`Ljt z5=utmjOzIr#g-`L(4rUK>Bqi_SUzDF_eC0K?wR5ZXAx%HT;&A+1rYYiK}V>fa3TCKHjejT0?srU(518A`@;`4Yuq0^6aljXUPh>ouZ zof0^JI}RefDhm;;Y=91s-j0g-6I{4E5o#D{98eRd;|or_l|g4HU|pT1q33K1xO z7=b}Y;?5+jw$eI~QP#4b#cLh>LeVG;73yIF{>7tdCK;0XYr9!iQh4a1S+@?A1zTP~ z19qE@1k#NY$Hk}?9>dTs1~;KyT68sf^8&q*yLpetbczr98)OzAi0^-JZvMk#`ImFU zz`^og&dpERk&`4w*@i`zE;?2#Yv?DrZR(f=U(Yd)6Ll0`)k=veCO?n zeyblKn66Mt#Ljhecjk{Tt_uhnINTt(zBzl$Am|?428;=OXMS_o5Yp%XH}4wK11O2* ztL*9K3?qmOHd=OiTmfVt<8>43b=ONKC5YOw#acdKnbU6K3iJ(_e&Ji~n9!yWW`B3! z;qhW-CTO`{ahJJ4y(W=;lkACA`X3>J?&EB-G_o3}w7~^10MdzuRV{I=D2jwZPKq!i`fEKI5cj}GsakZHO#ZBs&$?+l?qNQwYp z$Grt6KNRc}X-cXIWSfyl@-dgmSiM9@?6tz#^RE1u(-N&lR@!y{87Kh}x-0?r5^ zA`qh|Da@Pe%|{Tco#sn)_`=H2l_R^>mij#rIJbZxQlNs$s`Q?orAjlFy~xEMTg>P` zm&-6lj|n%j5z9c%-Q*0AH`7dsBpOFbMyC}MF>O6#QeBqrf$tn#oyszHGI!7$1;uFU z1c`|zjK=L{Z+2ED!lD*GH`=b7_DcbRKsc_55Ug2GMx?LVWd~EMx^CP~R47d({nv4R z&iVO8A1%Qku$8nNVtNN)0RyEd;-XiLxP%Fh22rPZ(1dW~xNxeN-{d87X@wWIowL)x_5J`> zAt8nKO^wp=z{#@FiBEb|TSnFazWA{sT~-_Y+Y|a6-cdolL%$bV2GrJEnV0WRb-uxf zp{o_v2pnDf;hj;U^dNkaYj0Ap)&IE5Sk%^v$j!m;f}9PglU% zyT8Uc-=OyP$*~K$it=EEm4#@R@)`6c_RtM17lc}}h|#gJG2lY7R9k`dnsmD9m{(1j zUG-YdLI9->y&4^Hx^19?RnN`42wE|Z=9|&)_<$5uVn(OBPjBaEJa)iLz(rpG6lGfF z=F}T=5liOYL(^e);Eb0ToUzdt5t`i5OEw<217WZ9y#83-_P^n11-VT321Z)m3Ukai ziqfht_r0xot*ErtU50TU?8qDL&!B+Mk2WS_%We(4H&oB40^qjtz@897!i+cb*a)Lh zkrvLhL~HIvOwx8Qm- zh-HQx?L&1aci~u&i@T9wC-=u#)<*TCX0R=OUy!c6{SO<;e{K^0>>La%3~c}2Q5aeO z-xL!DMz;Sg$K*e`QyK6X=-CRMys2iG=xz zZD*WbOVs_gJX=D3>U4KA`)L{B*VrG?wD6& zD(Ss$y_kAQUj4Yka`*y0V-wNa(U>gW_NS)Ai~*jniT&=#4iL`;w{ywpo zaF0YnGhU>yxkWv1UQ?YWu_`G;K*j8QR7`@7k^?6^Db#$I{M441hT`*ARHs=|X{D zg&?;@3~B;l?;Lhq3xOXcZ&l6Xfw(-wFgfSM&}XJKC~BgQlvNUUv$cVp5E73gBMj0z zp~?HEY}6U2;Dve&QO(xiW5@bapF%xL*KsN;v}w@7ACrYu6;m2)i>pd0L(=y+Iq;#? zVqtkW4N0}Qcsvjo5PLdxOb5xihFGCtc&N3w@3`xAb6VfkV6#1xYmm>ejN=NLEo_!# z7hsa`?+;BC(BFc0oQL4}+Axro=sO(m$%Fw+OpBqd);A{(q4lf6LIDfl(a*3Ua=o zyb=~{#_xc?u`~45>Dkc@TD2es0}vvthXc4*=4)a>8WufL#aVLVr<^I1=O`kpW%8kA z6&m(_6CSmcH~WYmO{j?tfkwXs)N!|=RG#CKFcfIezWiRGfz_ZH*vo`JMEO&>n2?Tt zkAhB|oJs5NQWy@MahkW|c6KgAhFMmp9x_Z=q42;Q4)c5MI#w5q5}y7~3~n?0QuCxD z51^v@r23$}4UA+k7lT2)XQp(!sPve~UZk}vN-L&e^`){fQc{%aL{XzuQlb^+Ne3ck zDCfBf8nTX|)Vs*uWHbyNeD<-oF#yCO)zmOm{RAau=o-;xL*)WSzsvFu(eq{l@&I($ z(38111y}T+E(S81SorD`mrI#yLnSG(cfx6el)SS22`pO$q82U{7IpuI{0;mjCRx_t zXj5Tu0N^1MsauNkNf`Gmh8^Te2DWcllA_|1HaWD|I*3%SiFUa&9uT-ry zB#@Y^wG>n$s%?hrz~8k(NmjPk=6vU(&DuZD*Jc^}sxGto}vH=5QiBdRRt)4 z1Te@mm-hPjUw@;K2!c|i6oPYJVFCy`gi>I~*C2=vXQ&9F{EZhzFS4f;$=o}FH$=OA7j z{Ip?{ZgpwOog^X`%{4alS+)k?FPDnKSH=T~S`YK2!Q_Ltp$F zcpek*E)|7VV>ccp9A$4`wM`Jrm&&-xegty#l-@QZ4~3ZOuckoVfC|-^D!W0*psIPv z6pQkgY%)gaK`S{}B3N~NUEm}nX@vrbPCz{=8VW7f~3_kP@$1M zDp_=w0SGc|*MU4FMA;K(!9NhqKWwa7GR=y|TC8^S_|7XeC|#O&k}Y~qM*Jjs!Bcn}pK zze+LU_^UC*xqZo-HA||AY-eX?3uW_mOvj_{wry4W`q6j}6DyaFcl~}+=!0aVcY{{u zOU`r2aA6_ZAWsQ&VGb%fdUOEj%KYoXT=?9)JrZc4GsDMHM{9$1ZPNPBpyu~WmehsU z1{V?@AImL}?{$*pCr@DLUzSQTAVs1mO^zA;1DR|Yof-_HXpG$o1ko6Ltlc&Q(3p7o zKqxjMBJC_uPJ`ek-@*W7}^@4=FlpQ+(JjU%qNUz>0+iy*lMV+M2LJ zh--f5zYT;7*T$`X*H_D9zX(L#IJ3ew(~%|(Q+_(P z$hTaXOvQ5;Xo@_A`yxPgn1}++l1F|YU=Kj3h0C^m4nHY(j@!<8_7s|JEUTm=Ww~|Q!g3P5 zlqIP9qtnM0KqZ#ROly*CH{Xmx5@l6t_$XgpDe>A?ct3!2$UP8g+Nkv$kZ10wj49Yq z@!K*qS@+#KG|=w&8^KbvK;5$9F>C7*&qw8~L5bxutF`O)asw)qiP?%Wt-3#OF3tmQ z9ICC1erxL1D>jrz;UM&*ozPdnJ^(?T9Ndr9Nqp~{c1X{cm}8%|EZi?gakia>Lq9}j z4GN`JZGYw--A};0o28}o4pr(J`-59&niR99cBI;duwC!hA6U3Is zG&Q+Zd_rh}n-!S-1jB|f&`H_ij@hj@K=I>Y5yF<5Z#R^f77-mB7TamOb=u)N=ac?0 zy;5#GF4yg%w?3qmZna;rmz?-u;OUXt?Z353akPDLDx1+buS7+P=j$GmPZ1Th$&aQ?Q!^9`l8{&{B=F!+4ltQg{w)kM_^kE2Nx0eDgTv)| zeJHH#y-2fCe;(pJembuGz};%gBq;4Qd4Fdb#pTrS;kxfMN}A;272RgbzxLaSOQfu# zSHq>CLWy$W*3*`3%RJ+DGH}>G>yp={j$=6tU{7o(r4d!;*QRrgvhD%*(9Ri~Nhh#| z7@ove29+`COJthX0T-`5WE z^pwGWtT{bZZhDS7EFH+#^|C*8yHdL9wcj21$5O#AgG*Sq3=eOBbG4|J+o zV%tq;mhbCgH55!&r^iif788#9;nOep>W+u&dE6*}$G0Qs9)(t*wSf3uzNTfZak%6^z%?d`=G_?V25@n~> ze-I_U?3WsdF{OSLpePl;4bAGb*g4H=QmyNY%WNhQs##W~C!reL00_McVve+LN6zug zc@9|Yv1RVs``UI-!+X9>=f``T&!MmB($M)f%g3T|__B`|0b6e;eS^i_{&?=^-eRoO zsPEewoOw*Dfsn>TZ|pWeSkcyp;2(g&6?QB$x*3LDf-v}x2Z&He7t*@+3zjF-chq;> zcY=3^&Mo6-BKHyBaoN{-$k*^(Zii9WNSl~+6O{eW6#nlePJ)K4I<-MGqC zsCQR95BzHcp`ELno^qdrPnf4sM8woo&6p8v#YCk&>Soj=LvAvZta}-bA=hGLQ7Hl1 z5UYm_ijaE6F+#ay!Ged8RO!~;a-N-%Sw`zm%hk>K_xfeJp3AF0Kw`62)4r>%uD^XU zJ$%<=LCkVH=5~>?LbtQa^1pixOtGVKyNtIDHQyi{%D=hXW!`E>OTT8SyoXPObzj#Q z^>HfDCQj32d;_YczBn*#U+YLf7XHnkkAL(Ma)srWW(Sm!lC}8@7KZ1mXgdsYy4KI zH1)0Oe=UmreOK+DT<3BEVK0i!Gs|j4AD?j%49ZmG?_^0Yg=&A#oKD2lE`vFhgm~rq zv#{dTIc60Zp3@+vHNMkpDa(qu4LJt5 zKm4XHM~$#bm5!krO8E_9*(g}*tz z@lKVuw(f@?B6J~T;4Gl#jHSL>Aw|O+2CisH)T`w=BxzDfXD&dTnL-(&m41U!8&9SL zc}0t-cg-!x70Qg7Bsj#G5j)~3ENMO1{dt-feB4})Iwh7A0RszutwO6ZM4uf8M;vN5 zAiR#2YGq>+=R8u;SdBRYmdJ^FGsEWpuF@%K2`O(qaV~ zda7-73H-Sn@jh3Xmr8k!F*B#X>p;TIlDZys45#PdxIG#u6cOsVu@eGhO0Ub>@+s zn>T(;fdc_{#7;JY{B6zUr(`7kz$wM?fX+DkJfhDov*oFr%5%v=1PKcZm)lhptd+*; zK#_9cQRZx6u^UN&CS_t|>Q@g-`YQm5)w+dcibPY5E)g>sJuiT>>>qT>kn+rIJsv%# z0E1|f`SLU6s#?yRi*gItDuPGipF=3w{q8JbiC}Q|Fb8bWvZx`6zzaf&iNz@OfGLp+ zZf~#K8(uuSfGWWn_W5^t-9A-Zu`ghs(DE&I4E#m$@%*OQLW3tlv-hP7RwVs z94mQSD#cPORB@_UQ1Qr#$N0+=F?5bpq7oE7k+^Vr>D+@*#Yg zY8d$%mt2E-3>w!$Z?x%MWHYqI>c4>%9I^JbsMB8b^AcwQ!4opmG$EMQrPHPPNgb&eX=VW)+6yKTs$eVOYdXk&r90Cm?vu0uLaLG6PY&{ z(9i+BAc^TNN{N&xM!B*1l$31(G!fue9jWK#P;R+w?}hLap_bgLIc>%LWj?$V^v3(T zm{-;08xd>nP|=d&I-R(fp22zDY&18u8xpKewXJ3aiIl;|F71&Msf{!hzU7nbR?MT< zIn`1qh+9+mJSk>DD8Y!a#E@08D=k2tGc9&c&r7GA=dTpd*i@iN#HypKdX+0ywh?-rHLpqty1vBykz>Y5KOFG%e0j&A{viUsQcG+gq10N?NKKDHlWd zP`IyBRmL`HU8pLr6tCWW$Q7oQ-p5)#Kx~34X3={)tvD%mNyC@A`K1xGMZve&^j1=v zLTgj3Y{k3$fj3FvOJWXOS{rG+l;->35Yuu5)?j7x-lW2n5}|s*ZDpP&ZvKe$hmdTe zKtO}4^=bUINp6c9su|OWT~^6YkvVnI8J0)fP87;2xv}Bx@&+;|nr~n8id6Iv8ahe> zi+W*;il0zq2xRj1a^M+f2<%5+34B|e4DlqTS7hPXVW&umo)f@~)L)uk_TB})W{|AE zy(SLON#`4*ZQ6xq$H4|;n`tI}Bx|%a8V{3?Y38`+zA~dm?~8DU@e{B}4>(eX4-cws z3$v4xm3Ktas3#<#-hc2h3y!G-FUQ(VBXkO3=p`ftz!7%-YKR+Dg_k~)x zF? zv7%~VJy6|Cthj@N9;+?9sCO&H7nm(^pM;Zft;R*-P(;mtX&}XE4xNDPelpVd?O6(r zRNQkR`WE}#{V|~V>z9{I0hF>F9|R1@?^jO#=A0R$N;hs zV`Cu#?4s7BWU`b{Wk|H2!XJv2XnRO4m_4Nzk#Om;g40<`L7*%J1m5|I2xZ7a#WJ$u z3@2;SZi-kkaR)@ z_TnZMX68SGY#fkuO3o%Ws`x+rbN^cYM-I@05uf2d!eWY$KM}6~5>jDg`yVs}8Cd>5 z3W7R`leR(h@FLG&kaX)J!7{or!SRZS#Hc=HO?dnl1kK9PCfOi5H(TnL2AFob0KI*S>ZDFJF_1sG11!$5CJO5_O8)uvpX$ieUQu33 zH%z@;XSaaQ)LyrkUG-|d&kD`;wstc<{l8f4`zI2uPWH1)XL#IzjEgi`IBm1&YYPr) zcYR#@oGMHj3PfR&&XHecbOTTXle@~!69X2%pBxj2({z1Yd0Qq{D2G&&6P)&ZV!KGU z7RR!N#13rsXzpllF+FmdunxfXvQ2Tr*|-jj&iL^8NNeINhsX|;3?sZrb&-}WjyM{S zEpT|ohWR|>qx`tlXuoc?+h6#U@F&xZnBCp@5}aJ_c}HZ>q&bWRMh33=c0|J{J$nF7 zPBK3JXNdi`?Ee3P5}lHZq4PhuQg(DP`B$%?fs@HUVEJDWW#Q=LEM#ur_zzN~4gRyu z_+zM97(1IgY5h}Oz{<*s&&0%p&%(lr&&bI5uRF^R+8Ei`{&lf3{Iq}i|GC)zbKbw^ zvavz_bN}c0|Fr*g*ZB{G{Tui{F#FeELlKfr*-pjQ;vezJfBOD`+5Zi_|Cbp3XX5|H zjQ;N^Ei*d{>wk~ZZhCroD-S=uWWTSd**18wt!`SQu5@bLG185Z^1B}*Cc*>D;0rY3 zlS(xZ^b>cQqRCg1l&OF#*6kH)hG_aOy%`E1d!txaui+`qy_GeqHmNsT=F@2Q3flUb znc4|Pule%!&R*AU+g|<7U^>p`a5~CnG9$&h*25x2#K7lPSoac|3&Zs~(G?t%3ij{z z(b4uWmZ^h0ABEUubrDs2-tyQvLx>KK?! zI_rm~c5oarT=hol5%Lkr{e#OfG^doqC42MKa{s^kQ8XSw%E-_6X z@^2#gs@#rIQIvLM>h8~b6o&*Vv9n7(v90n zEbl2OQgqcRofv1#;|J!ILo8MY2o_t?;!E%j&Cu4wEEd~uu%_@O5W;p45_i1Rc$=|9 zgi8n;w)W(UU0t%tF}`u1z(IfA;a)N84Yi?)Zcsn0FEoxZV>>bmp{3o~q$y~ukjEi{ zE)g5^7q&Ifzvl{fa(}-n%j|8m2Q-GYuKCqCpjQIPgUz0oN>%ykiNMLq3|n z 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 FxB!UT`=9^- literal 0 HcmV?d00001 diff --git a/docs/source/_static/novelwriter-dark.png b/docs/source/_static/novelwriter-dark.png deleted file mode 100644 index 0ad6329d367d819c0b0b27a8935be083849e2d70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33051 zcmeEthd-NN_;!?{cI-V;MXB0*R}`&HP#Kfm{{c=P%27|F@WdCq%Ok*j@QvrB_(1c0)arJ>S|Cu5a^yU@N+#OKJcBx zy~qH#Jn&R|1qXp}-~9K5!&R)#4*Ze9N5$Aj-^0Pj-`d+A5?+$8e>bPP`Wr{g|n=g|dKcS$ZDKs`y z5Jn>X>t=-49R~?4rmMDhe0@$Qw#UPxV5);X_VUv5qoNg9I&n%WeBM4h8$R$6Uja%W7bs1$(q_)eq)y&_=V&5)}p#EX%AKJ1VE7m z5oNdQT@AmF0$Ba=Qhjk{StpYd;qDQL&>#e)hmf6s{pHJ-9Unig8NlMz$g1N18!@Z* z|2|QHZDH@6Cmhn!LL?$0a&&y`O&G3SD8DxZ0iM+MOXvZZSFw##V!fyz4<$W?zC4T` zYVzlOKcWu0APIU?tP`L`fyc7vBmDJ5%Q~n7{pr(Ees1n){4k4+z8q^RHpjb?qYsTn z>tdIx4bH0Lw;q5TK+ULLT!ZQmlHSgWiV8kZQJ3qFVsBw_aq+0H?I_T`5oWUd_0@b@S{f+~wiF`ax&3`Wx@-FP5i_I2@4%H! zL05*&pjyNJrKLPv;Pbv&JqeIar6U`N0wy6g|HmgZ9T7^X=#5@C5POL9kF^d6g2Ulx zjPuvNzR03Nyx>1poh>cm&b1+r!s~GlZ6HBc@^wn_TOuI&-MnkH2IHGGM}_P8^(C@Iko3Jx@D?jVbh0c`)n3?v+C-tDG~AB&hMzCuW+&0S}x*Y1pkaB!R4B zTygS;?Pd??P|J3#i;9ZM?Uba8C2Evhn04|IiHlX)>#P2n1x}=~N9vea_lZAw|Kj&!(25i^cwfqUV>ryj z%&=#Dn=;Gs?Yhu^E&t8$yC9D#tqLd3$2 zOkE^g&ZCYAnk?(Wu@ znwe#U@|@<>=F}KgTavB~^k;A&E$>m_>MVK16)F5Yvj%RUCzfzWYgKA7@nMM7o-qR1ZAP!IoO4c>2Z~X`a zrLI&1^-8*s9_Pg)9336&eQNF>rpOtbB#oQwO*k}35f!y{cNY}R`M-i)$ZU4`B5WP= z;Ppa-Nh!(6G!o+C-!3mN4|nIAHbDzQB;0Ybp)*n!(}vYzv##Q*f)XDLt1A=ezjr;oV(V@0-6zR$g95IY}%iM$q3)>k*1+%ZwLgM~c2CP%i2G38$R$ z$D3H((~q0^S@^Ru&SN`dn32B7SQ!mggAu zbhLox%TaS8^62dB?Azw1!y6mF*)_%_24F|ifXd?@Q{DeO1X?QX)nLSXH_<+wF7!ZL zLiu!sA++Ws0DrA$sY@Y15!=3!O@~w%Ik|CM+&ljZjFPbEo|u`L85z9aJ~X*2TcE56 zTkVb$9?FAHI%2Bowk1_{8>XGU@KI6WoIA2pfN#$Qhp>>Q6*p?Sb0tXA<2YYl_%g|BxiP=Q_D z-EDk*Y0xI5%kKU?7&Gm+h#RYt>AzHd`&FdKUwva5Z-!X>qZ1PopLapfLvX7+DLDS? zoC2IF`WDv$djTVimm!PqAa%0PQG=l956i5VJ7-sxxPNzh?;BWt9-~4^4~{*0hyCNg z94G1ZONZP3_It7iLwOR1G?QQ|g>HN#yDwB1Bq+_Yu(C3Ed@MWxjIYLj9>8k3yGXBt z!TwBV7~kCFGO4A7;=RhW4hdnIklTmm9Bd^lReRmMp(ZM8KRVg~P7e;Pe4E1D-$%P_ zkZ)yWk60KOs;H(5$xJOG7FB$q*@+uw5ht=${+-ju$H$?4aA^q%?lD=<<1SQ+f77m( zE`NjCG`KJt_qRbatZ6UGj=f2^KRnc>4l#k+K#20-B#MiHPZ@{&e0;u-kB@)- zT3{D7v>r%J*0<=r`;GkRnQ;|w;}e;1RGmz@%L>HegDkeqX1Ce z_TI-Li14ZNGZ@QwjX5EL(pn3?89g-A-`?JS8@tIl+UQR)-5ZsIJ}>){M+uL}o_R8A z!z|p-IIB6xH5bkK!%^4oqfp%x*#{5lJbv3a6;0B z;WkO}vvYG7XvY1qY>|aMKR#aXgCcu8W02=N1oe;hXB2qqT^6ZROx$E|l5}9o&{b)T z?B1;FDW;11;b>Gg$`4xl5E9tB=-U?I1A<{PMt{#4@V;+_@xD(5t^>e=@&RXd%vRU+ z--6(Mp$hH7^Yvz|jwB)Ztimv?=201r-`_$y`ky4&f33mtHuQBwJwGv z3zg6B`JI%UbqvT4I>UYgXk^hZw8Gpp)$Y}E-BVA}ec&u3z+$mGohd#0J*JbM0pzov zg%{jg|0U976GvKUKod?*PNt>m!*`E%6Ga>+oT@Qb!Z=}4uJcRni~fDl&fVfbLz zlvO7^o*UiJ{PA&Pf40b6NWe$EstnMLJU&Gi7ZQv6_lQ${@I{&41ATq+c(ns-vA=_Z zg9yZvy&FlaeAkI>(q#sG-&&!hq@>>1#Dr)X?h`=1u>SV8`}DToQ%V92oU)Biaue1A zn&(|G%rGy(_|-2Ycs80|LG8~yUN4!yzG8yqy+6Yg{8(qsP;G+<`))T;NVI6Zcn{i zi|6US+{`833WSxwu@f=Cp4e>ua22?;=CQ$1gz|TIb-s*}{+Y+5p3YfoSp8oVpR0w5 z6>Df~_b@5i5C5{>UxLT;a3MbEfB=HwGi9c7lSS1&d zsa0-Yh@3Pt*NQmb(P$aNgWcwyY->HKLx`Szkiw!9|9(xCTgQ4IcjtI zOe8qn^~Vv4pP&DM$iG<%m3fnEWT?!y#IO5sIKi)hZSA#gHw1(-=BGjt8!H}s(E5iV z#&65a(Zz*NRp8%t2RZrqV=_}kXiB`0e#Ca$gMK|~m9N9YY9D{-FDQs62~p`%kK;k~ zt?5&W>1IhjWMPq#;VP)jmrZS#1WK zgr8qlW1Iy(3;tBvypFG|Lq!(s!;GK{>gmm#2_3e!wgd&?VOf>iA7c6Yl&mldLb@zX z5Fll$Q>%QA=Xiha&y6M@jy4Gv#Z3Vw)!NdMz{kgTw6#?!@sXALFWoH%1TwiLeZdQ6 zXjKxlspYrgj{g=%Bv;VIp0DNjpm!+li6e9)P-!)BVC;cN9L5;~`k}Ap;NK_{6pz8> zfauS**K;C)wa>onO<#^9CC9%#WCsmFf%6=OUjE}FJ<(2rom|yfFdEEK=%W~97YPGH z7U&{0%@0<22qW`@y_{mna7u_Y8n)XI8A(E4x3-)pVmWM6CrhzceLWlG4B$&vTn#2n zgI<+^t0-d^=L zn3iQZ2K0Bocma9!gily=?9YUJJOb&daLoyzW;&dNv~CB zSAd1=b z$J9gsroIXO>D9OX+$jU$jQ1uj+bBLT3elkhyAOe}7^UUr4Zjmw+N>amgzsaRhYsS@ zI(2xO_s;$3FwUXpFD)ryUDitntdqWx)Tzw#PO$2Odxt%wx+w-5engQczsz_cRW7Y1 zqM8D+590O`p`}2k=8NAtFRlX#PPEezP4gx_rpMqxr_FNwuD@cGiHq*h^U4NPoxkUB z_OEqIh3a;$%ys8nclPK9?YVfA9;q`WKbMz~xTpu}b%@A67g)$uT@SH{0>%B$mW;L& z$}lJfoMykdgxb_u?!x{mHkT2hVp4tqosbe9ELwICUf=fmLh5D()*XbhNhlp2O=Ss- zTu8^DB-57ngY6w?z>N^qfd!eGVdAMpkwX)~<%vcrFN&q=3A84?UrT(4zP|oB0Aud; znwR(m$V!MpOG9G3A4IP0%`RAzJTZ1jeMx^u4tWtNneu4#SM3vGAVlRA6neNlNWop! zKZHSRdBNbApM}btzNi?@G&|XZ1#Ydp;W!wT5e#Kmja$$9DgB%VC;v;3(e?ARt*nUD|;iXL?B z&ze+4UHF~c94!-Tfo9stmRvntOd4cG}n5r+!^V6K*2#e)^fmA4boC`dh**;5$6TtN*tZ zcq*y?(EpueXsb|p#0uBPc9dUMWfjU7JP$$o-gCCopi#u&(9r91V0dxFxPm@X4E1C~ zrzb^D7OY!MVTE~#veWjj;`8~kWwlFqh$_m<-#pC(QjE;k{j;ED;gbVWQ~B0&vU@gQ zbvH{*ITB^~`R;r%)fm79wBly5wvfu$&30#8jqK01H|N`X)fzGdfl0um@1Ah#QrtXb zRah|k00FrIpE!wLpBqVfZ}v;O7|txhiKSN>T>DPQB|=CHe>M-DJD3$zfXxr;a>w}_ zU+-wS044mA1^>WYly z&tAjC@>7(M5VvjveZ|+%2GeL~ogb#mB}5Hi(>=pcW|7_&H3XglK>lTVYUj>D#>V zHRUBG)ywX!%qr8o@fjj%7&BkYz3OMpjPxh-&(c4|BPQ53#U!!r73^wxYH9g=8-WG8 zgd{=cPf{HvSp(K^UW+nX>R|>r5+^nSq%-`xR>Q@<9C_=TFn+Fc`HVh$?#rp1D0<4_c|dF7AKHfRAT4KCvpZmbWwj(2mJ9uZViF`fj+ zR(>ASg26%Nl&>G?=0~{cE~mtABNr%7HWL!#FQ6D(D@2Urq5lCo=Mn$8mdOV>*kB-xzmCuq^I3ZKqkFd}Ux@ z2<$|>ITQSyKg=Y~{OmhYNHTd>lS=TLne^XSFV(5r*RR#NxVT)G-2J(@ zxuYZ}HQyn`5=1A2=~7b{z1v-P&N}k5vOfB+hO`#QhnTSLxEwD1U81mf$3wD8h4+~; z$og|$whYvO4oM0@y(&s@Z*|UgdDYZD?cUl7&@4L-A3jVzIy#~a3X)<0<-dRb-Vbf! zmYkF%N)*%QS1~isiS$iD0!eIy;xpZ09^78Ur*<}1`h}nCt_dEH6qGIn`(b3W#uU>bpj!v6a&9Jbsc>t_xQ0wAyLe?B{ z*|$Q9X;reFAn4iS=6CwPghN94LyLGbdvAG zXzNPYzgyK)VoNoxLizDqv(&iTUhXYR{n^>s=W}y&g>676j5(u&f&qGWe`I82Nmb!$ z`qcSi%geouFI*f<+KF@8Hr`l~pay|F{vBwO#9-{xY*-y~D`?%W`mpJ|44JcU=*Px6 z-(~hx!4g&|>LjN`Axq6_H__Ma9ZZ4eHaFMZ2h?UN0f)qvf1fqBw6q+qMUj^Whh)a9 z#j8z;G<$=j92}eOt~qc&zMDEg49DH~Z)j)`>TYN_D)nx^skt*g$xBX7=9XJoB|wc- zS66F4{V=L}ArotH!O_P0k(*P%U&fGtw+oU!$0dW_B zSdjp*{L#R`K>ooYlD&|vh%Dv5JHyJx_Ky)HaR1N@7E%XyCjw<%{t6Kuv)F)z{eNkaVX zTFvzUTLUnJ+^{oCP(U&Ksm6?tH9(Z!QE_YWgeTfguol|hF9Kp%B465f+vYNAiz@iX z?q#FC+3$aG6EVbeM1xsIH~~UnIf7pJi7XpI&_P6g1J@+nAW@}#r>?WxtQsRWM+Z!ZL0HVZDP{#LZiN*dmd|a^)SRkvMJsI5w3hh0hz+M1UjZX7w zHT-gqd|{A&HRhKA`Ulq)E-YkThm;ir8A*73MP+E$UzsVA_MRt!ciaJfJ)HKC1tQ&i z6ZfJ_n0zf6b>()QAM$SdL&lc&iR{1nT0@t1fo}o2w*kg!Uw}QBV2=CUezFXnGPb{p zLvK4VJ~sBmcJ9KLf~5l>91PCxcghDD2m7_!b9+tD!}sUHZs$JuZr1`Ha0jM%}CfYG?FGrx3yQO6|574i!3Q|XT zfY^xNQaTlPR}hS3{~I-&b&XVxj*a;{6wUX6EMnl+#9gS6kp~cjh6sT;Qa$QjD_l^*#GK zm=%+V74B4h1gu+rszInk>}AwomH^KbWg5SO#puZ!V#RmaO?SN7_rn2QAbWm(p1||a zc6kmr05{wNr1N8_>MkC!VpAJ(ttqkG9HUI{;*4IP%7b^pE(JPUK>*a*0q!o_0B9BS z-%eGcxG@EJ;CasM_f&10PqeQ}`R&qA6e`r`YpDX41Fbws4kIR}r@uSSt+LtMQnLrD zq#Y)prS~x%c%L)XXKE!=0N$h-XJcsfgtd6lV^x3mlLA#HKxM4~#NP7RnJ)zMFO(iX z6ZIS=ZJkur-_GNZ((#IxRm$7Wk-&F1407rT@Q6)_E$->X7yZm%F42xfx4D8O|1f|5 z{*?nTe~;kwpFHKJKY|Jee)w@p(uQH3X$KFH)kNaw;;pM^!Sze4NbIsMsH3f|vj6)v z;SE6I->|#=`8SELn!f-VtcU2g`X{?@VT`=u`n$?Q}b2FIbuT&=2{A5WgtlO)Z@Iy*xtp}LY160Pf+ z-8tB;GfAs;qOHfBFDc&iB1#R>Zxcn4JkA3A{Mj=V+LoCC=GB;einNF%KG0LE(6;mU zLT)Z@GL(N@-Q0cy`~>c6OQDn3u-n<{+mMPd#PA~MhTYFCp*G0NMezPpEr-zWCfDn2 zUp{uIF_$plJzQ|AMErQDnqr5Y$RE9y(kFMRMokEvT(i}>Db^pLw(}+Z_u10y6L7Bb zmaHE9uNQzH-1Qt5*6>fx>~wecKKC&ZH~F$2mbI~1h4qg2W%|Yt&P`9-O(*kJeg(>) z%Aor_B9y52zwk>v$L5}{`<_UAs5R84(7FWb+ml%M<%JJho*BGa#Z=62%f=0^3mrWF4h75CqEjF zoY6T?1O!w;8VU@5b?YA!Mizpm7|U&7U2t35b(jB$pPikZKW?qdw8awc`+=XsQTV8! zxwiwMbM){#AX;L82c9AxJCJxpqng+IVu-a-2D}&d{<48An&TW47}rrj9YE5z1BN>X z6pVY}TTAWmRJwSI`|L5#ev(fyQQ;3JENKB%(;z3nf(C3cbbLatgvJmsvM%}V*M|?+ zLNkcq9AaD#`UOqsS;TF;l}#V?kFLyftxqh>pvQr&7KZzF-$zyF6^$m zibf4CE-nrL(|lHI_!5Y5wtTtZS&~J6AoaLmoX?b@r$dsG>&_CYh%LGr^ns7r<9K-2M9X z>%E|0x!4fpb#NgX_)VZhLYaxkWltNF1rih&7S`LHW3WPKeayY9amYG+%}vtZI@rq@ ze&p9Fr&m8mj5gty_xkmvq@@@DhctMfE2iMnY`mze_p=KZCN7qXfM8o*Uhda(>V68V z(c__>H8@)-CW@DRX^;rb5Y)A)N#5b(Gs1iU_zM9X&&47vdD0~liFNbFL>JUx=51w# z@ao?4tbDQ7CGS!D3ssnkLQ&Yo!8twd-5vM%EnuMUPGTmiKJP6OYX#XYUk-XGHzWG|C`{{~fPneTl{RcCt; zSp>2YR!?tuR;zFtVv?(Z&K?5KArLkmhnckBQ<|jT z%f(HIq`&y|xXu)JRM2R8v2-2`cL)x)M<=0cCE zLfo3>nQ`7O`7OH9Ihv-5(nEDmi9sXEhL_JkmMlX@8Hgl_+;Ir2ygq9t8|$VO*HsB8 zAv;tx-f2Nnf;HA!C}mysw7kk$r^+A{IN3-QUz6`G@!f~rdgN#(d1tq z9vXk-+u$6#kx{1A&Knj{;q-z$W_p(;NHY4n@$;`=@4Hj2KN;-_k|>Pz(h=~l)pRXO zNdVLyg1V7i?IW7T0wrt-R22lCJM7q{+K_=U!|Mej$TzA(4)inU1Y0-yUy6(f6Qb3@>?VT*fs(&&}s@h&O zt)0E7tFKSR3=I-#zxhSAURGAN2FQa_-w#^(yiGqim3w5Qisp`g+uGU^xRCRwipq;Z z5P9w+HZDsMN%Xeq?ybSPBld64EnD$v5iJex1d4J*bcYblQ8o=-?d{oYI}D0(?-d%G zk}h>Jmes`~;k!^0&^v74lrvhR-E0#`RJ%LHfLo=f2=$MRj6>CqaIgmL;nBLdn&20F zYFw4E@AE{quh(~fsU2U0yjI!af4LDv9}bHrMaIT`*FXXQE>7crZIrxouZ56WECAehDj7*m{8A z(YJaAQ&mB4f~s;rND&fW?^PvMZ`1vR^xB9pxA?D)d#B`oW_33O)K6wZS7pHauL5=VL#fq2 zH5zqQ*t)Ps=N_NLrasdLJO*%nzL*QR`RirBxeAc&jO7sBt`gflIF5!S_IVV$8X)`q zC$f>!JflbplhEi=qWH8*p+c5rhtcKYl|&+rZ%`bsV98mRsW_kBWOb0?c%wYr?dInQ zlmwqb%78kSX?A$wPtz@F(LbxKM#wP+8u4Kd3NgRt&2Vz!w;XM*#>=GtZ+QFp#CjB zAG=aAFO1U661y6yyRw%flu=*0Qbs1Eq6FfI;`QcI zJ$`R($VKpjF3;~6@wQpu@FnS8R-RQBpX`asR#mZUX5&j$y^8-xZ>0l7iUfJp<7Sq) zpRe}Z0;v<#2q8oUk4TiOlVgot{0-|)pK84aYV8Bi-vy3(4}$w22gfY!HUTy`3fkN- z1jkWi@13-Jb)Es}#z2DK7jN`jkH_Bz;20F;(N95aU?BE!R^Fdm!zurzwT7gO%qX{Z z!6$kmdam|m`V)2AG4UY1dn^ujol2t#!+EfLDr83TV+vh8^8T%x%ahN2efRz&5{fHK zFDj>Kn(J$}Rrac7Cr@TMoB^Cn0;Vykoy`@rQiDsNoU^4sR(Wt{JRGE+`pfVF0?*Ac z`>V!&qO|f-6L#cx@BllJNq!2>iSOBNwX+8FKIBC_kKY)F5Ys;TB;hgP*i9V$#KRsY z939)u-*?7v*4|n9ryLV1p(_cpK+0&~R8KIrT+J7_-pcx{A0O|;D_B0|yN`h!^9R5F z2;zm{oUP0x$r6A7@hD=_Yk5R;OZgvR9(=?VhDk|El6)e-x4Bt2@a?$2DvAN}Y>+-u z2nt==^UJ>YQDtOgRHy;{M-v>z;m)wB&9o`NU^`2*#Y^F&{p!lWYu`{egy`Jit+sz{ z2vywAT$SkDaju0u@$!m_P9tN78UXvh`}60|#b)505y(Q==@Bw$Ny^`aGM2xg1*-c! zVZAMQe@UtucNbfZ^7ukwjVTfcPh|0$z5C{mZs~_C>oGf4qZhO+67MOZO}dDC{mD#* z$e=z?MK{+6v#zt-JKQ=4q|RbFK3#v6*(H^LWTOHnvtLIKpI1IOP-cDTB`%`al_NC}IbVKs~XH_eWXHfMx9<88tTQbG}$~zD<+v%GK zp?_3eWv5yyjgln)8khIFNpR1nWhX72lceY<$^eQP;Dz%;8?Sq*aBRnhaQ;U$-}~Lz zg5v|26eKkDWu&D3cy4F>@>bfseJH##eqvhIda*vm^EQ!?$$>D9w@9dn6a1F&WnU-d@4tMf%L3RC79BOq?C{Ij58th8WDS<1pXw zWYnV7=qnTc6DC}m2%+ac^PE>2Q518v5%)o3H487td`h>4Ve>01v$`%G zl}i8gO0nCVU=BTdJe=YW@`^*Ls#Yv2gH6&n=#y!c0w0oYx)+|=g*G^22|#Hy7`X(BOF0@zbQU6)87BA z00IGSjblN=g=}~&qWwM7j8a3^WNNCV26G+8S(`4+k{(G00?y7;$6_HF^>2+cY^6r^ zxJ~q!@(2CSR%8E~Czi8cT{E7_#sDI?fM?*{3#!-y39nrn2cud;`-@wK#^uxpGC9To z>*zhRwIu@(ZTS3rkXEU*ui;L);n4=`v1naDYMU~xy1|Vq7^Oc^v>M|TNrXS;-b_iz z0BpOc4neEneNId;6g{CBp^{!(TYG-AK6C=OeB7ce48JcrxUMc31rJC`*w)@|c4cxS zld@@0L|E#v92}DlJq=fKwN-tWEcKe-!}J@ep0XlgjJ-1PJiEc~^&l^b7^8T+C**<= z+aIniv~BchO0qD?$|M-FqX^juvQutk%etc<5NnkDpu&SpU_<=7}G^4a&rD9NpThcs0A~ zmtO8M-nd&LKDk=d`&aNx3VTPMf=rdcn$gF)8-*Z2x?yjPU{WUai{pR(Xy22J;*gbx zcvrw~iEK5f6w|VERa05_nB{^*jbPn5#cw&k?!bJVr-G2|e?j2|S-kK%2#br`Xr(w5 zZ6ECXLoTFcMNAj);S$H{ybvFIsY`z+B;u$TR?X9U4%ZX5)!5H#vTA`e&d>pVhYPA{ zY-ixz!~XABgH;NKlM8bf{yca*ntZvi;Cv?e+iJ63)i8in3%S&f6H(GqR#FrBGOYNY z6W~u{)JAlvktSl8g6A{6>V2_WwxBOn(vL<+bcq%75qCa1xWtM%uYw)vW5aNYtPttp zJV5!b{SX;p2g$mG1As5sb1y|yIh~W=#m&EFTGDf``sLS}kVp6dWbu`*SqcQmli0u+ zL5Z?CN13hPKTRB3KzV+h_Y^;T_#gyOLG|ZnjtQK&n-~8%hUj_;rkyexd2&V?ce!xK z1W(#v`Oat)Tqa{(iZ`Mp{VZj{?UL=wz6x2d{BPw5tdfOV-q};e4_{qLb8)i;eYviF zxB3E#+q0OxMdd}^m7DDrrpFGdq52vx73J)n1$RuM!ahwAv!?IPh03q)`GsX6kT}&3 zxsP#CR}55mv-Gxr#NC!3ZRxa%ds(612PrV|Tku$??4(!ZS*_6b(=&;1 z9p9AUVu`y1)JWaYxo&$M=@SsBi4lyJDb5_%T^?t>NSI}xo^LJ^d(R_ES4mpn?7iRX7K6OP1tLE7aH`bb$0MedSXirN`rF|Mvn6k zvO2Xadf*FwOf-f%Eap$}Dci2Nz)k>$C^?xFFy?ceprlxXBKS73%aU(v>+Am|0Zt2g zv%%gUPp+RQtP|i+tCUVTi7u4sy;l9Uo=~~nTYZVx8ts$~%{1tI_FbG`~>s$tZ z1^jDIgX*sq_ym}n$s8)g7H%kT+fM&g|k~pl) z^RC;?HmLD;*CEF=uPROw1XEU3p$r$^KfUv0xZ5Ia&dsG4Ae)7l5y-Z$c!GyZ4E__~-+J(}985h)0t ztjdQI_JUk-LS5?H6UMe<9f%*@tUnc>qblY3sRBbQujX2oCvej~89zBs!bTC($me*W z8Sy?7tV`aB`X%k$!G{J3akcWAv`UF>{lxKGU*~iH98*ML=Z@6ZF2}A5+g^}pitRGq z$I~C;JH4KejXb6|D6a>=87d&Fh1hCYBi`$RtW>(qlNrQNwu9~e<3)RvFxLF=oahI@0i6tH zRl6s9KFZrpRNJ5zq04TkH_o+D&A%kFsQ{?^9gr-1>O@ud6bIA?ty7>tzd}ETBxj;) zWl3btfTFf66+cO2qE#MHX0GfvK&Yc z#?oE)Jw8Fx*08;5+?ep^)Uh7wzy7*eQ@mm5a&yc>IgBhhAj?tW26nlKmW;qfL5c5K z05IIlgTH*`SNo8m>uPh;?u!Xito4WN+-J=!c%K2M1-WnIopA*!GlC2(M)(5|k?%Qo zhT$1gS(zimG3~JpE&Py6K>Qqt5xq5tn~9p89J5SV|7!vM^GB1?UdN4U+RtiCy2|kL zrxz%`Vj()wQ9}dYMv{@r2Q9+$l%y^KkSl3<8~$f=gJ5Dzo7JkTYI5i zKN4NO2=>&!c0G8an`pp@6zQ1V};E({#@{~T8OrE`AuY$wwea4h{`bz=#7Ll>S)Q1fti z^HQR8%!8w7cuBJ$5LCrE@G%1&T|YD16c$eu;D1mtzR))cDR>GyzT4s+cLr z&i=aJc_1$Vc%I(v2-^WrB<&D&1|P9w8Rn1DJDFm*!VUQ*7on$%sH-a_fj&UT zC5j|?X8TPmQ;w-uAOGLme1o*!?X4_Mw=RQJ79rhhwIRL^uYxR=iW5;a%SZi*zbT0GW+D1QtB*gZy) z?%@FdA~|kV;7DTfV2jTT=&&vEPKm+7Dw+vMKb~~^zlGd<5;Iz59?6q~Oh-AmP4h7~hLi z`yvA|IO-LBEQ@jOD-1IPaAxNHXDInkq)|_24qpnnup(ZrcOlQjZUY8eW(J4%)F8Up zpTSf5o%^*sdTv6~hE`*YG1fFnI@TCoFh&@8R=T{RgBf>dY8Mj{YDnY!`uusbd%QEX z%6rL_e{X}XnaPsS=l7uooST$P`hb0@v8bqw-y4Ef;_=cCH z|70u9ExS?^>PtCK63!UTTRqE@GMC=a02qy069#^XzbFf;GC+M0m)MC33IQ~7541^b z@{>_ssAv*hV$E=(-+pS>$Zh*d`_@2CzVFygXMYBImi2?hu{TiD0j763{5)MI+q*i2 zK#EhJ8)@Ee!h@pyg03pM-?Iw}x;?VwZB;CaP%7ZZ#+Zm!eWJ)Un*bew>27bYJpR^2 zGR(G^<)e|Y>H*PPF$lm|Sv&l>D9c2+fT&m`AWtLj7|d zN!=6S5emZo1X_lyuOtt~!d zd$Q*dmb$7v%Pp;*e@9g*4H?O3*Y4#C_Z8bT;cR3aI&v-xs-^z}|~x zhzIA70L^zpT|nOirHF@Ba~g3m1qKJ~rrL`S2F`ulUWaPT!sqYb-LZdApQ>N z2=s@GXwLZPz+i>htq3Q`ts?B!+rEGj^T$eKh<;{ae!d;Obr?eocpA27X=zvR*Qlt> z9{O?I7;%2HP9uA@n1bZ7FgNddWw9PMkaYkOQ5YYq)|6bnU-X^p%r+}-<(gT7QTEMw zQ(?Xy#!IH#DoY{S!HAFJyFeIW=d3ONEmxe+em* z@UryHhmVeq{&e@U>>6(S_G6({IL!rO1mQ4%ii!@pO8ogKMh{Ph)u(NvjqQ@NgD2W+ zQK%~^U7(Fur$E|wWL+L=a8Xu-8{W9MSNO{RVx?y!w7N%w!gguyx;vQ@YY72T$^B*QY*JP^MB}I>e(@m#moC^ zXETJj0Ma5Qg-s#!J95f7>2hFrVG<;$X|z4uzCfC+?*gk=+Q`xGDo43c_eq1@D5m{% zsrxrszniy`>qi%^0z>6QuRauZ@HleV)Cp=eM_=(F!pV+dOz{ z!Ce-`s8p(Hf#mCue7Gf>(7!mIgjk#{dSng883wrdEnvPtpL|`sR#Mbk=yazJQ&Y#9 z8E5MtN!0M$ywP$x+83e|7$+mQS~=Vzk%{{VCyB;$XDSXjDu-CGd3GNUx%bnvZFDSZ zd3hNimEHv}Tb4Xk0d1WHS=Lo(hY_3w)e{`vC-`G$gb^TtB+h1KAj;b?&f^0|YQ`68 z(r@-TN`bD2Z=$!FpjF|0rIqV?VW>bGMK_skxh$j2w7h|G<&1euK%org3GG)?IJ{&+ zPcwak?pQXAYOvJMYb7i{ew|OWobWQgt$xaV?(u5pK#1?P*KH8BN=T)i{+M~XkTT6Y zP0=JOL|gO__Wm_B4NI@J`vmS=3$D_EIiFfvC1*XSDG<$7d+JZE?AtDU#1pmPlZ7Whiq8Ep@w*m??1NUPKc`-`9O z^CxdE4|k{L=Y+oXDlF-FTL8@ z-F+t-o(i5`?f(hwejI(s2yuP@UkqM-UN8h2D|`8Yb=bxq95~83^n$Sd5%P!31>KY32vFMJN=>ZD@Rmcrv1SZYSjE>gwv!qW*$e zBn;GhVKqiN%-_?7I^)QrOfzJ67J(;bmvE|+OyqYyQob!^XLoz_Ri>BZJ2kO<|=xOOQ88CZqRapD@S5hHkL)BFgFRR*Dnv!N+( zChk1n=u*Qvm*rC*Y$S3=A}6lU-xqO4%bZWDhk;F-c4a z-}C<5kNb!F5BU0x8O(Lgb)9ow&+VmZ&tOuE6;7_3~C0Z2~t%Jn#vwNDU~nel9lpSa1wyF!*!b z-AGG-{s~}5%u!v<%`Ov#3cKp^mU6eiQhZ_rDQ;gEx-9I!s6e@JE~PUu^fg!Wy#L07 zy5ua(1--3lb5eIbusv&E7q_;yhPjLx8~)-E#-?0*GB#E$h+7ptTquA3+u~)m9(Qgt zDrmO7-j^!%2SbCGVU}&RZUwv((lT@eyl{kUb5yTcWiLkhyO3Rou(ldyslDUURV54gbC!TjV%2QAb{`twX zIr7tJALp(rT%=9!-FZ`AZ18a2*5~6KjBheF80a$Smyeiql*zmm3|G|cI)iH;oz)dM zYjwtiT!(i5qWqxPi=mnzdzp!zm@R7g4|tS6J@t$}m!4l-&=Bj+1<#+maL@bwqA zSXFgozxz($TRVS-YDj-c0ShuoY;+MYrP#d(tVn5#jjJX)m!AD8nT3kT2yVz5+gGymMx;n|jVJl4*^WlDIQAKAy(yPZa|c zXbC_oD&$j3FL`#C!n1RJwWot>>>oHDaupcn<1d=~nH(;+7lCP>!sDsj-R~u)3|St?8~^%tFL+c6Jxc`6k3d+jfj4zTjDh0N)uC=8DRNcP{Bgn)MVl$X^Mn zJiQT9-!t&vkQ2V9{d@iY^y&_1ZbKWV-&W;}e8dqIMQqJLX=f_^%;lO0e0RB?phy6+D;8Q*p#hi!aAmEYkrUX$m^aRW9Y zM43SCKFcY@#qO|I*W++skP*TJ;XyT#7|T zGAb&^kF-o97-QxF>Mm&ft$+NDM=Vn@!zfe0T615X<#+eHW`wrM`?ydBC(pztu~9tO z?rndct=|0C7M~c4yRKh%@YMV^HNHl;=dL7Y1g3K8KsH6sM1ejo|1>Yrq6T4n*@eFE zxk2VElp00{O~kMSWI9Q5nt*BvdetiI`^Rgc#Fq|x&HQn_={sfnE*T3q8^F$|BgPdo zBIz+!h$qaMTjyBT+098U8YZEpq?tj~huTZLT5unEPoZSN-!YWDE1!ne&-(s7d_2-E z^+2ABz#u5{SXgtZv1JP2jCv?$gp^t3+(co%Jo7H}=^Phx={ckqT5#`y@*q1wJ%#20^%o_|n1H1EMBWRx#VoAIGW?{#?nMRh&o*Xi?xZ@Uyik^#nYUH6Ix2GmcEH)Z z10k(1SJD-F6fAS1$GLWY4CH|$hBd0!tB|G^L6#YCt-5AhnDk}5eN1pT=B0?CyGL?$ z^>xa2NUxIe{wEhXiE~KgQ{_r1G7GEm^;k3#71<1lW^q}bLhvE~bh(|xAxPFP2K3Ud zJp`8vsuzvN@{4$nw7yryNsPSi~!t(&rRv+K*Ox&c9&rq^CN%K^0 zb?xisW_NfRHv6h8^z1)GmT=`JKwjmnaiROqLf&$7g)v2_BE~ILcBNd~<@h1AR z)?}w%P#*E!RtR#6NP)pjYtNpD+#QFLs1hVo%7iuREmuD^Hr!7R4nb+v&t6vY+thp@ zjV=5|u=@G2ZE%T79|@!cgW|hVsoLvUMD$zsD1CLGCLLhuY#QLmqPBIEM1l!Dt#HHSKyUgM3(apPSK<^kel= zqk@L{FTHp*hVm5XBm>940u8F1e}szDhhz?k5wyc@L9}1Wftp~7*z(1h`ScXHwmDn> z#q%2O=4MIjT}2xx%Ej;NrtD~7dGoHT?Y7Qm-Eu-q7RZpA4FqnLlX|H4+v*E3K>>Nmbb*H_4HBd_NxlAC2AG*7iTudis?@?Q#L!5{!deisPIs zwJVo~X^tBmU!hXScAad5@=tgK1SmZd8##O-AE3Oiu}bg4V+s6vnIC@sWGy9PFK@#l^>9T z*micD0X9B9lv88Klx=J2o^#$e1)lS>DTrPbqSvv1rogFU%f2R3`ePCk1RsAXstOs& zKY!7QjsMT*W3Cvqr27Ttzs+lXmoBJ~~T2e7~kpKH#oX%B5$#t1JvV zTUf39=7vt&x||n&em`hwq^MC znfathuU8kG$HE{px_dURV|cv7y1c_xoQkcKoSbDzMY;TR;pYO1q2?0ea~CE1UKUe@ zhVYbhw~#)B^{N%CAopg=N#iEw;if2(7D?-4<(Z6Y+l^gc7Zo~aa9*bNN>72vZvaIv z|HLkbif^>|%-EPgJFqmh;ZL{*J_D}~8+FP%G`sbYk#vv@@{75Q2j8CIpOM_Jyi_a6 z-6hDtJ**d(Z^(%n99Ov}e-h`mUDnQONP|>g(j#5)xBFpaVInRQM-6{$m}!Sh8acY3 z)IRhQpR(2_0L!KEbq)(l4>`kj{%3cVWD+M8)Md<85a!pzQ$vu0CQ}OsA;q``Lq*d@ zzaJbdD84A0b~F2VauHRe%d#nmbvvQm$2eA#9Awh9{;RRvJv_+Cj|`9wiE%L{{0~O$ z!iYEbPb|2~Sp)p-=zh6{d|%Y>yC6#%w%}fYURd!%ze;0QsInNyyMwwdQ^A0LY$`r| zL_NUHKB#3Pu1CPj$`+zq?X7c8fF3Wrg{0L)?Q&}~q=`jjdR+uXi1Q+Bbx$U3>T76W2ZF83-RX2B7KXkie*@lQg#HSjqcM7dbdi zr@+?bSGCzDkP?#81e+uZC#fd>5dZq(yjuk>V(1aMEt2RgS*10Yt1Bslw z<=~p7$Phk%9&FD1Je}b0(u#?yw?|3&j8y0LdL;ESU+TgT90=1!GZgB)2I|7&DT`6& z1kK`&(4i&+fcEYGD2!AUdw3JL)=R-6b^)cr?w5BDt3`kr3gHUP%>+j@7~a{S4m>0r zvYI6vOzI39j}{=xR3(v58YOoz;MnmLU$Y^GSx#CX3wIo6NuWGB@}hl=rAIXm$k(FP zi&|_>Hi<9q1BhlKw>`-57INYntxdLzdj0_`^RC(0zgN!7wGw6$m#^BEA)h~Ewgbgl zy9v=&5mjRo55?p0g_Pxqi3z1Z#iO}{^qCn+Q4Og3zQz}Cc9f|&?{`}*hm6e~|n zLYug1EP_W2MR|bK@n62$fI^`mK0FIeXpnuvHK|-#LuUmia1|r_O{X2$7l61 zheKb!l*CBBT-bH$7!m5{RbI?>jl%HlIN_w_MC}jxKWrKcQ!gc9G#xo4av#0!1MZPj z&7s3Fm{vm_V%MyZV2s$MmwGDAOf|+`&SD&P?Kkr~O*d_zcjDtqpgc}u|6=!=KWq6& z#3)lAb<9Uwoa;8!(&`o&X&=`8ysoC^+rjRJn%~9V$amYfP|>`7YASEr9U8{KovzPd z&DK3K(o{qL=ruELMS}VgOM)%PgzK%q*Q*UZ5ZfiMUmJ)doPpCdbD>O$>U$N}08t~^ z-MG^iT+uV_ANQ<%e&+2C*_I>@n-jgtHCGUsA>x76M~S~kOft*-VpGYeHdwtG+zG}( zJVn+YWZfLe`S+PlJcjm-ok!B*OYbnFauO*Hin`A$JOlNIn#VM3T$Q5CCQGE~h zyO<%z?f);_i3Dh3hZrm_kyg@_3mGpdSlc9~7WMDJ-j}Dw@saaRt&g!0d0y938-Gmg zJFDCzU1!rWMtpE_mo~akI=2(BWeyS(^q!t*GH6vXDl(dim+R4K5Xx5@^lNHcP>~lN zI<71-S?4UxR4XE5zu&%aPQ8obmx}uqZAqlbAZQOnKx3eolE}9>ar6|d@tZts{P^8A zIPChd@EiOD`3o1dCdJ$9k>$Xe)+w=QDW^_rSYTqJWYg5y`BG|hl(p#PRccuGX~{DM z_+gE6)W}SW`{miGXzBa-ww8o6NNes1O%jCt-1MXf!2IvK_we6Ap|U4-mBAD>0IM0t z4ZFVXPks07pH?|tWTd^rywm97e6&f_3=vLC{%@p{KmR9=~}cq%Z*7>nN@6W(I8;&#Cv=;d9#;)bomq>kp4l z(#=tOKAg?Tq~HwzfeeSz(u;&FF~wTKX2^B{nhbd_;{R(dj%5mY z4CB}ieY4^3h(e+6`fNq{J1$A^->hSgenE@@+FZ&ACoF2|_c;s55P@tRJwK%Bk zrN10c(@raPBj0c}Cw1(XTnrS{&-d&b(rFY~D>MxX00r!BYx_hF?j-aqe7uwR)h%JF@_4S< zqyL8kr9<+g+_P`)DkYvIrkvbdnwLF$d>c+AR@}Xq7_VbHWYf(!1K(R>isN|&6ZG%` z-7GFh4I%vdEw?&2$*#b+T0jpk8p`|O$I^;s z*e(sUR?0-9%s?I$Y>U&0El88dmC~+_oQ2qx33q72(OClBcmUxNi>jf)Knd{u-=YQoy@Yyl+NYCKH&NIP;jaF$9n0bJw8pUfB)hJ4YJ)1S|1Gs z7-h;zwSpqUROhD9{U%VFu9y)|12U5@lhP5GcVikdh%zZW$~q@1Bt*`x{k|LS!xmUT z_rM0gKYEigG(Bxh)uufBV4weOz(3#V_`Y=7qC`(&ex@SK+@C*ziNj#@33R6ygUp@n zo-nT3+B4LDtQb$h-r?V5Y|tC1d2~=f2);0GpKM;AI8@M^YDOee(#vE^%F=2xus=ud z$0|8au@AjzbUB6fc?c>0)4n^|A$6St2kZ!;WXBY%KkrHaWs-@Zy&I9w$I{>Fw~>m~A)HuAp#w!tfzJ$mgMg2Bl5b`((ZjGaOiKL7cyV!NAJz5a(+ zSP0Rt^XR|j13Srh4bB1i_gY{OzsZrce5ziBw|oq(s3mNHuf`%V)C;gDl2OEH)z!R6 z*R7*~qL{iHPoyhSkoe%vEuftff~)v!KPt4d-5qL@PQuY#xN(_uJ0}#N>_NF^ukqKV zSRW$pv1<{G=D);Y0Y&D|%%=DT&xJzmSr%bV6=u(~YQpDRX7)cXhB4K^CKcZWNK}X9 zDaj%6n_2~6V%4hfn{3?K6Eo@T4;C_t^sqCu7eMs7~dC{wx zHt5|t3~_kj+nwdJ6KbbjS#IqtpXCNrNLuan8f|`ymaGAeC7>b#;AtWp1bSv;PMaB5 zZ|7H;WRyaE&Ny9#)`pX3^@57J>WlCeXb&$hFTiiHAk>Oo?o}`5Jx>c8Oe;F`>kk0H zPd^q?-2BinU<@BI+1uFMNB${eyHXW)Q#>geaG0&}Q1?uM{CSXHcEhpfAI@loL?R6X zSmGrl%Ksz!=I&q$;@C7madsS4ihnT>AhWUfS|MP(@T{)c#8|HplKkw82^^m(A0@%{ zE=YRww}CW!MkzryXq+b=YFfK#T(wzl8?vtPcgFd7w6Qg+o;|~v!FiTS>3L`+9)Dt< z!~9{Z0zk7@D@XQ@j>p00T>(y-Q-K({(wX+79-!!wpohw+U5qg$(#)=?^i`SN&%KdI z*!Mu3L@)-hM{>6ET_mitB`=>xmIMB+0|ejJ5Ki64`c-p}l04tOar~p{|DbQ!`O4k9 zRfPRP$<=>m#32QWCH=iadZFJ{Sf=5lo&ev3{16e`3A^OC;LhuHexDb?Xe@$*D+Rxa zf%LGdYaso=Pr~T-7YdR&bXrJm37XOO;EHz!Mqq8SEUY_g;6UGd4aaI2#s>@PqMXW4 z>^d4u{i)}2Nds0wkz2=iI8kn>7lL>9=D{sm%)qalK*LT?!}%)nO9%pgq52xzxr@Ws zCX$0Y?E#=cqaYbBBQ`o31)xR&+^(lr`+)KZ4N}_EX1nMnhe4Jsi{WOs1PG{nVlgjl zlW9S$%s&tkQV6DnDcFae0^sqV`SXqF{@ky#7AS%_3ia@vmbahZm1o7pr%hly$;+QU z{e1z(J`WoehjnNtOZ&hM)XqG*bw=GIJiL>cPy4vg(U-*{Rm#Ivo?G>dQ%%Sq=WYt3 zVggBp=f%GWE4GB^BTZF#2HiBX9`GfQfR!R_XH5&lZS-OhxQ|Ee^&fr_&SHScut`2` zbPnH9{NL>y&4&p+G39RPF^R4BA3xs337@#Z-Z);Crx4w&Rqy1urM%MutiAR8;$nnP z{cN~P{p?YYdk+q;2KhW0S((p~afOG8z6ce+ZK7npViYjd(l-!fDIuf8|6V<_aFaX$YQEYlsBo z6GT!AnGC=fdAkta8ba@+=OVEqYJh$DsL;IG$)~Zoc^y0tEQ|U*#VxBamm7P?s7rmV z%RBOX=yy9uR9IQlvttGvYrsAFGMF`Q|6bZ8Ku`XoSgycve$@V(kd0XUb83J6 z#AUv^nV_w`u&0GwqtI%kYXvSMFj;Spl)1}noyPs__J9NFbvLd3BVLi zJ;d_hxn_TO^fKqJKFk`6>;TatTYzt1;E>?0O8!GNh-Lec3yBza_f_L;dxI>#1WVQ+ zm}k_SAJ$-Kr$n9qZv@jF@MgJcnn+@DgzVBpr+hhopLWmZ%dgQ7wgc?zZY zv50NlJ6ysY>SA$o7M3+R|5pp3JfT!IdS1*yJE%B81|PQih7ROtcdnL?fkXY^$B*a1 ztP7<%z$Pne2rM!2(CL@waJnD7hG%9j-mvco9CD71a#Bk$LZRFM8*<%t_HZ$z-z%?J z5BDt>qT3&Uf?>vyK%2lkMuYaTV1M^%@s5jAdc-txGX3SI&-y&_kFR@hc!V~-J*k}Q zjY*9mh!#r%T)YBgRg!<}c!EJaTVUAl-At4Oto_mRT}ddh*XqHk4XUVl#%M5LksewE z5IwgG@8&6XUs(Q`J)6nfXh^rp%9pCf*D&xKXO@HYo^P6|m3wMrBncG3SOlr%-L_o$ znf#Z+Np%>U9c}V&D`;anR-IphxO_VIC^o?=Jr#XgqMwB3-=}y*4vlXW_vQ!o z=%3|2u2uL`w+Br9B5<`h1h6h>WD19+V`K8^2594;=0vo(x+t(QhkDA_X;Q_BjTTZ= zJ(rfSeFE|+)|>H@fpS#%oYsbeuWek4Bn?`rG8qK#-;F*TplzN^oznF?;LM}5jn{9hnxA9rX9ge?Z!H4>M{@zIa4 zhdP1VdQKoWhgE6f(c_81rc|n}O@(~dArog`2pes$#13lhwQo>0l5C+ixry9!6 zkNzi2@0xPJCsi*;R~(yP^k++8$I3QbxW0k)YUN!YpZ6SErZ(t=0Pa&DcMFXDwF__PolxVuU-zv8EDbd{)A2fVrVwJ_`Pvoit1Sq+Qie)_HE7LhX-me z|Jf<8?a@N`18|TBB1b=WOH4vByrt_vEBm|a=0G_J+@aXufKNti(;?+!) zRd=y-MjfK!ses-Oz{{U;{}XKzU>3Nu;d|1FC+THI1e%Ax7`vupDu}G+P6OxffjE$= zGedCbTLAAM?DyljUIiZ(QopDofzQr8=w_Y6+FtK4@W~GipuyYZNG`2OKU;L~GpuT8 z`e0Lk-_x^sVlfBKQqFx2ULN#ukv z!G`?yWIIor=HAHsc{uOw+fUvWsD1nWcc0zNuS`0bcTzBNm~3nG=~Fz( zhNDFZC2kIqwx{3XmVASLe@h;wJm>&8?xjWf3MZ^;%3=48(~WLjXKa&`0ZhX`p)~0M zc7pZiOPwGsQ+BBVCU}O>Q-OmYGxO1pJ9*yYiL}d;N$XbtvwnpwBM0OqKr0F{wSX50 zCynLj(w$k!i2Dxm&#$^!hq*^xg&l9+8oz}s_9ZOz^1)*hy9h1Z7oJ+c+v82P&^}o0)mT?V-%`aQ^_skS%KhjROrt*Zw`uuslEqBANt;9MfGADnfojl8* zhzmh9(|k^BcO}~4MV=~8^l$sNcXVt9aKIZNF&o9XAVg>ax~rt!dzW;3An$#}hn|n_ zCpIp>KON=Xq!5{{6L1f{L${>|bn>lJ36wz9-#y`XQxrBmDj;vh$BhW~D23LA<2cW< z1+Q+mu&~4Eq?$-km=Diax@Uj({0s>TYG86!(3=Kl2O6$5sm8Uv^FH$^wmocbQw7tY zQ5BxFCf~C`epedWp#AUiy@GhQt^EeSu@z>1WeyFHYP8K+DO`gf8?OC5HetqdWmRQD z&(VJ@QFJ9qbDE7YCrF@Z?yoc!GkSS=RQ|X1X9|3VzNI>^H92wlvs}?oKJ*P*>k9%h z$m-8CIF?Hl(UdJv9Osp0qbVhcG3m=dyoh@uHK`t}TC?O#DtddgR6aEfF`rQ0HF)r| zad3MCP>%eRr5l zAxJA38kFVYh-n}YP(aps&N?Ty+j3NlTdViuoo*{2*}1isf{m!g8;!VKMZOvzvxK{- z`lhreg;Bw(^r1Tj=6dgSNY~@KDgSx$*`hb%y?@o>xk}yd`?8r{R@aC@#US+Qpw;nE zcIcf8GjC7)C%fYtrCH_jul#{s}9;3 zO-!t0d%|GVswZuC-L(Er>a1{U{Plyx#DrAoI@esI>*Bbcryz@nX#69T3)8lq@4+WOgR5;XF_PK7lk5PhLL;kAbZ&R8qZ3A zF&sfgbLvb!9gAR2tv$=HYZh->J>+>6j&<3xAX=rpes4nb;Q#HHC43j8ubM$BOcshd z;$OA*8fxI>5o%$7qv$Bu^jN}tk0@t< z`>d6l%k$Kf16_8lYUyO@82#>vTEa2(%|+`47gB1ISO{noA_duYcml_Z_;`CqME9GA zmphS?&1z@-@SExy-}2>8yiq7Lz5@vFa#x|%%_Qqz0NlobM^Hd7vl^-~%PCccZ(dtu z(lJdy&Yu&b&v?3g#D)wsFt>%+*qq8ig!9`4i2SylM#vvIp)^50cFh*D8B!=?!V^p> z$d+cYcD3wUkwHd!jQNL!DfaKJ?Q~w?pO2RK`}^zYKN`nm?eGsc8RJ1Ak}3a(mu6CuncSWZ9K&u?j!I=AaK}fyr&f{gSig#QT#zc{ng;hfQz7+ zChx0MHhiKeD}oYH0djjwKB^4iZiedgPQed70oMPSUF8+_jMvQ*s5o|(z4Sty!pQg&Z| zFibOu3d-?XaPBiu*6+nz;1t=Nrtl7;S~IAi&d}LK)8$_Gh)IB5nm_Zrm6r=q-i+1B zYp3D68$-+IyUbV{`umiHWK`Mgwe9EiuihV!{&k8x{{7nt*wwk0QC z31BzazN$L~%h?F^HDs_f%{h6bE$X_Ib9OAv7xAznL4d`S7iYU!{pNVgNH;46G z$$4llYR`@Ki~cKH_c@jr9+(~oWf65}!Q(+9SD9w;mea431Iym9UvJrrKS!ALg}u!6 zV;{D0a0vEfTJ)D^8vCiia4lt6TvA+J2PHMy<^EJaurCSg=JMdrQbFQ5*rmt`(^sWw zIo0Q+jy{x|FbE68rIdLxY{Wkgl`Il?6icxV3G1-3whs7jy$A7Wi}oRP+Uwn6dtN{% z0(ghxRpBHO@(T>c-z58xVuTHD$H%u*4$9J1!z`1w{RU1v*MZcJH`>ya-3X~qj`TI( zRiADgyXU$^4^z=3G~aC`UvgfNS`$2Op=b&i-g@Oh8mY3-&tiM>I?q{nG`2p~{#*%A z@+(NXM7Azu!fY0_*u}IfIZN zXOe|CLW0YDvowaR^7W}6p9rTDxi)f1R}bl&!VDkVdv_s5(zFz5Z6fZ1vUJNYSynbm zxt)7sHM!?o@~f%uw6JQvI}M2IMDn-ikK9r-_~0{u{BDM@+sqy$V!5ra&PLfb69|vn z+Z8~2CQu<@v)DC^0tz2P6~Vv9*7VOS)tv#u zgI>tLeqc|Xe*7g&R3hT^Hl6yEbT&Z1YYA@LgY6VmH&8aAc&rxAXIvyQ-Yth)V3{B! zcV~h5-K(fN%$ZOq9W?dgHXQo}SC9|AQ(s_%W7-N{t}U}T0h~nHtIsOdx|Hqyk)**y zHmw}D>RKN5o#nxE@`7Byrl)^RR#)Ezlu;q*9`c->fgG;AcWh;!ZPB^aEMKR=Kv!cZ zj!DqSgS*TNeAGBjxeIHgcjD~lTPx8wp!N0Id9$|^AUSs90a*-Fs=lf@l&#P_c0E(T zg)B9lk$&#X?Oss`XtZ$-NU_iHyUL~=3Bb>58131?Aq?ZMIoZnf-g|MgzQl`w=s$3G zcNhP@R7Ms9HJ$+qWvp-N%B7`gLH80b$qFeK`%krM@t^X52oqAj&1Mh#6n5dZe0V6rGzGs`)$F45xg;=2CvW>03q0im?xeNkKqpJ8| z$h69AQUSKrgt4CXCBG+h?$iO$;%t4yS5ZyTw3qrajQCDm66DL5LYaY|KAcooD9Rw~ zK7*!S=G_kBeJdNq4-wwNaT)~`<4IrMC_`az(nGHzOKQjlw^K??)m=UpasuAw=l1rY zN#$00O$OQ(4Z6*I1#sLSFejxe&S^Gi@$k@mNfgl%kb!4FG}Hy* z1CSuyG0tqLvbF8a-I715;5qkhU7);5I~sypl_fmb8MzQWGLKu>g2%ug=9!k36vWG$Yy5HwF1M*hxVicODOM?-4)zsk?rv=XH1r)6 zET416KdZ#7<$F*Y6$g*E7p~>y&#{@(_Q^3*DL`f$)>fK@M@~|Q-x){hPdrz-{Lx_o z==(^a&vi_*T2k*NVtgcpXa$_o_|MbzWn!uNDiHH!MG}|T-X5vrpT~@FnbDDNBPy^s zVW5sQC%#dz9Dn~FY^Tvjd z+SdtkUGY$!3*Q+f=&yBz1#jaV59xAtP7HV}GB0&?3!_$S%@THba zY34q%C2Auf>zbFqWIWXw4b5RrP6Hb4jc}UFn{e6ZjNdFI!onnp|Wu04AwYM2f zByX{Ks&h(*(N=Y53EzQhJuk-l(eUcO()O~=(H$loA(vtU^eaI`W{nv*9u;DM;O?Vy zj*0l~l3$_EXT1nwNw}^RGDrVv11C$u*WuyBdU~wRf~>02vsq8+fo=!TVnJkJy|ETF z7U|DpdOADtb$R)D6lE!dQGCOjhl)Y%^dIKa;-VNBgx#oKH4)_9^+@lJ(jrOl4CQq1 zO!@1#6NV{QQ;`|$TKuq*@%G`0cxiFOfwxJlm?(6xJ?_ay=<}CejZIB=KI5C)$=(mC zg+UtCpuEx)_+4ipgFh%f!VM+KkK;9kC0cK( zy=68>`}_K+4FA!K zFlOl(>4wMXf*m3V(7C~qoboOyY%o#yMQho64e*BoK*?`{GK`V5VAjigfZF4a`wgDe zrJ<>^3e&o1`h|W* z5!MJ0s}6)L8v*7nMPlCKgjDdi)eQJ?GZHsC!cTV>3fBPLPNs|L?o zAB#Qd7BhS47IjE^RyFA52K_!z zPLoFcin=DMv8N80O+i=Af5vLZU%&%Y85?%vYj;HaK1EzsPQ0bnr*(l)5jG)XAGa_V z2s0(}CckmHpY}<}?@hi#cy#m%pkC&;^3nClL*Ww|WdZGrG%;%O^1l)i5>7yXJuBrv zdDeb3>7FAdQkgToh|}eTL#uU;{hka_O`ACDDRa@_r~o)iRt$najlZa`9zpv*_@=0L z_u2c3?|Wf#Gh2s;VW8pw3X@YHWNa$%w6b!PYQ+fqdU?vL1Z|8P((gbVXcbz9&^T+-9h)_|C6qx`$a6JsfWiepm!`P#sIV)CHRzr5JicNv9O;TRc{=vb5fQ6hVQzR~L?d&vy2}vG) zU7y>F{iV+OQM21%K67q8QE^z?&_~%IX3%llH27j}n)`h+=)>9TQUu*^2 zZt(D#qxSdrzwG^(1*$nwNO>5^Pk6b%MWfZv zAkAk|PsN;fn0Y^r2J+irpxASW_bLI>+L`g{rbO1D^dpZ|?=t@7#n!ok0Lg{94SbOE8 zcdaW?jCMhB%|-GoEG(|1pEs6PRt66a+E4mjE%WdCv08mtQN*OjcB{NDUnHJ7Y(1oV zzRYpS_b*C~nPtjx4f8DYVCd%iZHFFZ$IV9ax_1$IYiqa_Qc-O3;OAkFgDk|#{2*$} zI#2cKZpVIQn~!o;gq@>VDr53CI;z(9icjJQ?nak} zO|s&M&@#VR8AWj`aZWM(57ibXWQ(KQ>E`Z3t+B3%5`(^@;|-79{&-d6pXhB5XR069 z^Na|+;glPuM9}crmgZLw&`wbQ`o@178Q+LGg$?eQ5>Tx*?sfE3-?U5vfldKf$ev>h ziwg@=01A1rg*VP~SeM|L|L6>CY3?Sy7?qP!kr4=aw(O#y-K?Xwx=%Tj?+%^MaOT!J z{azE9Fnpu?4K{_CcZ;EvT&T3O9AcTGu(fee0)z)g-jZ-QydeF|!KdAgyY8{ifX)8E zQ@UfQ>8ElMt{1Gp#t_~^m!T`Rb%6XmMLmAW2iMx;Un`CKaOT6_aWlstnxg)$p7{?COC0i7<}JSD!h6ltupi~RC6ULyW(^ray7IwQSOc}n zJMV7Yd%gjBT+58xfnQe%xXsZJqc=3aw8UI%@xxSe#&C6nQ)&jf%xWnrTImserLJu| z35)%fXoHurntjjx#4|lToegELHx(!z_v=V`VxvRwhtcZh&y>~w-X_&FO~v@2@X21| z>WIz4=i_=yZ>PSFvIUJC z%(k2j`{lc1HYT-BSA5ozFmTnSRc) z3gKyEYO@;y)2rRg%NyEX?97i^EPi|qRdxEmgA@7inTAC{1(#MvZqwK+F*FoGoMh5O z@s!_F)SU`s`rlAV@Y2i(b?`PR7Nz0wAzB$5GXvFl&&;gH6l>?PHu@B*tpD@p)&iJk zRI2e#_pgXUX19W`qH8a}tQq+OlW^X$&*%hK_5XL-JrRYkJ1V^d86nCszeXi`|H`Mm zQxH0}%mGWcX7k*iyg1Rw#gFH~tib*0e^8;Ct^1BB*{{Kj|G!_crEfza5Xb{P68tIbwQ0fRlLb@cSyFrl=1BM_aEspL+ z>N$RY|HiYI2rof)@7(8H*SoI0)7E@SPQpY2fk4PrpDF1=Ah?F$>q??q;P2rwv;+7= z?Dovi0|LQ&^X~)aXyway@Q;k1%5cvYuC|^&mhLtXA0Hn9dlyF!gr%E}fUCP*`i?9U z1i}GPReGZ5o3YjI7s&ne&FM|aj+9vb5B~Dc?d>V;^GUY0&IloksEg+uPH&rGk}%wU zJgKZd{U6=#<0M>Wg?~VrFbvZE7bjY}Aa68LK?MuiI~vn^{QiBP$PEqnA;(+2;{qWH98)K4 z*;F;X1F?y^xQoJTdGDFT6^Qsl72IMtm6E}?MR^KbAVF*2)Ch7CgB`G#@R2*<*~nh1 zTaPno7B{}Gm!{#3Zc(UGeU&x=$zg42;>GR5;ZdlJ3Z(w`EdPa0v4W2Pb3b}7`ueD-?A-MzZH8VdjLm?QjB5JJwkRVsqo;iDm8Z*g1thF^*hO@5p0COO;15O6yr1-FTy zt6G1!lj&}BHMlUC>lFgGfZ9>WC#$0XtkqFRKTj_+5dWJRXuezHa2Fj_dWRs`aW5V zg7(+sKD7c}%GlW0c}fmE4jc;#1sVmC79aJ9+}H?MZZO`6UB?PENyG=tw)f50ki^2Q$Ifgs2EwyFRo(q#Z0wK@!ValQuO_dY6yf}%OcAxL zEaSa(=4ofQnj(@qSfrw&g3=})5TmdoQoKtw6)wDjzZfAwuMi1REq(@UenSEMeqJ6;NmOg;I}Ek0_&?3LVXjbF#MF2 zC6Ct?BfX#)y6B%*XlYEk$XXez|0}P=%FY%4fQ_FeyEqK>EuGw|uBZO(5B%g($XMD z7nfmT4oLf~*;?MG$AX~>7NP#{pznlF{MI6^<)}IrZeD{sJrn+6!k@+|Q<8|&)ycuZ z|MF~qIeTM$eLY-&D+Z-Hbw^t{}g1kma}5 zj=kr87dma0jn08vL5g)3RiuAo^2+)BHXIK3mGZ2%PPG;}e+-e%#o#-u)GN@4YjIRP zGgUI+q4B2ZpsagJE9t`g#IKX>Y}2ev!()1nLVq!(K?Os;Ym4(S#`}fN8i6Ik#$i(SRqC^ zNJUBmhF`Qq`06F{&K^$zx3KOxzj1m&sYo9Xc;@^fa-M=ka`Aq6 z+pl3^Qc}{#VL=N`r$ap25t-CPPE{`Pv}Ctrh&VCALHE%jEv>DuyU8UiFj;c1Uct4J zBg&(o=*pQ!*M;ES`F5FTHA)Oi*6rxE~VDz2YAo zACFMrfJAlU#Px<{%J_U+_pCV8H6~F_plE1pTnOG<{QB~V#IqH%8Y&~)CvPlLpRXXR zvda?dz&9yOp?izY5)6{5{oC=B+Jr9vT&q z)aPuplyad#3u0@JU@`X2__$wreSEGv7dymm_s*2D8o*_GhG zkDRgfS!N=9|FLz?TeR`m{{&cmdL>2`jI9S485spp-q*TzuWM-Nu<-Uip(%YZESW=^ z+9LQ6^fEJ&_Cq^j6rA(UJF>i*jR;)1H!5r`axX0S@7^RYHO#n>^1MGVfC&D#L-kA; z6;xQdu?luYKyzFAvpU`>p^c^fmYo3HaH{^*!#8d>*H@iy2R!XOa*7o=;LK!@nBL_Z zV!20c{J&;qnkA5~f$(5CuaB`YF{ZhR@=nK)xL%K1%<3|=ZPQ4pdi0dYRGR*$AzMsI zsgaVh1#K>A87?oEEaB~R zKaBbxMi@OFlRwGC;#8%1lce__tpkpbuoWS=LT3 zTyggJp{yc*$V=z^`{mfv@_0_0=5|tyWHooTw%QO*PEJfooZV1Q=CAYv704J#|D$5k zkaEL_YWNnTUQKI?_P!Ms9qgK(`^`=AGME984Y#CEfue&)CnqPp{vNHzDbRn=S8uT- zUpNE>OFpW*`jbMsa0Aa2Pl5V(V|8`($lW;Eyj1!5gVUE5nO*@k(SkpYDLB86&(B|{ zu(GmBzi26R>iczLH!p;ee%mhrS;WZ zb}+j%E!``A@w~iysR^&kuj4lC*4cgZfz)C0Gj8w1$_Jwell-@z-Mu2Ie($cYpErRE z_>U-(o@j$uF8KQL1!Q&%`#Q3-u&A4Ku7iGwU z@F^x<6HsYyC5h1vDMrW!IUB!)LUyB$8)86xY@@D!G~?RTK5Q)#$Q8aXN45Q3ES$=a zIRuI(q+pXh!_iCNIJhVGy~P~&>n}J<-k9i+_b{|`fjx5Gmr3r~rfP20nkjnq@Kk}_ zsz%Atag(&l6NQ))DT(T~e1I4nE`!m%Q_{@$FCJOz`K=f+1G5%Jxir>--qfqz-)BB~ z?~}pZ6tr0WsOBDbeR=~^h{f`4O82lm|MtqcddEqmcK;`nMy0p7U%g216dt7xrsPUo zCdb<}7qquuad=aZ`f*E)2*~BC6U*w_#)dx|?|hj* zxjAlCr@_+Y<>grMi4~(V9Xs!=04^y6wY^nlZ$#44We>YIc2&roY@lCHtw7Jq^#v#W z!)JpkKce{h(7u}4`rw;uiCljva>OMl%vHm`sWMAT&8q^cFsr1h3DzuyU!n-#J;^>l zB5P?F%|`Xp9j>=-BRQ54JSof&<(Nbx|B=X!UwN=Zz3QyGN|pLod-TGvi<>fu)k z%R6+_cNZo$D4gbqJu$!Cb;d?if%83V*5WegW|N7T`7~emU6Tx)jF|-AF)T~QXNOa! z_Af*sfjc-L;6eu+S(Du|fwbrMNvqDI^hRxxp#Mw$_fk zxq1-XYyk;6NTOOnC!yQdfsV97oBIzQut&Lk%#0l3se(SHY^~j9M6BG){?61i$pEu< z77C~G(bB>Q!cMIj>7!`h?c(EJKm{BTb_VQ4V4veBvx}H`_{Fk64&n>x7Dpch{#-~u zR`>QcYiQWLSy!gUtOasQ`=YDMC zE9wg2id5^*X@&O=&o6ze;TaEUNQgDY-Y4n~Ex9$#x%hmX+1@$uZx6`ST%~Y2P@f$c zaeJw#*gfgdY=SdB{jo6a z^lVrGP63T0TAH8A1TzvJAOGX}`Z`-J!82++jEFM}p!Q_|=T)JZHcf?PoBtTm*Ofcs z*cwTnt*O_&Saiw_^*G}qvRJes8ExfLER?C~A(-r!36s}r%5+CCT0-i;6+Z@r>M?Fp zWkEr~!nXIWK1~Go$A0dDpG^k2`>m@3kDEuzFVYh36NNXm6i!!gA#G}>Yr!q^E1soZ zid0Z6cxiRn{7Ujs>T~Ij{U)!aGbwlG+b?W1G&BTJ_h$(KNQD_DcUIV^RdX?ur-IXh zva(Yfs}r2MZ3r3p`P0+N-~aDxJ>111Qn8skv@VSoE^D=;PJrfR5iZIU52q$Y3H;q6 zkw?SggjH5oRtB4^qZK1m;;S>2=mh!bWBnkc{niJ#AyRrpsvMD5H6w@(x}2F+uJ6yn zawwcyKS5D&OVHaL6%`dvv4`h0yjG=kC6pLaeLzP*_0UxZBCM=qtMc?|r|0@; zL0oC617CbQ*^9<%n`Wwq{KXqnD z3^{~|6nlm=Zl2OR-EK(g8x?g9^7K41FDxp;2(~RnXjg?!6ZIGwS^l+A%MJ|LUI<6&2C*6f_iWQNvh3mwckP zT4FQ??WTU9IZL)EE;A`HHa|c7sEgT-@z+KmKNNb&a#UamjaMr$K*!W)640^ZVl#Ki zc%8dV{L;m)q7~?k9F9C9j%rSxjfhwV$D2k=?`D4a>C%__3zIVUc0ew37NJ zanxtJy@9cElG(j=P6KXsWcV-&n+sF34+2%IU=}{(ZeU3sJzpm3sYSaRg6SHWj-2=A z=4^xiCtb_0M2C3J$jp49E1s^AC;e<}O7UF|S79C&bEmi|d=+h6b16k{a~tAOplfdh zR+ds!qc%f(L;zGUW8)b$MLgImV*Hbwu&^+#GI7&|=$UQ#tz&;<9aZdN!e3r9<3Hfw&f1=J8g%Wgq(j?%gqGShq4M1ldCSZm%Pb+C9E>=dzObp zMrJ}Jc`dnh^km^Gq4jf}ofgO{ekB80wqTwlwV|9}JPdqfl~Ca|uJ?pOw%WuZtLxOh zi++V|7}`7MCZx1pxR24svR_$R^wOwN3Ebeu{XYio=1Wk%Br zFKLYcy*M73%ruq&+yRXpG}1q^5lL;`N?bM?jHxF>rLK_hS|!W`&8t}JobSLah&Kk3 zvrXZ=N%t#Fnh&^nRlZ6p^~veC(OYHEBBCD_>s$J|+$m^Ff+T`25Ii+I8<>s8)+GJJ z@B&&5IWj91X3V30DMj%c=xzs#ixyp2_j-r4iDyKwg;2VhMIM|fXq#KNZn^U%YA}4( z_&qGMd-UC^`o3XuI=3nbB&5)uRI}(&5a7P}6`gk9AtO%W7d?fAg>ed%TFF3ovxF=t zZgY0)GaJGB((vYo0He%mXlTIO_mvHE5R_;nMO)bwYwM4{5=qVYF5VyGb+O&pNQdmH zD9R4b4IY5R`n^p4a>1-nst!?LJ2Ku3WC^z790`2Q~Ld*zh9Lg>7pqihqf%fqoX6g2HHuY zK%@{XohiTO-dx(2MG=J;Eew<*GQ^f_ND+=~#`F6(c7cJiVFehXriLtPaLi-y!@l0! z4~J)2uDUm$oL^m6JilKXQcz3wl198Yj(ve}adqOwt4G`0+k$U*kpkWQ~~UDkwPM{>T2DJA_tjg;O6=4knCUok7u&AmV%=rXk~UxHlL6 z0sMLSEaB(^z{NwbGz&WV$B*nVSyzk>4}$A29@r9VX zIy(Lg;w$jD?P21efL^tqDaHQ^%tKt&yGcWIod>BCi!mX>zuw-%u;9=`5y zF!hwq+_i`v_j=ozo4XV~vwiJQ4L1==d=RAuxlE|}*Y%Z&*6ZAZkj>R}+lwSc3IBav z%tNBJ%e{vqvUo*N=o&}hAb+}2krR)0lshvZ3 zinOwW>Qr7QUtQ;F^E@Y-)>$^eSMY6KcK9(m>aO_W#fw&MRYSw%Q_0NXCNx$}CUZE0 zTlGD+D$*-S_~h&nMwa?1tMlByO#+aIIahcaZ5PsFj(UqDd<(CNvWvORZ1;xrR#*{F z!HtNp@P3jKmyUuL>axX`_~PQ?^bU+-uQMf}p#}<$=il`Q4kjL|jInHT8$PXbK!ykq z3@L3C|is){L$n)fS5rp=9BKZ>!j*{f2&r{Bdut1j2j^5(;_s>ebqS zwaAMR_IZDgqcSyfbMs0ly0fTsqm`KRc))$(IZ^WrPMnoruA>hHrvhuN7!jWy`%+(5jV1$6>5I$8=iPZK}QE?serZ@ITdVf}u&_&scP3sMMRq;ZcSEByp-h+NbBZW`=VSVPt57`k=;;T2p$&Sjc%0%ZJh{2juw|gxuEzqcf zS7|3CkjDyMQv5UUyV!f5=EbF@{{~0r*~n4BzSTgZPIu-mp0>BQ2fG7Fw=%~+@ztD_ zdctQ3oU=#WVkt9TR0>plvXe5qZDaA8MGH8KcOfl~nM(Tlw6v~Skpr!E1&{Jl{OVaN z?)O`}GmL-TF|p>)xl;zO#8RG-k3kZFfE#2|5#rUmdF-9%KkkDFag>WlrXh(4(n!W0 zLgXQGUP^PE;cX+^-oZsc*5d=05}?h9D>fW!v>%Wu?X^J)$uhFfW@?gS!|diH-040c zt2P3~B=kTE`jsKQHSqkvPDk$1-~r90o8%U_cQ;YO;zWyYra~+oR1?e?iSUE|^@D{_ z7txA>)N(Qi;F9+=#yYH46^h2DXJ%*HNY#}mYIa8o)Kkoc8D^S?MJ7L&l&t%~(;j`X z_0!CfefS7v=6XEaZ#~Me=R#-5d}mMd;r0_Rp&P4qqNwg+ZJwhd$gov}({Z#gfr7E0 zqYCz_Ui4xVSJr1KS(B=q=e?iR_cZBMP&zRLdt{t{%yE7yTcl25J*97&!7IK7+hQB;6fx}1{3Cn$u+o=@lpWz{9Z5X5hWOanZ zyzla6PUoyQlY6QCuieU}v|T4x*9FQP;&GIZ#_7R7iHiz0@3Q(SQ2r+Fozgeb9D={? zCtt1R3&5pC3!iW!wx~y9zN-8Npr3tnbMwOF#S3Z)m_GS}n$6trJ;+&r#S8Ab`ue%| z8ySX|ndRqO42Wb8gfQx0+Udmp%LQ<6Y0Ea!3(cOuOMqmRe@mSZG#tfJ+}7UQbaq9S!man}BucRN726etaQ&s!4v-yPU= z!P^+onIQs}2ITLlE35|vciE!^@*+h9EEre;aSVv35_MIOf9m(i$w}1d?!t}i&q{>d z@h5}EnK>NOM2T;ECjT@wMuZmbn(;Pwv&k^|)tsYXHu~hE+D17!k4G3vJ~C(-7W3ay zn!|LhwvV-;w_@k1iS=tO10aYCF$9Kkt3D28gSMAY!HmFOl>@Zw&Eun#8Hc>5?m+iq z$306PQumjUmYye_gEZ!F?M!gj>FK|baC)n&wtm#t*B7T_k8hL{ysJ;G2JTzRBUBJR zX;OO@*y|hZ0jKkmqoXy{lgTfUv7lGzss%f&<{W&;eT~|!+1+$*oSPt6)HlFnsug6V zrKVQv1CmIAxOHZORwSyH{UQV)tMDI`$gMWlR~xu$>jHXACHEm60PA*4Z60Ug?gb)O zA)dn12J%nkBB;LV;`C06n`l~_C;iRkQ6oL7VcHG57w@%M-q=8Z8@h{ors3q$+L;eX z$p3^g{bCnN@m~{Em6HJsNvDO?xw*N*3}n}jIR6|L-yShXT2-$pV&{r&CSUU3B-0wE z749GRo&5?|)^j&?R{18WMwB}HmYN#Y)R&+!)6z~1OfLTL4^cWrmvWAwR9ww-JCyUD zV;bx*WdTu*i~ln5Yz$po9UWV7fFviTY*(V^ zFLQKZP2g;6yYkLcn)P9l+RL)!-qpi7K=%B?>P+V1x4sjk0umiX;=GFDr9*np$FMQT z;zUuf`WK+Ff0>S2tcc~Hwk1z-lm6DP_|(h#SrQ)ob#!``Qmg;6+^o3t7fE{t@;_Qy znOdjjzu=6Pmj0y)p?6V{a$F?G$IYGw)c~opPgh|RL1273zvRZEFf9OjbOR9GgOaHZ z_`HX8*z&NazcSP@cZ?0J8U0OB3cW*W480H`6x`virhsY!bCRrS;FQ~(SsBn^c_F73 z$mg&2d}Vi~0?+mWasqtlqDZPP)d&6oHMNKBlfzy_oaCVHKB)egpeqShI{u-AYM+{274t z8f!TqFz|m_fN%PJG0H?s`_Q4exjD@K#Z~aj9cv?%G12!}x!;?|qVewM4&=wKC8y?N zb&yi$nDZd$L)U+(fLoOkXR6ijo+%0Fx1*y6>p))Nn6S*>mNf>tEArq<8od1M{+dho zR;&SyvWi!$&5W(9VSI#I!D<#%KK=VEDsgue8a{KMyFvye{Z-}7x`bh0siUiJy6A4h zS+X&wt*zeM{=%gZ2IcST*WP_fO+A-HIL!}x0fmk)Q`3%+H*)mkuWR^+)0yTKpg;R) z&k?B=XRWmUk=x$gz43$GSk*tb?kTe8cT*mZ2exZbY{|WON7{F9u~nOK zMKqwUfjRKUn(LHQ-|lG?x9aci2vmk4(o2kitb)yxuT~C-k@%%bUT`Xrs7;J9_trf4=XX z5%CHUHgLSXZ`1uemQ`lrx0_@KvYr&Gp_ABbRV`@%E|69^bySqU7UdC2b!Tm7=h_3T zV}yg2iRp5@R1MSzvd1dvw)h#1^juk(F zZWk(iqP#{sA|7206G)NC?zct(xlT`ioCOgLG(9b#(xd>tY;)h&g*RYv>-Q^1$BX4x zqSAUiG|{MdB;2DEPg+8PesgQuE0i3x8&gR>t7f5$jK7jI>S52hCI3;>9K zv28$^$o+5x{D34=OregPHC(~3r6H4SHv(ok`@zsuM_&x^Xtu-iK*1XKHZ=Cv0stF6 zK|%8wIA%BySf3zViV_j6!WI9hSTmwFc^2*nOZcn;sgMgeX;V)4oLrv~rdw)J(o~bg zWRy2Z77#uWWp`shtQ$M1&I8e&5fZKR=e1x(nAd z*@!TL!!L+?R^7eiw*9Mq(+|iJg7?Fx)Py-ybx#3l4nhDB|3ZiuAKH$g3HjIYQ@f)m zfs9%+x#5+TmSzU56(4jvFr;B(eLSvW&`}wK>x=>O@^|1%%7{XBHe(XTv*ltqW#mhFy2UsqBDzPxyQP zY&UPTM0ho2S^!20Sp#Hh`K!equpsV#A*^p`m}iPn!Q}Y&jo>Iqd2hiXU4lk(FM%BM zkoQy<=L52Ld7*)~1_Vfgzyp7LGlLEwg+q(i#Z_=9#p>ZXLD?gT38Q%K{>izyH?zz8 zN0rAcg(LAXukfNXXZ2-TqpP3d{NX9kR4a+n-6%-(XFv!5X=Ki&5nD$Dx>=xMAGA68 zJPejwWpUj0ulYgdUwPdeoIkAqYR&fZ``y%8Of&P!0UyUx6S&s_#o7&4=AiN=b|TUR zDr@A##Ki7{JMSJ92W5G^si27J?sS~2FbASQOm118EUZhWzkXg$9XmU~^+ioC-@9RB zJK9i>pBYhg2RxS~V(X2!pc#}-(7eTS-7)e{xNTgnu-vT|`bp6-F;_2vV~hwm-FZ_q zwqD25iVu;`-dzR8d&ft$0s)2!gr;_*WbGp${~VlO_A)auT~BX_ajQDagOiCd?Fc+i zABnmj^Pk=H>3tRopGPR@1A;-U`zr9>xJwBP=x8a^{0MNvyO&OdP&;-0^($d@%EXm?x7kyPeE&}NSj?}3jV2f{;q-Rx_Cr`?m3i*#B;lrqI&XgBX3^Tm>wY~1=4M37eKJ|5?4|64`8Qe8E7ji;P0BQUX6kl_`P9<4} z3CW*55*{ix_Li2Me%F`hacolVfACxyWnL}U#P00uNEGUpXyqDl{1k!GgosE=o*e^D zZ2pmZJ$xJ~DkjzrOUB;(5alO*GngGv8?Dkm7+@)E5H75OhJA0!vSn-O`mO5jmqCix z^-m74i#BBY`#Et1q*3IJhpFwHIL^0>b|MzL6E3fBLuv}`hrfRP`nYwyFkODdQ<}e* z3LMmq*T6JjgwnbN>dbBH?lK&)owftRf&IT8J3dui#g{K9haNq8bXaU=TLf8TH4u~s z(1U^ZG=>sm1NjWP?MtvlSzqvBkUth_Jep^tEQ%%<-Rh2yj@lqePM!6Ifr9RfT{sd2 zM{@?4>>=B1&7!f}dREDUs_rSi)g}UyggK|)T_G*5v0f8vQwr} zW@Kh%y+R`C1?4}Xv1hOq72!{zE-9l}XPhsj(L-IY@_pwr2tRz_?mzgDwQiy!lY#po z#05EIjGJ^1>)W=N$HCY8#z<4~BN{vWui_P77VW>m+PuwkI@NJ@jTW~%87)vZe@KUX zT;*>Hd;9apk9~)=29%^>iJ9T$OPG4m*n+&z&aAnXh6ec_M*DWMRHBS>WQ^dFouewN z#l>6b6LJQGYgkN*&RAW9dWry}931sNGu$kEbiLbiSMDJgbm=EI+P=K$4ni zAJ+ZG_V^cM1SBi&=w-zTe>qA{v#%#Xvgv4yIThgT`?vRIXgGMMZJ^Iql?G>{Hv(u;zV0$pf z`PD1m*s=9L#1?)P9pu|Tg;DQ8pSTef5jnt9D-kYC_DLSXO$lDWKROkmOwXj|tZ5L$ z=PNtaN5Y{e+PT_&xh*)0xM;Y&)d-*aofaMSq+s`MdP1t09;+hKh;Q`UiCJOMQz>6T_~*uaEWFf=$Q8(P4|$X59hq74kl)2TDI z&tJZ5o?{QrSSX?jO4i=zDGlrgs#LoS$9U<>J^@*=B6(1)dI_F$W#KCrIrFq@BmXMv6#>59y;Cg>@2p|yw^-E0k z6Kmb?s2`Z&E;c0{CPjq?MHf`V8TEGbZ>tAPx;7ZCfWGd}c-x7l@vxXpe5o2&Ha3qa z#|@}X4adjF?VBUHiZ>LdLWZEbp@4Xlt{w1te4D!lvW8T7q1O3-Dhz2zV^L8QrG43Z z&U2o)abW?12FY|+0D4tXQ>j>tUJPcnwOv+SP>HCzEKkg{fA?~9KZCY1H>@Q=vJ<}?( z@DIj}wJzof{eLi3dK{m1N(KWn7Mwzc=sAT3Z>bjOeoQNTGq>c{Hg45iu(Gnk3_h%i zMSss)>{(}_EVRfKK>fV30Z?4^OH6e4($q8n%2oj0#~*l|iP70H>8kNRVCO&mJBZuI zfV9crvDm`4b!~{Ptgd46|1$D3x$1qV-Bo4elg0@#c5ScM*B_Tq*d3Y=6=Ny|4G00E zXDp@EmMiGh8G&^cx+pUBf>ArrAW{V%cuD|?fA;6KUVxNC zd1EQ=2R;3(48SP)_*RnufFB$4__lkxTUXQhP{N=*VSS|!ury2+SnXT*anyPXWyy~a#C&!MVXeRM?^&Q0^BJG zYqUh=>de2>$Q60r4}X~wunP^S3Ge$=sI8`Gz(;e$c0~YXjr>h6xm(VF_b?^ko$o#4 zGT#E-^`I|Yq!8fJ2tbN-+6`EC+h0YR(npbl-?x30n zF6i=%9AajPl}#Ky`t#@WYlgS0-+v2u%yZ~5MlAQn`Fv|>)KTZYvmEKj5YUI3x7Nq= z11@X;z*LSPeypp1Obb_3aeic0c?vnQvl_(w2@l$B7=tQ8azK$`0MYR{;GQ-5M$gXA zv#Ax00ZcAwiwyxB-PWiD^-03HpKQ`o`f3-j?_k`kIg=!CTUS$FidN~;AzYw{?jgBs z<}_wUDG+4$0Y#_b@lkaALMysK+yW7bmnlL zv@=O|aotX$Mp|J3s<*e({CbSxqL<(veNr2_=!E|AmHAlf1EmS^2r4V7kCk~mKWcu6 zX{w6FemNwu89!qR(D{^>=6|&KHH;V9q2yEKage9;)4b)z@ey*asOQR^*cNe_wxg+HHctJ+*9>OCLOlU zxv1~TLI94%;?7IllQ7`d(~r^8(#jT3{4%kZ_?o9`!t=VEqbxZL2f}Gxo|lvSwydU> zuIg^h8JRsIDrAOf87l1$%$GMH*RK)&q4i;b#Pr&1B@v=55sej4s6@2M3B8B1rPNof zW@4;i~UB*wLQQ85p;my-ynoVAx*7zn4riLd z-rmEcmzD~TC0=65M(<3DBg%S9pXlm)v5k}GSH}zBYX1cf8^aEnOwCT61a9@QGk!Cf z_;}tXQ03#dj9pgiL&Rk@Wf1OwbFiG<+tH3lgw&m-(=LvkeF(c5EL7ka@ zr>B$_ui)W(_qdtu2JV_N*ux=^$jZWTj4qiz3fTubNkMz&Jw-%qDv%zQ#z*PXVdh9K+C>PIX}Nv&5r^fe$dZrStQ1k7~HnGkyIX zQ)~n3T>;Xp^1Q<#EjcwU?RT}^iedQWoO-@DgNi8?m*a(%)O1E3kG-D6V&GtKJLz`; z)EyyIAu(sNl7XKg8=~MhEqaYhJVhBVCktf5LO~{h9f+lYe3Kxf^YovW9jlW6SW!yK z9z;oJaxk`rNv}lz$|npww^ zRybr=J573cc(@9>~AtsPnp%v!lQ-7Ku9~sDEuJT4FMh0$IR860*Tx0 zuBVIxv9gYD+HHaFNcrrz?djT80bt=u!}`A9G;J)^TSb=ujZPFK0+A~DVOY3Oez~yS ze&x*IH-H6(KTwZKruMPiG+LKW`*SKyoP>9wqO08nzLb(gY@9_OpEy0_U^{KF`1^kdW@h+t8}nm;cFcqpr=MN2aL1&_6x|r%iBEKkJpK0~@c2 zfZ|!9T<|FeNtX*ZU^u4*r5 z)StmLs0?eS*IYD2ZzEO##UMUAG{2=sKNc7FZm6!_cLbM~Qf>kW1G}TMvs~gcZg*gJ z1W|;{8N{Zir(dv@_C)!8^6JTIX3Ml6eKSagZ9Z|vipA7#BY zs%0$K7CYRBj7uo#YR>;H)(nhD6=-1k6sier@LyrYcC&b4T(bhU={t4H+nPK`9UY7C+ORg122RZuQ27B$ zJd@*_Jo1(O3heQPd*}lVI&V85?ra`d;cz655~%*l2<%K8b;{M2eEj%vusA3LFVteH z^)3xPUbkmdiMX!xd~2*N3dP9*8<`XcEbQUrm`!&OwyrJwlaC5H9iNagKC-n>^VVWj zN*0%pc*zHn%H0>hI>80~)h|fOOkkO7_2nav3$gB!7exoT+RvV_&To!eMC&N)pg<5L zb$WW*4sEy`1p#qoY_}4PgBt2 zF$LX93YlasLJV7xF{%9T^SGb0Prs8gDii(q@niNpI1I*RCh1gM^^J|4V*>d-u!JZjU()*-~`_(qJ@oklOpNq=ro^l#fQRjyFNbkXo@MuR*R6N8=XYX zFnLg2w#*Zw7%-8TsDqf@`9eRhh`wJP8F+hKd@^bYe?TLcVT|j=runZ zZ0v5p^E&?R8%B1l4XJV8Is7;F7v8&k&W!9edgn0;C>}dXH!eh~^`bNe6eA|L=7~(^ zIubSe363%p85>`IKu2=5aRs40ok6{blR0S&N2gX=Y3XT;Nrl(DGWP==kIOL!duE5E zeg=rE)Q^f(C9Z0$J0evmIi}O`W$W`vn&@gA*t0n$14DRBV#Ihe6Wf>Z@TBSay&Fd7 z7^yFRjdWj@or!77aaq>APCu9@j<46SjLxin)ZS2D&d&hs2A$i6C|T}Jtc#3{ok(ix zL1i0mS%H-fWZ?X8wUdSH)`Buobz*9O7#Q-*0#hDBD<2HKZm-!`02Go=aVv}%_H5IC z;5!4D&^Y%1oAd}I5PO)u4X6p$Ob9Kye|w*MjdJ1Zf{cKXzTGWw3h2b!Ds2G*Mih(B z*N=+Bh5wNGLoVCFiDCcFguD2#VEUD)|84bl_)9T~WE>ADNASxbdw%@Q5Ml3Fsv0v& zegKOQfcJZH-;qj3V?~Pj%cB3Ao{E1o!&KWl04*DIR%kDwMLIQjFu$iVoT1Uz&-Roq zr|i54>@Wswq7l?b+0u{d6@183z^DI99BnPzskArZ&D8AG5w2P}6XFwWe_d!TpFR{c zwR;-e?Y%Vgz@|)8O6r`4_ghbf?@yL3mjawOEFsR_z2H^5Hy^Hh{+(`aZjN`as7U(F zHZh89V+&7W1S2-@!9ln#4ci_TWY^UOi>G6e z9Ly1;MgjD|!UCgb&&apr)K215)l0 z8R;SXjH%v^AgyTQaERr`y`6-Q;wDxUtWyQ(iNTiStYpmnPs_a-APRhR$Wz~Tvl%Wv zrkJ!tPe$gpFLZk@-_Ej#DJO1yz^t;LoCo?%?M1E}o`-@1PKc(KJbT1h%<63+RyZaL z^$`oF?Y*3L_LrN=DAD>aCPYW=20e@eJc4Fmra#e}`247n+6DC8dpjn%w(!)33PD1NH#{G)v zDfQbRr5BL>#=zY0A`5unh=CY=M}s8;Ka}m0PTB#_At6!GM7!e-gg*qr9Qf~j0iv(s zG7+Hj2Lb;{<@mYvwPsQ7Oq!%;x>bXiNDc!m^($j1enX9q4N?u55^!^4U)8**u>2T^q@rR7?1-t;+tDUNpwi#?r=w zP@vIg*9^Oo?z*E=Pq^a{AXaf7`8z|`zj7{YZ2jn?7yz<%v=#TR`{DP|o9@Zz+It=d zLyIp-t484VJhoefz~1^Sbi8qO2D5c4UUn{E+R(7%t{t7hIe_41dTnqO<7V2dQ~m0I zf30LHf|Pz(7}ept3WlV*!Myw9pFp+1O;Y|iSH)QeU=JS=C*?OQl%FPmI>M!45q|j= zWr^N~M?Bnj-f6*h-%JB$cn+e7MbWD;WsCAAqPnDmlG48~z0rS8FSJ;IW7#BYUt?ToiM4+O);%I81PSXf&HfSdu{0OOc<72*KB zrW4T{`@bu02Y!b02*_nHl=Ffiq#d9U1ZxtVi+hIJGAiqE)z;SH!KLpaduht+d)ooZ z01tjTWOj|8EU2fa=zXpFJ8>0u=LEMxfLndi8(rPIos0&mjOW?j(&Je$=F95RX#B+H z$=vw?MwFM!O?X6j|c@#c}lKvQH z#5)G$Bd)|z!m_Ol%xV-6IO*!`Xw688tqD#nAjNq+!Oo`&hRIi8r0epS*|lw-AHWqH zJvyD0hDcIAM1v<5%=XKG{P+VR&hGFJ=27!Om)pSOPS>89F!4FDoeSLsu?jhZ4e=LQ z8u#Fg(xReA&p~mL1I4L(rP>_~N6k>ON*}!k?-+0nRFO;Ee7Sr+)D13wRLy zh-7lP=eJEhm|i#ylwY`8Ww;6g|8;eBZM7g828@C@5M6!-B30t)L$P3l6>xDPppkon zna?S-(rmA0lSQ(0>wRV^wDUdYd<@_Th9(NC@PXwTxHzPoY#qhpAkOLh| zkH#iPo9DZ!06lmk2QY^^m%u%TvuG3kVb#(;`Me!OKG@sbu>;w;Rxc&w9*=|IA(wgr zw}+Lg5Xyqi%$u$fAD}^J$_o=aDC+fd4UIQ$V2oCfJABsSlskdj%ddTY0Sqf`y5kv)t7 zMYIwZAAfdtJA9Lt%M&QZM5*$K!xKcS4+?cu#>-#keYlom2I;sKKmIg`TIbkrAieXmLze77XWcrlsMyuEvfki zw!Oewx03^U06UYR5KNYnELm}vcTSGDU`;RX-Nxoy-z4W( z#C?qb%Rl-|1$4BiT=o9@=HaA|O-*g2At4dpp;l^(HkDO3A+yud(@%09A1s=RN7Vr8 zaT%fvsfBFbVF}W=cDAD;Fk1kPygKz0?19A%Mom#fSV@U66ICh_`4*$t;8+V4m$y7S zEqT=TUGOnv4H#GrfN$K^K6a*ggyUUF$vc47qm6W%9!n3-%Ifdg=(Q%XEx43GqJg|M z!Au3He7^5jzC7qntWBfT_6(utU<4s2?)JWc5vbKWI(bvV>SN4S+nc9PbM{ z&v5)p#ufaF*qGZ>vffa#>@Uk(zrdrah!iZpQY`YgkbQvp7?>SX1`$o-dQChh(1Mjn zuEr=N2Mjt?uJWNU_Zm!Z+fdJuor^2WDOL?iKt<<11kmMB?vHMnK~W}H8G*w-w+Z*74kaia4ezL*qkM=xw3L|RlZ0IeJKOz@6=0dQ!Ha1P>G61rveUm zfjg#qL0=@!y1bJqrdtnqyw20VIVBYnpZL7#b}kvW>N9ya_*(T05D%q(Ph&wf$!=ej zVs)t*7H5iB0vBX!0$xBVk?5y^J$4}K!R57&xw$!ObKf?g=UP2!Y}wAN>+tM+x~`r9 z0qGQ}87wsDB9;NDAQuE~P<|c2BO)fw=L-6KSNZgwAZb2;CH?z-@S1z8!kk~`@w~m{ zyFR<8|Es?6>O^KUp>uNljzbj~O@IrRTJx54kVz{7wAzi5W$uIe1>OR;52AJ?JSxCr z@dbD~;iiVk#u?Vkx6&S}2LVBhRI=I!Cwk@~vn;LC2%6s1QM-qR$#B_?_XGGWK-S+g zfFz)$B}v<^#;i#$B)n8sQvr&Nj3#T(?wk_ZQ2g_zA|S!L+B~w%M`)G_y37hu2511p z-hJ?7=z@VTF~#dVYwAr3tXjKPV_x&{!b-zs@BV#PLjtstaceXiRz7UkKTp_?T-Iu+ zN?Tf53Ixyug>b=Gh6RJEDgW=60SwKVu6G6KcAI~XPq@7f%~B6OGEOhF7*sNj0o9*0 znCm5Xmv^ws(0Z!X^%+@Mz${er34cX?--U} znuyYTs0X9VQ*e{+`g?G20ALu6g}J#vb7SK%H>Cb*^;qYWRu#0BF{V2N%nOCLSnmp@ z_ek^`gz)*`6v_8u=O)%(1r-$zAZR`o1Ar$bq|d45LJ`FF>ssd1L;MT*Tz6}iEx!!N zZ8`Q}us2}ZYieZ2Cnt&2!0A{2oGR|s9TAC@=S?->^}iHn=7>7Wq0~hHPSoXKbzC4N@3&H}uTY@TK7Ab&bP|

;Y?jlX7sS!MnitJpObOGt;@qr3~#GNdB`w2fY#&z>nS?FD%GHd(jW?Fcx>j(~v( zuzgA3SEQbCmwyvoEKTuan^((zWigIjuq@*8?WPNZ3GfnZmcD(T1u7@0yhK%lu2Jo5 z1u**F;>31-USfZDH|u}y@^u0J$QEGZ66z>44p7rK-cFYk0c$PzH(=fDj;R!KIV|{A=ORt!67treUzE(UFIL zF1o_oaPhC5O8wd(W9g}Dg>oFyje)h}6VPA*jc|ZKft(UxkuL4#un48}JoV5Os{Qoo zossT2LV6`TxmlzD_GpZq)t*cDccL@U`}wB??a1O7SnZujf>Mmu#6O~Qtko6lW1YDv zAgs!JD}K)(F=!RC+S@MfM@~`nr1l1RK!nlZO@{kDitaA4j|yho&01b>{b&b-+4-Me zzZ6HNr&)n5UqLU?L5zgk{vq&U8H2?}oINfJ1XxI$%(1KI+F1(+e8VM36KO<;CXGYt z7c7U&44Jt83o>19BK+?EL3=+0BzD1o()s})i|4_Z1d&$$;Z4pkQG^pH`xgOro^%+b z?x9S<{ZvNsdNtdrLuo0)*IOS<@~WnKaZu-ahghTe5w zKb^#J9-YLG3_tkZK1d!VEvU^dd4@3&?FJy*_s&i&;AxU%C|){QDI5D9?)bft!cF$e z_EYU42bm^dgy{t(9SEwB+ZE|<{YqU7SAI8vB{%s(cOJ`eLm*S11q^tmQ1Cx&E6B?e zfqk(|Y*=;hfR zrvdq(EF3Z~J57`QABaF&AGW{wnoE)nz1%}HY~He5`#QnIv~3Dl-9V&Q5_m|}UB8Hr zGpTbPc`8&fB%8^jfnXREf8f-)Up|!^GpBfy+=JIokSuB3}Fr7WS0aU8&_HzfvjVt<-W9RU|{hhf!i(a1- zS>Hc$GajQJV(h+}CXX2&;49az)_JdP+~$1WxJlvt^Af4gdQMac6CKZ9XbDyHC7b-P zVr;cqEyr!s<@^j6zl?dsc>CV5uCEF^d&@rZRlW8?A?^8wA?}l0L$W)nDE};Hr-i_c zlzi^|jh*D`l8yvMK3^#!apdnBAnYkz;3Zzc$yVcD)*I9a?Ez7ygL=m1$+P$cnDZ+JN7WLCO zSR`wCI_wqh9e5Cq9U`mlnTUKMRX4YO>e~`*-cP2zekKqCi>D7Uf_JlWJtS5-bLdws z@SeAp`f#IUqxp5_=`~I^4HC~jC#8Qu3ei|%zMe1)7l0=JJJ4ZNwzM3> z1$s+g)kV#(`#U-yTn}!0it%&b45nWwM)h-d9qSVF;*U723F}{5-mTV#n|Xn}5kGS@ z?9`%WCP=$qSyEJth1w#C(})XNl%e2-@jpG0s&o~-rO*_k= zx0nona%{uK&lqyPFBG!3>*voqK(wGPUH+4VS=h^yQ4^T!LESyh;ECF>VN#Jl9a=(S z1`;LLh3dyO%FGQzGQ3sG%0Cl3+Uw((r~8SDJ3a=YbNr5uZUj8ab7>AW$~2QC;7EZq zt^AM=1E7(wa%N#QW&M)ouRu*Ag6S>2g+19Mr@;NR%jK4~qj+^Q$T zvh$ak6iuYg9z(S!P3ni{f=H;}`YwKaMP91m6x6r<;;NKL2J$R5(Y{wygTPKdAWR1v zqNnF-oT}dz$rbuy9zdZRW+f2(h4%(u1-~@#ykql7af+PaIaC(Y?_--B=|4zEtw~nC?_J{Twvy=xVf%?H8u3TIkL7 z0>Yw@^>GqeM!L-38r@Sj%Rpwa8U3@6mrJ;aeOlz*;q4{GAkwII1}8%cE44pAt=8zF zW^fC&Cqgl1LkIG9$;-V_${B7>j)D1~i_8xg+oW1$ z9yg8IG(Cd$zoX0$WY0#OaC8V$@0LQ(>o`{>_d3gVv*(b5o-vH>p{Cv%cLH(F-(Uv8 z#aw-&iAN#-+KzEQCI(@SqFZ zhwYE&KTpRn?(t3s$auJliM9KPa^awIh`lF=xnxZ=j>E*X!SJ@SH!gZ`Q95W|5mFGji(NZi5EuV)q{(Hjr-hqHfhDEkrUOSz%|> zg7LPCOI|~Ej;e#i-SzBwjqTf~LYx?91sc+%)-`3-Lt-X}osGTFg1_bfeA4@FR|7@J z)(g#ErG?-bv$3h_O8k@*-I+Yn*CaZ3^}2=QGek#jwVr}URb(gK-S#94{&lb2RiVrA zbz^Otoz%9m%LhY=m|KH{W)zs z0*D{nzMRVRD_zdJvftmm8%QwS+V4)k$58vKwp**h)Z32jkgL~w`lggagrh?{M<89a z)xK^sEXI@aPBwiq#(84Wbr#%ryrbj(-X{_2_aQD#vBBNg`ilIm7G#B$>8WPjpPnwF z@tj6^(JeGDF)ow-B#A1r{##i*wE<68Qlj^PEAps=yNkHMY}V<`%OLj~NPn zc329gpM(xhd~r{(95@c6y?IXfZr=B9h5$vrz3%!d9DVQbg+lx@Ayk?F5_m#70d;n$R_rCKKM zF^zB70rW^k!>%h9i>07v^#a@kkQX>y1e9d!^&je9~%8)zqaW*D?3gmg#05+ z9QZv5MsVVcb31^S5DK}%M**5>HOdP_+SU`4&wAk`Vo3B#6s-ZjYU7DM$C*2PV-vNnVZNV^gK2&G2ijZEP)5p9=8H><#Ge*87ptMLa^u&}jSCG2TFaIt%2xmQ36)zlGHoq{unB&bbez%_s-r+G;#6%5 zh3d$YioNbh%o!yl=|qa3bNSP1r$!FAT#An(1(p&FKzoS+0O#3VL6R%Y&dvt-gVI)G zjT_ex`pRG?PVGLWMHX7P3(Iq|7H)PB%z5&y414@=)ipB;VJM8Y#vYZ8yx(}F`t#Ug zsjCd7!cH7bY16mstN(A@;ukq`Pe#M_v~w@*%O<{ZpAP5693Pd`xm3A_atidGtO zm#ZHcP<{C58@z!)cL>ffaH6jMU0<&QJ3EdGezLWZxl;H^KzSBqmF$5a+S3O!Y;KDF zLpa5nVx2^PI|X#FsCbbD>r;+y|10J>-WjOE*ur@|bBB%FGo5z%u3mac=ENF@$$_AB zf;?faho;o66Xjg4mfGZDW!ASH`D!4gPBVV&5Y*P1paOMvbi}vFGSX{0Hr$t2j!T z=I;-rTRQbO)(niSz&MsR)(qf*D{ml zYcLrVF!H_j<3n{RBMGE~lgHtj7!^kMtr!T_{(n6dRS!6|g_ojBumO4(Kcu6rf44&G HN!b4a0x3)I diff --git a/docs/source/usage_projectformat.rst b/docs/source/usage_projectformat.rst index 3bc594a0..7ab5f059 100644 --- a/docs/source/usage_projectformat.rst +++ b/docs/source/usage_projectformat.rst @@ -4,13 +4,20 @@ Project Format Changes ********************** +.. _File Format Spec 1.5: _static/fileformatspec15.pdf +.. _documentation: https://novelwriter.readthedocs.io + Most of the changes to the file formats over the history of novelWriter have no impact on the user-side of things. The project files are generally updated automatically. However, some of the changes require minor actions from the user. -The key changes in the formats are listed below, as well as the user actions required where +The key changes in the formats are listed below, as well as the user actions required, where applicable. +.. only:: not html + + A full project file format specification is available in the online documentation_. + .. caution:: When you update a project from one format version to the next, the project can no longer be @@ -18,13 +25,22 @@ applicable. introduced. You will get a notification about any updates to your project file format and will have the option to decline the upgrade. +.. only:: html + + **For Developers** + + A full description of the current file format is available in the `File Format Spec 1.5`_ + document, available as a PDF. This document is intended for contributors to novelWriter, those + building project conversion tools, either to or from tne novelWriter format, and for those who + wish to make their own templating system. + .. _a_prjfmt_1_5: Format 1.5 Changes ================== -This project format was introduced in novelWriter version 2.0. +This project format was introduced in novelWriter version 2.0 RC 2. This is a modification of the 1.4 format. It makes the XML more consistent in that meta data have been moved to the section nodes, and key/value settings now have a consistent format. Logical flags