%s
\n" % tText) @@ -296,17 +301,17 @@ class ToHtml(Tokenizer): refTags = [] if theBits[0] in nwLabels.KEY_NAME: retText += " " % 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 @@ -{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: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?