From 3b7927f0ff306d6192498aea54ddd76bcf805a76 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 25 May 2020 21:14:50 +0200 Subject: [PATCH 01/38] Back-references not consideres all tags in a document, not just the first one --- nw/core/index.py | 17 ++++++++--------- nw/gui/elements/doceditor.py | 5 ++++- nw/gui/elements/docviewer.py | 27 +++++++++++++++++++++++++++ nw/gui/elements/viewdetails.py | 18 ++++++++++++++---- nw/guimain.py | 18 ++++++++++-------- sample/data_a/e7339df26ded_main.nwd | 2 +- sample/data_f/1471bef9f2ae_main.nwd | 6 ++++++ sample/nwProject.nwx | 22 +++++++++++----------- 8 files changed, 81 insertions(+), 34 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index e585a03a..79e6ddba 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -260,9 +260,9 @@ class NWIndex(): logger.debug("Indexing item with handle %s" % tHandle) # Check file type, and reset its old index - # Also add a dummy entry for T0 in case the file has no title + # Also add a dummy entry T000000 in case the file has no title self.refIndex[tHandle] = {} - self.refIndex[tHandle]["T0"] = { + self.refIndex[tHandle]["T000000"] = { "tags" : [], "updated" : time(), } @@ -606,18 +606,17 @@ class NWIndex(): if tHandle is None: return theRefs - theTag = None + theTags = [] for tTag in self.tagIndex: if tHandle == self.tagIndex[tTag][1]: - theTag = tTag - break + theTags.append(tTag) - if theTag is not None: + if theTags: for tHandle in self.refIndex: for sTitle in self.refIndex[tHandle]: - for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]: - if tTag == theTag: - theRefs[tHandle] = nLine + for _, _, tTag in self.refIndex[tHandle][sTitle]["tags"]: + if tTag in theTags and tHandle not in theRefs: + theRefs[tHandle] = sTitle return theRefs diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 906d4c65..d220a8bd 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -395,6 +395,8 @@ class GuiDocEditor(QTextEdit): 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) @@ -410,12 +412,13 @@ class GuiDocEditor(QTextEdit): def setCursorLine(self, theLine): """Move the cursor to a given line in the document. """ - if theLine is None: + if not isinstance(theLine, int): return False if theLine >= 0: theBlock = self.qDocument.findBlockByLineNumber(theLine) if theBlock: self.setCursorPosition(theBlock.position()) + logger.verbose("Cursor moved to line %d" % theLine) return True ## diff --git a/nw/gui/elements/docviewer.py b/nw/gui/elements/docviewer.py index 7fdf22e4..6d90c2f5 100644 --- a/nw/gui/elements/docviewer.py +++ b/nw/gui/elements/docviewer.py @@ -206,6 +206,33 @@ class GuiDocViewer(QTextBrowser): self.docTitle.setTitleFromHandle(self.theHandle) return + ## + # 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.qDocument.findBlockByLineNumber(theLine) + if theBlock: + self.setCursorPosition(theBlock.position()) + logger.verbose("Cursor moved to line %d" % theLine) + return True + ## # Events ## diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/elements/viewdetails.py index 589ffee5..d5ab7ee8 100644 --- a/nw/gui/elements/viewdetails.py +++ b/nw/gui/elements/viewdetails.py @@ -107,7 +107,11 @@ class GuiDocViewDetails(QWidget): for tHandle in theRefs: tItem = self.theProject.projTree[tHandle] if tItem is not None: - theList.append("%s" % (tHandle,tItem.itemName)) + theList.append("%s" % ( + tHandle, theRefs[tHandle], tItem.itemName + )) + + # print(theList) self.refList.setText(", ".join(theList)) self.refList.adjustSize() @@ -122,9 +126,15 @@ class GuiDocViewDetails(QWidget): """Capture the link-click and forward it to the document viewer class for handling. """ - if len(theLink) == 18: - tHandle = theLink[-13:] - self.theParent.viewDocument(tHandle) + logger.verbose("Clicked link: '%s'" % theLink) + if len(theLink) == 26: + tHandle = theLink[5:18] + tLine = theLink[19:26] + if tLine[1:].isdigit(): + nLine = int(tLine[1:]) + else: + nLine = 1 + self.theParent.viewDocument(tHandle, nLine) return def _doShowHide(self, chState): diff --git a/nw/guimain.py b/nw/guimain.py index 9c4d5d3a..8aeb7143 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -485,7 +485,7 @@ class GuiMain(QMainWindow): self.docEditor.saveText() return True - def viewDocument(self, tHandle=None): + def viewDocument(self, tHandle=None, nLine=0): """Load a document for viewing in the view panel. """ if tHandle is None: @@ -503,13 +503,15 @@ class GuiMain(QMainWindow): # Make sure main tab is in Editor view self.tabWidget.setCurrentWidget(self.splitView) - if self.docViewer.loadText(tHandle) and not self.viewPane.isVisible(): - bPos = self.splitMain.sizes() - self.viewPane.setVisible(True) - vPos = [0,0] - vPos[0] = int(bPos[1]/2) - vPos[1] = bPos[1]-vPos[0] - self.splitView.setSizes(vPos) + if self.docViewer.loadText(tHandle): + if not self.viewPane.isVisible(): + bPos = self.splitMain.sizes() + self.viewPane.setVisible(True) + vPos = [0,0] + vPos[0] = int(bPos[1]/2) + vPos[1] = bPos[1]-vPos[0] + self.splitView.setSizes(vPos) + self.docViewer.setCursorLine(nLine) return True diff --git a/sample/data_a/e7339df26ded_main.nwd b/sample/data_a/e7339df26ded_main.nwd index 05ed60d5..8b92a69c 100644 --- a/sample/data_a/e7339df26ded_main.nwd +++ b/sample/data_a/e7339df26ded_main.nwd @@ -2,6 +2,6 @@ ### We Found John! @pov: John -@location: Mars +@location: Mars, OuterSpace Jane has been searching for a while, and she finally found John on Mars. He was indeed in space! What was he doing on Mars anyway? Well, it turns out, he was farming potatoes. diff --git a/sample/data_f/1471bef9f2ae_main.nwd b/sample/data_f/1471bef9f2ae_main.nwd index 9b2d9368..3bd03e9d 100644 --- a/sample/data_f/1471bef9f2ae_main.nwd +++ b/sample/data_f/1471bef9f2ae_main.nwd @@ -5,4 +5,10 @@ Space … it’s an awful lot of nothing, with bits in it here and there. Some of which, people like to call home. +## Outer Space +@tag: OuterSpace + +Now even further into space! + +You can have more than one tag in a file, as long as there is only one tag per heading. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index ad832035..c2d74892 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -10,9 +10,9 @@ True True - 636b6aa9b697b - ba8a28a246524 - 914 + ae7339df26ded + ae7339df26ded + 941 B E @@ -73,7 +73,7 @@ False True PAGE - 208 + 210 40 2 213 @@ -174,7 +174,7 @@ 139 28 1 - 343 + 237 We Found John! @@ -187,7 +187,7 @@ 189 37 1 - 224 + 236 Characters @@ -257,10 +257,10 @@ False True NOTE - 115 - 24 - 1 - 133 + 241 + 51 + 3 + 286 Mars From b3e4af1394a671c2054a785b9d8a72489414147b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 25 May 2020 23:30:55 +0200 Subject: [PATCH 02/38] Added header anchor tags option to the html generator, and line numbers to the tokens list --- nw/core/tohtml.py | 37 ++++++++++++--------- nw/core/tokenizer.py | 77 +++++++++++++++++++++++++------------------- 2 files changed, 65 insertions(+), 49 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 53503ba5..cb2a4baf 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -143,7 +143,7 @@ class ToHtml(Tokenizer): parStyle = None tmpResult = [] hasHardBreak = False - for tType, tText, tFormat, tStyle in self.theTokens: + for tType, tLine, tText, tFormat, tStyle in self.theTokens: # Styles aStyle = [] @@ -174,6 +174,11 @@ class ToHtml(Tokenizer): else: hStyle = "" + if self.linkHeaders: + aNm = "" % (self.theHandle, tLine) + else: + aNm = "" + # Process TextType if tType == self.T_EMPTY: if parStyle is None: @@ -191,23 +196,23 @@ class ToHtml(Tokenizer): elif tType == self.T_TITLE: tHead = tText.replace(r"\\", "
") - tmpResult.append("

%s

\n" % (hStyle, tHead)) + tmpResult.append("

%s%s

\n" % (hStyle, aNm, tHead)) elif tType == self.T_HEAD1: tHead = tText.replace(r"\\", "
") - tmpResult.append("<%s%s>%s\n" % (h1, hStyle, tHead, h1)) + tmpResult.append("<%s%s>%s%s\n" % (h1, hStyle, aNm, tHead, h1)) elif tType == self.T_HEAD2: tHead = tText.replace(r"\\", "
") - tmpResult.append("<%s%s>%s\n" % (h2, hStyle, tHead, h2)) + tmpResult.append("<%s%s>%s%s\n" % (h2, hStyle, aNm, tHead, h2)) elif tType == self.T_HEAD3: tHead = tText.replace(r"\\", "
") - tmpResult.append("<%s%s>%s\n" % (h3, hStyle, tHead, h3)) + tmpResult.append("<%s%s>%s%s\n" % (h3, hStyle, aNm, tHead, h3)) elif tType == self.T_HEAD4: tHead = tText.replace(r"\\", "
") - tmpResult.append("<%s%s>%s\n" % (h4, hStyle, tHead, h4)) + tmpResult.append("<%s%s>%s%s\n" % (h4, hStyle, aNm, tHead, h4)) elif tType == self.T_SEP: tmpResult.append("

%s

\n" % tText) @@ -296,17 +301,17 @@ class ToHtml(Tokenizer): refTags = [] if theBits[0] in nwLabels.KEY_NAME: retText += "%s: " % nwLabels.KEY_NAME[theBits[0]] - if self.genMode == self.M_PREVIEW: - for tTag in theBits[1:]: - refTags.append("%s" % ( - theBits[0][1:], tTag, tTag - )) - retText += ", ".join(refTags) + if theBits[0] == nwKeyWords.TAG_KEY: + retText += "%s" % ( + theBits[1], theBits[1] + ) else: - if theBits[0] == nwKeyWords.TAG_KEY: - retText += "%s" % ( - theBits[1], theBits[1] - ) + if self.genMode == self.M_PREVIEW: + for tTag in theBits[1:]: + refTags.append("%s" % ( + theBits[0][1:], tTag, tTag + )) + retText += ", ".join(refTags) else: for tTag in theBits[1:]: refTags.append("%s" % ( diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 2fa784f5..fba25fa3 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -102,6 +102,8 @@ class Tokenizer(): self.hideScene = False # Do not include scene headers self.hideSection = False # Do not include section headers + self.linkHeaders = False # Add an anchor before headers + # Instance Variables self.numChapter = 0 # Counter for chapter numbers self.numChScene = 0 # Counter for scene number within chapter @@ -174,6 +176,10 @@ class Tokenizer(): self.hideSection = hideSection return + def setLinkHeaders(self, linkHeaders): + self.linkHeaders = linkHeaders + return + def setBodyText(self, doBodyText): self.doBodyText = doBodyText return @@ -307,12 +313,14 @@ class Tokenizer(): self.theTokens = [] self.theMarkdown = "" tmpMarkdown = [] + nLine = 0 for aLine in self.theText.splitlines(): + nLine += 1 # Tag lines starting with specific characters if len(aLine.strip()) == 0: self.theTokens.append(( - self.T_EMPTY, "", None, self.A_NONE + self.T_EMPTY, nLine, "", None, self.A_NONE )) tmpMarkdown.append("\n") @@ -320,45 +328,45 @@ class Tokenizer(): cLine = aLine[1:].strip() if cLine.lower().startswith("synopsis:"): self.theTokens.append(( - self.T_SYNOPSIS, cLine[9:].strip(), None, self.A_NONE + self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE )) if self.doSynopsis: tmpMarkdown.append("%s\n" % aLine) else: self.theTokens.append(( - self.T_COMMENT, aLine[1:].strip(), None, self.A_NONE + self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE )) if self.doComments: tmpMarkdown.append("%s\n" % aLine) elif aLine[0] == "@": self.theTokens.append(( - self.T_KEYWORD, aLine[1:].strip(), None, self.A_NONE + self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE )) if self.doKeywords: tmpMarkdown.append("%s\n" % aLine) elif aLine[:2] == "# ": self.theTokens.append(( - self.T_HEAD1, aLine[2:].strip(), None, self.A_NONE + self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) elif aLine[:3] == "## ": self.theTokens.append(( - self.T_HEAD2, aLine[3:].strip(), None, self.A_NONE + self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) elif aLine[:4] == "### ": self.theTokens.append(( - self.T_HEAD3, aLine[4:].strip(), None, self.A_NONE + self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) elif aLine[:5] == "#### ": self.theTokens.append(( - self.T_HEAD4, aLine[5:].strip(), None, self.A_NONE + self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) @@ -383,13 +391,13 @@ class Tokenizer(): # sorted by position fmtPos = sorted(fmtPos, key=itemgetter(0)) self.theTokens.append(( - self.T_TEXT, aLine, fmtPos, self.A_NONE + self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) # Always add an empty line at the end self.theTokens.append(( - self.T_EMPTY, "", None, self.A_NONE + self.T_EMPTY, nLine, "", None, self.A_NONE )) tmpMarkdown.append("\n") @@ -414,7 +422,8 @@ class Tokenizer(): tToken = self.theTokens[n] tType = tToken[0] - tText = tToken[1] + tLine = tToken[1] + tText = tToken[2] # In case we see text before a scene, we reset the flag if tType == self.T_TEXT: @@ -426,7 +435,7 @@ class Tokenizer(): tText = self._formatHeading(self.fmtTitle, tText) self.theTokens[n] = ( - tType, tText, None, self.A_NONE + tType, tLine, tText, None, self.A_NONE ) elif tType == self.T_HEAD2: @@ -442,7 +451,7 @@ class Tokenizer(): # Format the chapter header self.theTokens[n] = ( - tType, tText, None, self.A_PBB + tType, tLine, tText, None, self.A_PBB ) # Set scene variables @@ -459,29 +468,29 @@ class Tokenizer(): tTemp = self._formatHeading(self.fmtScene, tText) if tTemp == "" and self.hideScene: self.theTokens[n] = ( - self.T_EMPTY, "", None, self.A_NONE + self.T_EMPTY, tLine, "", None, self.A_NONE ) elif tTemp == "" and not self.hideScene: if self.firstScene: self.theTokens[n] = ( - self.T_EMPTY, "", None, self.A_NONE + self.T_EMPTY, tLine, "", None, self.A_NONE ) else: self.theTokens[n] = ( - self.T_SKIP, "", None, self.A_NONE + self.T_SKIP, tLine, "", None, self.A_NONE ) elif tTemp == self.fmtScene: if self.firstScene: self.theTokens[n] = ( - self.T_EMPTY, "", None, self.A_NONE + self.T_EMPTY, tLine, "", None, self.A_NONE ) else: self.theTokens[n] = ( - self.T_SEP, tTemp, None, self.A_CENTRE + self.T_SEP, tLine, tTemp, None, self.A_CENTRE ) else: self.theTokens[n] = ( - tType, tTemp, None, self.A_NONE + tType, tLine, tTemp, None, self.A_NONE ) # Definitely no longer the first scene @@ -494,19 +503,19 @@ class Tokenizer(): tTemp = self._formatHeading(self.fmtSection, tText) if tTemp == "" and self.hideSection: self.theTokens[n] = ( - self.T_EMPTY, "", None, self.A_NONE + self.T_EMPTY, tLine, "", None, self.A_NONE ) elif tTemp == "" and not self.hideSection: self.theTokens[n] = ( - self.T_SKIP, "", None, self.A_NONE + self.T_SKIP, tLine, "", None, self.A_NONE ) elif tTemp == self.fmtSection: self.theTokens[n] = ( - self.T_SEP, tTemp, None, self.A_CENTRE + self.T_SEP, tLine, tTemp, None, self.A_CENTRE ) else: self.theTokens[n] = ( - tType, tTemp, None, self.A_NONE + tType, tLine, tTemp, None, self.A_NONE ) # For title page and partitions, we need to centre all text. @@ -516,20 +525,21 @@ class Tokenizer(): if self.isTitle or self.isPart: for n, tToken in enumerate(self.theTokens): tType = tToken[0] - tText = tToken[1] - tFormat = tToken[2] + tLine = tToken[1] + tText = tToken[2] + tFormat = tToken[3] if tType == self.T_HEAD1: if self.isTitle: self.theTokens[n] = ( - self.T_TITLE, tText, tFormat, self.A_PBB_NO | self.A_CENTRE + self.T_TITLE, tLine, tText, tFormat, self.A_PBB_NO | self.A_CENTRE ) else: self.theTokens[n] = ( - tType, tText, tFormat, self.A_PBB | self.A_CENTRE + tType, tLine, tText, tFormat, self.A_PBB | self.A_CENTRE ) else: self.theTokens[n] = ( - tType, tText, tFormat, self.A_CENTRE + tType, tLine, tText, tFormat, self.A_CENTRE ) # Add a page break after the last entry @@ -537,7 +547,7 @@ class Tokenizer(): if n >= 0: tToken = self.theTokens[n] self.theTokens[n] = ( - tToken[0], tToken[1], tToken[2], tToken[3] | self.A_PBA + tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA ) # A single page is always left-aligned and starts on a fresh @@ -545,15 +555,16 @@ class Tokenizer(): if self.isPage: for n, tToken in enumerate(self.theTokens): tType = tToken[0] - tText = tToken[1] - tFormat = tToken[2] + tLine = tToken[1] + tText = tToken[2] + tFormat = tToken[3] if n == 0: self.theTokens[n] = ( - tType, tText, tFormat, self.A_LEFT | self.A_PBB + tType, tLine, tText, tFormat, self.A_LEFT | self.A_PBB ) else: self.theTokens[n] = ( - tType, tText, tFormat, self.A_LEFT + tType, tLine, tText, tFormat, self.A_LEFT ) return From 4340b6b8b5725323c9366712f27ec54bdf9ffd56 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 25 May 2020 23:31:41 +0200 Subject: [PATCH 03/38] It should now be possible to navigate to positions within the documents when clicking links in various places --- nw/core/index.py | 21 +++++++------- nw/gui/elements/docviewer.py | 52 +++++++++++++++------------------- nw/gui/elements/viewdetails.py | 15 +++------- nw/guimain.py | 4 +-- 4 files changed, 40 insertions(+), 52 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 79e6ddba..456b415c 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -199,7 +199,7 @@ class NWIndex(): try: for tTag in self.tagIndex: - if len(self.tagIndex[tTag]) != 3: + if len(self.tagIndex[tTag]) != 4: self.indexBroken = True for tHandle in self.refIndex: @@ -228,7 +228,7 @@ class NWIndex(): if self.indexBroken: self.clearIndex() self.theParent.makeAlert( - "The index loaded from project cache contains errors. Rebuilding index.", + "The project index is outdated or broken. Rebuilding index.", nwAlert.WARN ) @@ -301,7 +301,7 @@ class NWIndex(): elif aLine.startswith(r"@"): self._indexNoteRef(tHandle, aLine, nLine, nTitle) - self._indexTag(tHandle, aLine, nLine, itemClass) + self._indexTag(tHandle, aLine, nLine, nTitle, itemClass) elif aLine.startswith(r"%"): if nTitle > 0: @@ -436,7 +436,7 @@ class NWIndex(): return True - def _indexTag(self, tHandle, aLine, nLine, itemClass): + def _indexTag(self, tHandle, aLine, nLine, nTitle, itemClass): """Validate and save the information from a tag. """ isValid, theBits, thePos = self.scanThis(aLine) @@ -444,7 +444,8 @@ class NWIndex(): return False if theBits[0] == nwKeyWords.TAG_KEY: - self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name] + sTitle = "T%06d" % nTitle + self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle] return True @@ -606,10 +607,10 @@ class NWIndex(): if tHandle is None: return theRefs - theTags = [] + theTags = set() for tTag in self.tagIndex: if tHandle == self.tagIndex[tTag][1]: - theTags.append(tTag) + theTags.add(tTag) if theTags: for tHandle in self.refIndex: @@ -625,8 +626,8 @@ class NWIndex(): """ if theTag in self.tagIndex: theRef = self.tagIndex[theTag] - if len(theRef) == 3: - return theRef[1], theRef[0] - return None, 0 + if len(theRef) == 4: + return theRef[1], theRef[0], theRef[3] + return None, 0, "T000000" # END Class NWIndex diff --git a/nw/gui/elements/docviewer.py b/nw/gui/elements/docviewer.py index 6d90c2f5..df1355be 100644 --- a/nw/gui/elements/docviewer.py +++ b/nw/gui/elements/docviewer.py @@ -28,7 +28,7 @@ import logging import nw -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QUrl from PyQt5.QtWidgets import QTextBrowser from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor @@ -133,6 +133,7 @@ class GuiDocViewer(QTextBrowser): sPos = self.verticalScrollBar().value() aDoc = ToHtml(self.theProject, self.theParent) aDoc.setPreview(True, self.mainConf.viewComments) + aDoc.setLinkHeaders(True) aDoc.setText(tHandle) aDoc.doAutoReplace() aDoc.tokenizeText() @@ -162,19 +163,17 @@ class GuiDocViewer(QTextBrowser): index being up to date. """ logger.debug("Loading document from tag '%s'" % theTag) - - if theTag in self.theParent.theIndex.tagIndex.keys(): - theTarget = self.theParent.theIndex.tagIndex[theTag] + tHandle, onLine, sTitle = self.theParent.theIndex.getTagSource(theTag) + if tHandle is None: + self.theParent.makeAlert(( + "Could not find the reference for tag '%s'. It either doesn't " + "exist, or the index is out of date. The index can be updated " + "from the Tools menu, or by pressing F9." + ) % theTag, nwAlert.ERROR) + return else: - logger.debug("The tag was not found in the index") - return False - - if len(theTarget) != 3: - # Just to make sure the index is not messed up - return False - - self.loadText(theTarget[1]) - + self.loadText(tHandle) + self.navigateTo("#head_%s:%s" % (tHandle, sTitle)) return True def docAction(self, theAction): @@ -198,6 +197,15 @@ class GuiDocViewer(QTextBrowser): return False return True + def navigateTo(self, navLink): + """Go to a specific #link in the document. + """ + if not isinstance(navLink, str): + return False + if navLink.startswith("#"): + self.setSource(QUrl(navLink)) + return True + def updateDocTitle(self, tHandle): """Called when an item label is changed to check if the document title bar needs updating, @@ -271,25 +279,11 @@ class GuiDocViewer(QTextBrowser): """Slot for a link in the document being clicked. """ theLink = theURL.url() - tHandle = None - onLine = 0 - theTag = "" + logger.verbose("Clicked link: '%s'" % theLink) if len(theLink) > 0: theBits = theLink.split("=") if len(theBits) == 2: - theTag = theBits[1] - tHandle, onLine = self.theParent.theIndex.getTagSource(theBits[1]) - - if tHandle is None: - self.theParent.makeAlert(( - "Could not find the reference for tag '%s'. It either doesn't exist, or the index " - "is out of date. The index can be updated from the Tools menu.") % theTag, - nwAlert.ERROR - ) - return - else: - self.loadText(tHandle) - + self.loadFromTag(theBits[1]) return def _makeStyleSheet(self): diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/elements/viewdetails.py index d5ab7ee8..d4b2621c 100644 --- a/nw/gui/elements/viewdetails.py +++ b/nw/gui/elements/viewdetails.py @@ -107,12 +107,10 @@ class GuiDocViewDetails(QWidget): for tHandle in theRefs: tItem = self.theProject.projTree[tHandle] if tItem is not None: - theList.append("%s" % ( + theList.append("%s" % ( tHandle, theRefs[tHandle], tItem.itemName )) - # print(theList) - self.refList.setText(", ".join(theList)) self.refList.adjustSize() @@ -127,14 +125,9 @@ class GuiDocViewDetails(QWidget): class for handling. """ logger.verbose("Clicked link: '%s'" % theLink) - if len(theLink) == 26: - tHandle = theLink[5:18] - tLine = theLink[19:26] - if tLine[1:].isdigit(): - nLine = int(tLine[1:]) - else: - nLine = 1 - self.theParent.viewDocument(tHandle, nLine) + if len(theLink) == 27: + tHandle = theLink[6:19] + self.theParent.viewDocument(tHandle, theLink) return def _doShowHide(self, chState): diff --git a/nw/guimain.py b/nw/guimain.py index 8aeb7143..f0d0d3cc 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -485,7 +485,7 @@ class GuiMain(QMainWindow): self.docEditor.saveText() return True - def viewDocument(self, tHandle=None, nLine=0): + def viewDocument(self, tHandle=None, navLink=None): """Load a document for viewing in the view panel. """ if tHandle is None: @@ -511,7 +511,7 @@ class GuiMain(QMainWindow): vPos[0] = int(bPos[1]/2) vPos[1] = bPos[1]-vPos[0] self.splitView.setSizes(vPos) - self.docViewer.setCursorLine(nLine) + self.docViewer.navigateTo(navLink) return True From 494997cc202f8469e3c6bacbb6329f3c86b7c872 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 25 May 2020 23:31:59 +0200 Subject: [PATCH 04/38] Fixed tests --- sample/nwProject.nwx | 4 ++-- tests/test_project.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index c2d74892..2dce2260 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -11,7 +11,7 @@ True True ae7339df26ded - ae7339df26ded + bb2c23b3c42cc 941 B diff --git a/tests/test_project.py b/tests/test_project.py index 369c866d..44821856 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -155,7 +155,7 @@ def testIndexCheckThese(nwTempProj): "# Hello World!\n" "@pov: Jane" )) - assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER']}" + assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}" assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]" @@ -195,7 +195,7 @@ def testIndexMeta(nwTempProj): "\n" "Well, not really.\n" )) - assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER']}" + assert str(theIndex.tagIndex) == "{'Jane': [2, '2858dcd1057d3', 'CHARACTER', 'T000001']}" assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" # The novel structure should contain the pointer to the novel file header @@ -214,6 +214,6 @@ def testIndexMeta(nwTempProj): # The character file should have a record of the reference from the novel file theRefs = theIndex.getBackReferenceList(cHandle) - assert str(theRefs) == "{'41cfc0d1f2d12': 3}" + assert str(theRefs) == "{'41cfc0d1f2d12': 'T000001'}" assert theProject.closeProject() From 03751c5ff9db4a764256d831f3ca5a7b38d62248 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 May 2020 20:31:09 +0200 Subject: [PATCH 05/38] No more document .bak files, .nwd~ works just fine. --- nw/core/document.py | 14 +++++++------ sample/data_b/8136a5a774a0_main.nwd | 2 +- sample/data_e/dca4be2fcaf8_main.nwd | 2 +- sample/nwProject.nwx | 32 ++++++++++++++--------------- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index 4c502bea..4cfd09a3 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -147,12 +147,10 @@ class NWDoc(): mkdir(dataPath) logger.debug("Created folder %s" % dataPath) - docTemp = path.join(dataPath, docFile+"~") - docBack = path.join(dataPath, docFile[:-3]+"bak") - itemPath = self.theProject.projTree.getItemPath(self.docHandle) docMeta = "%%~ "+":".join(itemPath)+":"+self.theItem.itemName+"\n" + docTemp = path.join(dataPath, docFile+"~") try: with open(docTemp,mode="w",encoding="utf8") as outFile: outFile.write(docMeta) @@ -161,12 +159,16 @@ class NWDoc(): self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR) return False - # If we're here, the file was successfully saved, - # so let's sort out the temps and backups + # Remove bak files from old file save method, if one exists + # This part can eventually be removed + docBack = path.join(dataPath, docFile[:-3]+"bak") if path.isfile(docBack): unlink(docBack) + + # If we're here, the file was successfully saved, so we can + # replace the temp file with the actual file if path.isfile(docPath): - rename(docPath, docBack) + unlink(docPath) rename(docTemp, docPath) self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName) diff --git a/sample/data_b/8136a5a774a0_main.nwd b/sample/data_b/8136a5a774a0_main.nwd index 4e63373d..a304cb35 100644 --- a/sample/data_b/8136a5a774a0_main.nwd +++ b/sample/data_b/8136a5a774a0_main.nwd @@ -1,4 +1,4 @@ -%%~ b8136a5a774a0:98acd8c76c93a:Delete Me! +%%~ b8136a5a774a0:7031beac91f75:Delete Me! ### Delete Me! This scene is trash. \ No newline at end of file diff --git a/sample/data_e/dca4be2fcaf8_main.nwd b/sample/data_e/dca4be2fcaf8_main.nwd index 679e4e27..5fef1f34 100644 --- a/sample/data_e/dca4be2fcaf8_main.nwd +++ b/sample/data_e/dca4be2fcaf8_main.nwd @@ -1,4 +1,4 @@ -%%~ edca4be2fcaf8:7031beac91f75:Part 1 +%%~ edca4be2fcaf8:7031beac91f75:Part One # Part One The first part. \ No newline at end of file diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index ad832035..e114568a 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -12,7 +12,7 @@ True 636b6aa9b697b ba8a28a246524 - 914 + 920 B E @@ -73,7 +73,7 @@ False True PAGE - 208 + 210 40 2 213 @@ -89,7 +89,7 @@ 23 5 1 - 0 + 27
A Folder @@ -122,7 +122,7 @@ 1199 216 7 - 527 + 825 Another Scene @@ -135,7 +135,7 @@ 476 93 3 - 551 + 428 Interlude @@ -148,7 +148,7 @@ 633 101 3 - 1238 + 752 A Note on Structure @@ -161,7 +161,7 @@ 1692 313 6 - 1721 + 551 Chapter Two @@ -174,7 +174,7 @@ 139 28 1 - 343 + 242 We Found John! @@ -214,7 +214,7 @@ 49 9 1 - 24 + 65 Jane Smith @@ -227,7 +227,7 @@ 55 9 1 - 25 + 71 Locations @@ -247,7 +247,7 @@ 76 15 1 - 20 + 93 Space @@ -260,7 +260,7 @@ 115 24 1 - 133 + 135 Mars @@ -290,9 +290,9 @@ False True SCENE - 0 - 0 - 0 + 30 + 6 + 1 36 From a758bae4e7b765e8821d70d358f80fe46d8ebb8e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 May 2020 21:32:10 +0200 Subject: [PATCH 06/38] Updated chaneglog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44965876..462dc91d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # novelWriter ChangeLog +## Version 0.7 [2020-xx-xx] + +**Other Changes** + +* Dropped the usage of .bak copies of document files. This was the old method to ensure the document data was written successfully, but it uses twice the storage space. Instead, writing via a temp file is the safe way to save files. PR #248. + ## Not Yet Released **Bugfixes** From 492023fed5ef94a2731a8d141636d85adbb792dd Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 27 May 2020 21:52:44 +0200 Subject: [PATCH 07/38] Seems to be an issue with PyVirtualDisplay in the build --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index de4a683a..89ca3727 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ install: - pip install --upgrade pip - pip install -r requirements.txt # - pip install pytest-faulthandler + - pip install PyVirtualDisplay==0.2.5 - pip install pytest-xvfb - pip install pytest-cov - pip install pytest-qt From 192427deaa69b460963da56c0cf0904e246b9956 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 18:55:44 +0200 Subject: [PATCH 08/38] Changed the data structure of the project folder --- nw/core/document.py | 33 ++++---------- nw/core/project.py | 102 +++++++++++++++++++++++++++++++++++++++++--- nw/core/tools.py | 7 +-- 3 files changed, 104 insertions(+), 38 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index 4cfd09a3..a28804a9 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -93,8 +93,9 @@ class NWDoc(): if self.theItem.parHandle == self.theProject.projTree.trashRoot(): self.docEditable = False - docDir, docFile = self._assemblePath(self.docHandle, self.FILE_MN) - self.fileLoc = path.join(docDir,docFile) + docDir = "content" + docFile = self.docHandle+".nwd" + self.fileLoc = path.join(docDir, docFile) logger.debug("Opening document %s" % self.fileLoc) dataDir = path.join(self.theProject.projPath, docDir) docPath = path.join(dataDir, docFile) @@ -139,8 +140,9 @@ class NWDoc(): if self.docHandle is None or not self.docEditable: return False - docDir, docFile = self._assemblePath(self.docHandle, self.FILE_MN) - logger.debug("Saving document %s" % path.join(docDir,docFile)) + docDir = "content" + docFile = self.docHandle+".nwd" + logger.debug("Saving document %s" % path.join(docDir, docFile)) dataPath = path.join(self.theProject.projPath, docDir) docPath = path.join(dataPath, docFile) if not path.isdir(dataPath): @@ -159,12 +161,6 @@ class NWDoc(): self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR) return False - # Remove bak files from old file save method, if one exists - # This part can eventually be removed - docBack = path.join(dataPath, docFile[:-3]+"bak") - if path.isfile(docBack): - unlink(docBack) - # If we're here, the file was successfully saved, so we can # replace the temp file with the actual file if path.isfile(docPath): @@ -179,7 +175,8 @@ class NWDoc(): """Permanently delete a document source file and its backups from the project data folder. """ - docDir, docFile = self._assemblePath(tHandle, self.FILE_MN) + docDir = "content" + docFile = self.docHandle+".nwd" dataPath = path.join(self.theProject.projPath, docDir) chkList = [] chkList.append(path.join(dataPath, docFile)) @@ -226,18 +223,4 @@ class NWDoc(): return theMeta, thePath - ## - # Internal Functions - ## - - @staticmethod - def _assemblePath(tHandle, docExt): - """Assemble the file path for a given handle. - """ - if tHandle is None: - return None, None - docDir = "data_"+tHandle[0] - docFile = tHandle[1:13]+"_"+docExt - return docDir, docFile - # END Class NWDoc diff --git a/nw/core/project.py b/nw/core/project.py index 0c303f74..fbf04177 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -33,7 +33,7 @@ import logging import nw -from os import path, mkdir, listdir, unlink, rename +from os import path, mkdir, listdir, unlink, rename, rmdir from lxml import etree from hashlib import sha256 from time import time @@ -74,6 +74,7 @@ class NWProject(): # Class Settings self.projPath = None # The full path to where the currently open project is saved self.projMeta = None # The full path to the project's meta data folder + self.projData = None # The full path to the project's data folder self.projDict = None # The spell check dictionary self.projFile = None # The file name of the project main XML file @@ -195,6 +196,7 @@ class NWProject(): # Project Settings self.projPath = None self.projMeta = None + self.projData = None self.projDict = None self.projFile = nwFiles.PROJ_FILE self.projName = "" @@ -247,10 +249,13 @@ class NWProject(): logger.debug("Opening project: %s" % self.projPath) self.projMeta = path.join(self.projPath,"meta") + self.projData = path.join(self.projPath,"content") self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) if not self._checkFolder(self.projMeta): return False + if not self._checkFolder(self.projData): + return False if overrideLock: self._clearLockFile() @@ -291,7 +296,7 @@ class NWProject(): self.clearProject() return False - xRoot = nwXML.getroot() + xRoot = nwXML.getroot() nwxRoot = xRoot.tag appVersion = "Unknown" @@ -314,13 +319,40 @@ class NWProject(): logger.verbose("XML root is %s" % nwxRoot) logger.verbose("File version is %s" % fileVersion) - if not nwxRoot == "novelWriterXML" or not fileVersion == "1.0": + # Check File Type + # =============== + if not nwxRoot == "novelWriterXML": self.makeAlert( - "Project file does not appear to be a novelWriterXML file version 1.0", + "Project file does not appear to be a novelWriterXML file.", nwAlert.ERROR ) return False + # Check Project Storage Version + # ============================= + if fileVersion == "1.0": + msgBox = QMessageBox() + msgRes = msgBox.question(self.theParent, "Old Project Version", ( + "The project file and data is created by a %s version lower than 0.7. " + "Do you want to upgrade the project to the most recent format?

" + "Note that after the upgrade, you cannot open the project with an older " + "version of novelWriter any more, so make sure you have a recent backup." + ) % nw.__package__) + if msgRes == QMessageBox.Yes: + self._updateStorage() + else: + return False + elif fileVersion != "1.1": + self.makeAlert(( + "Unknown or unsupported %s project format. " + "The project cannot be opened by this version of %s." + ) % ( + nw.__package__, nw.__package__ + ), nwAlert.ERROR) + return False + + # Check novelWriter Version + # ========================= if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Version Conflict", ( @@ -333,6 +365,8 @@ class NWProject(): if msgRes != QMessageBox.Yes: return False + # Start Parsing XML + # ================= for xChild in xRoot: if xChild.tag == "project": logger.debug("Found project meta") @@ -404,16 +438,21 @@ class NWProject(): file. """ if self.projPath is None: - self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR) + self.makeAlert( + "Project path not set, cannot save project.", nwAlert.ERROR + ) return False - self.projMeta = path.join(self.projPath,"meta") + self.projMeta = path.join(self.projPath, "meta") + self.projData = path.join(self.projPath, "content") saveTime = time() if not self._checkFolder(self.projPath): return False if not self._checkFolder(self.projMeta): return False + if not self._checkFolder(self.projData): + return False logger.debug("Saving project: %s" % self.projPath) @@ -427,7 +466,7 @@ class NWProject(): nwXML = etree.Element("novelWriterXML",attrib={ "appVersion" : str(nw.__version__), "hexVersion" : str(nw.__hexversion__), - "fileVersion" : "1.0", + "fileVersion" : "1.1", "saveCount" : str(self.saveCount), "autoCount" : str(self.autoCount), "timeStamp" : formatTimeStamp(saveTime), @@ -1016,6 +1055,55 @@ class NWProject(): return True + def _updateStorage(self): + """Updates the project storage folder from 1.0 to 1.1. + """ + contDir = path.join(self.projPath, "content") + self._checkFolder(contDir) + errList = [] + + for projItem in listdir(self.projPath): + itemPath = path.join(self.projPath, projItem) + if not path.isdir(itemPath) or not projItem.startswith("data_"): + continue + for dataFile in listdir(itemPath): + dataPath = path.join(itemPath, dataFile) + if dataFile.endswith(".bak"): + try: + unlink(dataPath) + logger.info("Deleted file: %s" % dataPath) + except: + errList.append("Failed to delete: %s" % dataPath) + + elif dataFile.endswith(".nwd") and len(dataFile) == 21: + tHandle = projItem[-1]+dataFile[:12] + newPath = path.join(contDir, tHandle+".nwd") + try: + rename(dataPath, newPath) + logger.info("Moved file: %s" % dataPath) + logger.info("New location: %s" % newPath) + except: + errList.append("Failed to move: %s" % dataPath) + + else: + newPath = path.join(self.projPath, "unknown_"+dataFile) + try: + rename(dataPath, newPath) + logger.info("Moved file: %s" % dataPath) + logger.info("New location: %s" % newPath) + except: + errList.append("Failed to move: %s" % dataPath) + try: + rmdir(itemPath) + logger.info("Removed folder: %s" % itemPath) + except: + errList.append("Failed to delete: %s" % itemPath) + + if errList: + self.makeAlert(errList, nwAlert.ERROR) + + return + # END Class NWProject # ================================================================================================ # diff --git a/nw/core/tools.py b/nw/core/tools.py index a6eb244b..8aa81d7b 100644 --- a/nw/core/tools.py +++ b/nw/core/tools.py @@ -89,7 +89,7 @@ def projectMaintenance(theProject): if path.isdir(theProject.projPath): cacheDir = path.join(theProject.projPath, "cache") if path.isdir(cacheDir): - logger.info("Deprecated cache folder found") + logger.info("Deprecated cache folder content found") rmList = [] for i in range(10): rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i)) @@ -101,11 +101,6 @@ def projectMaintenance(theProject): unlink(rmFile) except Exception as e: logger.error(str(e)) - logger.info("Deleting: %s" % cacheDir) - try: - rmdir(cacheDir) - except Exception as e: - logger.error(str(e)) # Remove no longer used meta files rmList = [] From 6fac354f691cceb1d011cfec3f6186babde9a4a0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 18:56:58 +0200 Subject: [PATCH 09/38] Added the converted sample project --- .../{data_1/4298de4d9524_main.nwd => content/14298de4d9524.nwd} | 0 .../{data_5/3b69b83cdafc_main.nwd => content/53b69b83cdafc.nwd} | 0 .../{data_5/eaea4e8cdee8_main.nwd => content/5eaea4e8cdee8.nwd} | 0 .../{data_6/36b6aa9b697b_main.nwd => content/636b6aa9b697b.nwd} | 0 .../{data_6/a2d6d5f4f401_main.nwd => content/6a2d6d5f4f401.nwd} | 0 .../{data_8/8706ddc78b1b_main.nwd => content/88706ddc78b1b.nwd} | 0 .../{data_9/6b68994dfa3d_main.nwd => content/96b68994dfa3d.nwd} | 0 .../{data_9/74e400180a99_main.nwd => content/974e400180a99.nwd} | 0 .../{data_a/e7339df26ded_main.nwd => content/ae7339df26ded.nwd} | 0 .../{data_b/3e74dbc1f584_main.nwd => content/b3e74dbc1f584.nwd} | 0 .../{data_b/8136a5a774a0_main.nwd => content/b8136a5a774a0.nwd} | 0 .../{data_b/a8a28a246524_main.nwd => content/ba8a28a246524.nwd} | 0 .../{data_b/b2c23b3c42cc_main.nwd => content/bb2c23b3c42cc.nwd} | 0 .../{data_b/c0cbd2a407f3_main.nwd => content/bc0cbd2a407f3.nwd} | 0 .../{data_e/dca4be2fcaf8_main.nwd => content/edca4be2fcaf8.nwd} | 0 .../{data_f/1471bef9f2ae_main.nwd => content/f1471bef9f2ae.nwd} | 0 sample/nwProject.nwx | 2 +- 17 files changed, 1 insertion(+), 1 deletion(-) rename sample/{data_1/4298de4d9524_main.nwd => content/14298de4d9524.nwd} (100%) rename sample/{data_5/3b69b83cdafc_main.nwd => content/53b69b83cdafc.nwd} (100%) rename sample/{data_5/eaea4e8cdee8_main.nwd => content/5eaea4e8cdee8.nwd} (100%) rename sample/{data_6/36b6aa9b697b_main.nwd => content/636b6aa9b697b.nwd} (100%) rename sample/{data_6/a2d6d5f4f401_main.nwd => content/6a2d6d5f4f401.nwd} (100%) rename sample/{data_8/8706ddc78b1b_main.nwd => content/88706ddc78b1b.nwd} (100%) rename sample/{data_9/6b68994dfa3d_main.nwd => content/96b68994dfa3d.nwd} (100%) rename sample/{data_9/74e400180a99_main.nwd => content/974e400180a99.nwd} (100%) rename sample/{data_a/e7339df26ded_main.nwd => content/ae7339df26ded.nwd} (100%) rename sample/{data_b/3e74dbc1f584_main.nwd => content/b3e74dbc1f584.nwd} (100%) rename sample/{data_b/8136a5a774a0_main.nwd => content/b8136a5a774a0.nwd} (100%) rename sample/{data_b/a8a28a246524_main.nwd => content/ba8a28a246524.nwd} (100%) rename sample/{data_b/b2c23b3c42cc_main.nwd => content/bb2c23b3c42cc.nwd} (100%) rename sample/{data_b/c0cbd2a407f3_main.nwd => content/bc0cbd2a407f3.nwd} (100%) rename sample/{data_e/dca4be2fcaf8_main.nwd => content/edca4be2fcaf8.nwd} (100%) rename sample/{data_f/1471bef9f2ae_main.nwd => content/f1471bef9f2ae.nwd} (100%) diff --git a/sample/data_1/4298de4d9524_main.nwd b/sample/content/14298de4d9524.nwd similarity index 100% rename from sample/data_1/4298de4d9524_main.nwd rename to sample/content/14298de4d9524.nwd diff --git a/sample/data_5/3b69b83cdafc_main.nwd b/sample/content/53b69b83cdafc.nwd similarity index 100% rename from sample/data_5/3b69b83cdafc_main.nwd rename to sample/content/53b69b83cdafc.nwd diff --git a/sample/data_5/eaea4e8cdee8_main.nwd b/sample/content/5eaea4e8cdee8.nwd similarity index 100% rename from sample/data_5/eaea4e8cdee8_main.nwd rename to sample/content/5eaea4e8cdee8.nwd diff --git a/sample/data_6/36b6aa9b697b_main.nwd b/sample/content/636b6aa9b697b.nwd similarity index 100% rename from sample/data_6/36b6aa9b697b_main.nwd rename to sample/content/636b6aa9b697b.nwd diff --git a/sample/data_6/a2d6d5f4f401_main.nwd b/sample/content/6a2d6d5f4f401.nwd similarity index 100% rename from sample/data_6/a2d6d5f4f401_main.nwd rename to sample/content/6a2d6d5f4f401.nwd diff --git a/sample/data_8/8706ddc78b1b_main.nwd b/sample/content/88706ddc78b1b.nwd similarity index 100% rename from sample/data_8/8706ddc78b1b_main.nwd rename to sample/content/88706ddc78b1b.nwd diff --git a/sample/data_9/6b68994dfa3d_main.nwd b/sample/content/96b68994dfa3d.nwd similarity index 100% rename from sample/data_9/6b68994dfa3d_main.nwd rename to sample/content/96b68994dfa3d.nwd diff --git a/sample/data_9/74e400180a99_main.nwd b/sample/content/974e400180a99.nwd similarity index 100% rename from sample/data_9/74e400180a99_main.nwd rename to sample/content/974e400180a99.nwd diff --git a/sample/data_a/e7339df26ded_main.nwd b/sample/content/ae7339df26ded.nwd similarity index 100% rename from sample/data_a/e7339df26ded_main.nwd rename to sample/content/ae7339df26ded.nwd diff --git a/sample/data_b/3e74dbc1f584_main.nwd b/sample/content/b3e74dbc1f584.nwd similarity index 100% rename from sample/data_b/3e74dbc1f584_main.nwd rename to sample/content/b3e74dbc1f584.nwd diff --git a/sample/data_b/8136a5a774a0_main.nwd b/sample/content/b8136a5a774a0.nwd similarity index 100% rename from sample/data_b/8136a5a774a0_main.nwd rename to sample/content/b8136a5a774a0.nwd diff --git a/sample/data_b/a8a28a246524_main.nwd b/sample/content/ba8a28a246524.nwd similarity index 100% rename from sample/data_b/a8a28a246524_main.nwd rename to sample/content/ba8a28a246524.nwd diff --git a/sample/data_b/b2c23b3c42cc_main.nwd b/sample/content/bb2c23b3c42cc.nwd similarity index 100% rename from sample/data_b/b2c23b3c42cc_main.nwd rename to sample/content/bb2c23b3c42cc.nwd diff --git a/sample/data_b/c0cbd2a407f3_main.nwd b/sample/content/bc0cbd2a407f3.nwd similarity index 100% rename from sample/data_b/c0cbd2a407f3_main.nwd rename to sample/content/bc0cbd2a407f3.nwd diff --git a/sample/data_e/dca4be2fcaf8_main.nwd b/sample/content/edca4be2fcaf8.nwd similarity index 100% rename from sample/data_e/dca4be2fcaf8_main.nwd rename to sample/content/edca4be2fcaf8.nwd diff --git a/sample/data_f/1471bef9f2ae_main.nwd b/sample/content/f1471bef9f2ae.nwd similarity index 100% rename from sample/data_f/1471bef9f2ae_main.nwd rename to sample/content/f1471bef9f2ae.nwd diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 481ef3c1..5c33e33c 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project From 7373f9636b56d411c22ca04abf65977661076a3c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 19:03:59 +0200 Subject: [PATCH 10/38] Updated orphan file check --- nw/core/project.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index fbf04177..96e22103 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -976,29 +976,20 @@ class NWProject(): if self.projPath is None: return - # First, scan the project data folders - itemList = [] - for subItem in listdir(self.projPath): - if subItem[:5] != "data_": - continue - dataDir = path.join(self.projPath,subItem) - for subFile in listdir(dataDir): - if subFile[-4:] == ".nwd": - newItem = path.join(subItem,subFile) - itemList.append(newItem) - - # Then check the valid files + # Then check the files in the data folder orphanFiles = [] - for fileItem in itemList: - if len(fileItem) != 28: - # Just to be safe, shouldn't happen + for fileItem in listdir(self.projData): + if not fileItem.endswith(".nwd"): logger.warning("Skipping file %s" % fileItem) continue - fHandle = fileItem[5]+fileItem[7:19] + if len(fileItem) != 17: + logger.warning("Skipping file %s" % fileItem) + continue + fHandle = fileItem[:13] if fHandle in self.projTree: - logger.debug("Checking file %s, handle %s: OK" % (fileItem,fHandle)) + logger.debug("Checking file %s, handle %s: OK" % (fileItem, fHandle)) else: - logger.debug("Checking file %s, handle %s: Orphaned" % (fileItem,fHandle)) + logger.debug("Checking file %s, handle %s: Orphaned" % (fileItem, fHandle)) orphanFiles.append(fHandle) # Report status From 18132277c31dd69ae5b8d457b6040aa50ecd5e0c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 19:10:33 +0200 Subject: [PATCH 11/38] Fixed tests --- ...e17daca5f3e1_main.nwd => 1_0e17daca5f3e1.nwd} | 0 ...a6562590ef19_main.nwd => 1_1a6562590ef19.nwd} | 0 ...1489056e0916_main.nwd => 1_31489056e0916.nwd} | 0 ...8010bd9270f9_main.nwd => 1_98010bd9270f9.nwd} | 0 tests/test_gui.py | 16 ++++++++-------- 5 files changed, 8 insertions(+), 8 deletions(-) rename tests/reference/gui/{1_e17daca5f3e1_main.nwd => 1_0e17daca5f3e1.nwd} (100%) rename tests/reference/gui/{1_a6562590ef19_main.nwd => 1_1a6562590ef19.nwd} (100%) rename tests/reference/gui/{1_1489056e0916_main.nwd => 1_31489056e0916.nwd} (100%) rename tests/reference/gui/{1_8010bd9270f9_main.nwd => 1_98010bd9270f9.nwd} (100%) diff --git a/tests/reference/gui/1_e17daca5f3e1_main.nwd b/tests/reference/gui/1_0e17daca5f3e1.nwd similarity index 100% rename from tests/reference/gui/1_e17daca5f3e1_main.nwd rename to tests/reference/gui/1_0e17daca5f3e1.nwd diff --git a/tests/reference/gui/1_a6562590ef19_main.nwd b/tests/reference/gui/1_1a6562590ef19.nwd similarity index 100% rename from tests/reference/gui/1_a6562590ef19_main.nwd rename to tests/reference/gui/1_1a6562590ef19.nwd diff --git a/tests/reference/gui/1_1489056e0916_main.nwd b/tests/reference/gui/1_31489056e0916.nwd similarity index 100% rename from tests/reference/gui/1_1489056e0916_main.nwd rename to tests/reference/gui/1_31489056e0916.nwd diff --git a/tests/reference/gui/1_8010bd9270f9_main.nwd b/tests/reference/gui/1_98010bd9270f9.nwd similarity index 100% rename from tests/reference/gui/1_8010bd9270f9_main.nwd rename to tests/reference/gui/1_98010bd9270f9.nwd diff --git a/tests/test_gui.py b/tests/test_gui.py index 47432f51..3d202b18 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -237,14 +237,14 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): # Check the files refFile = path.join(nwTempGUI,"nwProject.nwx") assert cmpFiles(refFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2]) - refFile = path.join(nwTempGUI,"data_0","e17daca5f3e1_main.nwd") - assert cmpFiles(refFile, path.join(nwRef,"gui","1_e17daca5f3e1_main.nwd")) - refFile = path.join(nwTempGUI,"data_9","8010bd9270f9_main.nwd") - assert cmpFiles(refFile, path.join(nwRef,"gui","1_8010bd9270f9_main.nwd")) - refFile = path.join(nwTempGUI,"data_3","1489056e0916_main.nwd") - assert cmpFiles(refFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd")) - refFile = path.join(nwTempGUI,"data_1","a6562590ef19_main.nwd") - assert cmpFiles(refFile, path.join(nwRef,"gui","1_a6562590ef19_main.nwd")) + refFile = path.join(nwTempGUI,"content","0e17daca5f3e1.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_0e17daca5f3e1.nwd")) + refFile = path.join(nwTempGUI,"content","98010bd9270f9.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_98010bd9270f9.nwd")) + refFile = path.join(nwTempGUI,"content","31489056e0916.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_31489056e0916.nwd")) + refFile = path.join(nwTempGUI,"content","1a6562590ef19.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_1a6562590ef19.nwd")) nwGUI.closeMain() # qtbot.stopForInteraction() From bdbfa1d3c333e13aefdf8c72a2199159614674f0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 19:13:51 +0200 Subject: [PATCH 12/38] Some more cleanup --- nw/core/document.py | 2 -- sample/nwProject.nwx | 10 +++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index a28804a9..7ce4de4e 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -37,8 +37,6 @@ logger = logging.getLogger(__name__) class NWDoc(): - FILE_MN = "main.nwd" - def __init__(self, theProject, theParent): self.mainConf = nw.CONFIG diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 5c33e33c..f1d9840c 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -12,7 +12,7 @@ True 636b6aa9b697b ba8a28a246524 - 920 + 914 B E @@ -290,9 +290,9 @@ False True SCENE - 30 - 6 - 1 + 0 + 0 + 0 36 From f7fb98cde5c99849a7735b091359efb4c521954e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 19:23:00 +0200 Subject: [PATCH 13/38] Updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56c45a0c..dfd9e8cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Version 0.7 [2020-xx-xx] +**Project Structure** + +* The project folder structure has been simplified and cleaned up. We also now pin the main entry values in the main XML file. the XML file is now given version 1.1, and locking it to only be opened by version 0.7 or later. The project is converted on first open, if the user approves. PR #253. + **Other Changes** * Dropped the usage of .bak copies of document files. This was the old method to ensure the document data was written successfully, but it uses twice the storage space. Instead, writing via a temp file is the safe way to save files. PR #248. From 7b15dc78673b7f696a1d132f2834f64de7392d59 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 19:25:57 +0200 Subject: [PATCH 14/38] Updated technical docs --- docs/source/technical.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/technical.rst b/docs/source/technical.rst index 32d79545..027d2aaf 100644 --- a/docs/source/technical.rst +++ b/docs/source/technical.rst @@ -30,9 +30,9 @@ The project XML file is indent-formatted, suitable for diff tools and version co Project Documents ----------------- -The project documents are saved in folders starting with ``data_``. +The project documents are saved in a folder in the main project folder named ``content``. Each document has a file handle taken from the first 13 characters of a SHA256 hash of the system time when the file was first created. -The documents are saved with a folder and filename derived from this hash. +The documents are saved with a filename assembled from this hash and the file extension ``.nwd``. If you wish to find the physical location of a file in the project, you can either look it up in the project XML file, or select :menuselection:`Document --> Show File Details` in the menu when having the document open. The reason for this cryptic file naming is to avoid issues with file naming conventions and restrictions on different operating systems, and also to have a file name that does not depend on what the user names the files, or changes it to. From abe549c65ad5213ecee0b766f5e98febc1a88c4e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 19:40:11 +0200 Subject: [PATCH 15/38] Updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd9e8cd..732de25e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Version 0.7 [2020-xx-xx] +**User Interface** + +* The back-references list now shows references to any tag in the open document, not just the first tag. Issue #227, PR #234. +* Clicking a tag should also tries to scroll to the header where the tag is set. The index needed a couple of minor changes for this feature, so this will invalidate the old index for a project, and require a new to be built. This is done automatically. PR #234. + **Project Structure** * The project folder structure has been simplified and cleaned up. We also now pin the main entry values in the main XML file. the XML file is now given version 1.1, and locking it to only be opened by version 0.7 or later. The project is converted on first open, if the user approves. PR #253. From 28a74d802956b9a67467869a881ed6a95b4987ec Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 20:00:01 +0200 Subject: [PATCH 16/38] Fixed the text in the changelog a bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8e00c4..34e02c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Project Structure** -* The project folder structure has been simplified and cleaned up. We also now pin the main entry values in the main XML file. the XML file is now given version 1.1, and locking it to only be opened by version 0.7 or later. The project is converted on first open, if the user approves. PR #253. +* The project folder structure has been simplified and cleaned up. We also now freeze the main entry values in the main XML file. The XML file is now given version 1.1, and no further core changes to its structure will be made without bumping this version. We're also locking it to only be opened by version 0.7 or later. An old project file is converted on first open. PR #253. **Other Changes** From 65f74e6fdcf3bd3dc4e823ce7f385993f6939eaa Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 20:01:44 +0200 Subject: [PATCH 17/38] Fixed typo in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed683147..4ea1d757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ **User Interface** * The back-references list now shows references to any tag in the open document, not just the first tag. Issue #227, PR #234. -* Clicking a tag should also tries to scroll to the header where the tag is set. The index needed a couple of minor changes for this feature, so this will invalidate the old index for a project, and require a new to be built. This is done automatically. PR #234. +* Clicking a tag now tries to scroll to the header where the tag is set. The index needed a couple of minor changes for this feature, so this will invalidate the old index for a project, and require a new to be built. This is done automatically. PR #234. **Project Structure** From 2bbde2c4451286c135c0caaa9940e6336764e02a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 20:19:55 +0200 Subject: [PATCH 18/38] Improved the tokenizer class a bit --- nw/core/tokenizer.py | 190 +++++++++++++++++++++++++++++++------------ nw/gui/build.py | 3 - sample/nwProject.nwx | 21 ++--- 3 files changed, 141 insertions(+), 73 deletions(-) diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index fb632140..1dc573ca 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -320,7 +320,9 @@ class Tokenizer(): # Tag lines starting with specific characters if len(aLine.strip()) == 0: self.theTokens.append(( - self.T_EMPTY, nLine, "", None, self.A_NONE + self.T_EMPTY, nLine, + "", None, + self.A_NONE )) tmpMarkdown.append("\n") @@ -328,45 +330,59 @@ class Tokenizer(): cLine = aLine[1:].strip() if cLine.lower().startswith("synopsis:"): self.theTokens.append(( - self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE + self.T_SYNOPSIS, nLine, + cLine[9:].strip(), None, + self.A_NONE )) if self.doSynopsis: tmpMarkdown.append("%s\n" % aLine) else: self.theTokens.append(( - self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE + self.T_COMMENT, nLine, + aLine[1:].strip(), None, + self.A_NONE )) if self.doComments: tmpMarkdown.append("%s\n" % aLine) elif aLine[0] == "@": self.theTokens.append(( - self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE + self.T_KEYWORD, nLine, + aLine[1:].strip(), None, + self.A_NONE )) if self.doKeywords: tmpMarkdown.append("%s\n" % aLine) elif aLine[:2] == "# ": self.theTokens.append(( - self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE + self.T_HEAD1, nLine, + aLine[2:].strip(), None, + self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) elif aLine[:3] == "## ": self.theTokens.append(( - self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE + self.T_HEAD2, nLine, + aLine[3:].strip(), None, + self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) elif aLine[:4] == "### ": self.theTokens.append(( - self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE + self.T_HEAD3, nLine, + aLine[4:].strip(), None, + self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) elif aLine[:5] == "#### ": self.theTokens.append(( - self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE + self.T_HEAD4, nLine, + aLine[5:].strip(), None, + self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) @@ -391,13 +407,17 @@ class Tokenizer(): # sorted by position fmtPos = sorted(fmtPos, key=itemgetter(0)) self.theTokens.append(( - self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE + self.T_TEXT, nLine, + aLine, fmtPos, + self.A_NONE )) tmpMarkdown.append("%s\n" % aLine) # Always add an empty line at the end self.theTokens.append(( - self.T_EMPTY, nLine, "", None, self.A_NONE + self.T_EMPTY, nLine, + "", None, + self.A_NONE )) tmpMarkdown.append("\n") @@ -421,101 +441,146 @@ class Tokenizer(): for n in range(len(self.theTokens)): tToken = self.theTokens[n] - tType = tToken[0] - tLine = tToken[1] - tText = tToken[2] # In case we see text before a scene, we reset the flag - if tType == self.T_TEXT: + if tToken[0] == self.T_TEXT: self.firstScene = False - elif tType == self.T_HEAD1: + elif tToken[0] == self.T_HEAD1: # Main Title # ========== - tText = self._formatHeading(self.fmtTitle, tText) + tTemp = self._formatHeading(self.fmtTitle, tToken[2]) self.theTokens[n] = ( - tType, tLine, tText, None, self.A_NONE + tToken[0], + tToken[1], + tTemp, + None, + self.A_NONE ) - elif tType == self.T_HEAD2: + elif tToken[0] == self.T_HEAD2: # Novel Chapter # ============= # Numbered or Unnumbered if self.isUnNum: - tText = self._formatHeading(self.fmtUnNum, tText) + tTemp = self._formatHeading(self.fmtUnNum, tToken[2]) else: self.numChapter += 1 - tText = self._formatHeading(self.fmtChapter, tText) + tTemp = self._formatHeading(self.fmtChapter, tToken[2]) # Format the chapter header self.theTokens[n] = ( - tType, tLine, tText, None, self.A_PBB + tToken[0], + tToken[1], + tTemp, + None, + self.A_PBB ) # Set scene variables self.firstScene = True self.numChScene = 0 - elif tType == self.T_HEAD3: + elif tToken[0] == self.T_HEAD3: # Novel Scene # =========== self.numChScene += 1 self.numAbsScene += 1 - tTemp = self._formatHeading(self.fmtScene, tText) + tTemp = self._formatHeading(self.fmtScene, tToken[2]) if tTemp == "" and self.hideScene: self.theTokens[n] = ( - self.T_EMPTY, tLine, "", None, self.A_NONE + self.T_EMPTY, + tToken[1], + "", + None, + self.A_NONE ) elif tTemp == "" and not self.hideScene: if self.firstScene: self.theTokens[n] = ( - self.T_EMPTY, tLine, "", None, self.A_NONE + self.T_EMPTY, + tToken[1], + "", + None, + self.A_NONE ) else: self.theTokens[n] = ( - self.T_SKIP, tLine, "", None, self.A_NONE + self.T_SKIP, + tToken[1], + "", + None, + self.A_NONE ) elif tTemp == self.fmtScene: if self.firstScene: self.theTokens[n] = ( - self.T_EMPTY, tLine, "", None, self.A_NONE + self.T_EMPTY, + tToken[1], + "", + None, + self.A_NONE ) else: self.theTokens[n] = ( - self.T_SEP, tLine, tTemp, None, self.A_CENTRE + self.T_SEP, + tToken[1], + tTemp, + None, + self.A_CENTRE ) else: self.theTokens[n] = ( - tType, tLine, tTemp, None, self.A_NONE + tToken[0], + tToken[1], + tTemp, + None, + self.A_NONE ) # Definitely no longer the first scene self.firstScene = False - elif tType == self.T_HEAD4: + elif tToken[0] == self.T_HEAD4: # Novel Section # ============= - tTemp = self._formatHeading(self.fmtSection, tText) + tTemp = self._formatHeading(self.fmtSection, tToken[2]) if tTemp == "" and self.hideSection: self.theTokens[n] = ( - self.T_EMPTY, tLine, "", None, self.A_NONE + self.T_EMPTY, + tToken[1], + "", + None, + self.A_NONE ) elif tTemp == "" and not self.hideSection: self.theTokens[n] = ( - self.T_SKIP, tLine, "", None, self.A_NONE + self.T_SKIP, + tToken[1], + "", + None, + self.A_NONE ) elif tTemp == self.fmtSection: self.theTokens[n] = ( - self.T_SEP, tLine, tTemp, None, self.A_CENTRE + self.T_SEP, + tToken[1], + tTemp, + None, + self.A_CENTRE ) else: self.theTokens[n] = ( - tType, tLine, tTemp, None, self.A_NONE + tToken[0], + tToken[1], + tTemp, + None, + self.A_NONE ) # For title page and partitions, we need to centre all text. @@ -524,22 +589,30 @@ class Tokenizer(): # We also swap header level 1 with a title type instead. if self.isTitle or self.isPart: for n, tToken in enumerate(self.theTokens): - tType = tToken[0] - tLine = tToken[1] - tText = tToken[2] - tFormat = tToken[3] - if tType == self.T_HEAD1: + if tToken[0] == self.T_HEAD1: if self.isTitle: self.theTokens[n] = ( - self.T_TITLE, tLine, tText, tFormat, self.A_PBB_NO | self.A_CENTRE + self.T_TITLE, + tToken[1], + tToken[2], + tToken[3], + self.A_PBB_NO | self.A_CENTRE ) else: self.theTokens[n] = ( - tType, tLine, tText, tFormat, self.A_PBB | self.A_CENTRE + tToken[0], + tToken[1], + tToken[2], + tToken[3], + self.A_PBB | self.A_CENTRE ) else: self.theTokens[n] = ( - tType, tLine, tText, tFormat, self.A_CENTRE + tToken[0], + tToken[1], + tToken[2], + tToken[3], + self.A_CENTRE ) # Add a page break after the last entry @@ -547,24 +620,32 @@ class Tokenizer(): if n >= 0: tToken = self.theTokens[n] self.theTokens[n] = ( - tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA + tToken[0], + tToken[1], + tToken[2], + tToken[3], + tToken[4] | self.A_PBA ) # A single page is always left-aligned and starts on a fresh # page, unless it's empty. if self.isPage: for n, tToken in enumerate(self.theTokens): - tType = tToken[0] - tLine = tToken[1] - tText = tToken[2] - tFormat = tToken[3] if n == 0: self.theTokens[n] = ( - tType, tLine, tText, tFormat, self.A_LEFT | self.A_PBB + tToken[0], + tToken[1], + tToken[2], + tToken[3], + self.A_LEFT | self.A_PBB ) else: self.theTokens[n] = ( - tType, tLine, tText, tFormat, self.A_LEFT + tToken[0], + tToken[1], + tToken[2], + tToken[3], + self.A_LEFT ) return @@ -577,10 +658,11 @@ class Tokenizer(): """Replaces the %keyword% strings. """ theTitle = theTitle.replace(r"%title%", theText) - theTitle = theTitle.replace(r"%ch%", str(self.numChapter)) - theTitle = theTitle.replace(r"%sc%", str(self.numChScene)) - theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene)) - theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter,"en")) + theTitle = theTitle.replace(r"%ch%", str(self.numChapter)) + theTitle = theTitle.replace(r"%sc%", str(self.numChScene)) + theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene)) + if r"%chw%" in theTitle: + theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter,"en")) return theTitle # END Class Tokenizer diff --git a/nw/gui/build.py b/nw/gui/build.py index 6a7c99cc..b73f1017 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -294,9 +294,6 @@ class GuiBuildNovel(QDialog): # ============== self.buttonForm = QGridLayout() - self.btnHelp = QPushButton("Help") - self.btnHelp.clicked.connect(self._showHelp) - self.btnPrint = QPushButton("Print") self.btnPrint.clicked.connect(self._printDocument) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index abe767ea..b6f63962 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -11,8 +11,8 @@ True True 636b6aa9b697b - ba8a28a246524 - 914 + bc0cbd2a407f3 + 941 B E @@ -122,7 +122,7 @@ 1199 216 7 - 825 + 1066 Another Scene @@ -174,11 +174,7 @@ 139 28 1 -<<<<<<< HEAD - 237 -======= 242 ->>>>>>> dev We Found John! @@ -191,7 +187,7 @@ 189 37 1 - 236 + 224 Characters @@ -261,17 +257,10 @@ False True NOTE -<<<<<<< HEAD 241 51 3 - 286 -======= - 115 - 24 - 1 135 ->>>>>>> dev Mars From 2d7708a51abe2956b1888c7b8bea2397027f7a6e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 20:46:14 +0200 Subject: [PATCH 19/38] Bumped version to 0.7 RC1 --- CHANGELOG.md | 2 +- docs/source/conf.py | 4 ++-- nw/__init__.py | 4 ++-- setup.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00ee8cfb..6e55459e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # novelWriter ChangeLog -## Version 0.7 [2020-xx-xx] +## Version 0.7 RC1 [2020-xx-xx] **Project Structure** diff --git a/docs/source/conf.py b/docs/source/conf.py index cce19db2..02f7ed85 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen" author = "Veronica Berglyd Olsen" # The short X.Y version -version = "0.6.3" +version = "0.7.0" # The full version, including alpha/beta/rc tags -release = "0.6.3" +release = "0.7.0rc1" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index 1eff781f..9956c0d0 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -40,8 +40,8 @@ __package__ = "novelWriter" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "0.6.3" -__hexversion__ = "0x000603f0" +__version__ = "0.7.0rc1" +__hexversion__ = "0x000700c1" __date__ = "2020-05-28" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" diff --git a/setup.py b/setup.py index 14ad5413..7f774046 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open("README.md", "r") as inFile: setuptools.setup( name = "novelWriter", - version = "0.6.3", + version = "0.7.0rc1", author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", From 56df009817e4c92d1dc7138f2b21592f61cabed2 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 20:57:35 +0200 Subject: [PATCH 20/38] Fixed Python versions for PyPi --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7f774046..f0cbdbef 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,10 @@ setuptools.setup( "Source Code": "https://github.com/vkbo/novelWriter", }, classifiers = [ - "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Development Status :: 3 - Alpha", "Operating System :: OS Independent", From 0c75f24c5d4f007432659a050b859f1fb983db50 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 22:00:42 +0200 Subject: [PATCH 21/38] Updated the status reporting on index rebuild --- nw/guimain.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/nw/guimain.py b/nw/guimain.py index 5abf464b..69090ac9 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -668,6 +668,12 @@ class GuiMain(QMainWindow): theDoc = NWDoc(self.theProject, self) for nDone, tItem in enumerate(self.theProject.projTree): + + if tItem is not None: + self.statusBar.setStatus("Indexing: '%s'" % tItem.itemName) + else: + self.statusBar.setStatus("Indexing: Unknown item") + if tItem is not None and tItem.itemType == nwItemType.FILE: logger.verbose("Scanning: %s" % tItem.itemName) theText = theDoc.openDocument(tItem.itemHandle, showStatus=False) @@ -683,16 +689,14 @@ class GuiMain(QMainWindow): self.treeView.propagateCount(tItem.itemHandle, wC) self.treeView.projectWordCount() - self.statusBar.setStatus("Building index: %.2f%%" % (100.0*(nDone + 1)/nItems)) - - self.docEditor.reloadText() - qApp.restoreOverrideCursor() tEnd = time() + self.statusBar.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0)) + self.docEditor.reloadText() + + qApp.restoreOverrideCursor() if self.mainConf.showGUI: - self.makeAlert( - "Project index rebuilt in %.3f seconds." % (tEnd - tStart), nwAlert.INFO - ) + self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO) return True From 4e1fe109f3a1de68b0e6b92f7d699687923a6e14 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 22:01:32 +0200 Subject: [PATCH 22/38] Bumped the default timeout for the status bar to 20 seconds --- nw/gui/statusbar.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index c7ee28df..d268ab74 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -137,13 +137,17 @@ class GuiMainStatus(QStatusBar): self._updateTime() return True + ## + # Setters + ## + def setRefTime(self, theTime): """Set the reference time for the status bar clock. """ self.refTime = theTime return - def setStatus(self, theMessage, timeOut=10.0): + def setStatus(self, theMessage, timeOut=20.0): """Set the status bar message to display for 'timeOut' seconds. """ self.showMessage(theMessage, int(timeOut*1000)) From 611d81a1c698152bc1765cfe4692fd6be31f2d36 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 22:07:58 +0200 Subject: [PATCH 23/38] Moving around buttons on the build tool --- nw/gui/build.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index b73f1017..2031256a 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -83,8 +83,7 @@ class GuiBuildNovel(QDialog): self.optState.getInt("GuiBuildNovel", "winHeight", 800) ) - self.outerBox = QVBoxLayout() - self.innerBox = QHBoxLayout() + self.outerBox = QHBoxLayout() self.toolsBox = QVBoxLayout() self.docView = GuiBuildNovelDocView(self, self.theProject) @@ -292,7 +291,7 @@ class GuiBuildNovel(QDialog): # Action Buttons # ============== - self.buttonForm = QGridLayout() + self.buttonBox = QHBoxLayout() self.btnPrint = QPushButton("Print") self.btnPrint.clicked.connect(self._printDocument) @@ -326,12 +325,13 @@ class GuiBuildNovel(QDialog): self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT)) self.saveMenu.addAction(self.saveTXT) - self.buttonForm.addWidget(self.btnSave, 0, 0) - self.buttonForm.addWidget(self.btnPrint, 0, 1) + self.btnClose = QPushButton("Close") + self.btnClose.clicked.connect(self._doClose) - # Buttons - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) - self.buttonBox.rejected.connect(self._doClose) + self.buttonBox.addWidget(self.btnSave) + self.buttonBox.addWidget(self.btnPrint) + self.buttonBox.addWidget(self.btnClose) + self.buttonBox.setSpacing(4) # Assemble GUI # ============ @@ -343,18 +343,15 @@ class GuiBuildNovel(QDialog): self.toolsBox.addWidget(self.buildProgress) self.toolsBox.addWidget(self.buildNovel) self.toolsBox.addSpacing(8) - self.toolsBox.addLayout(self.buttonForm) + self.toolsBox.addLayout(self.buttonBox) - self.innerBox.addLayout(self.toolsBox) - self.innerBox.addWidget(self.docView) + self.outerBox.addLayout(self.toolsBox) + self.outerBox.addWidget(self.docView) + self.outerBox.setStretch(0, 0) + self.outerBox.setStretch(1, 1) - self.outerBox.addLayout(self.innerBox) - self.outerBox.addWidget(self.buttonBox) self.setLayout(self.outerBox) - self.innerBox.setStretch(0, 0) - self.innerBox.setStretch(1, 1) - self.show() logger.debug("GuiBuildNovel initialisation complete") From 6f5a30092e24e1e07fb5d1558fd0d62fbfa642cc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 22:10:03 +0200 Subject: [PATCH 24/38] Clean up imports --- nw/gui/build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index 2031256a..10eaaa54 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -34,12 +34,12 @@ from time import time from PyQt5.QtCore import Qt, QByteArray from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtGui import ( - QTextOption, QPalette, QColor, QTextDocumentWriter, QFont + QPalette, QColor, QTextDocumentWriter, QFont ) from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel, QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction, - QFileDialog, QFontComboBox, QSpinBox, QDialogButtonBox + QFileDialog, QFontComboBox, QSpinBox ) from nw.gui.additions import QSwitch From d852a1a144b1ad4e4434b0c7a58123a2b9d0bab9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 22:48:37 +0200 Subject: [PATCH 25/38] Some reformatting of error messages, mostly regarding line breaks. --- nw/core/project.py | 4 ++-- nw/gui/dialogs/projectsettings.py | 4 +++- nw/gui/elements/doctree.py | 4 ++-- nw/gui/icons.py | 4 +++- nw/gui/theme.py | 8 ++++++-- nw/guimain.py | 4 ++-- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index 96e22103..b3a2661f 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -113,7 +113,7 @@ class NWProject(): CUSTOM, and always have parent handle set to None. """ if not self.projTree.checkRootUnique(rootClass): - self.makeAlert("Duplicate root item detected!", nwAlert.ERROR) + self.makeAlert("Duplicate root item detected.", nwAlert.ERROR) return None newItem = NWItem(self) newItem.setName(rootName) @@ -995,7 +995,7 @@ class NWProject(): # Report status if len(orphanFiles) > 0: self.makeAlert( - "Found %d orphaned file(s) in project folder!" % len(orphanFiles), + "Found %d orphaned file(s) in project folder." % len(orphanFiles), nwAlert.WARN ) else: diff --git a/nw/gui/dialogs/projectsettings.py b/nw/gui/dialogs/projectsettings.py index 8d501585..a49193d1 100644 --- a/nw/gui/dialogs/projectsettings.py +++ b/nw/gui/dialogs/projectsettings.py @@ -362,7 +362,9 @@ class GuiProjectEditStatus(QWidget): self.listBox.takeItem(iRow) self.colChanged = True else: - self.theParent.makeAlert("Cannot delete status item that is in use.",nwAlert.ERROR) + self.theParent.makeAlert( + "Cannot delete status item that is in use.", nwAlert.ERROR + ) return def _saveItem(self): diff --git a/nw/gui/elements/doctree.py b/nw/gui/elements/doctree.py index 2c81cf91..02db9bde 100644 --- a/nw/gui/elements/doctree.py +++ b/nw/gui/elements/doctree.py @@ -396,7 +396,7 @@ class GuiDocTree(QTreeWidget): trItemP.takeChild(tIndex) del self.theProject.projTree[tHandle] else: - self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR) + self.makeAlert("Cannot delete folder. It is not empty.", nwAlert.ERROR) return False elif nwItemS.itemType == nwItemType.ROOT: @@ -407,7 +407,7 @@ class GuiDocTree(QTreeWidget): self.theParent.mainMenu.setAvailableRoot() self.theProject.setProjectChanged(True) else: - self.makeAlert(["Cannot delete root folder.","It is not empty."], nwAlert.ERROR) + self.makeAlert("Cannot delete root folder. It is not empty.", nwAlert.ERROR) return False return True diff --git a/nw/gui/icons.py b/nw/gui/icons.py index a57f3029..d4ac4ef5 100644 --- a/nw/gui/icons.py +++ b/nw/gui/icons.py @@ -249,7 +249,9 @@ class GuiIcons: try: confParser.read_file(open(themeConf, mode="r", encoding="utf8")) except Exception as e: - self.theParent.makeAlert(["Could not load theme config file.",str(e)],nwAlert.ERROR) + self.theParent.makeAlert( + ["Could not load theme config file.",str(e)], nwAlert.ERROR + ) continue themeName = "" if confParser.has_section("Main"): diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 5b4893a6..04a00c0e 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -284,7 +284,9 @@ class GuiTheme: try: confParser.read_file(open(themeConf, mode="r", encoding="utf8")) except Exception as e: - self.theParent.makeAlert(["Could not load theme config file.",str(e)],nwAlert.ERROR) + self.theParent.makeAlert( + ["Could not load theme config file.",str(e)], nwAlert.ERROR + ) continue themeName = "" if confParser.has_section("Main"): @@ -314,7 +316,9 @@ class GuiTheme: try: confParser.read_file(open(syntaxPath, mode="r", encoding="utf8")) except Exception as e: - self.theParent.makeAlert(["Could not load syntax file.",str(e)],nwAlert.ERROR) + self.theParent.makeAlert( + ["Could not load syntax file.",str(e)], nwAlert.ERROR + ) return [] syntaxName = "" if confParser.has_section("Main"): diff --git a/nw/guimain.py b/nw/guimain.py index 69090ac9..38471f97 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -553,7 +553,7 @@ class GuiMain(QMainWindow): if self.docEditor.theHandle is None: self.makeAlert( - ["Please open a document to import the text file into."], + "Please open a document to import the text file into.", nwAlert.ERROR ) return False @@ -782,7 +782,7 @@ class GuiMain(QMainWindow): 0 = info, 1 = warning, and 2 = error. """ if isinstance(theMessage, list): - popMsg = " ".join(theMessage) + popMsg = "
".join(theMessage) logMsg = theMessage else: popMsg = theMessage From 1a9b6906c6519d9cdc2e5709b2b2bc5c9a868125 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 23:00:20 +0200 Subject: [PATCH 26/38] Wrap some long or too long code lines --- nw/config.py | 4 +++- nw/core/project.py | 12 ++++++------ nw/core/spellcheck.py | 12 ++++++------ nw/gui/build.py | 24 ++++++++++++++++++------ nw/gui/dialogs/about.py | 23 +++++++++++++---------- nw/gui/dialogs/docsplit.py | 12 +++++++++--- nw/gui/dialogs/sessionlog.py | 20 ++++++++++++++------ nw/gui/elements/doceditor.py | 12 +++++++++--- nw/gui/icons.py | 4 +++- 9 files changed, 81 insertions(+), 42 deletions(-) diff --git a/nw/config.py b/nw/config.py index bc40259b..18529f61 100644 --- a/nw/config.py +++ b/nw/config.py @@ -533,7 +533,9 @@ class Config: # Write config file try: - cnfParse.write(open(path.join(self.confPath,self.confFile),mode="w",encoding="utf8")) + cnfParse.write( + open(path.join(self.confPath, self.confFile), mode="w", encoding="utf8") + ) self.confChanged = False except Exception as e: logger.error("Could not save config file") diff --git a/nw/core/project.py b/nw/core/project.py index b3a2661f..cf35f629 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1097,10 +1097,10 @@ class NWProject(): # END Class NWProject -# ================================================================================================ # +# =============================================================================================== # # NWTree # Class holding the project tree for the NWProject -# ================================================================================================ # +# =============================================================================================== # class NWTree(): @@ -1435,10 +1435,10 @@ class NWTree(): # END Class NWTree -# ================================================================================================ # +# =============================================================================================== # # NWItem # Class holding the project items making up the NWProject -# ================================================================================================ # +# =============================================================================================== # class NWItem(): @@ -1680,10 +1680,10 @@ class NWItem(): # END Class NWItem -# ================================================================================================ # +# =============================================================================================== # # NWStatus # Class holding the item status values stored in the NWProject -# ================================================================================================ # +# =============================================================================================== # class NWStatus(): diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index 2af06f42..6296bc60 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -35,9 +35,9 @@ from nw.constants import isoLanguage logger = logging.getLogger(__name__) -# ================================================================================================ # +# =============================================================================================== # # SpellChecking SuperClass -# ================================================================================================ # +# =============================================================================================== # class NWSpellCheck(): @@ -129,9 +129,9 @@ class NWSpellCheck(): # END Class NWSpellCheck -# ================================================================================================ # +# =============================================================================================== # # Enchant Based SpellChecking -# ================================================================================================ # +# =============================================================================================== # class NWSpellEnchant(NWSpellCheck): @@ -211,9 +211,9 @@ class NWSpellEnchantDummy: # END Class NWSpellEnchantDummy -# ================================================================================================ # +# =============================================================================================== # # Fallback SpellChecking Using difflib -# ================================================================================================ # +# =============================================================================================== # class NWSpellSimple(NWSpellCheck): """Internal spell check tool that uses standard Python packages with diff --git a/nw/gui/build.py b/nw/gui/build.py index 10eaaa54..f191d063 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -172,7 +172,9 @@ class GuiBuildNovel(QDialog): self.textFont = QFontComboBox() self.textFont.setFixedWidth(220) - self.textFont.setToolTip("The font is used for PDF and printing. Other formats have no font set.") + self.textFont.setToolTip( + "The font is used for PDF and printing. Other formats have no font set." + ) self.textFont.setCurrentFont( QFont(self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont)) ) @@ -182,13 +184,17 @@ class GuiBuildNovel(QDialog): self.textSize.setMinimum(5) self.textSize.setMaximum(48) self.textSize.setSingleStep(1) - self.textSize.setToolTip("The size is used for PDF and printing. Other formats have no size set.") + self.textSize.setToolTip( + "The size is used for PDF and printing. Other formats have no size set." + ) self.textSize.setValue( self.optState.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize) ) self.justifyText = QSwitch() - self.justifyText.setToolTip("Applies to PDF, printing, HTML, and Open Document exports.") + self.justifyText.setToolTip( + "Applies to PDF, printing, HTML, and Open Document exports." + ) self.justifyText.setChecked( self.optState.getBool("GuiBuildNovel", "justifyText", False) ) @@ -210,15 +216,21 @@ class GuiBuildNovel(QDialog): self.includeGroup.setLayout(self.includeForm) self.includeSynopsis = QSwitch() - self.includeSynopsis.setToolTip("Include synopsis type comments in the output.") + self.includeSynopsis.setToolTip( + "Include synopsis type comments in the output." + ) self.includeSynopsis.setChecked(self.theProject.titleFormat["withSynopsis"]) self.includeComments = QSwitch() - self.includeComments.setToolTip("Include plain comments in the output.") + self.includeComments.setToolTip( + "Include plain comments in the output." + ) self.includeComments.setChecked(self.theProject.titleFormat["withComments"]) self.includeKeywords = QSwitch() - self.includeKeywords.setToolTip("Include meta keywords (tags, references) in the output.") + self.includeKeywords.setToolTip( + "Include meta keywords (tags, references) in the output." + ) self.includeKeywords.setChecked(self.theProject.titleFormat["withKeywords"]) self.includeForm.addWidget(QLabel("Include synopsis"), 0, 0, 1, 1, Qt.AlignLeft) diff --git a/nw/gui/dialogs/about.py b/nw/gui/dialogs/about.py index f71f6e4c..653f87ae 100644 --- a/nw/gui/dialogs/about.py +++ b/nw/gui/dialogs/about.py @@ -119,16 +119,19 @@ class GuiAbout(QDialog): "

About {name:s}

" "

{copyright:s}.

" "

Website: {domain:s}

" - "

{name:s} is a markdown-like text editor designed for organising and writing " - "novels. It is written in Python 3 with a Qt5 GUI, using PyQt5.

" - "

{name:s} is free software: you can redistribute it and/or modify it under the " - "terms of the GNU General Public License as published by the Free Software Foundation, " - "either version 3 of the License, or (at your option) any later version.

" - "

{name:s} is distributed in the hope that it will be useful, but WITHOUT ANY " - "WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A " - "PARTICULAR PURPOSE.

" - "

See the License tab for the full text, or visit the GNU website at " - "GPL v3.0 for more details.

" + "

{name:s} is a markdown-like text editor designed for " + "organising and writing novels. It is written in Python 3 with a " + "Qt5 GUI, using PyQt5.

" + "

{name:s} is free software: you can redistribute it and/or " + "modify it under the terms of the GNU General Public License as " + "published by the Free Software Foundation, either version 3 of " + "the License, or (at your option) any later version.

" + "

{name:s} is distributed in the hope that it will be useful, " + "but WITHOUT ANY WARRANTY; without even the implied warranty of " + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

" + "

See the License tab for the full text, or visit the GNU website " + "at GPL v3.0 " + "for more details.

" "

Credits

" "

{credits:s}

" ).format( diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py index 25cc2f1d..7142c6e5 100644 --- a/nw/gui/dialogs/docsplit.py +++ b/nw/gui/dialogs/docsplit.py @@ -150,7 +150,9 @@ class GuiDocSplit(QDialog): ), nwAlert.ERROR) return - fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemClass, srcItem.parHandle) + fHandle = self.theProject.newFolder( + srcItem.itemName, srcItem.itemClass, srcItem.parHandle + ) self.theParent.treeView.revealTreeItem(fHandle) logger.verbose("Creating folder %s" % fHandle) @@ -174,7 +176,9 @@ class GuiDocSplit(QDialog): newItem = self.theProject.projTree[nHandle] newItem.setLayout(itemLayout) logger.verbose( - "Creating new document %s with text from line %d to %d" % (nHandle, iStart, iEnd-1) + "Creating new document %s with text from line %d to %d" % ( + nHandle, iStart, iEnd-1 + ) ) theText = "\n".join(theLines[iStart:iEnd]) @@ -227,7 +231,9 @@ class GuiDocSplit(QDialog): spLevel = self.splitLevel.currentData() self.optState.setValue("GuiDocSplit", "spLevel", spLevel) - logger.debug("Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel)) + logger.debug( + "Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel) + ) lineNo = 0 for aLine in theText.splitlines(): diff --git a/nw/gui/dialogs/sessionlog.py b/nw/gui/dialogs/sessionlog.py index 59750dce..fdda4166 100644 --- a/nw/gui/dialogs/sessionlog.py +++ b/nw/gui/dialogs/sessionlog.py @@ -180,11 +180,15 @@ class GuiSessionLogView(QDialog): inData = inLine.split() if len(inData) != 8: continue - dStart = datetime.strptime("%s %s" % (inData[1],inData[2]), nwConst.tStampFmt) - dEnd = datetime.strptime("%s %s" % (inData[4],inData[5]), nwConst.tStampFmt) + dStart = datetime.strptime( + "%s %s" % (inData[1],inData[2]), nwConst.tStampFmt + ) + dEnd = datetime.strptime( + "%s %s" % (inData[4],inData[5]), nwConst.tStampFmt + ) nWords = int(inData[7]) - tDiff = dEnd - dStart - sDiff = tDiff.total_seconds() + tDiff = dEnd - dStart + sDiff = tDiff.total_seconds() self.timeTotal += sDiff if abs(nWords) > 0: @@ -196,7 +200,9 @@ class GuiSessionLogView(QDialog): if hideNegative and nWords < 0: continue - newItem = QTreeWidgetItem([str(dStart),self._formatTime(sDiff),str(nWords),""]) + newItem = QTreeWidgetItem( + [str(dStart), self._formatTime(sDiff), str(nWords), ""] + ) newItem.setTextAlignment(1,Qt.AlignRight) newItem.setTextAlignment(2,Qt.AlignRight) @@ -208,7 +214,9 @@ class GuiSessionLogView(QDialog): self.listBox.addTopLevelItem(newItem) except Exception as e: - self.theParent.makeAlert(["Failed to read session log file.",str(e)], nwAlert.ERROR) + self.theParent.makeAlert( + ["Failed to read session log file.",str(e)], nwAlert.ERROR + ) return False self.labelFilter.setText(self._formatTime(self.timeFilter)) diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 7e071023..667d4845 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -479,7 +479,9 @@ class GuiDocEditor(QTextEdit): self.hLight.rehighlight() qApp.restoreOverrideCursor() afTime = time() - logger.debug("Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) + logger.debug( + "Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)) + ) return True @@ -716,7 +718,9 @@ class GuiDocEditor(QTextEdit): """ sinceActive = time()-self.lastEdit if sinceActive > 5*self.wcInterval: - logger.debug("Stopping word count timer: no activity last %.1f seconds" % sinceActive) + logger.debug( + "Stopping word count timer: no activity last %.1f seconds" % sinceActive + ) self.wcTimer.stop() elif self.wCounter.isRunning(): logger.verbose("Word counter thread is busy") @@ -952,7 +956,9 @@ class GuiDocEditor(QTextEdit): theText = newText cOffset -= 0 else: - logger.error("Unknown or unsupported block format requested: %s" % str(docAction)) + logger.error( + "Unknown or unsupported block format requested: %s" % str(docAction) + ) return # Replace the block text diff --git a/nw/gui/icons.py b/nw/gui/icons.py index d4ac4ef5..db5d6e9e 100644 --- a/nw/gui/icons.py +++ b/nw/gui/icons.py @@ -297,7 +297,9 @@ class GuiIcons: # Finally. we check if we have a fallback icon if self.mainConf.guiDark: - fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey) + fbackIcon = path.join( + self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey + ) if path.isfile(fbackIcon): logger.verbose("Loading icon '%s' from fallback theme" % iconKey) return QIcon(fbackIcon) From fb4a1ca266704124675ca3502e425ef957572d60 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 23:07:56 +0200 Subject: [PATCH 27/38] Renamed the preferences class to GuiPreferences --- nw/gui/__init__.py | 4 ++-- nw/gui/dialogs/__init__.py | 4 ++-- nw/gui/dialogs/{configeditor.py => preferences.py} | 10 +++++----- nw/guimain.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) rename nw/gui/dialogs/{configeditor.py => preferences.py} (99%) diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 393f501d..8360a460 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -9,7 +9,7 @@ from nw.gui.theme import GuiTheme # Dialogs from nw.gui.dialogs.about import GuiAbout -from nw.gui.dialogs.configeditor import GuiConfigEditor +from nw.gui.dialogs.preferences import GuiPreferences from nw.gui.dialogs.docmerge import GuiDocMerge from nw.gui.dialogs.docsplit import GuiDocSplit from nw.gui.dialogs.itemeditor import GuiItemEditor @@ -40,7 +40,7 @@ __all__ = [ "GuiMainStatus", "GuiTheme", "GuiAbout", - "GuiConfigEditor", + "GuiPreferences", "GuiDocMerge", "GuiDocSplit", "GuiItemEditor", diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py index 271a7a1d..5f4e4a03 100644 --- a/nw/gui/dialogs/__init__.py +++ b/nw/gui/dialogs/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from nw.gui.dialogs.about import GuiAbout -from nw.gui.dialogs.configeditor import GuiConfigEditor +from nw.gui.dialogs.preferences import GuiPreferences from nw.gui.dialogs.docmerge import GuiDocMerge from nw.gui.dialogs.docsplit import GuiDocSplit from nw.gui.dialogs.itemeditor import GuiItemEditor @@ -11,7 +11,7 @@ from nw.gui.dialogs.sessionlog import GuiSessionLogView __all__ = [ "GuiAbout", - "GuiConfigEditor", + "GuiPreferences", "GuiDocMerge", "GuiDocSplit", "GuiItemEditor", diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/preferences.py similarity index 99% rename from nw/gui/dialogs/configeditor.py rename to nw/gui/dialogs/preferences.py index 77abc231..34b5d280 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/preferences.py @@ -44,12 +44,12 @@ from nw.constants import nwAlert, nwQuotes logger = logging.getLogger(__name__) -class GuiConfigEditor(PagedDialog): +class GuiPreferences(PagedDialog): def __init__(self, theParent, theProject): PagedDialog.__init__(self, theParent) - logger.debug("Initialising ConfigEditor ...") + logger.debug("Initialising GuiPreferences ...") self.mainConf = nw.CONFIG self.theParent = theParent @@ -74,7 +74,7 @@ class GuiConfigEditor(PagedDialog): self.show() - logger.debug("ConfigEditor initialisation complete") + logger.debug("GuiPreferences initialisation complete") return @@ -122,7 +122,7 @@ class GuiConfigEditor(PagedDialog): self.close() return -# END Class GuiConfigEditor +# END Class GuiPreferences class GuiConfigEditGeneralTab(QWidget): @@ -228,7 +228,7 @@ class GuiConfigEditGeneralTab(QWidget): ## Backup Path self.backupPath = self.mainConf.backupPath - self.backupGetPath = QPushButton(self.theTheme.getIcon("folder-open"),"Select Folder") + self.backupGetPath = QPushButton("Browse") self.backupGetPath.clicked.connect(self._backupFolder) self.backupPathRow = self.mainForm.addRow( "Backup storage location", diff --git a/nw/guimain.py b/nw/guimain.py index 38471f97..a3b7d46e 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -42,7 +42,7 @@ from PyQt5.QtWidgets import ( from nw.gui import ( GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails, - GuiConfigEditor, GuiProjectSettings, GuiItemEditor, GuiProjectOutline, + GuiPreferences, GuiProjectSettings, GuiItemEditor, GuiProjectOutline, GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad, GuiBuildNovel ) from nw.core import NWProject, NWDoc, NWIndex @@ -741,7 +741,7 @@ class GuiMain(QMainWindow): def editConfigDialog(self): """Open the preferences dialog. """ - dlgConf = GuiConfigEditor(self, self.theProject) + dlgConf = GuiPreferences(self, self.theProject) if dlgConf.exec_() == QDialog.Accepted: logger.debug("Applying new preferences") self.initMain() From 834ed1f0b6da7df4380db67e2c1f0e2d68b4a158 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 23:11:13 +0200 Subject: [PATCH 28/38] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cda74459..2ad67b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * The back-references list now shows references to any tag in the open document, not just the first tag. Issue #227, PR #234. * Clicking a tag now tries to scroll to the header where the tag is set. The index needed a couple of minor changes for this feature, so this will invalidate the old index for a project, and require a new to be built. This is done automatically. PR #234. +* Moved the Close button on the "Build Novel project" dialog to the area with the other buttons since we anyway increased the size of that area. PR #256. **Project Structure** From 055ffdaac9299f31d96af2ef2873b49f5eb973d2 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 May 2020 23:19:12 +0200 Subject: [PATCH 29/38] Split document section headers should be SCENE files not PAGE --- nw/gui/dialogs/docsplit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nw/gui/dialogs/docsplit.py b/nw/gui/dialogs/docsplit.py index 7142c6e5..f95e0041 100644 --- a/nw/gui/dialogs/docsplit.py +++ b/nw/gui/dialogs/docsplit.py @@ -167,7 +167,7 @@ class GuiDocSplit(QDialog): elif wTitle.startswith("### "): itemLayout = nwItemLayout.SCENE elif wTitle.startswith("#### "): - itemLayout = nwItemLayout.PAGE + itemLayout = nwItemLayout.SCENE wTitle = wTitle.lstrip("#") wTitle = wTitle.strip() From 36ca2ee309b4fff304e3d581e925a0e63547dcd7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 May 2020 23:01:25 +0200 Subject: [PATCH 30/38] Cleane dup project open and handling of legacy project content --- nw/constants/constants.py | 2 + nw/core/__init__.py | 2 - nw/core/project.py | 209 ++++++++++++++++++++++--------- nw/core/tools.py | 40 ------ sample/content/ae7339df26ded.nwd | 2 +- sample/content/b8136a5a774a0.nwd | 2 +- sample/content/edca4be2fcaf8.nwd | 2 +- sample/content/f1471bef9f2ae.nwd | 6 - sample/nwProject.nwx | 34 ++--- 9 files changed, 172 insertions(+), 127 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index bc20af31..cf0ab4eb 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -39,6 +39,8 @@ class nwFiles(): PROJ_FILE = "nwProject.nwx" PROJ_DICT = "wordlist.txt" PROJ_LOCK = "nwProject.lock" + TOC_TXT = "ToC.txt" + TOC_JSON = "ToC.json" SESS_INFO = "sessionInfo.log" INDEX_FILE = "tagsIndex.json" OPTS_FILE = "guiOptions.json" diff --git a/nw/core/__init__.py b/nw/core/__init__.py index 6dd65fd2..0107dac2 100644 --- a/nw/core/__init__.py +++ b/nw/core/__init__.py @@ -9,7 +9,6 @@ from nw.core.spellcheck import NWSpellSimple from nw.core.tokenizer import Tokenizer from nw.core.tohtml import ToHtml from nw.core.tools import countWords -from nw.core.tools import projectMaintenance from nw.core.tools import numberToWord __all__ = [ @@ -22,6 +21,5 @@ __all__ = [ "Tokenizer", "ToHtml", "countWords", - "projectMaintenance", "numberToWord", ] diff --git a/nw/core/project.py b/nw/core/project.py index cf35f629..c89bb645 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -42,7 +42,6 @@ from shutil import make_archive from PyQt5.QtWidgets import QMessageBox from nw.gui.tools import OptionState -from nw.core.tools import projectMaintenance from nw.core.document import NWDoc from nw.common import checkString, checkBool, checkInt, formatTimeStamp from nw.constants import ( @@ -72,11 +71,12 @@ class NWProject(): self.autoCount = 0 # Meta data: number of automatic saves # Class Settings - self.projPath = None # The full path to where the currently open project is saved - self.projMeta = None # The full path to the project's meta data folder - self.projData = None # The full path to the project's data folder - self.projDict = None # The spell check dictionary - self.projFile = None # The file name of the project main XML file + self.projPath = None # The full path to where the currently open project is saved + self.projMeta = None # The full path to the project's meta data folder + self.projCache = None # The full path to the project's cache folder + self.projContent = None # The full path to the project's content folder + self.projDict = None # The spell check dictionary + self.projFile = None # The file name of the project main XML file # Project Meta self.projName = "" # Project name (working title) @@ -196,7 +196,8 @@ class NWProject(): # Project Settings self.projPath = None self.projMeta = None - self.projData = None + self.projCache = None + self.projContent = None self.projDict = None self.projFile = nwFiles.PROJ_FILE self.projName = "" @@ -248,14 +249,37 @@ class NWProject(): self.projPath = path.abspath(path.dirname(fileName)) logger.debug("Opening project: %s" % self.projPath) - self.projMeta = path.join(self.projPath,"meta") - self.projData = path.join(self.projPath,"content") - self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) + # Standard Folders and Files + # ========================== + + self.projMeta = path.join(self.projPath, "meta") + self.projCache = path.join(self.projPath, "cache") + self.projContent = path.join(self.projPath, "content") + self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) if not self._checkFolder(self.projMeta): return False - if not self._checkFolder(self.projData): + if not self._checkFolder(self.projCache): return False + if not self._checkFolder(self.projContent): + return False + + # Check for Old Legacy Data + # ========================= + + errList = [] + for projItem in listdir(self.projPath): + logger.verbose("Project contains: %s" % projItem) + if projItem.startswith("data_"): + self._legacyDataFolder(projItem) + + if errList: + self.makeAlert(errList, nwAlert.ERROR) + + self._deprecatedFiles() + + # Project Lock + # ============ if overrideLock: self._clearLockFile() @@ -272,10 +296,8 @@ class NWProject(): else: logger.verbose("Project is not locked") - try: - projectMaintenance(self) - except Exception as E: - logger.error(str(E)) + # Open The Project XML File + # ========================= try: nwXML = etree.parse(fileName) @@ -321,6 +343,7 @@ class NWProject(): # Check File Type # =============== + if not nwxRoot == "novelWriterXML": self.makeAlert( "Project file does not appear to be a novelWriterXML file.", @@ -330,14 +353,17 @@ class NWProject(): # Check Project Storage Version # ============================= + if fileVersion == "1.0": msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Old Project Version", ( "The project file and data is created by a %s version lower than 0.7. " "Do you want to upgrade the project to the most recent format?

" "Note that after the upgrade, you cannot open the project with an older " - "version of novelWriter any more, so make sure you have a recent backup." - ) % nw.__package__) + "version of %s any more, so make sure you have a recent backup." + ) % ( + nw.__package__, nw.__package__ + )) if msgRes == QMessageBox.Yes: self._updateStorage() else: @@ -353,6 +379,7 @@ class NWProject(): # Check novelWriter Version # ========================= + if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question(self.theParent, "Version Conflict", ( @@ -367,6 +394,7 @@ class NWProject(): # Start Parsing XML # ================= + for xChild in xRoot: if xChild.tag == "project": logger.debug("Found project meta") @@ -384,6 +412,7 @@ class NWProject(): self.bookAuthors.append(xItem.text) elif xItem.tag == "backup": self.doBackup = checkBool(xItem.text, False) + elif xChild.tag == "settings": logger.debug("Found project settings") for xItem in xChild: @@ -411,6 +440,7 @@ class NWProject(): for xEntry in xItem: titleFormat[xEntry.tag] = checkString(xEntry.text, "", False) self.setTitleFormat(titleFormat) + elif xChild.tag == "content": logger.debug("Found project content") self.projTree.unpackXML(xChild) @@ -444,14 +474,14 @@ class NWProject(): return False self.projMeta = path.join(self.projPath, "meta") - self.projData = path.join(self.projPath, "content") + self.projContent = path.join(self.projPath, "content") saveTime = time() if not self._checkFolder(self.projPath): return False if not self._checkFolder(self.projMeta): return False - if not self._checkFolder(self.projData): + if not self._checkFolder(self.projContent): return False logger.debug("Saving project: %s" % self.projPath) @@ -978,7 +1008,7 @@ class NWProject(): # Then check the files in the data folder orphanFiles = [] - for fileItem in listdir(self.projData): + for fileItem in listdir(self.projContent): if not fileItem.endswith(".nwd"): logger.warning("Skipping file %s" % fileItem) continue @@ -1046,52 +1076,113 @@ class NWProject(): return True - def _updateStorage(self): - """Updates the project storage folder from 1.0 to 1.1. + ## + # Legacy Data Structure Handlers + ## + + def _legacyDataFolder(self, theFolder): + """Clean up legacy data folders. """ - contDir = path.join(self.projPath, "content") - self._checkFolder(contDir) errList = [] + theData = path.join(self.projPath, theFolder) + if not path.isdir(theData): + errList.append("Not a folder: %s" % theData) + return errList - for projItem in listdir(self.projPath): - itemPath = path.join(self.projPath, projItem) - if not path.isdir(itemPath) or not projItem.startswith("data_"): + logger.info("Old data folder %s found" % theFolder) + + # Move Documents to Content + # ========================= + for dataItem in listdir(theData): + theFile = path.join(theData, dataItem) + if not path.isfile(theFile): + theErr = self._moveUnknownItem(theData, dataItem) + if theErr: + errList.append(theErr) continue - for dataFile in listdir(itemPath): - dataPath = path.join(itemPath, dataFile) - if dataFile.endswith(".bak"): - try: - unlink(dataPath) - logger.info("Deleted file: %s" % dataPath) - except: - errList.append("Failed to delete: %s" % dataPath) - elif dataFile.endswith(".nwd") and len(dataFile) == 21: - tHandle = projItem[-1]+dataFile[:12] - newPath = path.join(contDir, tHandle+".nwd") - try: - rename(dataPath, newPath) - logger.info("Moved file: %s" % dataPath) - logger.info("New location: %s" % newPath) - except: - errList.append("Failed to move: %s" % dataPath) + if len(dataItem) == 21 and dataItem.endswith("_main.nwd"): + tHandle = theFolder[-1]+dataItem[:12] + newPath = path.join(self.projContent, tHandle+".nwd") + try: + rename(theFile, newPath) + logger.info("Moved file: %s" % theFile) + logger.info("New location: %s" % newPath) + except Exception as e: + logger.error(str(e)) + errList.append("Could not move: %s" % theFile) - else: - newPath = path.join(self.projPath, "unknown_"+dataFile) - try: - rename(dataPath, newPath) - logger.info("Moved file: %s" % dataPath) - logger.info("New location: %s" % newPath) - except: - errList.append("Failed to move: %s" % dataPath) - try: - rmdir(itemPath) - logger.info("Removed folder: %s" % itemPath) - except: - errList.append("Failed to delete: %s" % itemPath) + elif len(dataItem) == 21 and dataItem.endswith("_main.bak"): + try: + unlink(theFile) + logger.info("Deleted file: %s" % theFile) + except Exception as e: + logger.error(str(e)) + errList.append("Could not delete: %s" % theFile) - if errList: - self.makeAlert(errList, nwAlert.ERROR) + else: + theErr = self._moveUnknownItem(theData, dataItem) + if theErr: + errList.append(theErr) + + # Remove Data Folder + # ================== + try: + rmdir(theData) + logger.info("Removed folder: %s" % theFolder) + except: + errList.append("Failed to remove: %s" % theFolder) + + return errList + + def _moveUnknownItem(self, theDir, theItem): + """Move an item that doesn't belong in the project folder to + a junk folder. + """ + theJunk = path.join(self.projPath, "junk") + if not self._checkFolder(theJunk): + return "Could not make folder: %s" % theJunk + + theSrc = path.join(theDir, theItem) + theDst = path.join(theJunk, theItem) + + try: + rename(theSrc, theDst) + logger.info("Moved to junk: %s" % theSrc) + except Exception as e: + logger.error(str(e)) + return "Could not move item %s to junk." % theSrc + + return "" + + def _deprecatedFiles(self): + """Delete files that are no longer used by novelWriter. + """ + rmList = [] + rmList.append(path.join(self.projCache, "nwProject.nwx.0")) + rmList.append(path.join(self.projCache, "nwProject.nwx.1")) + rmList.append(path.join(self.projCache, "nwProject.nwx.2")) + rmList.append(path.join(self.projCache, "nwProject.nwx.3")) + rmList.append(path.join(self.projCache, "nwProject.nwx.4")) + rmList.append(path.join(self.projCache, "nwProject.nwx.5")) + rmList.append(path.join(self.projCache, "nwProject.nwx.6")) + rmList.append(path.join(self.projCache, "nwProject.nwx.7")) + rmList.append(path.join(self.projCache, "nwProject.nwx.8")) + rmList.append(path.join(self.projCache, "nwProject.nwx.9")) + rmList.append(path.join(self.projMeta, "mainOptions.json")) + rmList.append(path.join(self.projMeta, "exportOptions.json")) + rmList.append(path.join(self.projMeta, "outlineOptions.json")) + rmList.append(path.join(self.projMeta, "timelineOptions.json")) + rmList.append(path.join(self.projMeta, "docMergeOptions.json")) + rmList.append(path.join(self.projMeta, "sessionLogOptions.json")) + + for rmFile in rmList: + if path.isfile(rmFile): + logger.info("Deleting: %s" % rmFile) + try: + unlink(rmFile) + except Exception as e: + logger.error(str(e)) return diff --git a/nw/core/tools.py b/nw/core/tools.py index 8aa81d7b..8115105d 100644 --- a/nw/core/tools.py +++ b/nw/core/tools.py @@ -8,7 +8,6 @@ File History: Created: 2019-04-22 [0.0.1] countWords Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN - Created: 2020-02-13 [0.4.3] projectMaintenance Merged: 2020-05-08 [0.4.5] All of the above into this file This file is a part of novelWriter @@ -81,45 +80,6 @@ def countWords(theText): return charCount, wordCount, paraCount -def projectMaintenance(theProject): - """Wrapper class for handling various tasks related to managing old - projects with content from older versions of novelWriter. - """ - # Remove no longer used project cache folder - if path.isdir(theProject.projPath): - cacheDir = path.join(theProject.projPath, "cache") - if path.isdir(cacheDir): - logger.info("Deprecated cache folder content found") - rmList = [] - for i in range(10): - rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i)) - rmList.append(path.join(cacheDir, "projCount.txt")) - for rmFile in rmList: - if path.isfile(rmFile): - logger.info("Deleting: %s" % rmFile) - try: - unlink(rmFile) - except Exception as e: - logger.error(str(e)) - - # Remove no longer used meta files - rmList = [] - rmList.append(path.join(theProject.projMeta, "mainOptions.json")) - rmList.append(path.join(theProject.projMeta, "exportOptions.json")) - rmList.append(path.join(theProject.projMeta, "outlineOptions.json")) - rmList.append(path.join(theProject.projMeta, "timelineOptions.json")) - rmList.append(path.join(theProject.projMeta, "docMergeOptions.json")) - rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json")) - for rmFile in rmList: - if path.isfile(rmFile): - logger.info("Deleting: %s" % rmFile) - try: - unlink(rmFile) - except Exception as e: - logger.error(str(e)) - - return - def numberToWord(numVal, theLanguage): """Wrapper for converting numbers to words for chapter headings. """ diff --git a/sample/content/ae7339df26ded.nwd b/sample/content/ae7339df26ded.nwd index 8b92a69c..05ed60d5 100644 --- a/sample/content/ae7339df26ded.nwd +++ b/sample/content/ae7339df26ded.nwd @@ -2,6 +2,6 @@ ### We Found John! @pov: John -@location: Mars, OuterSpace +@location: Mars Jane has been searching for a while, and she finally found John on Mars. He was indeed in space! What was he doing on Mars anyway? Well, it turns out, he was farming potatoes. diff --git a/sample/content/b8136a5a774a0.nwd b/sample/content/b8136a5a774a0.nwd index a304cb35..4e63373d 100644 --- a/sample/content/b8136a5a774a0.nwd +++ b/sample/content/b8136a5a774a0.nwd @@ -1,4 +1,4 @@ -%%~ b8136a5a774a0:7031beac91f75:Delete Me! +%%~ b8136a5a774a0:98acd8c76c93a:Delete Me! ### Delete Me! This scene is trash. \ No newline at end of file diff --git a/sample/content/edca4be2fcaf8.nwd b/sample/content/edca4be2fcaf8.nwd index 5fef1f34..679e4e27 100644 --- a/sample/content/edca4be2fcaf8.nwd +++ b/sample/content/edca4be2fcaf8.nwd @@ -1,4 +1,4 @@ -%%~ edca4be2fcaf8:7031beac91f75:Part One +%%~ edca4be2fcaf8:7031beac91f75:Part 1 # Part One The first part. \ No newline at end of file diff --git a/sample/content/f1471bef9f2ae.nwd b/sample/content/f1471bef9f2ae.nwd index 3bd03e9d..9b2d9368 100644 --- a/sample/content/f1471bef9f2ae.nwd +++ b/sample/content/f1471bef9f2ae.nwd @@ -5,10 +5,4 @@ Space … it’s an awful lot of nothing, with bits in it here and there. Some of which, people like to call home. -## Outer Space -@tag: OuterSpace - -Now even further into space! - -You can have more than one tag in a file, as long as there is only one tag per heading. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index b6f63962..58b97959 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -11,8 +11,8 @@ True True 636b6aa9b697b - bc0cbd2a407f3 - 941 + ba8a28a246524 + 914 B E @@ -73,7 +73,7 @@ False True PAGE - 210 + 208 40 2 213 @@ -89,7 +89,7 @@ 23 5 1 - 27 + 0
A Folder @@ -122,7 +122,7 @@ 1199 216 7 - 1066 + 1266 Another Scene @@ -135,7 +135,7 @@ 476 93 3 - 428 + 551 Interlude @@ -148,7 +148,7 @@ 633 101 3 - 752 + 1238 A Note on Structure @@ -161,7 +161,7 @@ 1692 313 6 - 551 + 1721 Chapter Two @@ -174,7 +174,7 @@ 139 28 1 - 242 + 343 We Found John! @@ -214,7 +214,7 @@ 49 9 1 - 65 + 24 Jane Smith @@ -227,7 +227,7 @@ 55 9 1 - 71 + 25 Locations @@ -247,7 +247,7 @@ 76 15 1 - 93 + 20 Space @@ -257,10 +257,10 @@ False True NOTE - 241 - 51 - 3 - 135 + 115 + 24 + 1 + 133 Mars From cae9705e4f0fe7f09e1736ba010aa781f2edf09d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 May 2020 23:03:04 +0200 Subject: [PATCH 31/38] Files don't need the expanded flag saved to XML --- nw/core/project.py | 3 ++- sample/nwProject.nwx | 18 +----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index c89bb645..abaed265 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1572,7 +1572,6 @@ class NWItem(): xSub = self._subPack(xPack,"type", text=str(self.itemType.name)) xSub = self._subPack(xPack,"class", text=str(self.itemClass.name)) xSub = self._subPack(xPack,"status", text=str(self.itemStatus)) - xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded)) if self.itemType == nwItemType.FILE: xSub = self._subPack(xPack,"exported", text=str(self.isExported)) xSub = self._subPack(xPack,"layout", text=str(self.itemLayout.name)) @@ -1580,6 +1579,8 @@ class NWItem(): xSub = self._subPack(xPack,"wordCount", text=str(self.wordCount), none=False) xSub = self._subPack(xPack,"paraCount", text=str(self.paraCount), none=False) xSub = self._subPack(xPack,"cursorPos", text=str(self.cursorPos), none=False) + else: + xSub = self._subPack(xPack,"expanded", text=str(self.isExpanded)) return def unpackXML(self, xItem): diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 58b97959..60752873 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -57,7 +57,6 @@ FILE NOVEL Started - False True TITLE 72 @@ -70,7 +69,6 @@ FILE NOVEL New - False True PAGE 208 @@ -83,7 +81,6 @@ FILE NOVEL New - False True PARTITION 23 @@ -103,7 +100,6 @@ FILE NOVEL Notes - False True CHAPTER 12 @@ -116,7 +112,6 @@ FILE NOVEL 1st Draft - False True SCENE 1199 @@ -129,7 +124,6 @@ FILE NOVEL 1st Draft - False True SCENE 476 @@ -142,7 +136,6 @@ FILE NOVEL Finished - False True UNNUMBERED 633 @@ -155,7 +148,6 @@ FILE NOVEL 2nd Draft - False False NOTE 1692 @@ -168,7 +160,6 @@ FILE NOVEL 1st Draft - False True CHAPTER 139 @@ -181,7 +172,6 @@ FILE NOVEL 1st Draft - False True SCENE 189 @@ -208,7 +198,6 @@ FILE CHARACTER Minor - False True NOTE 49 @@ -221,7 +210,6 @@ FILE CHARACTER Major - False True NOTE 55 @@ -241,7 +229,6 @@ FILE WORLD Main - False True NOTE 76 @@ -254,7 +241,6 @@ FILE WORLD Minor - False True NOTE 115 @@ -267,7 +253,6 @@ FILE WORLD Major - False True NOTE 28 @@ -287,7 +272,6 @@ FILE NOVEL New - False True SCENE 0 From 421c40c288508f551c4875aa3c88b097d52aa280 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 May 2020 23:08:27 +0200 Subject: [PATCH 32/38] Fixed tests --- tests/reference/gui/0_nwProject.nwx | 3 +-- tests/reference/gui/1_nwProject.nwx | 6 +----- tests/reference/gui/2_nwProject.nwx | 3 +-- tests/reference/gui/3_nwProject.nwx | 3 +-- tests/reference/proj/1_nwProject.nwx | 3 +-- tests/reference/proj/2_nwProject.nwx | 3 +-- tests/reference/proj/3_nwProject.nwx | 5 +---- 7 files changed, 7 insertions(+), 19 deletions(-) diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index 99ea2a3d..6d2bf739 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -55,7 +55,6 @@ FILE NOVEL New - False True SCENE 0 diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index c9bb4dad..112a4be0 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -55,7 +55,6 @@ FILE NOVEL New - False True SCENE 331 @@ -75,7 +74,6 @@ FILE CHARACTER New - False True NOTE 34 @@ -95,7 +93,6 @@ FILE PLOT New - False True NOTE 48 @@ -115,7 +112,6 @@ FILE WORLD New - False True NOTE 51 diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index bf3c5b61..c4668fe4 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -59,7 +59,6 @@ FILE NOVEL New - False True SCENE 0 diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index f7db491f..35e1b8df 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -55,7 +55,6 @@ FILE NOVEL Note - False False PAGE 0 diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index 651ae852..53499ddc 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -76,7 +76,6 @@ FILE NOVEL New - False True SCENE 0 diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index 7bd8d7cb..22a9e0fb 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -76,7 +76,6 @@ FILE NOVEL New - False True SCENE 0 diff --git a/tests/reference/proj/3_nwProject.nwx b/tests/reference/proj/3_nwProject.nwx index 94f11ba7..ee8f6e1a 100644 --- a/tests/reference/proj/3_nwProject.nwx +++ b/tests/reference/proj/3_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -76,7 +76,6 @@ FILE NOVEL New - False True SCENE 0 @@ -117,7 +116,6 @@ FILE NOVEL New - False True SCENE 0 @@ -130,7 +128,6 @@ FILE CHARACTER New - False True NOTE 0 From 5d2f6d33b5785c606f0d6f5c33e62a79928ef999 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 May 2020 23:31:38 +0200 Subject: [PATCH 33/38] Also updated the documen class with new project variables --- .gitignore | 10 ++++------ nw/core/document.py | 41 +++++++++++++++++++---------------------- nw/core/project.py | 44 +++++++++++++++++++++++++------------------- sample/nwProject.nwx | 6 +++--- 4 files changed, 51 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 2905634a..c87816d0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,12 +14,10 @@ docs/source/_* __pycache__ # Sample Project -sample/**/cache -sample/**/wordlist.txt -sample/**/sessionInfo.log -sample/**/*.bak -sample/**/*.json -sample/**/*.lock +sample/cache +sample/meta +sample/*.bak +sample/*.lock # PyTest tests/temp diff --git a/nw/core/document.py b/nw/core/document.py index 7ce4de4e..6ab4d500 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -91,18 +91,17 @@ class NWDoc(): if self.theItem.parHandle == self.theProject.projTree.trashRoot(): self.docEditable = False - docDir = "content" docFile = self.docHandle+".nwd" - self.fileLoc = path.join(docDir, docFile) - logger.debug("Opening document %s" % self.fileLoc) - dataDir = path.join(self.theProject.projPath, docDir) - docPath = path.join(dataDir, docFile) + logger.debug("Opening document %s" % docFile) + + docPath = path.join(self.theProject.projContent, docFile) + self.fileLoc = docPath theText = "" self.docMeta = "" if path.isfile(docPath): try: - with open(docPath,mode="r",encoding="utf8") as inFile: + with open(docPath, mode="r", encoding="utf8") as inFile: fstLine = inFile.readline() if fstLine.startswith("%%~ "): # This is the meta line @@ -112,7 +111,7 @@ class NWDoc(): theText += inFile.read() except Exception as e: - self.makeAlert(["Failed to open document file.",str(e)], nwAlert.ERROR) + self.makeAlert(["Failed to open document file.", str(e)], nwAlert.ERROR) # Note: Document must be cleared in case of an io error, # or else the auto-save or save will try to overwrite it # with an empty file. Return None to alert the caller. @@ -138,25 +137,23 @@ class NWDoc(): if self.docHandle is None or not self.docEditable: return False - docDir = "content" + self.theProject.ensureFolderStructure() + docFile = self.docHandle+".nwd" - logger.debug("Saving document %s" % path.join(docDir, docFile)) - dataPath = path.join(self.theProject.projPath, docDir) - docPath = path.join(dataPath, docFile) - if not path.isdir(dataPath): - mkdir(dataPath) - logger.debug("Created folder %s" % dataPath) + logger.debug("Saving document %s" % docFile) + + docPath = path.join(self.theProject.projContent, docFile) + docTemp = path.join(self.theProject.projContent, docFile+"~") itemPath = self.theProject.projTree.getItemPath(self.docHandle) docMeta = "%%~ "+":".join(itemPath)+":"+self.theItem.itemName+"\n" - docTemp = path.join(dataPath, docFile+"~") try: - with open(docTemp,mode="w",encoding="utf8") as outFile: + with open(docTemp, mode="w", encoding="utf8") as outFile: outFile.write(docMeta) outFile.write(docText) except Exception as e: - self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR) + self.makeAlert(["Could not save document.", str(e)], nwAlert.ERROR) return False # If we're here, the file was successfully saved, so we can @@ -173,13 +170,12 @@ class NWDoc(): """Permanently delete a document source file and its backups from the project data folder. """ - docDir = "content" docFile = self.docHandle+".nwd" - dataPath = path.join(self.theProject.projPath, docDir) + chkList = [] - chkList.append(path.join(dataPath, docFile)) - chkList.append(path.join(dataPath, docFile+"~")) - chkList.append(path.join(dataPath, docFile[:-3]+"bak")) + chkList.append(path.join(self.theProject.projContent, docFile)) + chkList.append(path.join(self.theProject.projContent, docFile+"~")) + for chkFile in chkList: if path.isfile(chkFile): try: @@ -188,6 +184,7 @@ class NWDoc(): except Exception as e: self.makeAlert(["Could not delete document file.",str(e)], nwAlert.ERROR) return False + return True ## diff --git a/nw/core/project.py b/nw/core/project.py index abaed265..d7ab3c2d 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -252,17 +252,10 @@ class NWProject(): # Standard Folders and Files # ========================== - self.projMeta = path.join(self.projPath, "meta") - self.projCache = path.join(self.projPath, "cache") - self.projContent = path.join(self.projPath, "content") - self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) + if not self.ensureFolderStructure(): + return False - if not self._checkFolder(self.projMeta): - return False - if not self._checkFolder(self.projCache): - return False - if not self._checkFolder(self.projContent): - return False + self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT) # Check for Old Legacy Data # ========================= @@ -473,15 +466,8 @@ class NWProject(): ) return False - self.projMeta = path.join(self.projPath, "meta") - self.projContent = path.join(self.projPath, "content") saveTime = time() - - if not self._checkFolder(self.projPath): - return False - if not self._checkFolder(self.projMeta): - return False - if not self._checkFolder(self.projContent): + if not self.ensureFolderStructure(): return False logger.debug("Saving project: %s" % self.projPath) @@ -582,6 +568,26 @@ class NWProject(): self.lockedBy = None return True + def ensureFolderStructure(self): + """Ensure that all necessary folders exist in the project + folder. + """ + if self.projPath is None or self.projPath == "": + return False + + self.projMeta = path.join(self.projPath, "meta") + self.projCache = path.join(self.projPath, "cache") + self.projContent = path.join(self.projPath, "content") + + if not self._checkFolder(self.projMeta): + return False + if not self._checkFolder(self.projCache): + return False + if not self._checkFolder(self.projContent): + return False + + return True + ## # Backup Project ## @@ -1058,7 +1064,7 @@ class NWProject(): def _appendSessionStats(self): """Append session statistics to the sessions log file. """ - if self.projMeta is None: + if not self.ensureFolderStructure(): return False sessionFile = path.join(self.projMeta, nwFiles.SESS_INFO) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 60752873..a0d7f3ce 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -11,7 +11,7 @@ True True 636b6aa9b697b - ba8a28a246524 + b3e74dbc1f584 914 B @@ -71,7 +71,7 @@ New True PAGE - 208 + 210 40 2 213 From 7b8a852101fb631688a70124c03f0fb1221d368b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 29 May 2020 23:46:13 +0200 Subject: [PATCH 34/38] Corrected default title formats, and added editTime meta data --- nw/core/project.py | 8 ++++++-- sample/nwProject.nwx | 2 +- tests/reference/gui/0_nwProject.nwx | 2 +- tests/reference/gui/1_nwProject.nwx | 2 +- tests/reference/gui/2_nwProject.nwx | 2 +- tests/reference/gui/3_nwProject.nwx | 2 +- tests/reference/proj/1_nwProject.nwx | 2 +- tests/reference/proj/2_nwProject.nwx | 2 +- tests/reference/proj/3_nwProject.nwx | 2 +- 9 files changed, 14 insertions(+), 10 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index d7ab3c2d..132066b3 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -69,6 +69,7 @@ class NWProject(): self.lockedBy = None # Data on which computer has the project open self.saveCount = 0 # Meta data: number of saves self.autoCount = 0 # Meta data: number of automatic saves + self.editTime = 0 # The accumulated edit time read from the project file # Class Settings self.projPath = None # The full path to where the currently open project is saved @@ -206,7 +207,7 @@ class NWProject(): self.autoReplace = {} self.titleFormat = { "title" : r"%title%", - "chapter" : r"Chapter %num%\\%title%", + "chapter" : r"Chapter %ch%: %title%", "unnumbered" : r"%title%", "scene" : r"* * *", "section" : r"", @@ -330,6 +331,8 @@ class NWProject(): self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False) if "autoCount" in xRoot.attrib: self.autoCount = checkInt(xRoot.attrib["autoCount"], 0, False) + if "editTime" in xRoot.attrib: + self.editTime = checkInt(xRoot.attrib["editTime"], 0, False) logger.verbose("XML root is %s" % nwxRoot) logger.verbose("File version is %s" % fileVersion) @@ -486,6 +489,7 @@ class NWProject(): "saveCount" : str(self.saveCount), "autoCount" : str(self.autoCount), "timeStamp" : formatTimeStamp(saveTime), + "editTime" : str(int(self.editTime + saveTime - self.projOpened)), }) # Save Project Meta @@ -535,7 +539,7 @@ class NWProject(): xml_declaration = True )) except Exception as e: - self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR) + self.makeAlert(["Failed to save project.", str(e)], nwAlert.ERROR) return False # If we're here, the file was successfully saved, diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index a0d7f3ce..b70706eb 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index 6d2bf739..dcf46837 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -14,7 +14,7 @@ %title% - Chapter %num%\\%title% + Chapter %ch%: %title% %title% * * *
diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 112a4be0..a09a089f 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -14,7 +14,7 @@ %title% - Chapter %num%\\%title% + Chapter %ch%: %title% %title% * * *
diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index c4668fe4..11fc5dbe 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -18,7 +18,7 @@
%title% - Chapter %num%\\%title% + Chapter %ch%: %title% %title% * * *
diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 35e1b8df..e7f9e514 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -14,7 +14,7 @@ %title% - Chapter %num%\\%title% + Chapter %ch%: %title% %title% * * *
diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index 53499ddc..5ba28cae 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -14,7 +14,7 @@ %title% - Chapter %num%\\%title% + Chapter %ch%: %title% %title% * * *
diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index 22a9e0fb..fe5bf878 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -14,7 +14,7 @@ %title% - Chapter %num%\\%title% + Chapter %ch%: %title% %title% * * *
diff --git a/tests/reference/proj/3_nwProject.nwx b/tests/reference/proj/3_nwProject.nwx index ee8f6e1a..2e3cb6c0 100644 --- a/tests/reference/proj/3_nwProject.nwx +++ b/tests/reference/proj/3_nwProject.nwx @@ -14,7 +14,7 @@ %title% - Chapter %num%\\%title% + Chapter %ch%: %title% %title% * * *
From d27e2dc60fbb86c05cc43232236f188da6c95705 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 May 2020 00:15:07 +0200 Subject: [PATCH 35/38] Added code to write contents txt and json files on project close --- nw/constants/constants.py | 4 ++-- nw/core/project.py | 48 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index cf0ab4eb..f538f2fe 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -39,8 +39,8 @@ class nwFiles(): PROJ_FILE = "nwProject.nwx" PROJ_DICT = "wordlist.txt" PROJ_LOCK = "nwProject.lock" - TOC_TXT = "ToC.txt" - TOC_JSON = "ToC.json" + TOC_TXT = "content.txt" + TOC_JSON = "content.json" SESS_INFO = "sessionInfo.log" INDEX_FILE = "tagsIndex.json" OPTS_FILE = "guiOptions.json" diff --git a/nw/core/project.py b/nw/core/project.py index 132066b3..f2abbcf4 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -31,6 +31,7 @@ """ import logging +import json import nw from os import path, mkdir, listdir, unlink, rename, rmdir @@ -566,6 +567,7 @@ class NWProject(): def closeProject(self): """Close the current project and clear all meta data. """ + self.projTree.writeToCFiles() self._appendSessionStats() self._clearLockFile() self.clearProject() @@ -1282,7 +1284,7 @@ class NWTree(): for tHandle in self._treeOrder: tItem = self.__getitem__(tHandle) tItem.packXML(xContent) - return + return def unpackXML(self, xContent): """Iterate through all items of a content XML object and add @@ -1300,6 +1302,50 @@ class NWTree(): return True + def writeToCFiles(self): + """Write the convenience table of contents files in the root of + the project directory. These files are there to assist the user + if they wish to browse the stored files. + """ + tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT) + tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON) + + jsonData = [] + try: + # Dump the text + with open(tocText, mode="w", encoding="utf8") as outFile: + outFile.write("\n") + outFile.write(" Table of Contents\n") + outFile.write("===================\n") + outFile.write("\n") + outFile.write(" %-25s %-9s %s\n" %("File Name","Class","Document Label")) + outFile.write("-"*80+"\n") + for tHandle in sorted(self._treeOrder): + tItem = self.__getitem__(tHandle) + if tItem is None: + continue + tFile = tHandle+".nwd" + if path.isfile(path.join(self.theProject.projContent, tFile)): + outFile.write(" %-25s %-9s %s\n" %( + path.join("content", tFile), + tItem.itemClass.name, + tItem.itemName, + )) + jsonData.append([ + path.join("content", tFile), + tItem.itemClass.name, + tItem.itemName, + ]) + + # Dump the JSON + with open(tocJson, mode="w+", encoding="utf8") as outFile: + outFile.write(json.dumps(jsonData, indent=2)) + + except Exception as e: + logger.error(str(e)) + + return + ## # Tree Structure Methods ## From a7bca8486f32993b0438163e6be13e25ff8ab4de Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 May 2020 00:16:53 +0200 Subject: [PATCH 36/38] Added the ToC files as well to the repo --- nw/constants/constants.py | 4 +- sample/ToC.json | 82 +++++++++++++++++++++++++++++++++++++++ sample/ToC.txt | 22 +++++++++++ sample/nwProject.nwx | 2 +- 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 sample/ToC.json create mode 100644 sample/ToC.txt diff --git a/nw/constants/constants.py b/nw/constants/constants.py index f538f2fe..cf0ab4eb 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -39,8 +39,8 @@ class nwFiles(): PROJ_FILE = "nwProject.nwx" PROJ_DICT = "wordlist.txt" PROJ_LOCK = "nwProject.lock" - TOC_TXT = "content.txt" - TOC_JSON = "content.json" + TOC_TXT = "ToC.txt" + TOC_JSON = "ToC.json" SESS_INFO = "sessionInfo.log" INDEX_FILE = "tagsIndex.json" OPTS_FILE = "guiOptions.json" diff --git a/sample/ToC.json b/sample/ToC.json new file mode 100644 index 00000000..4e6ff3d1 --- /dev/null +++ b/sample/ToC.json @@ -0,0 +1,82 @@ +[ + [ + "content/14298de4d9524.nwd", + "CHARACTER", + "John Smith" + ], + [ + "content/53b69b83cdafc.nwd", + "NOVEL", + "Title Page" + ], + [ + "content/5eaea4e8cdee8.nwd", + "WORLD", + "Mars" + ], + [ + "content/636b6aa9b697b.nwd", + "NOVEL", + "Making a Scene" + ], + [ + "content/6a2d6d5f4f401.nwd", + "NOVEL", + "Chapter One" + ], + [ + "content/88706ddc78b1b.nwd", + "NOVEL", + "Chapter Two" + ], + [ + "content/96b68994dfa3d.nwd", + "NOVEL", + "A Note on Structure" + ], + [ + "content/974e400180a99.nwd", + "NOVEL", + "Page" + ], + [ + "content/ae7339df26ded.nwd", + "NOVEL", + "We Found John!" + ], + [ + "content/b3e74dbc1f584.nwd", + "WORLD", + "Earth" + ], + [ + "content/b8136a5a774a0.nwd", + "NOVEL", + "Delete Me!" + ], + [ + "content/ba8a28a246524.nwd", + "NOVEL", + "Interlude" + ], + [ + "content/bb2c23b3c42cc.nwd", + "CHARACTER", + "Jane Smith" + ], + [ + "content/bc0cbd2a407f3.nwd", + "NOVEL", + "Another Scene" + ], + [ + "content/edca4be2fcaf8.nwd", + "NOVEL", + "Part One" + ], + [ + "content/f1471bef9f2ae.nwd", + "WORLD", + "Space" + ] +] \ No newline at end of file diff --git a/sample/ToC.txt b/sample/ToC.txt new file mode 100644 index 00000000..b1a7c2ed --- /dev/null +++ b/sample/ToC.txt @@ -0,0 +1,22 @@ + + Table of Contents +=================== + + File Name Class Document Label +-------------------------------------------------------------------------------- + content/14298de4d9524.nwd CHARACTER John Smith + content/53b69b83cdafc.nwd NOVEL Title Page + content/5eaea4e8cdee8.nwd WORLD Mars + content/636b6aa9b697b.nwd NOVEL Making a Scene + content/6a2d6d5f4f401.nwd NOVEL Chapter One + content/88706ddc78b1b.nwd NOVEL Chapter Two + content/96b68994dfa3d.nwd NOVEL A Note on Structure + content/974e400180a99.nwd NOVEL Page + content/ae7339df26ded.nwd NOVEL We Found John! + content/b3e74dbc1f584.nwd WORLD Earth + content/b8136a5a774a0.nwd NOVEL Delete Me! + content/ba8a28a246524.nwd NOVEL Interlude + content/bb2c23b3c42cc.nwd CHARACTER Jane Smith + content/bc0cbd2a407f3.nwd NOVEL Another Scene + content/edca4be2fcaf8.nwd NOVEL Part One + content/f1471bef9f2ae.nwd WORLD Space diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index b70706eb..f736c544 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project From f3cc6359c4d731a13567dcdb1143346d00410e3a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 May 2020 00:20:14 +0200 Subject: [PATCH 37/38] Add an empty line at the end of the ToC.txt file --- nw/core/project.py | 1 + sample/ToC.txt | 1 + sample/nwProject.nwx | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nw/core/project.py b/nw/core/project.py index f2abbcf4..13fa1f08 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1336,6 +1336,7 @@ class NWTree(): tItem.itemClass.name, tItem.itemName, ]) + outFile.write("\n") # Dump the JSON with open(tocJson, mode="w+", encoding="utf8") as outFile: diff --git a/sample/ToC.txt b/sample/ToC.txt index b1a7c2ed..da23e664 100644 --- a/sample/ToC.txt +++ b/sample/ToC.txt @@ -20,3 +20,4 @@ content/bc0cbd2a407f3.nwd NOVEL Another Scene content/edca4be2fcaf8.nwd NOVEL Part One content/f1471bef9f2ae.nwd WORLD Space + diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index f736c544..4a88e0c1 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project From d2ab6a4934986926dcca5047c5aab62f291363ec Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 30 May 2020 00:28:39 +0200 Subject: [PATCH 38/38] Updated changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8131905..e5f313e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,14 @@ **Project Structure** -* The project folder structure has been simplified and cleaned up. We also now freeze the main entry values in the main XML file. The XML file is now given version 1.1, and no further core changes to its structure will be made without bumping this version. We're also locking it to only be opened by version 0.7 or later. An old project file is converted on first open. PR #253. +* The project folder structure has been simplified and cleaned up. We also now freeze the main entry values in the main XML file. The XML file is now given version 1.1, and no further core changes to its structure will be made without bumping this version. We're also locking it to only be opened by version 0.7 or later. An old project file is converted on first open. PRs #253 and #261. +* When a project is closed, two table of contents files are written to the project folder. They are named `ToC.txt` and `ToC.json` and are there for the user's convenience if they want to find a specific file from the project in the data folders. As discussed in Issue #259, PR #261. +* The expanded node flag from the project tree was also saved for file entries, which cannot actually be expanded. These flags are no longer saved in the XML file. PR #261. **Other Changes** * Dropped the usage of .bak copies of document files. This was the old method to ensure the document data was written successfully, but it uses twice the storage space. Instead, writing via a temp file is the safe way to save files. PR #248. +* The project class now records the accumulated time in seconds a project has been opened. This data is not yet displayed anywhere, but it is being tracked in the project XML file. PR #261. ## Version 0.6.4 [2020-xx-xx]