From db2b95e4a52b60d004e504552fa0ad5053bf2aab Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Mon, 27 May 2019 20:50:01 +0200 Subject: [PATCH 01/23] Added NWIndex class file --- nw/project/index.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 nw/project/index.py diff --git a/nw/project/index.py b/nw/project/index.py new file mode 100644 index 00000000..0efe3608 --- /dev/null +++ b/nw/project/index.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +"""novelWriter Project Index + + novelWriter – Project Index +============================= + Class holding the index of tags + + File History: + Created: 2019-05-27 [0.1.4] + +""" + +import logging +import nw + +logger = logging.getLogger(__name__) + +class NWIndex(): + + def __init__(self, theParent): + + # Internal + self.theParent = theParent + self.mainConf = self.theParent.mainConf + + return + +# END Class NWIndex From ccef2a1e51a24d068725626becf114736ae399f5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Mon, 27 May 2019 22:28:58 +0200 Subject: [PATCH 02/23] Building a simple index now works --- nw/gui/statusbar.py | 4 +-- nw/gui/winmain.py | 1 + nw/gui/wordcounter.py | 2 -- nw/project/document.py | 7 ++-- nw/project/index.py | 77 ++++++++++++++++++++++++++++++++++++++++-- nw/project/project.py | 5 +++ 6 files changed, 86 insertions(+), 10 deletions(-) diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index 484e9519..79952289 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -88,7 +88,7 @@ class GuiMainStatus(QStatusBar): self.setRefTime(None) self.setStats(0,0) self.setCounts(0,0,0) - self.setDocHandleCount(None) + self.setDocHandle(None) self.setProjectStatus(None) self.setDocumentStatus(None) self._updateTime() @@ -132,7 +132,7 @@ class GuiMainStatus(QStatusBar): self.boxCounts.setText("Document: {:d} : {:d} : {:d}".format(cC,wC,pC)) return - def setDocHandleCount(self, theHandle): + def setDocHandle(self, theHandle): if theHandle is None: self.boxDocHandle.setText("0000000000000") else: diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index 0de1c27f..0bb8e315 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -283,6 +283,7 @@ class GuiMain(QMainWindow): self.docEditor.changeWidth() self.docEditor.setFocus() self.theProject.setLastEdited(tHandle) + self.theProject.theIndex.scanFile(tHandle) return True def saveDocument(self): diff --git a/nw/gui/wordcounter.py b/nw/gui/wordcounter.py index b44d1e60..4e044bca 100644 --- a/nw/gui/wordcounter.py +++ b/nw/gui/wordcounter.py @@ -13,8 +13,6 @@ import logging import nw -from time import time - from PyQt5.QtCore import QThread logger = logging.getLogger(__name__) diff --git a/nw/project/document.py b/nw/project/document.py index 83eaf153..5305c01b 100644 --- a/nw/project/document.py +++ b/nw/project/document.py @@ -41,11 +41,10 @@ class NWDoc(): self.docHandle = None return - def openDocument(self, tHandle): + def openDocument(self, tHandle, showStatus=True): self.docHandle = tHandle self.theItem = self.theProject.getItem(tHandle) - self.theParent.statusBar.setDocHandleCount(tHandle) docDir, docFile = self._assemblePath(self.FILE_MN) logger.debug("Opening document %s" % path.join(docDir,docFile)) @@ -63,7 +62,9 @@ class NWDoc(): logger.debug("The requested document does not exist.") return "" - self.theParent.statusBar.setStatus("Opened Document: %s" % self.theItem.itemName) + if showStatus: + self.theParent.statusBar.setStatus("Opened Document: %s" % self.theItem.itemName) + self.theParent.statusBar.setDocHandle(tHandle) return theDoc diff --git a/nw/project/index.py b/nw/project/index.py index 0efe3608..2d326098 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -13,16 +13,87 @@ import logging import nw +from nw.project.document import NWDoc + logger = logging.getLogger(__name__) class NWIndex(): - def __init__(self, theParent): + VALID_KEYS = ["todo","tag","pov","char","plot","time","location"] + + def __init__(self, theProject, theParent): # Internal - self.theParent = theParent - self.mainConf = self.theParent.mainConf + self.theProject = theProject + self.theParent = theParent + self.mainConf = self.theParent.mainConf + + # Indices + self.itemIndex = {} + self.keyIndex = {} + for aKey in self.VALID_KEYS: + self.keyIndex[aKey] = [] return + def scanFile(self, tHandle): + + theDocument = NWDoc(self.theProject, self.theParent) + theText = theDocument.openDocument(tHandle, False) + + self.itemIndex[tHandle] = {} + for aKey in self.VALID_KEYS: + self.itemIndex[tHandle][aKey] = [] + + nLine = 0 + for aLine in theText.splitlines(): + aLine = aLine.strip() + nLine += 1 + nChar = len(aLine) + if nChar > 0 and aLine[0] == "@": + self.indexThis(tHandle, aLine, nLine) + + for aKey in self.VALID_KEYS: + if len(self.itemIndex[tHandle][aKey]) > 0: + if tHandle not in self.keyIndex[aKey]: + self.keyIndex[aKey].append(tHandle) + else: + if tHandle in self.keyIndex[aKey]: + self.keyIndex[aKey].remove(tHandle) + + print(self.itemIndex) + print(self.keyIndex) + + return + + def indexThis(self, tHandle, aLine, nLine): + + nChar = len(aLine) + nPos = aLine.find(":") + if nPos < 2 or nChar < nPos+2: + return False + + aKey = aLine[1:nPos].strip().lower() + tVal = aLine[nPos+1:].strip() + if aKey not in self.VALID_KEYS: + return False + + if aKey == "todo": + self.itemIndex[tHandle]["todo"].append((nLine, tVal)) + elif aKey == "tag": + if tVal.find(",") >- 0: + return False + self.itemIndex[tHandle]["tag"].append((nLine, tVal)) + else: + kVal = tVal.split(",") + cVal = [] + for aVal in kVal: + cVal.append(aVal.strip().lower()) + if len(cVal) > 0: + self.itemIndex[tHandle][aKey].append((nLine, cVal)) + else: + return False + + return True + # END Class NWIndex diff --git a/nw/project/project.py b/nw/project/project.py index dce45ef3..a70a1057 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -21,6 +21,7 @@ from time import time from nw.project.item import NWItem from nw.project.status import NWStatus +from nw.project.index import NWIndex from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from nw.common import checkString, checkBool, checkInt from nw.constants import nwFiles @@ -64,6 +65,9 @@ class NWProject(): self.lastWCount = 0 self.currWCount = 0 + # Index + self.theIndex = None + # Set Defaults self.clearProject() @@ -165,6 +169,7 @@ class NWProject(): self.lastViewed = None self.lastWCount = 0 self.currWCount = 0 + self.theIndex = NWIndex(self, self.theParent) return From e828d9f041040a903cdf97c1dfdf2cf12f4f84d1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Mon, 27 May 2019 23:03:44 +0200 Subject: [PATCH 03/23] Added a tag index, and added more valid keywords --- nw/project/index.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/nw/project/index.py b/nw/project/index.py index 2d326098..668c5ade 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -14,12 +14,16 @@ import logging import nw from nw.project.document import NWDoc +from nw.enum import nwItemType logger = logging.getLogger(__name__) class NWIndex(): - VALID_KEYS = ["todo","tag","pov","char","plot","time","location"] + VALID_KEYS = [ + "todo","tag","pov","char","plot","time","location", + "object","custom","scene","chapter","part" + ] def __init__(self, theProject, theParent): @@ -30,6 +34,7 @@ class NWIndex(): # Indices self.itemIndex = {} + self.tagIndex = {} self.keyIndex = {} for aKey in self.VALID_KEYS: self.keyIndex[aKey] = [] @@ -40,6 +45,13 @@ class NWIndex(): theDocument = NWDoc(self.theProject, self.theParent) theText = theDocument.openDocument(tHandle, False) + theItem = self.theProject.getItem(tHandle) + + if theItem is None: + return False + + if theItem.itemType != nwItemType.FILE: + return False self.itemIndex[tHandle] = {} for aKey in self.VALID_KEYS: @@ -51,7 +63,7 @@ class NWIndex(): nLine += 1 nChar = len(aLine) if nChar > 0 and aLine[0] == "@": - self.indexThis(tHandle, aLine, nLine) + self.indexThis(tHandle, aLine, nLine, theItem) for aKey in self.VALID_KEYS: if len(self.itemIndex[tHandle][aKey]) > 0: @@ -62,11 +74,12 @@ class NWIndex(): self.keyIndex[aKey].remove(tHandle) print(self.itemIndex) + print(self.tagIndex) print(self.keyIndex) - return + return True - def indexThis(self, tHandle, aLine, nLine): + def indexThis(self, tHandle, aLine, nLine, theItem): nChar = len(aLine) nPos = aLine.find(":") @@ -84,6 +97,7 @@ class NWIndex(): if tVal.find(",") >- 0: return False self.itemIndex[tHandle]["tag"].append((nLine, tVal)) + self.tagIndex[tVal] = (nLine, tHandle, theItem.itemClass) else: kVal = tVal.split(",") cVal = [] From 8d871c02527ada5d44b5c3ce3574a30c046139a9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Tue, 28 May 2019 19:44:14 +0200 Subject: [PATCH 04/23] The index is now built and saved as a json file --- .gitignore | 1 + nw/__init__.py | 5 ++-- nw/config.py | 1 + nw/constants.py | 9 ++++--- nw/gui/winmain.py | 5 +++- nw/project/index.py | 63 ++++++++++++++++++++++++++++++++----------- nw/project/project.py | 5 ---- 7 files changed, 61 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index f5ebca0d..47043c35 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__ sample/**/cache sample/**/wordlist.txt sample/**/*.bak +sample/**/*.json # PyTest tests/temp diff --git a/nw/__init__.py b/nw/__init__.py index 82fb621c..0015aabd 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -151,8 +151,9 @@ def main(sysArgs): debugGUI = True # Set Config Options - CONFIG.showGUI = not testMode - CONFIG.debugGUI = debugGUI + CONFIG.showGUI = not testMode + CONFIG.debugGUI = debugGUI + CONFIG.debugInfo = debugLevel < logging.INFO # Set Logging if showTime: debugStr = timeStr+debugStr diff --git a/nw/config.py b/nw/config.py index 1627270f..dec3f113 100644 --- a/nw/config.py +++ b/nw/config.py @@ -37,6 +37,7 @@ class Config: self.appHandle = nw.__package__.lower() self.showGUI = True self.debugGUI = False + self.debugInfo = False # Set Paths self.confPath = None diff --git a/nw/constants.py b/nw/constants.py index 478a7f5f..d9629718 100644 --- a/nw/constants.py +++ b/nw/constants.py @@ -14,10 +14,11 @@ from nw.enum import nwItemClass, nwItemLayout class nwFiles(): - APP_ICON = "novelWriter.svg" - PROJ_FILE = "nwProject.nwx" - PROJ_DICT = "wordlist.txt" - SESS_INFO = "sessionInfo.log" + APP_ICON = "novelWriter.svg" + PROJ_FILE = "nwProject.nwx" + PROJ_DICT = "wordlist.txt" + SESS_INFO = "sessionInfo.log" + INDEX_FILE = "tagsindex.json" # END Class nwFiles diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index 0bb8e315..addc898d 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -33,6 +33,7 @@ from nw.gui.statusbar import GuiMainStatus from nw.project.project import NWProject from nw.project.document import NWDoc from nw.project.item import NWItem +from nw.project.index import NWIndex from nw.convert.tokenizer import Tokenizer from nw.convert.tohtml import ToHtml from nw.enum import nwItemType, nwAlert @@ -50,6 +51,7 @@ class GuiMain(QMainWindow): self.theTheme = Theme() self.theProject = NWProject(self) self.theDocument = NWDoc(self.theProject, self) + self.tagIndex = NWIndex(self.theProject, self) self.hasProject = False self.resize(*self.mainConf.winGeometry) @@ -260,6 +262,7 @@ class GuiMain(QMainWindow): self.treeView.saveTreeOrder() self.theProject.saveProject() + self.tagIndex.saveIndex() self.mainMenu.updateRecentProjects() return True @@ -283,7 +286,7 @@ class GuiMain(QMainWindow): self.docEditor.changeWidth() self.docEditor.setFocus() self.theProject.setLastEdited(tHandle) - self.theProject.theIndex.scanFile(tHandle) + self.tagIndex.scanFile(tHandle) return True def saveDocument(self): diff --git a/nw/project/index.py b/nw/project/index.py index 668c5ade..6cd6d7fc 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -11,10 +11,14 @@ """ import logging +import json import nw +from os import path + from nw.project.document import NWDoc from nw.enum import nwItemType +from nw.constants import nwFiles logger = logging.getLogger(__name__) @@ -41,6 +45,27 @@ class NWIndex(): return + def saveIndex(self): + + indexFile = path.join(self.theProject.projMeta,nwFiles.INDEX_FILE) + if self.mainConf.debugInfo: + nIndent = 2 + else: + nIndent = None + try: + with open(indexFile,mode="w+") as outFile: + outFile.write(json.dumps({ + "itemIndex" : self.itemIndex, + "tagIndex" : self.tagIndex, + "keyIndex" : self.keyIndex + }, indent=nIndent)) + except Exception as e: + logger.error("Failed to save index file") + logger.error(str(e)) + return False + + return True + def scanFile(self, tHandle): theDocument = NWDoc(self.theProject, self.theParent) @@ -54,8 +79,6 @@ class NWIndex(): return False self.itemIndex[tHandle] = {} - for aKey in self.VALID_KEYS: - self.itemIndex[tHandle][aKey] = [] nLine = 0 for aLine in theText.splitlines(): @@ -65,17 +88,15 @@ class NWIndex(): if nChar > 0 and aLine[0] == "@": self.indexThis(tHandle, aLine, nLine, theItem) + # Generate reverse index for aKey in self.VALID_KEYS: - if len(self.itemIndex[tHandle][aKey]) > 0: - if tHandle not in self.keyIndex[aKey]: - self.keyIndex[aKey].append(tHandle) - else: - if tHandle in self.keyIndex[aKey]: - self.keyIndex[aKey].remove(tHandle) - - print(self.itemIndex) - print(self.tagIndex) - print(self.keyIndex) + if aKey in self.itemIndex[tHandle]: + if len(self.itemIndex[tHandle][aKey]) > 0: + if tHandle not in self.keyIndex[aKey]: + self.keyIndex[aKey].append(tHandle) + else: + if tHandle in self.keyIndex[aKey]: + self.keyIndex[aKey].remove(tHandle) return True @@ -92,22 +113,32 @@ class NWIndex(): return False if aKey == "todo": - self.itemIndex[tHandle]["todo"].append((nLine, tVal)) + self._addItem(tHandle, aKey, nLine, tVal) elif aKey == "tag": if tVal.find(",") >- 0: return False - self.itemIndex[tHandle]["tag"].append((nLine, tVal)) - self.tagIndex[tVal] = (nLine, tHandle, theItem.itemClass) + self._addItem(tHandle, aKey, nLine, tVal) + self.tagIndex[tVal] = [nLine, tHandle, theItem.itemClass.name] else: kVal = tVal.split(",") cVal = [] for aVal in kVal: cVal.append(aVal.strip().lower()) if len(cVal) > 0: - self.itemIndex[tHandle][aKey].append((nLine, cVal)) + self._addItem(tHandle, aKey, nLine, cVal) else: return False return True + ## + # Internal Functions + ## + + def _addItem(self, tHandle, tKey, tLine, tVal): + if tKey not in self.itemIndex[tHandle].keys(): + self.itemIndex[tHandle][tKey] = [] + self.itemIndex[tHandle][tKey].append([tLine, tVal]) + return + # END Class NWIndex diff --git a/nw/project/project.py b/nw/project/project.py index a70a1057..dce45ef3 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -21,7 +21,6 @@ from time import time from nw.project.item import NWItem from nw.project.status import NWStatus -from nw.project.index import NWIndex from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from nw.common import checkString, checkBool, checkInt from nw.constants import nwFiles @@ -65,9 +64,6 @@ class NWProject(): self.lastWCount = 0 self.currWCount = 0 - # Index - self.theIndex = None - # Set Defaults self.clearProject() @@ -169,7 +165,6 @@ class NWProject(): self.lastViewed = None self.lastWCount = 0 self.currWCount = 0 - self.theIndex = NWIndex(self, self.theParent) return From c273198e7577d25497361c6caf4343ab3b1b455a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Tue, 28 May 2019 22:18:37 +0200 Subject: [PATCH 05/23] Reduced the index to a single dictionary --- nw/constants.py | 2 +- nw/gui/winmain.py | 15 ++++++--- nw/project/index.py | 75 +++++++++++++++++++++++++++------------------ 3 files changed, 57 insertions(+), 35 deletions(-) diff --git a/nw/constants.py b/nw/constants.py index d9629718..8a545d0c 100644 --- a/nw/constants.py +++ b/nw/constants.py @@ -18,7 +18,7 @@ class nwFiles(): PROJ_FILE = "nwProject.nwx" PROJ_DICT = "wordlist.txt" SESS_INFO = "sessionInfo.log" - INDEX_FILE = "tagsindex.json" + INDEX_FILE = "tagsIndex.json" # END Class nwFiles diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index addc898d..6580d7fa 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -210,6 +210,7 @@ class GuiMain(QMainWindow): if saveOK: self.theProject.closeProject() + self.tagIndex.clearIndex() self.clearGUI() self.hasProject = False @@ -232,6 +233,9 @@ class GuiMain(QMainWindow): if not self.theProject.openProject(projFile): return False + # Load the tag index + self.tagIndex.loadIndex() + # Update GUI self._setWindowTitle(self.theProject.projName) self.rebuildTree() @@ -286,19 +290,20 @@ class GuiMain(QMainWindow): self.docEditor.changeWidth() self.docEditor.setFocus() self.theProject.setLastEdited(tHandle) - self.tagIndex.scanFile(tHandle) return True def saveDocument(self): if self.theDocument.theItem is not None: docText = self.docEditor.getText() cursPos = self.docEditor.getCursorPosition() - self.theDocument.theItem.setCharCount(self.docEditor.charCount) - self.theDocument.theItem.setWordCount(self.docEditor.wordCount) - self.theDocument.theItem.setParaCount(self.docEditor.paraCount) - self.theDocument.theItem.setCursorPos(cursPos) + theItem = self.theDocument.theItem + theItem.setCharCount(self.docEditor.charCount) + theItem.setWordCount(self.docEditor.wordCount) + theItem.setParaCount(self.docEditor.paraCount) + theItem.setCursorPos(cursPos) self.theDocument.saveDocument(docText) self.docEditor.setDocumentChanged(False) + self.tagIndex.scanText(theItem.itemHandle, docText) return True def viewDocument(self, tHandle=None): diff --git a/nw/project/index.py b/nw/project/index.py index 6cd6d7fc..81157b3c 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) class NWIndex(): VALID_KEYS = [ - "todo","tag","pov","char","plot","time","location", + "todo","tag","pov","chars","plot","time","location", "object","custom","scene","chapter","part" ] @@ -38,27 +38,42 @@ class NWIndex(): # Indices self.itemIndex = {} - self.tagIndex = {} - self.keyIndex = {} - for aKey in self.VALID_KEYS: - self.keyIndex[aKey] = [] return + def clearIndex(self): + self.itemIndex = {} + return + + def loadIndex(self): + + indexFile = path.join(self.theProject.projMeta,nwFiles.INDEX_FILE) + if path.isfile(indexFile): + logger.debug("Loading index file") + try: + with open(indexFile,mode="r") as inFile: + theJson = inFile.read() + self.itemIndex = json.loads(theJson) + except Exception as e: + logger.error("Failed to load index file") + logger.error(str(e)) + return False + + return True + + return False + def saveIndex(self): indexFile = path.join(self.theProject.projMeta,nwFiles.INDEX_FILE) + logger.debug("Saving index file") if self.mainConf.debugInfo: nIndent = 2 else: nIndent = None try: with open(indexFile,mode="w+") as outFile: - outFile.write(json.dumps({ - "itemIndex" : self.itemIndex, - "tagIndex" : self.tagIndex, - "keyIndex" : self.keyIndex - }, indent=nIndent)) + outFile.write(json.dumps(self.itemIndex, indent=nIndent)) except Exception as e: logger.error("Failed to save index file") logger.error(str(e)) @@ -68,16 +83,29 @@ class NWIndex(): def scanFile(self, tHandle): - theDocument = NWDoc(self.theProject, self.theParent) - theText = theDocument.openDocument(tHandle, False) theItem = self.theProject.getItem(tHandle) - if theItem is None: return False - if theItem.itemType != nwItemType.FILE: return False + theDocument = NWDoc(self.theProject, self.theParent) + theText = theDocument.openDocument(tHandle, False) + + self.scanText(tHandle, theText) + + return + + def scanText(self, tHandle, theText): + + theItem = self.theProject.getItem(tHandle) + if theItem is None: + return False + if theItem.itemType != nwItemType.FILE: + return False + + logger.debug("Indexing item with handle %s" % tHandle) + self.itemIndex[tHandle] = {} nLine = 0 @@ -88,16 +116,6 @@ class NWIndex(): if nChar > 0 and aLine[0] == "@": self.indexThis(tHandle, aLine, nLine, theItem) - # Generate reverse index - for aKey in self.VALID_KEYS: - if aKey in self.itemIndex[tHandle]: - if len(self.itemIndex[tHandle][aKey]) > 0: - if tHandle not in self.keyIndex[aKey]: - self.keyIndex[aKey].append(tHandle) - else: - if tHandle in self.keyIndex[aKey]: - self.keyIndex[aKey].remove(tHandle) - return True def indexThis(self, tHandle, aLine, nLine, theItem): @@ -108,22 +126,21 @@ class NWIndex(): return False aKey = aLine[1:nPos].strip().lower() - tVal = aLine[nPos+1:].strip() + tVal = aLine[nPos+1:].strip().lower() if aKey not in self.VALID_KEYS: + logger.verbose("Not a valid key '%s'" % aKey) return False + logger.verbose("Found valid key '%s'" % aKey) if aKey == "todo": self._addItem(tHandle, aKey, nLine, tVal) elif aKey == "tag": if tVal.find(",") >- 0: return False self._addItem(tHandle, aKey, nLine, tVal) - self.tagIndex[tVal] = [nLine, tHandle, theItem.itemClass.name] else: kVal = tVal.split(",") - cVal = [] - for aVal in kVal: - cVal.append(aVal.strip().lower()) + cVal = [aVal.strip() for aVal in kVal] if len(cVal) > 0: self._addItem(tHandle, aKey, nLine, cVal) else: From 0a09dcd0a72247c0be075e68afebe50cf63f0d11 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Thu, 30 May 2019 12:38:43 +0200 Subject: [PATCH 06/23] Moved the word counting bit to a separate file --- nw/gui/wordcounter.py | 52 +++++------------------------------- nw/tools/wordcount.py | 61 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 45 deletions(-) create mode 100644 nw/tools/wordcount.py diff --git a/nw/gui/wordcounter.py b/nw/gui/wordcounter.py index 4e044bca..f1c53497 100644 --- a/nw/gui/wordcounter.py +++ b/nw/gui/wordcounter.py @@ -15,6 +15,8 @@ import nw from PyQt5.QtCore import QThread +from nw.tools.wordcount import countWords + logger = logging.getLogger(__name__) class WordCounter(QThread): @@ -29,52 +31,12 @@ class WordCounter(QThread): def run(self): - self.charCount = 0 - self.wordCount = 0 - self.paraCount = 0 + theText = self.theParent.getText() + cC, wC, pC = countWords(theText) - prevEmpty = True - - for n in range(self.theParent.theDoc.blockCount()): - - theBlock = self.theParent.theDoc.findBlockByNumber(n) - if not theBlock.isValid(): - continue - - countPara = True - theText = theBlock.text() - theLen = len(theText) - - if theLen == 0: - prevEmpty = True - continue - if theText[0] == "@" or theText[0] == "%": - prevEmpty = True - continue - - if theText[0:5] == "#### ": - self.wordCount -= 1 - self.charCount -= 5 - countPara = False - elif theText[0:4] == "### ": - self.wordCount -= 1 - self.charCount -= 4 - countPara = False - elif theText[0:3] == "## ": - self.wordCount -= 1 - self.charCount -= 3 - countPara = False - elif theText[0:2] == "# ": - self.wordCount -= 1 - self.charCount -= 2 - countPara = False - - theBuff = theText.replace("–"," ").replace("—"," ") - self.wordCount += len(theBuff.split()) - self.charCount += theLen - if countPara and prevEmpty: - self.paraCount += 1 - prevEmpty = countPara == False + self.charCount = cC + self.wordCount = wC + self.paraCount = pC return diff --git a/nw/tools/wordcount.py b/nw/tools/wordcount.py new file mode 100644 index 00000000..d6fad51a --- /dev/null +++ b/nw/tools/wordcount.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"""novelWriter Word Counter + + novelWriter – Word Counter +============================ + Simple word counter + + File History: + Created: 2019-04-22 [0.0.1] + Moved: 2019-05-30 [0.1.4] + +""" + +import logging +import nw + +logger = logging.getLogger(__name__) + +def countWords(theText): + + charCount = 0 + wordCount = 0 + paraCount = 0 + prevEmpty = True + + for aLine in theText.splitlines(): + + countPara = True + theLen = len(aLine) + + if theLen == 0: + prevEmpty = True + continue + if aLine[0] == "@" or aLine[0] == "%": + continue + + if aLine[0:5] == "#### ": + wordCount -= 1 + charCount -= 5 + countPara = False + elif aLine[0:4] == "### ": + wordCount -= 1 + charCount -= 4 + countPara = False + elif aLine[0:3] == "## ": + wordCount -= 1 + charCount -= 3 + countPara = False + elif aLine[0:2] == "# ": + wordCount -= 1 + charCount -= 2 + countPara = False + + theBuff = aLine.replace("–"," ").replace("—"," ") + wordCount += len(theBuff.split()) + charCount += theLen + if countPara and prevEmpty: + paraCount += 1 + prevEmpty = countPara == False + + return charCount, wordCount, paraCount From 2962ab40706b24e95079603339517fefaed6f57d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Thu, 30 May 2019 14:37:25 +0200 Subject: [PATCH 07/23] Added menu option and function to scan all files to count words and build index --- nw/gui/mainmenu.py | 76 +++++++++++++++++++++++++-------------------- nw/gui/winmain.py | 53 ++++++++++++++++++++++++++++++- nw/project/index.py | 44 ++++++++++++-------------- 3 files changed, 115 insertions(+), 58 deletions(-) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 981933ee..15a38754 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -135,27 +135,27 @@ class GuiMainMenu(QMenuBar): # Project > New Project menuItem = QAction(QIcon.fromTheme("folder-new"), "New Project", self) - menuItem.setStatusTip("Create New Project") + menuItem.setStatusTip("Create new project") menuItem.triggered.connect(lambda : self.theParent.newProject(None)) self.projMenu.addAction(menuItem) # Project > Open Project menuItem = QAction(QIcon.fromTheme("folder-open"), "Open Project", self) - menuItem.setStatusTip("Open Project") + menuItem.setStatusTip("Open project") menuItem.setShortcut("Ctrl+Shift+O") menuItem.triggered.connect(lambda : self.theParent.openProject(None)) self.projMenu.addAction(menuItem) # Project > Save Project menuItem = QAction(QIcon.fromTheme("document-save"), "Save Project", self) - menuItem.setStatusTip("Save Project") + menuItem.setStatusTip("Save project") menuItem.setShortcut("Ctrl+Shift+S") menuItem.triggered.connect(self.theParent.saveProject) self.projMenu.addAction(menuItem) # Project > Close Project menuItem = QAction(QIcon.fromTheme("document-revert"), "Close Project", self) - menuItem.setStatusTip("Close Project") + menuItem.setStatusTip("Close project") menuItem.setShortcut("Ctrl+Shift+W") menuItem.triggered.connect(lambda : self.theParent.closeProject(False)) self.projMenu.addAction(menuItem) @@ -166,7 +166,7 @@ class GuiMainMenu(QMenuBar): # Project > Project Settings menuItem = QAction(QIcon.fromTheme("document-properties"), "Project Settings", self) - menuItem.setStatusTip("Project Settings") + menuItem.setStatusTip("Project settings") menuItem.triggered.connect(self.theParent.editProjectDialog) self.projMenu.addAction(menuItem) @@ -193,7 +193,7 @@ class GuiMainMenu(QMenuBar): # Project > New Folder menuItem = QAction(QIcon.fromTheme("folder-new"), "Create Folder", self) - menuItem.setStatusTip("Create Folder") + menuItem.setStatusTip("Create folder") menuItem.setShortcut("Ctrl+Shift+N") menuItem.triggered.connect(lambda : self._newTreeItem(nwItemType.FOLDER, None)) self.projMenu.addAction(menuItem) @@ -203,14 +203,14 @@ class GuiMainMenu(QMenuBar): # Project > Edit menuItem = QAction(QIcon.fromTheme("document-properties"), "&Edit Item", self) - menuItem.setStatusTip("Change Item Settings") + menuItem.setStatusTip("Change item settings") menuItem.setShortcuts(["Ctrl+E", "F2"]) menuItem.triggered.connect(self.theParent.editItem) self.projMenu.addAction(menuItem) # Project > Delete menuItem = QAction(QIcon.fromTheme("edit-delete"), "&Delete Item", self) - menuItem.setStatusTip("Delete Selected Item") + menuItem.setStatusTip("Delete selected item") menuItem.setShortcut("Ctrl+Del") menuItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None)) self.projMenu.addAction(menuItem) @@ -234,28 +234,28 @@ class GuiMainMenu(QMenuBar): # Document > New menuItem = QAction(QIcon.fromTheme("document-new"), "&New Document", self) - menuItem.setStatusTip("Create New Document") + menuItem.setStatusTip("Create new document") menuItem.setShortcut("Ctrl+N") menuItem.triggered.connect(lambda : self._newTreeItem(nwItemType.FILE, None)) self.docuMenu.addAction(menuItem) # Document > Open menuItem = QAction(QIcon.fromTheme("document-open"), "&Open Document", self) - menuItem.setStatusTip("Open Selected Document") + menuItem.setStatusTip("Open selected document") menuItem.setShortcut("Ctrl+O") menuItem.triggered.connect(self.theParent.openSelectedItem) self.docuMenu.addAction(menuItem) # Document > Save menuItem = QAction(QIcon.fromTheme("document-save"), "&Save Document", self) - menuItem.setStatusTip("Save Current Document") + menuItem.setStatusTip("Save current document") menuItem.setShortcut("Ctrl+S") menuItem.triggered.connect(self.theParent.saveDocument) self.docuMenu.addAction(menuItem) # Document > Close menuItem = QAction(QIcon.fromTheme("document-revert"), "Close Document", self) - menuItem.setStatusTip("Close Current Document") + menuItem.setStatusTip("Close current document") menuItem.setShortcut("Ctrl+W") menuItem.triggered.connect(self.theParent.closeDocEditor) self.docuMenu.addAction(menuItem) @@ -265,14 +265,14 @@ class GuiMainMenu(QMenuBar): # Document > Preview menuItem = QAction(QIcon.fromTheme("text-html"), "View Document", self) - menuItem.setStatusTip("View Document in HTML") + menuItem.setStatusTip("View document as HTML") menuItem.setShortcut("Ctrl+R") menuItem.triggered.connect(lambda : self.theParent.viewDocument(None)) self.docuMenu.addAction(menuItem) # Document > Close Preview menuItem = QAction(QIcon.fromTheme("text-html"), "Close Document View", self) - menuItem.setStatusTip("Close Document View Pane") + menuItem.setStatusTip("Close document view pane") menuItem.setShortcut("Ctrl+Shift+R") menuItem.triggered.connect(self.theParent.closeDocViewer) self.docuMenu.addAction(menuItem) @@ -299,21 +299,21 @@ class GuiMainMenu(QMenuBar): # View > TreeView menuItem = QAction(QIcon.fromTheme("go-home"), "TreeView", self) - menuItem.setStatusTip("Move to TreeView Panel") + menuItem.setStatusTip("Move focus to project tree") menuItem.setShortcut("Ctrl+1") menuItem.triggered.connect(lambda : self.theParent.setFocus(1)) self.viewMenu.addAction(menuItem) # View > Document Pane 1 menuItem = QAction(QIcon.fromTheme("go-first"), "Left Document Pane", self) - menuItem.setStatusTip("Move to Left Document Pane") + menuItem.setStatusTip("Move focus to left document pane") menuItem.setShortcut("Ctrl+2") menuItem.triggered.connect(lambda : self.theParent.setFocus(2)) self.viewMenu.addAction(menuItem) # # View > Document Pane 2 menuItem = QAction(QIcon.fromTheme("go-last"), "Right Document Pane", self) - menuItem.setStatusTip("Move to Right Document Pane") + menuItem.setStatusTip("Move focus to right document pane") menuItem.setShortcut("Ctrl+3") menuItem.triggered.connect(lambda : self.theParent.setFocus(3)) self.viewMenu.addAction(menuItem) @@ -327,14 +327,14 @@ class GuiMainMenu(QMenuBar): # Edit > Undo menuItem = QAction(QIcon.fromTheme("edit-undo"), "Undo", self) - menuItem.setStatusTip("Undo Last Change") + menuItem.setStatusTip("Undo last change") menuItem.setShortcut("Ctrl+Z") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.UNDO)) self.editMenu.addAction(menuItem) # Edit > Redo menuItem = QAction(QIcon.fromTheme("edit-redo"), "Redo", self) - menuItem.setStatusTip("Redo Last Change") + menuItem.setStatusTip("Redo last change") menuItem.setShortcut("Ctrl+Y") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.REDO)) self.editMenu.addAction(menuItem) @@ -344,21 +344,21 @@ class GuiMainMenu(QMenuBar): # Edit > Cut menuItem = QAction(QIcon.fromTheme("edit-cut"), "Cut", self) - menuItem.setStatusTip("Cut Selected Text") + menuItem.setStatusTip("Cut selected text") menuItem.setShortcut("Ctrl+X") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.CUT)) self.editMenu.addAction(menuItem) # Edit > Copy menuItem = QAction(QIcon.fromTheme("edit-copy"), "Copy", self) - menuItem.setStatusTip("Copy Selected Text") + menuItem.setStatusTip("Copy selected text") menuItem.setShortcut("Ctrl+C") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.COPY)) self.editMenu.addAction(menuItem) # Edit > Paste menuItem = QAction(QIcon.fromTheme("edit-paste"), "Paste", self) - menuItem.setStatusTip("Paste Text from Clipboard") + menuItem.setStatusTip("Paste text from clipboard") menuItem.setShortcut("Ctrl+V") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.PASTE)) self.editMenu.addAction(menuItem) @@ -368,14 +368,14 @@ class GuiMainMenu(QMenuBar): # Edit > Select All menuItem = QAction(QIcon.fromTheme("edit-select-all"), "Select All", self) - menuItem.setStatusTip("Select All Text in Document") + menuItem.setStatusTip("Select all text in document") menuItem.setShortcut("Ctrl+A") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.SEL_ALL)) self.editMenu.addAction(menuItem) # Edit > Select Paragraph menuItem = QAction(QIcon.fromTheme("edit-select-all"), "Select Paragraph", self) - menuItem.setStatusTip("Select All Text in Paragraph") + menuItem.setStatusTip("Select all text in paragraph") menuItem.setShortcut("Ctrl+Shift+A") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.SEL_PARA)) self.editMenu.addAction(menuItem) @@ -389,21 +389,21 @@ class GuiMainMenu(QMenuBar): # Format > Bold Text menuItem = QAction(QIcon.fromTheme("format-text-bold"), "Bold Text", self) - menuItem.setStatusTip("Make Selected Text Bold") + menuItem.setStatusTip("Make selected text bold") menuItem.setShortcut("Ctrl+B") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.BOLD)) self.fmtMenu.addAction(menuItem) # Format > Italic Text menuItem = QAction(QIcon.fromTheme("format-text-italic"), "Italic Text", self) - menuItem.setStatusTip("Make Selected Text Italic") + menuItem.setStatusTip("Make selected text italic") menuItem.setShortcut("Ctrl+I") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.ITALIC)) self.fmtMenu.addAction(menuItem) # Format > Underline Text menuItem = QAction(QIcon.fromTheme("format-text-underline"), "Underline Text", self) - menuItem.setStatusTip("Underline Selected Text") + menuItem.setStatusTip("Underline selected text") menuItem.setShortcut("Ctrl+U") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.U_LINE)) self.fmtMenu.addAction(menuItem) @@ -413,14 +413,14 @@ class GuiMainMenu(QMenuBar): # Format > Double Quotes menuItem = QAction(QIcon.fromTheme("insert-text"), "Wrap Double Quotes", self) - menuItem.setStatusTip("Wrap Selected Text in Double Quotes") + menuItem.setStatusTip("Wrap selected text in double quotes") menuItem.setShortcut("Ctrl+D") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.D_QUOTE)) self.fmtMenu.addAction(menuItem) # Format > Single Quotes menuItem = QAction(QIcon.fromTheme("insert-text"), "Wrap Single Quotes", self) - menuItem.setStatusTip("Wrap Selected Text in Single Quotes") + menuItem.setStatusTip("Wrap selected text in single quotes") menuItem.setShortcut("Ctrl+Shift+D") menuItem.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE)) self.fmtMenu.addAction(menuItem) @@ -434,14 +434,14 @@ class GuiMainMenu(QMenuBar): # Tools > Move Up self.toolsMoveUp = QAction(QIcon.fromTheme("go-up"), "Move Tree Item Up", self) - self.toolsMoveUp.setStatusTip("Move Item Up") + self.toolsMoveUp.setStatusTip("Move item up") self.toolsMoveUp.setShortcut("Ctrl+Shift+Up") self.toolsMoveUp.triggered.connect(lambda : self._moveTreeItem(-1)) self.toolsMenu.addAction(self.toolsMoveUp) # Tools > Move Down self.toolsMoveDown = QAction(QIcon.fromTheme("go-down"), "Move Tree Item Down", self) - self.toolsMoveDown.setStatusTip("Move Item Down") + self.toolsMoveDown.setStatusTip("Move item down") self.toolsMoveDown.setShortcut("Ctrl+Shift+Down") self.toolsMoveDown.triggered.connect(lambda : self._moveTreeItem(1)) self.toolsMenu.addAction(self.toolsMoveDown) @@ -451,7 +451,7 @@ class GuiMainMenu(QMenuBar): # Tools > Toggle Spell Check self.toolsSpellCheck = QAction("Check Spelling", self) - self.toolsSpellCheck.setStatusTip("Toggle Check Spelling") + self.toolsSpellCheck.setStatusTip("Toggle check spelling") self.toolsSpellCheck.setCheckable(True) self.toolsSpellCheck.setChecked(self.theProject.spellCheck) self.toolsSpellCheck.toggled.connect(self._toggleSpellCheck) @@ -460,11 +460,21 @@ class GuiMainMenu(QMenuBar): # Tools > Update Spell Check menuItem = QAction(QIcon.fromTheme("tools-check-spelling"), "Re-Run Spell Check", self) - menuItem.setStatusTip("Rus the Spell Checker on Current Document") + menuItem.setStatusTip("Run the spell checker on current document") menuItem.setShortcut("F7") menuItem.triggered.connect(self.theParent.docEditor.updateSpellCheck) self.toolsMenu.addAction(menuItem) + # Tools > Separator + self.toolsMenu.addSeparator() + + # Tools > Rebuild Indices + menuItem = QAction(QIcon.fromTheme("edit-redo"), "Rebuild Indices", self) + menuItem.setStatusTip("Rebuild the tag indices and word counts") + menuItem.setShortcut("F9") + menuItem.triggered.connect(self.theParent.rebuildIndex) + self.toolsMenu.addAction(menuItem) + # # Tools > Settings # menuItem = QAction(QIcon.fromTheme("preferences-system"), "Preferences", self) # menuItem.setStatusTip("Preferences") diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index 6580d7fa..2521a3d0 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -11,6 +11,7 @@ """ import logging +import time import nw from os import path @@ -18,7 +19,7 @@ from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QIcon, QPixmap, QColor from PyQt5.QtWidgets import ( qApp, QWidget, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, - QShortcut, QMessageBox + QShortcut, QMessageBox, QProgressDialog ) from nw.theme import Theme @@ -38,6 +39,7 @@ from nw.convert.tokenizer import Tokenizer from nw.convert.tohtml import ToHtml from nw.enum import nwItemType, nwAlert from nw.constants import nwFiles +from nw.tools.wordcount import countWords logger = logging.getLogger(__name__) @@ -378,6 +380,55 @@ class GuiMain(QMainWindow): self.treeView.buildTree() return + def rebuildIndex(self): + + logger.debug("Rebuilding indices ...") + + self.treeView.saveTreeOrder() + nItems = len(self.theProject.treeOrder) + + dlgProg = QProgressDialog("Scanning files ...", "Cancel", 0, nItems, self) + dlgProg.setWindowModality(Qt.WindowModal) + dlgProg.setMinimumDuration(0) + dlgProg.setFixedWidth(480) + dlgProg.setLabelText("Starting file scan ...") + dlgProg.setValue(0) + dlgProg.show() + time.sleep(0.5) + + nDone = 0 + for tHandle in self.theProject.treeOrder: + + tItem = self.theProject.getItem(tHandle) + + dlgProg.setValue(nDone) + dlgProg.setLabelText("Scanning: %s" % tItem.itemName) + logger.verbose("Scanning: %s" % tItem.itemName) + + if tItem is not None and tItem.itemType == nwItemType.FILE: + theDoc = NWDoc(self.theProject, self) + theText = theDoc.openDocument(tHandle, False) + + # Run Word Count + cC, wC, pC = countWords(theText) + tItem.setCharCount(cC) + tItem.setWordCount(wC) + tItem.setParaCount(pC) + self.treeView.propagateCount(tHandle, wC) + self.treeView.projectWordCount() + + # Build tag index + self.tagIndex.scanText(tHandle, theText) + + time.sleep(0.05) + nDone += 1 + if dlgProg.wasCanceled(): + break + + dlgProg.setValue(nItems) + + return + ## # Main Dialogs ## diff --git a/nw/project/index.py b/nw/project/index.py index 81157b3c..2e1616f8 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -8,6 +8,18 @@ File History: Created: 2019-05-27 [0.1.4] +Structure: + + We need to scan all files to get the novel layout. This is done by heading depth. Each heading +is recorded in a sorted list. Each such heading again can have a set of meta tags. + In the other class root folders, each file can have a tag which the meta tags in the novel files +point to. These tags should be stored in a dictionary where the tag is the key pointing to a single +file handle. That way we can do lookups on both keys and values. + The timeline view then consists of novel header elements in the horizontal header, possibly just +truncated to a Ch or Sc abbreviation, possibly with a number. This can be extracted from the item +layout. The vertical header column is then whatever notes we want to compare against, and the links +from the novel files are dots on the row. + """ import logging @@ -24,10 +36,11 @@ logger = logging.getLogger(__name__) class NWIndex(): - VALID_KEYS = [ - "todo","tag","pov","chars","plot","time","location", - "object","custom","scene","chapter","part" - ] + TAG_KEY = "tag" + NOTE_KEYS = ["pov","char","plot","time","location","object","custom"] + NOVEL_KEYS = ["scene","chapter","part"] + + VALID_KEYS = [TAG_KEY] + NOTE_KEYS + NOVEL_KEYS def __init__(self, theProject, theParent): @@ -47,7 +60,7 @@ class NWIndex(): def loadIndex(self): - indexFile = path.join(self.theProject.projMeta,nwFiles.INDEX_FILE) + indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) if path.isfile(indexFile): logger.debug("Loading index file") try: @@ -65,7 +78,7 @@ class NWIndex(): def saveIndex(self): - indexFile = path.join(self.theProject.projMeta,nwFiles.INDEX_FILE) + indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) logger.debug("Saving index file") if self.mainConf.debugInfo: nIndent = 2 @@ -81,21 +94,6 @@ class NWIndex(): return True - def scanFile(self, tHandle): - - theItem = self.theProject.getItem(tHandle) - if theItem is None: - return False - if theItem.itemType != nwItemType.FILE: - return False - - theDocument = NWDoc(self.theProject, self.theParent) - theText = theDocument.openDocument(tHandle, False) - - self.scanText(tHandle, theText) - - return - def scanText(self, tHandle, theText): theItem = self.theProject.getItem(tHandle) @@ -132,9 +130,7 @@ class NWIndex(): return False logger.verbose("Found valid key '%s'" % aKey) - if aKey == "todo": - self._addItem(tHandle, aKey, nLine, tVal) - elif aKey == "tag": + if aKey == self.TAG_KEY: if tVal.find(",") >- 0: return False self._addItem(tHandle, aKey, nLine, tVal) From b526b0f5d9703b964d64a5b31af8a49bcbfe4986 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Thu, 30 May 2019 18:45:17 +0200 Subject: [PATCH 08/23] Got a working version of index builder that seems to do what is needed. --- nw/gui/winmain.py | 1 + nw/project/index.py | 194 +++++++++++++----- .../sampleNovel/data_1/4298de4d9524_main.nwd | 2 + .../sampleNovel/data_6/36b6aa9b697b_main.nwd | 9 +- .../sampleNovel/data_b/b2c23b3c42cc_main.nwd | 2 + sample/sampleNovel/meta/sessionInfo.log | 88 ++++++++ sample/sampleNovel/nwProject.nwx | 26 +-- tests/test_project.py | 37 ++++ 8 files changed, 292 insertions(+), 67 deletions(-) diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index 2521a3d0..d605f8ec 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -385,6 +385,7 @@ class GuiMain(QMainWindow): logger.debug("Rebuilding indices ...") self.treeView.saveTreeOrder() + self.tagIndex.clearIndex() nItems = len(self.theProject.treeOrder) dlgProg = QProgressDialog("Scanning files ...", "Cancel", 0, nItems, self) diff --git a/nw/project/index.py b/nw/project/index.py index 2e1616f8..c15409aa 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -29,18 +29,32 @@ import nw from os import path from nw.project.document import NWDoc -from nw.enum import nwItemType +from nw.enum import nwItemType, nwItemClass from nw.constants import nwFiles logger = logging.getLogger(__name__) class NWIndex(): - TAG_KEY = "tag" - NOTE_KEYS = ["pov","char","plot","time","location","object","custom"] - NOVEL_KEYS = ["scene","chapter","part"] + TAG_KEY = "@tag" + POV_KEY = "@pov" + CHAR_KEY = "@char" + PLOT_KEY = "@plot" + TIME_KEY = "@time" + WORLD_KEY = "@location" + OBJECT_KEY = "@object" + CUSTOM_KEY = "@custom" - VALID_KEYS = [TAG_KEY] + NOTE_KEYS + NOVEL_KEYS + NOTE_KEYS = [PLOT_KEY, POV_KEY, CHAR_KEY, WORLD_KEY, TIME_KEY, OBJECT_KEY, CUSTOM_KEY] + VALID_CLASS = { + nwItemClass.NOVEL : [], + nwItemClass.PLOT : [PLOT_KEY], + nwItemClass.CHARACTER : [POV_KEY, CHAR_KEY], + nwItemClass.WORLD : [WORLD_KEY], + nwItemClass.TIMELINE : [TIME_KEY], + nwItemClass.OBJECT : [OBJECT_KEY], + nwItemClass.CUSTOM : [CUSTOM_KEY], + } def __init__(self, theProject, theParent): @@ -50,28 +64,44 @@ class NWIndex(): self.mainConf = self.theParent.mainConf # Indices - self.itemIndex = {} + self.tagIndex = {} + self.noteIndex = {} + self.novelIndex = {} return def clearIndex(self): - self.itemIndex = {} + self.tagIndex = {} + self.noteIndex = {} + self.novelIndex = {} return + ## + # Load and Save Index to/from File + ## + def loadIndex(self): + theData = {} indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) if path.isfile(indexFile): logger.debug("Loading index file") try: with open(indexFile,mode="r") as inFile: theJson = inFile.read() - self.itemIndex = json.loads(theJson) + theData = json.loads(theJson) except Exception as e: logger.error("Failed to load index file") logger.error(str(e)) return False + if "tagIndex" in theData.keys(): + self.tagIndex = theData["tagIndex"] + if "noteIndex" in theData.keys(): + self.noteIndex = theData["noteIndex"] + if "novelIndex" in theData.keys(): + self.novelIndex = theData["novelIndex"] + return True return False @@ -86,7 +116,11 @@ class NWIndex(): nIndent = None try: with open(indexFile,mode="w+") as outFile: - outFile.write(json.dumps(self.itemIndex, indent=nIndent)) + outFile.write(json.dumps({ + "tagIndex" : self.tagIndex, + "noteIndex" : self.noteIndex, + "novelIndex" : self.novelIndex, + }, indent=nIndent)) except Exception as e: logger.error("Failed to save index file") logger.error(str(e)) @@ -94,64 +128,130 @@ class NWIndex(): return True + ## + # Index Building + ## + def scanText(self, tHandle, theText): theItem = self.theProject.getItem(tHandle) - if theItem is None: - return False - if theItem.itemType != nwItemType.FILE: - return False + if theItem is None: return False + if theItem.itemType != nwItemType.FILE: return False + itemClass = theItem.itemClass logger.debug("Indexing item with handle %s" % tHandle) - self.itemIndex[tHandle] = {} + # Check file type, and reset its old index + if itemClass == nwItemClass.NOVEL: + self.novelIndex[tHandle] = [] + self.noteIndex[tHandle] = [] + isNovel = True + else: + isNovel = False + + # Also clear references to file in tag index + for aTag in self.tagIndex: + if self.tagIndex[aTag][1] == tHandle: + self.tagIndex.pop(aTag) nLine = 0 for aLine in theText.splitlines(): aLine = aLine.strip() nLine += 1 nChar = len(aLine) - if nChar > 0 and aLine[0] == "@": - self.indexThis(tHandle, aLine, nLine, theItem) + if nChar == 0: continue + if aLine[0] == "#": + if isNovel: + self.indexTitle(tHandle, aLine, nLine) + elif aLine[0] == "@": + if isNovel: + self.indexNoteRef(tHandle, aLine, nLine) + else: + self.indexTag(tHandle, aLine, nLine, itemClass) return True - def indexThis(self, tHandle, aLine, nLine, theItem): + def indexTitle(self, tHandle, aLine, nLine): - nChar = len(aLine) - nPos = aLine.find(":") - if nPos < 2 or nChar < nPos+2: - return False - - aKey = aLine[1:nPos].strip().lower() - tVal = aLine[nPos+1:].strip().lower() - if aKey not in self.VALID_KEYS: - logger.verbose("Not a valid key '%s'" % aKey) - return False - - logger.verbose("Found valid key '%s'" % aKey) - if aKey == self.TAG_KEY: - if tVal.find(",") >- 0: - return False - self._addItem(tHandle, aKey, nLine, tVal) + if aLine.startswith("# "): + hDepth = 1 + hText = aLine[2:].strip() + elif aLine.startswith("## "): + hDepth = 2 + hText = aLine[3:].strip() + elif aLine.startswith("### "): + hDepth = 3 + hText = aLine[4:].strip() + elif aLine.startswith("#### "): + hDepth = 4 + hText = aLine[5:].strip() else: - kVal = tVal.split(",") - cVal = [aVal.strip() for aVal in kVal] - if len(cVal) > 0: - self._addItem(tHandle, aKey, nLine, cVal) - else: - return False + return False + + if hText != "": + self.novelIndex[tHandle].append([nLine, hDepth, hText]) return True - ## - # Internal Functions - ## + def indexNoteRef(self, tHandle, aLine, nLine): - def _addItem(self, tHandle, tKey, tLine, tVal): - if tKey not in self.itemIndex[tHandle].keys(): - self.itemIndex[tHandle][tKey] = [] - self.itemIndex[tHandle][tKey].append([tLine, tVal]) - return + isValid, theBits, thePos = self.scanThis(aLine) + if not isValid or len(theBits) == 0: + return False + + theKey = theBits[0] + if theKey in self.NOTE_KEYS: + for aVal in theBits[1:]: + self.noteIndex[tHandle].append([nLine, theKey, aVal]) + + return True + + def indexTag(self, tHandle, aLine, nLine, itemClass): + + isValid, theBits, thePos = self.scanThis(aLine) + if not isValid or len(theBits) != 2: + return False + + if theBits[0] == self.TAG_KEY: + self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name] + + return True + + def scanThis(self, aLine): + + theBits = [] + thePos = [] + + aLine = aLine.strip() + nChar = len(aLine) + if nChar < 2: + return False, theBits, thePos + if aLine[0] != "@": + return False, theBits, thePos + + cPos = 0 + cKey, cSep, cVals = aLine.partition(":") + sKey = cKey.strip() + if sKey == "@": + return False, theBits, thePos + + theBits.append(sKey.lower()) + thePos.append(cPos) + cPos += len(sKey) + 1 + + if cVals == "": + # No values, so we're done + return True, theBits, thePos + + aVals = cVals.split(",") + for cVal in aVals: + sVal = cVal.strip() + rLen = len(cVal.lstrip()) + tLen = len(cVal) + theBits.append(sVal.lower()) + thePos.append(cPos+tLen-rLen) + cPos += tLen + 1 + + return True, theBits, thePos # END Class NWIndex diff --git a/sample/sampleNovel/data_1/4298de4d9524_main.nwd b/sample/sampleNovel/data_1/4298de4d9524_main.nwd index 11127dab..9bd4961b 100644 --- a/sample/sampleNovel/data_1/4298de4d9524_main.nwd +++ b/sample/sampleNovel/data_1/4298de4d9524_main.nwd @@ -1,3 +1,5 @@ # John Smith +@tag: John + He’s pretty cool. Not Brad Pitt though. \ No newline at end of file diff --git a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd index eb71a80b..de9fae73 100644 --- a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd +++ b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd @@ -3,8 +3,8 @@ ## This is the Subtitle % Begin Meta -@POV: Sam -@Chars: Sam, Adam, Scott +@POV: Jane +@Char: Jane, John % End Meta Some text here would look good as well, and maybe some "dialogue"? @@ -17,8 +17,3 @@ This paragraph is also meaningless. At least a bit. It’s also very short. But This one is a bit longer. “It also has some dialogue in it” she said, before she moved on to check if the spellchecker worked. It did. “Cool,” she concluded. - -@ToDo: Stuff that will be done at some point - - - diff --git a/sample/sampleNovel/data_b/b2c23b3c42cc_main.nwd b/sample/sampleNovel/data_b/b2c23b3c42cc_main.nwd index 409a428a..1a91f10a 100644 --- a/sample/sampleNovel/data_b/b2c23b3c42cc_main.nwd +++ b/sample/sampleNovel/data_b/b2c23b3c42cc_main.nwd @@ -1,3 +1,5 @@ # Jane Smith +@tag: Jane + She’s pretty cool. Not Angelina Jolie though. \ No newline at end of file diff --git a/sample/sampleNovel/meta/sessionInfo.log b/sample/sampleNovel/meta/sessionInfo.log index 79d3266e..908773f6 100644 --- a/sample/sampleNovel/meta/sessionInfo.log +++ b/sample/sampleNovel/meta/sessionInfo.log @@ -13,3 +13,91 @@ Start: 2019-05-26 15:37:47 End: 2019-05-26 15:38:07 Words: 538 Start: 2019-05-26 15:38:11 End: 2019-05-26 15:43:07 Words: 2 Start: 2019-05-26 15:49:18 End: 2019-05-26 15:49:47 Words: 0 Start: 2019-05-26 15:52:39 End: 2019-05-26 15:52:56 Words: 0 +Start: 2019-05-27 21:23:07 End: 2019-05-27 21:26:41 Words: 0 +Start: 2019-05-27 21:26:56 End: 2019-05-27 21:27:48 Words: 0 +Start: 2019-05-27 21:29:11 End: 2019-05-27 21:30:30 Words: 0 +Start: 2019-05-27 21:33:30 End: 2019-05-27 21:35:14 Words: 0 +Start: 2019-05-27 21:35:20 End: 2019-05-27 21:36:03 Words: 0 +Start: 2019-05-27 21:36:07 End: 2019-05-27 21:36:23 Words: 0 +Start: 2019-05-27 21:36:27 End: 2019-05-27 21:37:46 Words: 0 +Start: 2019-05-27 21:37:50 End: 2019-05-27 21:38:05 Words: 0 +Start: 2019-05-27 21:38:14 End: 2019-05-27 21:38:24 Words: 0 +Start: 2019-05-27 21:39:08 End: 2019-05-27 21:39:23 Words: 0 +Start: 2019-05-27 21:42:21 End: 2019-05-27 21:42:39 Words: 0 +Start: 2019-05-27 21:45:07 End: 2019-05-27 21:45:18 Words: 0 +Start: 2019-05-27 21:58:30 End: 2019-05-27 21:58:43 Words: 0 +Start: 2019-05-27 22:02:01 End: 2019-05-27 22:02:09 Words: 0 +Start: 2019-05-27 22:05:47 End: 2019-05-27 22:06:02 Words: 0 +Start: 2019-05-27 22:08:25 End: 2019-05-27 22:09:35 Words: 0 +Start: 2019-05-27 22:11:40 End: 2019-05-27 22:14:18 Words: 3 +Start: 2019-05-27 22:20:55 End: 2019-05-27 22:21:40 Words: -2 +Start: 2019-05-27 22:22:38 End: 2019-05-27 22:23:12 Words: 16 +Start: 2019-05-27 22:55:34 End: 2019-05-27 22:56:09 Words: 0 +Start: 2019-05-27 22:58:47 End: 2019-05-27 23:00:47 Words: 17 +Start: 2019-05-28 18:20:15 End: 2019-05-28 18:20:30 Words: -1 +Start: 2019-05-28 18:21:04 End: 2019-05-28 18:21:24 Words: 0 +Start: 2019-05-28 18:29:41 End: 2019-05-28 18:30:16 Words: 0 +Start: 2019-05-28 18:30:59 End: 2019-05-28 18:32:06 Words: 0 +Start: 2019-05-28 18:34:33 End: 2019-05-28 18:34:48 Words: 1 +Start: 2019-05-28 18:43:08 End: 2019-05-28 18:43:21 Words: 0 +Start: 2019-05-28 18:49:29 End: 2019-05-28 18:49:40 Words: 0 +Start: 2019-05-28 19:13:20 End: 2019-05-28 19:13:24 Words: -1 +Start: 2019-05-28 19:17:05 End: 2019-05-28 19:17:13 Words: 1 +Start: 2019-05-28 19:49:44 End: 2019-05-28 19:52:23 Words: -1 +Start: 2019-05-28 19:52:29 End: 2019-05-28 19:54:42 Words: 0 +Start: 2019-05-28 19:55:33 End: 2019-05-28 19:55:56 Words: 0 +Start: 2019-05-28 19:57:29 End: 2019-05-28 20:02:33 Words: 0 +Start: 2019-05-28 20:02:38 End: 2019-05-28 20:05:19 Words: 0 +Start: 2019-05-28 20:05:23 End: 2019-05-28 20:06:48 Words: 0 +Start: 2019-05-28 20:07:00 End: 2019-05-28 20:12:26 Words: 0 +Start: 2019-05-28 20:12:31 End: 2019-05-28 20:17:58 Words: -16 +Start: 2019-05-28 20:28:31 End: 2019-05-28 20:28:53 Words: 0 +Start: 2019-05-28 20:28:57 End: 2019-05-28 20:29:05 Words: 16 +Start: 2019-05-28 21:00:49 End: 2019-05-28 21:01:45 Words: 1 +Start: 2019-05-28 21:02:01 End: 2019-05-28 21:02:06 Words: 0 +Start: 2019-05-28 21:17:57 End: 2019-05-28 21:25:48 Words: 0 +Start: 2019-05-28 21:35:39 End: 2019-05-28 21:35:47 Words: 0 +Start: 2019-05-28 22:22:54 End: 2019-05-28 22:23:12 Words: 0 +Start: 2019-05-30 11:58:19 End: 2019-05-30 11:59:15 Words: 0 +Start: 2019-05-30 11:59:19 End: 2019-05-30 11:59:26 Words: 0 +Start: 2019-05-30 11:59:55 End: 2019-05-30 12:00:09 Words: 0 +Start: 2019-05-30 12:01:47 End: 2019-05-30 12:02:08 Words: 0 +Start: 2019-05-30 12:03:56 End: 2019-05-30 12:04:23 Words: 0 +Start: 2019-05-30 12:09:24 End: 2019-05-30 12:09:47 Words: 0 +Start: 2019-05-30 12:10:13 End: 2019-05-30 12:10:40 Words: 0 +Start: 2019-05-30 12:12:14 End: 2019-05-30 12:12:33 Words: 0 +Start: 2019-05-30 12:13:56 End: 2019-05-30 12:14:08 Words: 0 +Start: 2019-05-30 12:14:24 End: 2019-05-30 12:14:44 Words: 0 +Start: 2019-05-30 12:14:53 End: 2019-05-30 12:15:31 Words: 0 +Start: 2019-05-30 12:17:36 End: 2019-05-30 12:17:49 Words: 0 +Start: 2019-05-30 12:20:12 End: 2019-05-30 12:20:24 Words: 0 +Start: 2019-05-30 12:36:42 End: 2019-05-30 12:37:21 Words: 0 +Start: 2019-05-30 12:37:42 End: 2019-05-30 12:38:01 Words: 0 +Start: 2019-05-30 12:47:13 End: 2019-05-30 12:47:27 Words: 0 +Start: 2019-05-30 12:47:47 End: 2019-05-30 12:48:03 Words: 0 +Start: 2019-05-30 12:50:01 End: 2019-05-30 12:50:15 Words: 0 +Start: 2019-05-30 12:54:26 End: 2019-05-30 12:54:34 Words: 0 +Start: 2019-05-30 15:25:50 End: 2019-05-30 15:25:59 Words: 0 +Start: 2019-05-30 15:43:36 End: 2019-05-30 15:43:44 Words: 0 +Start: 2019-05-30 15:52:46 End: 2019-05-30 15:53:12 Words: 0 +Start: 2019-05-30 15:53:44 End: 2019-05-30 15:53:56 Words: 0 +Start: 2019-05-30 15:56:29 End: 2019-05-30 15:57:35 Words: 0 +Start: 2019-05-30 15:57:41 End: 2019-05-30 15:59:58 Words: 0 +Start: 2019-05-30 16:00:01 End: 2019-05-30 16:00:28 Words: 0 +Start: 2019-05-30 16:00:56 End: 2019-05-30 16:01:06 Words: 0 +Start: 2019-05-30 16:01:32 End: 2019-05-30 16:01:48 Words: 0 +Start: 2019-05-30 16:02:35 End: 2019-05-30 16:03:03 Words: 0 +Start: 2019-05-30 17:00:23 End: 2019-05-30 17:21:07 Words: 0 +Start: 2019-05-30 17:21:11 End: 2019-05-30 17:21:24 Words: 0 +Start: 2019-05-30 17:24:41 End: 2019-05-30 17:24:46 Words: 0 +Start: 2019-05-30 17:29:08 End: 2019-05-30 17:29:19 Words: 0 +Start: 2019-05-30 18:12:59 End: 2019-05-30 18:13:07 Words: 0 +Start: 2019-05-30 18:13:32 End: 2019-05-30 18:13:42 Words: 0 +Start: 2019-05-30 18:16:13 End: 2019-05-30 18:16:18 Words: 0 +Start: 2019-05-30 18:22:24 End: 2019-05-30 18:22:30 Words: 0 +Start: 2019-05-30 18:22:43 End: 2019-05-30 18:24:11 Words: 0 +Start: 2019-05-30 18:25:40 End: 2019-05-30 18:25:45 Words: 0 +Start: 2019-05-30 18:26:01 End: 2019-05-30 18:27:56 Words: 0 +Start: 2019-05-30 18:28:00 End: 2019-05-30 18:30:22 Words: 0 +Start: 2019-05-30 18:42:16 End: 2019-05-30 18:43:10 Words: 0 +Start: 2019-05-30 18:43:24 End: 2019-05-30 18:43:31 Words: 0 diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 448da65a..c7108caf 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -10,7 +10,7 @@ True 636b6aa9b697b None - 540 + 558 New Notes @@ -76,7 +76,7 @@ 656 121 5 - 573 + 729 New File @@ -85,8 +85,8 @@ Notes False SCENE - 69 - 17 + 82 + 19 1 0 @@ -111,10 +111,10 @@ Minor False NOTE - 42 - 8 + 49 + 9 1 - 0 + 24 Jane Smith @@ -123,10 +123,10 @@ Major False NOTE - 51 + 55 9 1 - 0 + 71 Locations @@ -142,9 +142,9 @@ None False NOTE - 0 - 0 - 0 + 76 + 15 + 1 0 diff --git a/tests/test_project.py b/tests/test_project.py index db925c02..9ef638c8 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -11,6 +11,7 @@ from nwdummy import DummyMain from nw.config import Config from nw.project.project import NWProject from nw.project.item import NWItem +from nw.project.index import NWIndex from nw.enum import nwItemClass theConf = Config() @@ -60,3 +61,39 @@ def testProjectNewRoot(nwTempProj,nwRef): assert theProject.saveProject() assert cmpFiles(projFile, refFile, [2]) assert not theProject.projChanged + +@pytest.mark.project +def testIndexScanThis(nwTempProj): + projFile = path.join(nwTempProj,"nwProject.nwx") + assert theProject.openProject(projFile) + + theIndex = NWIndex(theProject,theMain) + tHandle = "31489056e0916" + + isValid, theBits, thePos = theIndex.scanThis("tag: this, and this") + assert not isValid + + isValid, theBits, thePos = theIndex.scanThis("@:") + assert not isValid + + isValid, theBits, thePos = theIndex.scanThis("@a:") + assert isValid + assert str(theBits) == "['@a']" + assert str(thePos) == "[0]" + + isValid, theBits, thePos = theIndex.scanThis("@a:b") + assert isValid + assert str(theBits) == "['@a', 'b']" + assert str(thePos) == "[0, 3]" + + isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d") + assert isValid + assert str(theBits) == "['@a', 'b', 'c', 'd']" + assert str(thePos) == "[0, 3, 5, 7]" + + isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this") + assert isValid + assert str(theBits) == "['@tag', 'this', 'and this']" + assert str(thePos) == "[0, 6, 12]" + + # assert False From a2d0f937ae96053bd58c9c13eae1e2a157970498 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Thu, 30 May 2019 19:57:02 +0200 Subject: [PATCH 09/23] Connected the syntax highlicghter to the index. It now shows valid keywords and values --- nw/gui/doceditor.py | 42 +++--- nw/gui/dochighlight.py | 122 ++++++++++++------ nw/gui/winmain.py | 18 ++- nw/project/index.py | 74 +++++++---- .../sampleNovel/data_6/36b6aa9b697b_main.nwd | 1 + .../sampleNovel/data_b/3e74dbc1f584_main.nwd | 2 + sample/sampleNovel/meta/sessionInfo.log | 23 ++++ sample/sampleNovel/nwProject.nwx | 6 +- 8 files changed, 196 insertions(+), 92 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index c708536e..13f1092a 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -34,11 +34,12 @@ class GuiDocEditor(QTextEdit): logger.debug("Initialising DocEditor ...") # Class Variables - self.mainConf = nw.CONFIG - self.theParent = theParent - self.docChanged = False - self.pwlFile = None - self.spellCheck = False + self.mainConf = nw.CONFIG + self.theParent = theParent + self.docChanged = False + self.pwlFile = None + self.spellCheck = False + self.theDocument = theParent.theDocument # Document Variables self.charCount = 0 @@ -54,9 +55,9 @@ class GuiDocEditor(QTextEdit): self.typApos = self.mainConf.fmtApostrophe # Core Elements - self.theDoc = self.document() + self.theQDoc = self.document() self.theDict = enchant.Dict(self.mainConf.spellLanguage) - self.hLight = GuiDocHighlighter(self.theDoc, self.theParent.theTheme) + self.hLight = GuiDocHighlighter(self.theQDoc, self.theParent) self.hLight.setDict(self.theDict) # Context Menu @@ -71,8 +72,8 @@ class GuiDocEditor(QTextEdit): self.clearEditor() self.initEditor() - self.theDoc.setDocumentMargin(0) - self.theDoc.contentsChange.connect(self._docChange) + self.theQDoc.setDocumentMargin(0) + self.theQDoc.contentsChange.connect(self._docChange) # Custom Shortcuts QShortcut(QKeySequence("Ctrl+."), self, context=Qt.WidgetShortcut, activated=self._openSpellContext) @@ -109,7 +110,18 @@ class GuiDocEditor(QTextEdit): theOpt.setTabStopDistance(self.mainConf.tabWidth) if self.mainConf.doJustify: theOpt.setAlignment(Qt.AlignJustify) - self.theDoc.setDefaultTextOption(theOpt) + self.theQDoc.setDefaultTextOption(theOpt) + return True + + def loadText(self, tHandle): + self.hLight.setHandle(tHandle) + self.setPlainText(self.theDocument.openDocument(tHandle)) + self.setCursorPosition(self.theDocument.theItem.cursorPos) + self.lastEdit = time() + self._runCounter() + self.wcTimer.start() + self.setDocumentChanged(False) + self.setReadOnly(False) return True ## @@ -121,14 +133,6 @@ class GuiDocEditor(QTextEdit): self.theParent.statusBar.setDocumentStatus(self.docChanged) return self.docChanged - def setText(self, theText): - self.setPlainText(theText) - self.lastEdit = time() - self._runCounter() - self.wcTimer.start() - self.setDocumentChanged(False) - return True - def getText(self): theText = self.toPlainText() return theText @@ -296,7 +300,7 @@ class GuiDocEditor(QTextEdit): if not self.wcTimer.isActive(): self.wcTimer.start() if self.mainConf.doReplace and not self.hasSelection: - self._docAutoReplace(self.theDoc.findBlock(thePos)) + self._docAutoReplace(self.theQDoc.findBlock(thePos)) # logger.verbose("Doc change signal took %.3f µs" % ((time()-self.lastEdit)*1e6)) return diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 697b2dea..a4f61a68 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -20,14 +20,17 @@ logger = logging.getLogger(__name__) class GuiDocHighlighter(QSyntaxHighlighter): - def __init__(self, theDoc, theTheme): + def __init__(self, theDoc, theParent): QSyntaxHighlighter.__init__(self, theDoc) logger.debug("Initialising DocHighlighter ...") self.mainConf = nw.CONFIG self.theDoc = theDoc - self.theTheme = theTheme + self.theParent = theParent + self.theTheme = theParent.theTheme + self.theIndex = theParent.theIndex self.theDict = None + self.theHandle = None self.spellCheck = False self.hRules = [] @@ -90,12 +93,12 @@ class GuiDocHighlighter(QSyntaxHighlighter): )) # Keyword/Value - self.hRules.append(( - r"^(@.+?)\s*:\s*(.+?)$", { - 1 : self.hStyles["keyword"], - 2 : self.hStyles["value"], - } - )) + # self.hRules.append(( + # r"^(@.+?)\s*:\s*(.+?)$", { + # 1 : self.hStyles["keyword"], + # 2 : self.hStyles["value"], + # } + # )) # Comments self.hRules.append(( @@ -152,6 +155,10 @@ class GuiDocHighlighter(QSyntaxHighlighter): return + ## + # Setters + ## + def setDict(self, theDict): self.theDict = theDict return True @@ -160,6 +167,75 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.spellCheck = theMode return True + def setHandle(self, theHandle): + self.theHandle = theHandle + return True + + ## + # Highlight Block + ## + + def highlightBlock(self, theText): + + if self.theHandle is None: + self.setCurrentBlockState(0) + return + + if theText.startswith("@"): + # Highlighting of keywords and commands + tItem = self.theParent.theProject.getItem(self.theHandle) + isValid, theBits, thePos = self.theIndex.scanThis(theText) + isGood = self.theIndex.checkThese(theBits, tItem) + if isValid: + for n in range(len(theBits)): + xPos = thePos[n] + xLen = len(theBits[n]) + if isGood[n]: + if n == 0: + self.setFormat(xPos, xLen, self.hStyles["keyword"]) + else: + self.setFormat(xPos, xLen, self.hStyles["value"]) + else: + kwFmt = self.format(xPos) + kwFmt.setUnderlineColor(self.colSpell) + kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) + self.setFormat(xPos, xLen, kwFmt) + + else: + # Other text just uses regex + for rX, xFmt in self.rules: + rxItt = rX.globalMatch(theText, 0) + while rxItt.hasNext(): + rxMatch = rxItt.next() + for xM in xFmt.keys(): + xPos = rxMatch.capturedStart(xM) + xLen = rxMatch.capturedLength(xM) + self.setFormat(xPos, xLen, xFmt[xM]) + + self.setCurrentBlockState(0) + + if self.theDict is None or not self.spellCheck or theText.startswith("@"): + return + + rxSpell = self.spellRx.globalMatch(theText.replace("_"," "), 0) + while rxSpell.hasNext(): + rxMatch = rxSpell.next() + if not self.theDict.check(rxMatch.captured(0)): + if rxMatch.captured(0) == rxMatch.captured(0).upper(): + continue + xPos = rxMatch.capturedStart(0) + xLen = rxMatch.capturedLength(0) + spFmt = self.format(xPos) + spFmt.setUnderlineColor(self.colSpell) + spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) + self.setFormat(xPos, xLen, spFmt) + + return + + ## + # Internal Functions + ## + def _makeFormat(self, fmtCol=None, fmtStyle=None, fmtSize=None): theFormat = QTextCharFormat() @@ -181,34 +257,4 @@ class GuiDocHighlighter(QSyntaxHighlighter): return theFormat - def highlightBlock(self, theText): - - for rX, xFmt in self.rules: - rxItt = rX.globalMatch(theText, 0) - while rxItt.hasNext(): - rxMatch = rxItt.next() - for xM in xFmt.keys(): - xPos = rxMatch.capturedStart(xM) - xLen = rxMatch.capturedLength(xM) - self.setFormat(xPos, xLen, xFmt[xM]) - - self.setCurrentBlockState(0) - - if self.theDict is None or not self.spellCheck or theText.startswith("@"): - return - - rxSpell = self.spellRx.globalMatch(theText.replace("_"," "), 0) - while rxSpell.hasNext(): - rxMatch = rxSpell.next() - if not self.theDict.check(rxMatch.captured(0)): - if rxMatch.captured(0) == rxMatch.captured(0).upper(): continue - xPos = rxMatch.capturedStart(0) - xLen = rxMatch.capturedLength(0) - spFmt = self.format(xPos) - spFmt.setUnderlineColor(self.colSpell) - spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) - self.setFormat(xPos, xLen, spFmt) - - return - # END Class DocHighlighter diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index d605f8ec..cf7592f9 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -53,7 +53,7 @@ class GuiMain(QMainWindow): self.theTheme = Theme() self.theProject = NWProject(self) self.theDocument = NWDoc(self.theProject, self) - self.tagIndex = NWIndex(self.theProject, self) + self.theIndex = NWIndex(self.theProject, self) self.hasProject = False self.resize(*self.mainConf.winGeometry) @@ -212,7 +212,7 @@ class GuiMain(QMainWindow): if saveOK: self.theProject.closeProject() - self.tagIndex.clearIndex() + self.theIndex.clearIndex() self.clearGUI() self.hasProject = False @@ -236,7 +236,7 @@ class GuiMain(QMainWindow): return False # Load the tag index - self.tagIndex.loadIndex() + self.theIndex.loadIndex() # Update GUI self._setWindowTitle(self.theProject.projName) @@ -268,7 +268,7 @@ class GuiMain(QMainWindow): self.treeView.saveTreeOrder() self.theProject.saveProject() - self.tagIndex.saveIndex() + self.theIndex.saveIndex() self.mainMenu.updateRecentProjects() return True @@ -286,9 +286,7 @@ class GuiMain(QMainWindow): def openDocument(self, tHandle): self.closeDocument() - self.docEditor.setText(self.theDocument.openDocument(tHandle)) - self.docEditor.setReadOnly(False) - self.docEditor.setCursorPosition(self.theDocument.theItem.cursorPos) + self.docEditor.loadText(tHandle) self.docEditor.changeWidth() self.docEditor.setFocus() self.theProject.setLastEdited(tHandle) @@ -305,7 +303,7 @@ class GuiMain(QMainWindow): theItem.setCursorPos(cursPos) self.theDocument.saveDocument(docText) self.docEditor.setDocumentChanged(False) - self.tagIndex.scanText(theItem.itemHandle, docText) + self.theIndex.scanText(theItem.itemHandle, docText) return True def viewDocument(self, tHandle=None): @@ -385,7 +383,7 @@ class GuiMain(QMainWindow): logger.debug("Rebuilding indices ...") self.treeView.saveTreeOrder() - self.tagIndex.clearIndex() + self.theIndex.clearIndex() nItems = len(self.theProject.treeOrder) dlgProg = QProgressDialog("Scanning files ...", "Cancel", 0, nItems, self) @@ -419,7 +417,7 @@ class GuiMain(QMainWindow): self.treeView.projectWordCount() # Build tag index - self.tagIndex.scanText(tHandle, theText) + self.theIndex.scanText(tHandle, theText) time.sleep(0.05) nDone += 1 diff --git a/nw/project/index.py b/nw/project/index.py index c15409aa..b08ed313 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -36,24 +36,25 @@ logger = logging.getLogger(__name__) class NWIndex(): - TAG_KEY = "@tag" - POV_KEY = "@pov" - CHAR_KEY = "@char" - PLOT_KEY = "@plot" - TIME_KEY = "@time" - WORLD_KEY = "@location" - OBJECT_KEY = "@object" - CUSTOM_KEY = "@custom" + TAG_KEY = "@tag" + POV_KEY = "@pov" + CHAR_KEY = "@char" + PLOT_KEY = "@plot" + TIME_KEY = "@time" + WORLD_KEY = "@location" + OBJECT_KEY = "@object" + CUSTOM_KEY = "@custom" - NOTE_KEYS = [PLOT_KEY, POV_KEY, CHAR_KEY, WORLD_KEY, TIME_KEY, OBJECT_KEY, CUSTOM_KEY] - VALID_CLASS = { - nwItemClass.NOVEL : [], - nwItemClass.PLOT : [PLOT_KEY], - nwItemClass.CHARACTER : [POV_KEY, CHAR_KEY], - nwItemClass.WORLD : [WORLD_KEY], - nwItemClass.TIMELINE : [TIME_KEY], - nwItemClass.OBJECT : [OBJECT_KEY], - nwItemClass.CUSTOM : [CUSTOM_KEY], + NOTE_KEYS = [TAG_KEY] + NOVEL_KEYS = [PLOT_KEY, POV_KEY, CHAR_KEY, WORLD_KEY, TIME_KEY, OBJECT_KEY, CUSTOM_KEY] + TAG_CLASS = { + POV_KEY : nwItemClass.CHARACTER, + CHAR_KEY : nwItemClass.CHARACTER, + PLOT_KEY : nwItemClass.PLOT, + TIME_KEY : nwItemClass.TIMELINE, + WORLD_KEY : nwItemClass.WORLD, + OBJECT_KEY : nwItemClass.OBJECT, + CUSTOM_KEY : nwItemClass.CUSTOM, } def __init__(self, theProject, theParent): @@ -137,7 +138,8 @@ class NWIndex(): theItem = self.theProject.getItem(tHandle) if theItem is None: return False if theItem.itemType != nwItemType.FILE: return False - itemClass = theItem.itemClass + itemClass = theItem.itemClass + itemLayout = theItem.itemLayout logger.debug("Indexing item with handle %s" % tHandle) @@ -162,7 +164,7 @@ class NWIndex(): if nChar == 0: continue if aLine[0] == "#": if isNovel: - self.indexTitle(tHandle, aLine, nLine) + self.indexTitle(tHandle, aLine, nLine, itemLayout) elif aLine[0] == "@": if isNovel: self.indexNoteRef(tHandle, aLine, nLine) @@ -171,7 +173,7 @@ class NWIndex(): return True - def indexTitle(self, tHandle, aLine, nLine): + def indexTitle(self, tHandle, aLine, nLine, itemLayout): if aLine.startswith("# "): hDepth = 1 @@ -189,7 +191,7 @@ class NWIndex(): return False if hText != "": - self.novelIndex[tHandle].append([nLine, hDepth, hText]) + self.novelIndex[tHandle].append([nLine, hDepth, hText, itemLayout.name]) return True @@ -200,7 +202,7 @@ class NWIndex(): return False theKey = theBits[0] - if theKey in self.NOTE_KEYS: + if theKey in self.NOVEL_KEYS: for aVal in theBits[1:]: self.noteIndex[tHandle].append([nLine, theKey, aVal]) @@ -254,4 +256,32 @@ class NWIndex(): return True, theBits, thePos + def checkThese(self, theBits, tItem): + + theBits = [aBit.lower() for aBit in theBits] + nBits = len(theBits) + isGood = [False]*nBits + if nBits == 0: + return [] + + # If we have a tag, the first value is always OK, rest is ignored + if theBits[0] == self.TAG_KEY and nBits > 1: + isGood[0] = True + isGood[1] = True + return isGood + + # If we're still here, we better check the references + if tItem.itemClass == nwItemClass.NOVEL: + isGood[0] = theBits[0] in self.NOVEL_KEYS + else: + isGood[0] = theBits[0] in self.NOTE_KEYS + if not isGood[0] or nBits == 1: + return isGood + + for n in range(1,nBits): + if theBits[n] in self.tagIndex: + isGood[n] = self.TAG_CLASS[theBits[0]].name == self.tagIndex[theBits[n]][2] + + return isGood + # END Class NWIndex diff --git a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd index de9fae73..60cccf00 100644 --- a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd +++ b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd @@ -5,6 +5,7 @@ % Begin Meta @POV: Jane @Char: Jane, John +@Location: Earth % End Meta Some text here would look good as well, and maybe some "dialogue"? diff --git a/sample/sampleNovel/data_b/3e74dbc1f584_main.nwd b/sample/sampleNovel/data_b/3e74dbc1f584_main.nwd index bcdf600f..b82ef89a 100644 --- a/sample/sampleNovel/data_b/3e74dbc1f584_main.nwd +++ b/sample/sampleNovel/data_b/3e74dbc1f584_main.nwd @@ -1,3 +1,5 @@ # Earth +@tag: Earth + Third planet from the sun, fairly dense, and with lots of people on it. \ No newline at end of file diff --git a/sample/sampleNovel/meta/sessionInfo.log b/sample/sampleNovel/meta/sessionInfo.log index 908773f6..ed02d290 100644 --- a/sample/sampleNovel/meta/sessionInfo.log +++ b/sample/sampleNovel/meta/sessionInfo.log @@ -101,3 +101,26 @@ Start: 2019-05-30 18:26:01 End: 2019-05-30 18:27:56 Words: 0 Start: 2019-05-30 18:28:00 End: 2019-05-30 18:30:22 Words: 0 Start: 2019-05-30 18:42:16 End: 2019-05-30 18:43:10 Words: 0 Start: 2019-05-30 18:43:24 End: 2019-05-30 18:43:31 Words: 0 +Start: 2019-05-30 18:48:54 End: 2019-05-30 18:49:19 Words: 0 +Start: 2019-05-30 18:49:23 End: 2019-05-30 18:49:53 Words: 0 +Start: 2019-05-30 18:51:06 End: 2019-05-30 19:02:40 Words: 0 +Start: 2019-05-30 19:07:17 End: 2019-05-30 19:07:26 Words: 0 +Start: 2019-05-30 19:17:16 End: 2019-05-30 19:17:39 Words: 0 +Start: 2019-05-30 19:17:44 End: 2019-05-30 19:17:55 Words: 0 +Start: 2019-05-30 19:18:44 End: 2019-05-30 19:19:15 Words: 0 +Start: 2019-05-30 19:19:20 End: 2019-05-30 19:19:47 Words: 0 +Start: 2019-05-30 19:21:41 End: 2019-05-30 19:21:44 Words: 0 +Start: 2019-05-30 19:24:00 End: 2019-05-30 19:24:03 Words: 0 +Start: 2019-05-30 19:24:45 End: 2019-05-30 19:26:48 Words: 0 +Start: 2019-05-30 19:28:23 End: 2019-05-30 19:32:41 Words: 0 +Start: 2019-05-30 19:32:57 End: 2019-05-30 19:33:18 Words: 0 +Start: 2019-05-30 19:33:22 End: 2019-05-30 19:33:39 Words: 0 +Start: 2019-05-30 19:34:08 End: 2019-05-30 19:34:31 Words: 0 +Start: 2019-05-30 19:34:43 End: 2019-05-30 19:34:45 Words: 0 +Start: 2019-05-30 19:34:58 End: 2019-05-30 19:36:33 Words: 0 +Start: 2019-05-30 19:36:38 End: 2019-05-30 19:36:47 Words: 0 +Start: 2019-05-30 19:43:30 End: 2019-05-30 19:43:35 Words: 0 +Start: 2019-05-30 19:43:42 End: 2019-05-30 19:43:49 Words: 0 +Start: 2019-05-30 19:45:34 End: 2019-05-30 19:45:59 Words: 0 +Start: 2019-05-30 19:53:25 End: 2019-05-30 19:55:00 Words: 0 +Start: 2019-05-30 19:55:21 End: 2019-05-30 19:56:16 Words: 0 diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index c7108caf..90e826e8 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -76,7 +76,7 @@ 656 121 5 - 729 + 105 New File @@ -145,7 +145,7 @@ 76 15 1 - 0 + 20 Trash From fa33d42fe2eae25a3c8b03788898697cf3496175 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Thu, 30 May 2019 20:18:40 +0200 Subject: [PATCH 10/23] Can't pop dictionary keys while looping --- nw/project/index.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nw/project/index.py b/nw/project/index.py index b08ed313..5ad504b6 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -152,9 +152,12 @@ class NWIndex(): isNovel = False # Also clear references to file in tag index + clearTags = [] for aTag in self.tagIndex: if self.tagIndex[aTag][1] == tHandle: - self.tagIndex.pop(aTag) + clearTags.append(aTag) + for aTag in clearTags: + self.tagIndex.pop(aTag) nLine = 0 for aLine in theText.splitlines(): From ce96ac996111b41856ad03ebd5a3cf9874c1586d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Thu, 30 May 2019 22:03:52 +0200 Subject: [PATCH 11/23] tags are now case sensitive, and need to be unique --- nw/project/index.py | 17 +++++++++++------ sample/sampleNovel/data_6/36b6aa9b697b_main.nwd | 6 +++--- sample/sampleNovel/meta/sessionInfo.log | 10 ++++++++++ sample/sampleNovel/nwProject.nwx | 8 ++++---- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/nw/project/index.py b/nw/project/index.py index 5ad504b6..56baf52b 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -240,7 +240,7 @@ class NWIndex(): if sKey == "@": return False, theBits, thePos - theBits.append(sKey.lower()) + theBits.append(sKey) thePos.append(cPos) cPos += len(sKey) + 1 @@ -253,7 +253,7 @@ class NWIndex(): sVal = cVal.strip() rLen = len(cVal.lstrip()) tLen = len(cVal) - theBits.append(sVal.lower()) + theBits.append(sVal) thePos.append(cPos+tLen-rLen) cPos += tLen + 1 @@ -261,16 +261,21 @@ class NWIndex(): def checkThese(self, theBits, tItem): - theBits = [aBit.lower() for aBit in theBits] - nBits = len(theBits) - isGood = [False]*nBits + nBits = len(theBits) + isGood = [False]*nBits if nBits == 0: return [] # If we have a tag, the first value is always OK, rest is ignored if theBits[0] == self.TAG_KEY and nBits > 1: isGood[0] = True - isGood[1] = True + if theBits[1] in self.tagIndex.keys(): + if self.tagIndex[theBits[1]][1] == tItem.itemHandle: + isGood[1] = True + else: + isGood[1] = False + else: + isGood[1] = True return isGood # If we're still here, we better check the references diff --git a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd index 60cccf00..831ee9fe 100644 --- a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd +++ b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd @@ -3,9 +3,9 @@ ## This is the Subtitle % Begin Meta -@POV: Jane -@Char: Jane, John -@Location: Earth +@pov: Jane +@char: Jane, John +@location: Earth % End Meta Some text here would look good as well, and maybe some "dialogue"? diff --git a/sample/sampleNovel/meta/sessionInfo.log b/sample/sampleNovel/meta/sessionInfo.log index ed02d290..4ac50074 100644 --- a/sample/sampleNovel/meta/sessionInfo.log +++ b/sample/sampleNovel/meta/sessionInfo.log @@ -124,3 +124,13 @@ Start: 2019-05-30 19:43:42 End: 2019-05-30 19:43:49 Words: 0 Start: 2019-05-30 19:45:34 End: 2019-05-30 19:45:59 Words: 0 Start: 2019-05-30 19:53:25 End: 2019-05-30 19:55:00 Words: 0 Start: 2019-05-30 19:55:21 End: 2019-05-30 19:56:16 Words: 0 +Start: 2019-05-30 20:51:36 End: 2019-05-30 20:51:40 Words: 0 +Start: 2019-05-30 20:51:40 End: 2019-05-30 20:51:45 Words: 0 +Start: 2019-05-30 20:25:18 End: 2019-05-30 21:55:25 Words: 0 +Start: 2019-05-30 21:56:09 End: 2019-05-30 21:57:09 Words: 0 +Start: 2019-05-30 21:57:14 End: 2019-05-30 21:58:29 Words: 0 +Start: 2019-05-30 21:58:33 End: 2019-05-30 21:58:59 Words: 0 +Start: 2019-05-30 21:59:33 End: 2019-05-30 22:00:08 Words: 0 +Start: 2019-05-30 22:00:27 End: 2019-05-30 22:01:18 Words: 0 +Start: 2019-05-30 22:01:35 End: 2019-05-30 22:01:57 Words: 0 +Start: 2019-05-30 22:02:05 End: 2019-05-30 22:02:24 Words: 0 diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 90e826e8..2437ad48 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -8,7 +8,7 @@ True - 636b6aa9b697b + bb2c23b3c42cc None 558 @@ -76,7 +76,7 @@ 656 121 5 - 105 + 69 New File @@ -126,7 +126,7 @@ 55 9 1 - 71 + 25 Locations From f719266846d96773979ea73164d0ef0772fb8995 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Thu, 30 May 2019 22:09:08 +0200 Subject: [PATCH 12/23] Added colour for tag errors --- nw/gui/dochighlight.py | 3 ++- nw/project/index.py | 4 ++-- nw/theme.py | 2 ++ nw/themes/default/theme.conf | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index a4f61a68..c99fa3ba 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -44,6 +44,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.colKey = QColor(*self.theTheme.colKey) self.colVal = QColor(*self.theTheme.colVal) self.colSpell = QColor(*self.theTheme.colSpell) + self.colTagErr = QColor(*self.theTheme.colTagErr) self.hStyles = { "header1" : self._makeFormat(self.colHead, "bold",1.8), @@ -197,7 +198,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.setFormat(xPos, xLen, self.hStyles["value"]) else: kwFmt = self.format(xPos) - kwFmt.setUnderlineColor(self.colSpell) + kwFmt.setUnderlineColor(self.colTagErr) kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(xPos, xLen, kwFmt) diff --git a/nw/project/index.py b/nw/project/index.py index 56baf52b..86a3ad57 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -266,7 +266,7 @@ class NWIndex(): if nBits == 0: return [] - # If we have a tag, the first value is always OK, rest is ignored + # If we have a tag, only the first value is accepted, the rest is ignored if theBits[0] == self.TAG_KEY and nBits > 1: isGood[0] = True if theBits[1] in self.tagIndex.keys(): @@ -278,7 +278,7 @@ class NWIndex(): isGood[1] = True return isGood - # If we're still here, we better check the references + # If we're still here, we better check that the references exist if tItem.itemClass == nwItemClass.NOVEL: isGood[0] = theBits[0] in self.NOVEL_KEYS else: diff --git a/nw/theme.py b/nw/theme.py index cab4c302..a34a2895 100644 --- a/nw/theme.py +++ b/nw/theme.py @@ -37,6 +37,7 @@ class Theme: self.colKey = [0,0,0] self.colVal = [0,0,0] self.colSpell = [0,0,0] + self.colTagErr = [0,0,0] # Changeable Settings self.guiTheme = None @@ -94,6 +95,7 @@ class Theme: self.colKey = self._loadColour(confParser,cnfSec,"keyword") self.colVal = self._loadColour(confParser,cnfSec,"value") self.colSpell = self._loadColour(confParser,cnfSec,"spellcheckline") + self.colTagErr = self._loadColour(confParser,cnfSec,"tagerror") return True diff --git a/nw/themes/default/theme.conf b/nw/themes/default/theme.conf index 117f53ea..bac88942 100644 --- a/nw/themes/default/theme.conf +++ b/nw/themes/default/theme.conf @@ -9,3 +9,4 @@ hidden = 150, 150, 150 keyword = 200, 46, 0 value = 184, 200, 0 spellcheckline = 200, 46, 0 +tagerror = 46, 200, 0 From 67c2912c7f531fbef069746382a3da30d82e09bd Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Fri, 31 May 2019 18:59:53 +0200 Subject: [PATCH 13/23] Started adding timelibe view GUI --- nw/gui/mainmenu.py | 12 +++- nw/gui/timelineview.py | 127 +++++++++++++++++++++++++++++++++++++++++ nw/gui/winmain.py | 6 ++ nw/project/index.py | 38 ++++++++++++ 4 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 nw/gui/timelineview.py diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 15a38754..a0816b36 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -311,13 +311,23 @@ class GuiMainMenu(QMenuBar): menuItem.triggered.connect(lambda : self.theParent.setFocus(2)) self.viewMenu.addAction(menuItem) - # # View > Document Pane 2 + # View > Document Pane 2 menuItem = QAction(QIcon.fromTheme("go-last"), "Right Document Pane", self) menuItem.setStatusTip("Move focus to right document pane") menuItem.setShortcut("Ctrl+3") menuItem.triggered.connect(lambda : self.theParent.setFocus(3)) self.viewMenu.addAction(menuItem) + # View > Separator + self.viewMenu.addSeparator() + + # View > Project Timeline + menuItem = QAction(QIcon.fromTheme("x-office-spreadsheet"), "Show Project Timeline", self) + menuItem.setStatusTip("Open the project timeline window") + menuItem.setShortcut("Ctrl+T") + menuItem.triggered.connect(self.theParent.showTimeLineDialog) + self.viewMenu.addAction(menuItem) + return def _buildEditMenu(self): diff --git a/nw/gui/timelineview.py b/nw/gui/timelineview.py new file mode 100644 index 00000000..a6acf870 --- /dev/null +++ b/nw/gui/timelineview.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +"""novelWriter GUI Timeline View + + novelWriter – GUI Timeline View +================================= + Class holding the timeline view window + + File History: + Created: 2019-05-30 [0.1.4] + +""" + +import logging +import nw + +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QDialogButtonBox + +logger = logging.getLogger(__name__) + +class GuiTimeLineView(QDialog): + + def __init__(self, theParent, theProject, theIndex): + QDialog.__init__(self, theParent) + + logger.debug("Initialising TimeLineView ...") + + self.mainConf = nw.CONFIG + self.theProject = theProject + self.theParent = theParent + self.theIndex = theIndex + self.theMatrix = {} + self.numRows = 0 + self.numCols = 0 + + self.outerBox = QVBoxLayout() + + self.setWindowTitle("Timeline View") + + self.mainTable = QTableWidget(1,1) + + self.setLayout(self.outerBox) + + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) + self.buttonBox.rejected.connect(self._doClose) + + self.outerBox.addWidget(self.mainTable) + self.outerBox.addWidget(self.buttonBox) + + self._buildMatrix() + self._buildNovelList() + + self.setMinimumSize(600,400) + + self.show() + + logger.debug("TimeLineView initialisation complete") + + return + + def _buildMatrix(self): + + self.theMatrix = { + "title" : [], # Size numRows - 1 + "depth" : [], # Size numRows - 1 + "handle" : [], # Size numRows - 1 + "line" : [], # Size numRows - 1 + "tags" : [], # Size numCols - 1 + "table" : [], # Size numRows - 1 x numCols - 1 + } + + self.numRows = 1 + self.numCols = 1 + for tHandle in self.theProject.treeOrder: + if tHandle not in self.theIndex.novelIndex: + continue + for nLine, nDepth, tTitle, tLayout in self.theIndex.novelIndex[tHandle]: + self.theMatrix["title"].append(tTitle) + self.theMatrix["depth"].append(nDepth) + self.theMatrix["handle"].append(tHandle) + self.theMatrix["line"].append(nLine) + self.numRows += 1 + + for tTag in self.theIndex.tagIndex: + self.theMatrix["tags"].append(tTag) + self.numCols += 1 + + # theTable = [[0]*(self.numCols-1)]*(self.numRows-1) + + # for i in range(self.numCols): + # for j in range(self.numRows): + + return + + def _buildNovelList(self): + + self.theIndex.buildNovelList() + self.numRows = len(self.theIndex.novelList) + 1 + + self.mainTable.setRowCount(self.numRows) + self.mainTable.setColumnCount(self.numCols) + + for n in range(self.numRows-1): + iDepth = self.theIndex.novelList[n][1] + iTitle = self.theIndex.novelList[n][2] + newItem = QTableWidgetItem(" "*iDepth + iTitle) + self.mainTable.setItem(n+1, 0, newItem) + + theTag = self.theMatrix["tags"][0] + theMap = self.theIndex.buildTagNovelMap(theTag) + newItem = QTableWidgetItem(theTag) + self.mainTable.setItem(0, 1, newItem) + for n in range(self.numRows-1): + newItem = QTableWidgetItem(str(theMap[n])) + self.mainTable.setItem(n+1, 1, newItem) + + # for n in range(self.numCols-1): + # iTag = self.theMatrix["tags"][n] + # newItem = QTableWidgetItem(iTag) + # self.mainTable.setItem(0, n+1, newItem) + + return + + def _doClose(self): + self.close() + return + +# END Class GuiItemEditor diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index cf7592f9..31744acc 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -31,6 +31,7 @@ from nw.gui.mainmenu import GuiMainMenu from nw.gui.projecteditor import GuiProjectEditor from nw.gui.itemeditor import GuiItemEditor from nw.gui.statusbar import GuiMainStatus +from nw.gui.timelineview import GuiTimeLineView from nw.project.project import NWProject from nw.project.document import NWDoc from nw.project.item import NWItem @@ -472,6 +473,11 @@ class GuiMain(QMainWindow): self._setWindowTitle(self.theProject.projName) return True + def showTimeLineDialog(self): + dlgTLine = GuiTimeLineView(self, self.theProject, self.theIndex) + dlgTLine.exec_() + return True + def makeAlert(self, theMessage, theLevel=nwAlert.INFO): """Alert both the user and the logger at the same time. Message can be either a string or an array of strings. Severity level is 0 = info, 1 = warning, and 2 = error. diff --git a/nw/project/index.py b/nw/project/index.py index 86a3ad57..543caa31 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -69,6 +69,9 @@ class NWIndex(): self.noteIndex = {} self.novelIndex = {} + # Lists + self.novelList = [] + return def clearIndex(self): @@ -222,6 +225,10 @@ class NWIndex(): return True + ## + # Check @ Lines + ## + def scanThis(self, aLine): theBits = [] @@ -292,4 +299,35 @@ class NWIndex(): return isGood + ## + # Extract Data + ## + + def buildNovelList(self): + + self.novelList = [] + for tHandle in self.theProject.treeOrder: + if tHandle not in self.novelIndex: + continue + for tEntry in self.novelIndex[tHandle]: + self.novelList.append(tEntry) + + return True + + def buildTagNovelMap(self, theTag): + + tagList = [] + if theTag not in self.tagIndex: + return tagList + + try: + tagClass = nwItemClass[self.tagIndex[theTag][2]] + except: + logger.error("Could not map '%s' to nwItemClass" % self.tagIndex[theTag][2]) + return tagList + + tagList = [0]*len(self.novelList) + + return tagList + # END Class NWIndex From 57c03a2c5cdefbf67230538e9469554369697603 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Fri, 31 May 2019 20:34:31 +0200 Subject: [PATCH 14/23] Generating the data for the timeline now works. Probably a bit fragile and in need of more checks. --- nw/gui/timelineview.py | 26 +++---- nw/project/index.py | 76 +++++++++++-------- .../sampleNovel/data_6/36b6aa9b697b_main.nwd | 2 +- .../sampleNovel/data_b/c0cbd2a407f3_main.nwd | 3 + .../sampleNovel/data_f/1471bef9f2ae_main.nwd | 6 ++ sample/sampleNovel/meta/sessionInfo.log | 19 +++++ sample/sampleNovel/nwProject.nwx | 24 ++++-- 7 files changed, 101 insertions(+), 55 deletions(-) create mode 100644 sample/sampleNovel/data_f/1471bef9f2ae_main.nwd diff --git a/nw/gui/timelineview.py b/nw/gui/timelineview.py index a6acf870..2920623d 100644 --- a/nw/gui/timelineview.py +++ b/nw/gui/timelineview.py @@ -84,11 +84,6 @@ class GuiTimeLineView(QDialog): self.theMatrix["tags"].append(tTag) self.numCols += 1 - # theTable = [[0]*(self.numCols-1)]*(self.numRows-1) - - # for i in range(self.numCols): - # for j in range(self.numRows): - return def _buildNovelList(self): @@ -105,18 +100,15 @@ class GuiTimeLineView(QDialog): newItem = QTableWidgetItem(" "*iDepth + iTitle) self.mainTable.setItem(n+1, 0, newItem) - theTag = self.theMatrix["tags"][0] - theMap = self.theIndex.buildTagNovelMap(theTag) - newItem = QTableWidgetItem(theTag) - self.mainTable.setItem(0, 1, newItem) - for n in range(self.numRows-1): - newItem = QTableWidgetItem(str(theMap[n])) - self.mainTable.setItem(n+1, 1, newItem) - - # for n in range(self.numCols-1): - # iTag = self.theMatrix["tags"][n] - # newItem = QTableWidgetItem(iTag) - # self.mainTable.setItem(0, n+1, newItem) + theMap = self.theIndex.buildTagNovelMap(self.theMatrix["tags"]) + nCol = 1 + for theTag, theCols in theMap.items(): + newItem = QTableWidgetItem(theTag) + self.mainTable.setItem(0, nCol, newItem) + for n in range(self.numRows-1): + newItem = QTableWidgetItem(str(theCols[n])) + self.mainTable.setItem(n+1, nCol, newItem) + nCol += 1 return diff --git a/nw/project/index.py b/nw/project/index.py index 543caa31..8a4c6bf1 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -48,13 +48,13 @@ class NWIndex(): NOTE_KEYS = [TAG_KEY] NOVEL_KEYS = [PLOT_KEY, POV_KEY, CHAR_KEY, WORLD_KEY, TIME_KEY, OBJECT_KEY, CUSTOM_KEY] TAG_CLASS = { - POV_KEY : nwItemClass.CHARACTER, - CHAR_KEY : nwItemClass.CHARACTER, - PLOT_KEY : nwItemClass.PLOT, - TIME_KEY : nwItemClass.TIMELINE, - WORLD_KEY : nwItemClass.WORLD, - OBJECT_KEY : nwItemClass.OBJECT, - CUSTOM_KEY : nwItemClass.CUSTOM, + CHAR_KEY : [nwItemClass.CHARACTER, 1], + POV_KEY : [nwItemClass.CHARACTER, 2], + PLOT_KEY : [nwItemClass.PLOT, 1], + TIME_KEY : [nwItemClass.TIMELINE, 1], + WORLD_KEY : [nwItemClass.WORLD, 1], + OBJECT_KEY : [nwItemClass.OBJECT, 1], + CUSTOM_KEY : [nwItemClass.CUSTOM, 1], } def __init__(self, theProject, theParent): @@ -66,7 +66,7 @@ class NWIndex(): # Indices self.tagIndex = {} - self.noteIndex = {} + self.refIndex = {} self.novelIndex = {} # Lists @@ -76,7 +76,7 @@ class NWIndex(): def clearIndex(self): self.tagIndex = {} - self.noteIndex = {} + self.refIndex = {} self.novelIndex = {} return @@ -101,8 +101,8 @@ class NWIndex(): if "tagIndex" in theData.keys(): self.tagIndex = theData["tagIndex"] - if "noteIndex" in theData.keys(): - self.noteIndex = theData["noteIndex"] + if "refIndex" in theData.keys(): + self.refIndex = theData["refIndex"] if "novelIndex" in theData.keys(): self.novelIndex = theData["novelIndex"] @@ -122,7 +122,7 @@ class NWIndex(): with open(indexFile,mode="w+") as outFile: outFile.write(json.dumps({ "tagIndex" : self.tagIndex, - "noteIndex" : self.noteIndex, + "refIndex" : self.refIndex, "novelIndex" : self.novelIndex, }, indent=nIndent)) except Exception as e: @@ -149,7 +149,7 @@ class NWIndex(): # Check file type, and reset its old index if itemClass == nwItemClass.NOVEL: self.novelIndex[tHandle] = [] - self.noteIndex[tHandle] = [] + self.refIndex[tHandle] = [] isNovel = True else: isNovel = False @@ -162,7 +162,8 @@ class NWIndex(): for aTag in clearTags: self.tagIndex.pop(aTag) - nLine = 0 + nLine = 0 + nTitle = 0 for aLine in theText.splitlines(): aLine = aLine.strip() nLine += 1 @@ -170,10 +171,12 @@ class NWIndex(): if nChar == 0: continue if aLine[0] == "#": if isNovel: - self.indexTitle(tHandle, aLine, nLine, itemLayout) + isTitle = self.indexTitle(tHandle, aLine, nLine, itemLayout) + if isTitle: + nTitle = nLine elif aLine[0] == "@": if isNovel: - self.indexNoteRef(tHandle, aLine, nLine) + self.indexNoteRef(tHandle, aLine, nLine, nTitle) else: self.indexTag(tHandle, aLine, nLine, itemClass) @@ -201,7 +204,7 @@ class NWIndex(): return True - def indexNoteRef(self, tHandle, aLine, nLine): + def indexNoteRef(self, tHandle, aLine, nLine, nTitle): isValid, theBits, thePos = self.scanThis(aLine) if not isValid or len(theBits) == 0: @@ -210,7 +213,7 @@ class NWIndex(): theKey = theBits[0] if theKey in self.NOVEL_KEYS: for aVal in theBits[1:]: - self.noteIndex[tHandle].append([nLine, theKey, aVal]) + self.refIndex[tHandle].append([nLine, theKey, aVal, nTitle]) return True @@ -295,7 +298,7 @@ class NWIndex(): for n in range(1,nBits): if theBits[n] in self.tagIndex: - isGood[n] = self.TAG_CLASS[theBits[0]].name == self.tagIndex[theBits[n]][2] + isGood[n] = self.TAG_CLASS[theBits[0]][0].name == self.tagIndex[theBits[n]][2] return isGood @@ -305,29 +308,40 @@ class NWIndex(): def buildNovelList(self): - self.novelList = [] + self.novelList = [] + self.novelOrder = [] for tHandle in self.theProject.treeOrder: if tHandle not in self.novelIndex: continue for tEntry in self.novelIndex[tHandle]: self.novelList.append(tEntry) + self.novelOrder.append("%s:%d" % (tHandle,tEntry[0])) return True - def buildTagNovelMap(self, theTag): + def buildTagNovelMap(self, theTags): - tagList = [] - if theTag not in self.tagIndex: - return tagList + tagMap = {} + tagClass = {} - try: - tagClass = nwItemClass[self.tagIndex[theTag][2]] - except: - logger.error("Could not map '%s' to nwItemClass" % self.tagIndex[theTag][2]) - return tagList + for theTag in theTags: + tagMap[theTag] = [0]*len(self.novelOrder) + try: + tagClass[theTag] = nwItemClass[self.tagIndex[theTag][2]] + except: + logger.error("Could not map '%s' to nwItemClass" % self.tagIndex[theTag][2]) + tagClass[theTag] = None - tagList = [0]*len(self.novelList) + for tHandle in self.refIndex: + for nLine, tKey, tTag, nTitle in self.refIndex[tHandle]: + if tTag in tagMap.keys() and tKey in self.TAG_CLASS: + try: + nPos = self.novelOrder.index("%s:%d" % (tHandle, nTitle)) + if self.TAG_CLASS[tKey][0] == tagClass[tTag]: + tagMap[tTag][nPos] = self.TAG_CLASS[tKey][1] + except: + logger.error("Could not find '%s:%d' in novelOrder" % (tHandle, nTitle)) - return tagList + return tagMap # END Class NWIndex diff --git a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd index 831ee9fe..a084c76f 100644 --- a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd +++ b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd @@ -4,7 +4,7 @@ % Begin Meta @pov: Jane -@char: Jane, John +@char: John @location: Earth % End Meta diff --git a/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd b/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd index 6edf9061..0a39ca10 100644 --- a/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd +++ b/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd @@ -1,3 +1,6 @@ # This is a New File! +@pov: John +@location: Space + Although, not so new now that it has text in it an everything … \ No newline at end of file diff --git a/sample/sampleNovel/data_f/1471bef9f2ae_main.nwd b/sample/sampleNovel/data_f/1471bef9f2ae_main.nwd new file mode 100644 index 00000000..caae39bc --- /dev/null +++ b/sample/sampleNovel/data_f/1471bef9f2ae_main.nwd @@ -0,0 +1,6 @@ +# Space + +@tag: Space + +This is somewhere in outer space. + diff --git a/sample/sampleNovel/meta/sessionInfo.log b/sample/sampleNovel/meta/sessionInfo.log index 4ac50074..ecc8f9ce 100644 --- a/sample/sampleNovel/meta/sessionInfo.log +++ b/sample/sampleNovel/meta/sessionInfo.log @@ -134,3 +134,22 @@ Start: 2019-05-30 21:59:33 End: 2019-05-30 22:00:08 Words: 0 Start: 2019-05-30 22:00:27 End: 2019-05-30 22:01:18 Words: 0 Start: 2019-05-30 22:01:35 End: 2019-05-30 22:01:57 Words: 0 Start: 2019-05-30 22:02:05 End: 2019-05-30 22:02:24 Words: 0 +Start: 2019-05-30 22:07:43 End: 2019-05-30 22:08:48 Words: 0 +Start: 2019-05-30 22:18:15 End: 2019-05-30 22:18:24 Words: 0 +Start: 2019-05-31 00:01:48 End: 2019-05-31 00:01:57 Words: 0 +Start: 2019-05-31 00:10:01 End: 2019-05-31 00:10:07 Words: 0 +Start: 2019-05-31 00:11:12 End: 2019-05-31 00:11:21 Words: 0 +Start: 2019-05-31 00:11:49 End: 2019-05-31 00:12:04 Words: 0 +Start: 2019-05-31 00:12:42 End: 2019-05-31 00:13:24 Words: 0 +Start: 2019-05-31 00:38:11 End: 2019-05-31 00:38:38 Words: 0 +Start: 2019-05-31 00:39:35 End: 2019-05-31 00:40:23 Words: 0 +Start: 2019-05-31 00:40:27 End: 2019-05-31 00:40:54 Words: 0 +Start: 2019-05-31 00:56:15 End: 2019-05-31 00:56:23 Words: 0 +Start: 2019-05-31 00:59:56 End: 2019-05-31 01:00:05 Words: 0 +Start: 2019-05-31 01:00:39 End: 2019-05-31 01:00:56 Words: 0 +Start: 2019-05-31 19:10:29 End: 2019-05-31 19:21:11 Words: 0 +Start: 2019-05-31 19:50:56 End: 2019-05-31 19:51:03 Words: 0 +Start: 2019-05-31 19:58:24 End: 2019-05-31 19:59:04 Words: 0 +Start: 2019-05-31 20:11:22 End: 2019-05-31 20:11:56 Words: 0 +Start: 2019-05-31 20:23:06 End: 2019-05-31 20:23:18 Words: 0 +Start: 2019-05-31 20:23:56 End: 2019-05-31 20:27:02 Words: 7 diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 2437ad48..7cad49d7 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -8,9 +8,9 @@ True - bb2c23b3c42cc + bc0cbd2a407f3 None - 558 + 565 New Notes @@ -27,7 +27,7 @@ Main - + Novel ROOT @@ -76,7 +76,7 @@ 656 121 5 - 69 + 77 New File @@ -88,7 +88,7 @@ 82 19 1 - 0 + 69 Characters @@ -147,6 +147,18 @@ 1 20 + + Space + FILE + WORLD + None + False + NOTE + 38 + 7 + 1 + 57 + Trash TRASH From 915d9807f712c9c57c5ebdfd7655c6a3a9696327 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Fri, 31 May 2019 21:53:10 +0200 Subject: [PATCH 15/23] Some more tweaking of the table view of the timeline --- nw/gui/timelineview.py | 70 +++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/nw/gui/timelineview.py b/nw/gui/timelineview.py index 2920623d..3ba9e8c8 100644 --- a/nw/gui/timelineview.py +++ b/nw/gui/timelineview.py @@ -13,7 +13,9 @@ import logging import nw -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QDialogButtonBox +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon, QColor, QBrush, QPixmap +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QDialogButtonBox, QLabel logger = logging.getLogger(__name__) @@ -36,7 +38,8 @@ class GuiTimeLineView(QDialog): self.setWindowTitle("Timeline View") - self.mainTable = QTableWidget(1,1) + self.mainTable = QTableWidget() + self.mainTable.setGridStyle(Qt.NoPen) self.setLayout(self.outerBox) @@ -46,7 +49,6 @@ class GuiTimeLineView(QDialog): self.outerBox.addWidget(self.mainTable) self.outerBox.addWidget(self.buttonBox) - self._buildMatrix() self._buildNovelList() self.setMinimumSize(600,400) @@ -57,57 +59,43 @@ class GuiTimeLineView(QDialog): return - def _buildMatrix(self): - - self.theMatrix = { - "title" : [], # Size numRows - 1 - "depth" : [], # Size numRows - 1 - "handle" : [], # Size numRows - 1 - "line" : [], # Size numRows - 1 - "tags" : [], # Size numCols - 1 - "table" : [], # Size numRows - 1 x numCols - 1 - } - - self.numRows = 1 - self.numCols = 1 - for tHandle in self.theProject.treeOrder: - if tHandle not in self.theIndex.novelIndex: - continue - for nLine, nDepth, tTitle, tLayout in self.theIndex.novelIndex[tHandle]: - self.theMatrix["title"].append(tTitle) - self.theMatrix["depth"].append(nDepth) - self.theMatrix["handle"].append(tHandle) - self.theMatrix["line"].append(nLine) - self.numRows += 1 - - for tTag in self.theIndex.tagIndex: - self.theMatrix["tags"].append(tTag) - self.numCols += 1 - - return - def _buildNovelList(self): self.theIndex.buildNovelList() - self.numRows = len(self.theIndex.novelList) + 1 + self.numRows = len(self.theIndex.novelList) + self.numCols = len(self.theIndex.tagIndex.keys()) self.mainTable.setRowCount(self.numRows) self.mainTable.setColumnCount(self.numCols) - for n in range(self.numRows-1): + for n in range(len(self.theIndex.novelList)): iDepth = self.theIndex.novelList[n][1] iTitle = self.theIndex.novelList[n][2] newItem = QTableWidgetItem(" "*iDepth + iTitle) - self.mainTable.setItem(n+1, 0, newItem) + self.mainTable.setVerticalHeaderItem(n, newItem) + self.mainTable.setRowHeight(n, 16) - theMap = self.theIndex.buildTagNovelMap(self.theMatrix["tags"]) - nCol = 1 + theMap = self.theIndex.buildTagNovelMap(self.theIndex.tagIndex.keys()) + nCol = 0 for theTag, theCols in theMap.items(): newItem = QTableWidgetItem(theTag) - self.mainTable.setItem(0, nCol, newItem) - for n in range(self.numRows-1): - newItem = QTableWidgetItem(str(theCols[n])) - self.mainTable.setItem(n+1, nCol, newItem) + self.mainTable.setHorizontalHeaderItem(nCol, newItem) + self.mainTable.setColumnWidth(nCol,50) + for n in range(len(theCols)): + if theCols[n] == 1: + pxNew = QPixmap(10,10) + pxNew.fill(QColor(0,120,0)) + lblNew = QLabel() + lblNew.setPixmap(pxNew) + lblNew.setAlignment(Qt.AlignCenter) + self.mainTable.setCellWidget(n, nCol, lblNew) + elif theCols[n] == 2: + pxNew = QPixmap(10,10) + pxNew.fill(QColor(120,0,0)) + lblNew = QLabel() + lblNew.setPixmap(pxNew) + lblNew.setAlignment(Qt.AlignCenter) + self.mainTable.setCellWidget(n, nCol, lblNew) nCol += 1 return From bcd3cf5bcdf1d0ccc79bf61672215839c5cb7cae Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sat, 1 Jun 2019 17:19:03 +0200 Subject: [PATCH 16/23] Added buttons to rebuild index and timeline view --- nw/config.py | 25 ++++++++++++++++++------- nw/gui/timelineview.py | 27 +++++++++++++++++++++------ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/nw/config.py b/nw/config.py index dec3f113..4815bd85 100644 --- a/nw/config.py +++ b/nw/config.py @@ -22,9 +22,6 @@ logger = logging.getLogger(__name__) class Config: - WIN_WIDTH = 0 - WIN_HEIGHT = 1 - CNF_STR = 0 CNF_INT = 1 CNF_BOOL = 2 @@ -58,6 +55,9 @@ class Config: self.mainPanePos = [300, 800] self.docPanePos = [400, 400] + ## Dialogs + self.dlgTimeLine = [600, 400] + ## Project self.autoSaveProj = 60 self.autoSaveDoc = 30 @@ -143,6 +143,7 @@ class Config: self.treeColWidth = self._parseLine(cnfParse, cnfSec, "treecols", self.CNF_LIST, self.treeColWidth) self.mainPanePos = self._parseLine(cnfParse, cnfSec, "mainpane", self.CNF_LIST, self.mainPanePos) self.docPanePos = self._parseLine(cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos) + self.dlgTimeLine = self._parseLine(cnfParse, cnfSec, "timeline", self.CNF_LIST, self.dlgTimeLine) ## Project cnfSec = "Project" @@ -192,6 +193,7 @@ class Config: cnfParse.set(cnfSec,"treecols", self._packList(self.treeColWidth)) cnfParse.set(cnfSec,"mainpane", self._packList(self.mainPanePos)) cnfParse.set(cnfSec,"docpane", self._packList(self.docPanePos)) + cnfParse.set(cnfSec,"timeline", self._packList(self.dlgTimeLine)) ## Project cnfSec = "Project" @@ -254,11 +256,20 @@ class Config: return True def setWinSize(self, newWidth, newHeight): - if abs(self.winGeometry[self.WIN_WIDTH] - newWidth) >= 10: - self.winGeometry[self.WIN_WIDTH] = newWidth + if abs(self.winGeometry[0] - newHeight) > 5: + self.winGeometry[0] = newWidth self.confChanged = True - if abs(self.winGeometry[self.WIN_HEIGHT] - newHeight) >= 10: - self.winGeometry[self.WIN_HEIGHT] = newHeight + if abs(self.winGeometry[1] - newHeight) > 5: + self.winGeometry[1] = newHeight + self.confChanged = True + return True + + def setTLineSize(self, newWidth, newHeight): + if abs(self.dlgTimeLine[0] - newHeight) > 5: + self.dlgTimeLine[0] = newWidth + self.confChanged = True + if abs(self.dlgTimeLine[1] - newHeight) > 5: + self.dlgTimeLine[1] = newHeight self.confChanged = True return True diff --git a/nw/gui/timelineview.py b/nw/gui/timelineview.py index 3ba9e8c8..d384478f 100644 --- a/nw/gui/timelineview.py +++ b/nw/gui/timelineview.py @@ -15,7 +15,10 @@ import nw from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QColor, QBrush, QPixmap -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem, QDialogButtonBox, QLabel +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, + QDialogButtonBox, QLabel, QPushButton +) logger = logging.getLogger(__name__) @@ -35,23 +38,33 @@ class GuiTimeLineView(QDialog): self.numCols = 0 self.outerBox = QVBoxLayout() + self.bottomBox = QHBoxLayout() self.setWindowTitle("Timeline View") + self.setMinimumSize(*self.mainConf.dlgTimeLine) self.mainTable = QTableWidget() self.mainTable.setGridStyle(Qt.NoPen) - self.setLayout(self.outerBox) - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) + self.btnRebuild = QPushButton("Rebuild Index") + self.btnRebuild.clicked.connect(self.theParent.rebuildIndex) + + self.btnRefresh = QPushButton("Refresh Table") + self.btnRefresh.clicked.connect(self._buildNovelList) + + self.setLayout(self.outerBox) self.outerBox.addWidget(self.mainTable) - self.outerBox.addWidget(self.buttonBox) + self.outerBox.addLayout(self.bottomBox) + self.bottomBox.addWidget(self.btnRebuild) + self.bottomBox.addWidget(self.btnRefresh) + self.bottomBox.addStretch() + self.bottomBox.addWidget(self.buttonBox) self._buildNovelList() - - self.setMinimumSize(600,400) + self.buttonBox.setFocus() self.show() @@ -65,6 +78,7 @@ class GuiTimeLineView(QDialog): self.numRows = len(self.theIndex.novelList) self.numCols = len(self.theIndex.tagIndex.keys()) + self.mainTable.clear() self.mainTable.setRowCount(self.numRows) self.mainTable.setColumnCount(self.numCols) @@ -101,6 +115,7 @@ class GuiTimeLineView(QDialog): return def _doClose(self): + self.mainConf.setTLineSize(self.width(), self.height()) self.close() return From 53e9234cfd0a93a6c761934ace5418abd60803a8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sat, 1 Jun 2019 17:34:55 +0200 Subject: [PATCH 17/23] Set autoresize of timeline table --- nw/gui/timelineview.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/nw/gui/timelineview.py b/nw/gui/timelineview.py index d384478f..c70ccc68 100644 --- a/nw/gui/timelineview.py +++ b/nw/gui/timelineview.py @@ -17,7 +17,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QColor, QBrush, QPixmap from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, - QDialogButtonBox, QLabel, QPushButton + QDialogButtonBox, QLabel, QPushButton, QHeaderView ) logger = logging.getLogger(__name__) @@ -46,6 +46,14 @@ class GuiTimeLineView(QDialog): self.mainTable = QTableWidget() self.mainTable.setGridStyle(Qt.NoPen) + self.hHeader = self.mainTable.horizontalHeader() + self.hHeader.setSectionResizeMode(QHeaderView.ResizeToContents) + self.mainTable.setHorizontalHeader(self.hHeader) + + self.vHeader = self.mainTable.verticalHeader() + self.vHeader.setSectionResizeMode(QHeaderView.ResizeToContents) + self.mainTable.setVerticalHeader(self.vHeader) + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) @@ -85,16 +93,14 @@ class GuiTimeLineView(QDialog): for n in range(len(self.theIndex.novelList)): iDepth = self.theIndex.novelList[n][1] iTitle = self.theIndex.novelList[n][2] - newItem = QTableWidgetItem(" "*iDepth + iTitle) + newItem = QTableWidgetItem("%s%s " % (" "*iDepth,iTitle)) self.mainTable.setVerticalHeaderItem(n, newItem) - self.mainTable.setRowHeight(n, 16) theMap = self.theIndex.buildTagNovelMap(self.theIndex.tagIndex.keys()) nCol = 0 for theTag, theCols in theMap.items(): - newItem = QTableWidgetItem(theTag) + newItem = QTableWidgetItem(" %s " % theTag) self.mainTable.setHorizontalHeaderItem(nCol, newItem) - self.mainTable.setColumnWidth(nCol,50) for n in range(len(theCols)): if theCols[n] == 1: pxNew = QPixmap(10,10) From 8cf85db94a222d44853bea4ddaa482d7ae824589 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sat, 1 Jun 2019 19:14:25 +0200 Subject: [PATCH 18/23] Made timeline view labels transparent, and changed red to blue --- nw/gui/timelineview.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nw/gui/timelineview.py b/nw/gui/timelineview.py index c70ccc68..05a041a3 100644 --- a/nw/gui/timelineview.py +++ b/nw/gui/timelineview.py @@ -14,7 +14,7 @@ import logging import nw from PyQt5.QtCore import Qt -from PyQt5.QtGui import QIcon, QColor, QBrush, QPixmap +from PyQt5.QtGui import QIcon, QColor, QPixmap from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QDialogButtonBox, QLabel, QPushButton, QHeaderView @@ -108,13 +108,15 @@ class GuiTimeLineView(QDialog): lblNew = QLabel() lblNew.setPixmap(pxNew) lblNew.setAlignment(Qt.AlignCenter) + lblNew.setAttribute(Qt.WA_TranslucentBackground) self.mainTable.setCellWidget(n, nCol, lblNew) elif theCols[n] == 2: pxNew = QPixmap(10,10) - pxNew.fill(QColor(120,0,0)) + pxNew.fill(QColor(0,0,120)) lblNew = QLabel() lblNew.setPixmap(pxNew) lblNew.setAlignment(Qt.AlignCenter) + lblNew.setAttribute(Qt.WA_TranslucentBackground) self.mainTable.setCellWidget(n, nCol, lblNew) nCol += 1 From 504e7fb70f80dcab60cf0adf47b79d1391075966 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sat, 1 Jun 2019 20:07:29 +0200 Subject: [PATCH 19/23] Fixed a small bug, and updated config test --- nw/config.py | 4 ++-- tests/reference/novelwriter.conf | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nw/config.py b/nw/config.py index 4815bd85..cdf07697 100644 --- a/nw/config.py +++ b/nw/config.py @@ -256,7 +256,7 @@ class Config: return True def setWinSize(self, newWidth, newHeight): - if abs(self.winGeometry[0] - newHeight) > 5: + if abs(self.winGeometry[0] - newWidth) > 5: self.winGeometry[0] = newWidth self.confChanged = True if abs(self.winGeometry[1] - newHeight) > 5: @@ -265,7 +265,7 @@ class Config: return True def setTLineSize(self, newWidth, newHeight): - if abs(self.dlgTimeLine[0] - newHeight) > 5: + if abs(self.dlgTimeLine[0] - newWidth) > 5: self.dlgTimeLine[0] = newWidth self.confChanged = True if abs(self.dlgTimeLine[1] - newHeight) > 5: diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index cb999b5a..a7df1756 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -7,6 +7,7 @@ geometry = 1100, 650 treecols = 120, 30, 50 mainpane = 300, 800 docpane = 400, 400 +timeline = 600, 400 [Project] autosaveproject = 60 From 48dc34c54d258543c8cedda69ac8092c90f5b8d0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sat, 1 Jun 2019 21:03:53 +0200 Subject: [PATCH 20/23] Updated tests and added a minimal test for timelineview. --- nw/gui/winmain.py | 7 +- tests/reference/gui/1_1489056e0916_main.nwd | 17 ++- tests/reference/gui/1_2d20bbd7e394_main.nwd | 5 + tests/reference/gui/1_688b6ef52555_main.nwd | 5 + tests/reference/gui/1_fca346db6561_main.nwd | 5 + tests/reference/gui/1_nwProject.nwx | 48 ++++++- tests/test_gui.py | 133 ++++++++++++++++++-- tests/test_project.py | 22 +++- 8 files changed, 217 insertions(+), 25 deletions(-) create mode 100644 tests/reference/gui/1_2d20bbd7e394_main.nwd create mode 100644 tests/reference/gui/1_688b6ef52555_main.nwd create mode 100644 tests/reference/gui/1_fca346db6561_main.nwd diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index 31744acc..a7ad1f2c 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -366,9 +366,10 @@ class GuiMain(QMainWindow): return logger.verbose("Requesting change to item %s" % tHandle) - dlgProj = GuiItemEditor(self, self.theProject, tHandle) - if dlgProj.exec_(): - self.treeView.setTreeItemValues(tHandle) + if self.mainConf.showGUI: + dlgProj = GuiItemEditor(self, self.theProject, tHandle) + if dlgProj.exec_(): + self.treeView.setTreeItemValues(tHandle) return diff --git a/tests/reference/gui/1_1489056e0916_main.nwd b/tests/reference/gui/1_1489056e0916_main.nwd index 2a3624ae..c5a065c0 100644 --- a/tests/reference/gui/1_1489056e0916_main.nwd +++ b/tests/reference/gui/1_1489056e0916_main.nwd @@ -1,13 +1,20 @@ -# Hello World! +# Novel -## With a Subtitle +## Chapter -### An Even Subier Title +@pov: Jane +@plot: MainPlot -#### Basically Not a Title at All +### Scene % How about a comment? -@keyword: value +@pov: Jane +@plot: MainPlot +@location: Home + +#### Some Section + +@char: Jane This is a paragraph of dummy text. diff --git a/tests/reference/gui/1_2d20bbd7e394_main.nwd b/tests/reference/gui/1_2d20bbd7e394_main.nwd new file mode 100644 index 00000000..f646f17e --- /dev/null +++ b/tests/reference/gui/1_2d20bbd7e394_main.nwd @@ -0,0 +1,5 @@ +# Main Plot + +@tag: MainPlot + +This is a file detailing the main plot. diff --git a/tests/reference/gui/1_688b6ef52555_main.nwd b/tests/reference/gui/1_688b6ef52555_main.nwd new file mode 100644 index 00000000..d4faeeb2 --- /dev/null +++ b/tests/reference/gui/1_688b6ef52555_main.nwd @@ -0,0 +1,5 @@ +# Main Location + +@tag: Home + +This is a file describing Jane’s home. diff --git a/tests/reference/gui/1_fca346db6561_main.nwd b/tests/reference/gui/1_fca346db6561_main.nwd new file mode 100644 index 00000000..4ba77a38 --- /dev/null +++ b/tests/reference/gui/1_fca346db6561_main.nwd @@ -0,0 +1,5 @@ +# Jane Doe + +@tag: Jane + +This is a file about Jane. diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index ae897362..fa8698ba 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -8,7 +8,7 @@ True 31489056e0916 31489056e0916 - 69 + 86 New Note @@ -22,7 +22,7 @@ Main - + Novel ROOT @@ -44,31 +44,67 @@ New False SCENE - 377 - 69 + 331 + 59 2 - 443 + 465 Characters ROOT CHARACTER New + True + + + New File + FILE + CHARACTER + New False + NOTE + 34 + 8 + 1 + 51 Plot ROOT PLOT New + True + + + New File + FILE + PLOT + New False + NOTE + 48 + 10 + 1 + 69 World ROOT WORLD New + True + + + New File + FILE + WORLD + New False + NOTE + 51 + 9 + 1 + 68 diff --git a/tests/test_gui.py b/tests/test_gui.py index 7f141b47..c4857ab8 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -9,6 +9,7 @@ from os import path, unlink from PyQt5.QtCore import Qt from nw.gui.projecteditor import GuiProjectEditor +from nw.gui.timelineview import GuiTimeLineView from nw.gui.itemeditor import GuiItemEditor from nw.enum import * @@ -47,8 +48,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): assert cmpFiles(projFile, path.join(nwRef,"gui","0_nwProject.nwx"), [2]) qtbot.wait(stepDelay) - # qtbot.stopForInteraction() - # Re-open project assert nwGUI.openProject(nwTempGUI) qtbot.wait(stepDelay) @@ -75,33 +74,101 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): assert nwGUI.treeView._getTreeItem("71ee45a3c0db9") is not None assert nwGUI.treeView._getTreeItem("811786ad1ae74") is not None + nwGUI.mainMenu.toolsSpellCheck.setChecked(True) + assert nwGUI.mainMenu._toggleSpellCheck() + + # Add a Character File + nwGUI.setFocus(1) + nwGUI.treeView.clearSelection() + nwGUI.treeView._getTreeItem("44cb730c42048").setSelected(True) + nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + assert nwGUI.openSelectedItem() + + # Type something into the document + nwGUI.setFocus(2) + for c in "# Jane Doe": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + for c in "@tag: Jane": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + for c in "This is a file about Jane.": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + + # Add a Plot File + nwGUI.setFocus(1) + nwGUI.treeView.clearSelection() + nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True) + nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + assert nwGUI.openSelectedItem() + + # Type something into the document + nwGUI.setFocus(2) + for c in "# Main Plot": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + for c in "@tag: MainPlot": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + for c in "This is a file detailing the main plot.": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + + # Add a World File + nwGUI.setFocus(1) + nwGUI.treeView.clearSelection() + nwGUI.treeView._getTreeItem("811786ad1ae74").setSelected(True) + nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + assert nwGUI.openSelectedItem() + + # Type something into the document + nwGUI.setFocus(2) + for c in "# Main Location": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + for c in "@tag: Home": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + for c in "This is a file describing Jane's home.": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + # Select the 'New Scene' file nwGUI.setFocus(1) + nwGUI.treeView.clearSelection() nwGUI.treeView._getTreeItem("73475cb40a568").setExpanded(True) nwGUI.treeView._getTreeItem("25fc0e7096fc6").setExpanded(True) nwGUI.treeView._getTreeItem("31489056e0916").setSelected(True) assert nwGUI.openSelectedItem() - nwGUI.mainMenu.toolsSpellCheck.setChecked(True) - assert nwGUI.mainMenu._toggleSpellCheck() # Type something into the document nwGUI.setFocus(2) - for c in "# Hello World!": + for c in "# Novel": qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) - for c in "## With a Subtitle": + for c in "## Chapter": qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) - for c in "### An Even Subier Title": + for c in "@pov: Jane": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + for c in "@plot: MainPlot": qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) - for c in "#### Basically Not a Title at All": + for c in "### Scene": qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) @@ -109,7 +176,23 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): for c in "% How about a comment?": qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) - for c in "@keyword: value": + for c in "@pov: Jane": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + for c in "@plot: MainPlot": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + for c in "@location: Home": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + + for c in "#### Some Section": + qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) + + for c in "@char: Jane": qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) @@ -133,13 +216,14 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): qtbot.wait(stepDelay) nwGUI.docEditor.wCounter.run() qtbot.wait(stepDelay) - nwGUI.docEditor._updateCounts() # Save the document assert nwGUI.docEditor.docChanged assert nwGUI.saveDocument() assert not nwGUI.docEditor.docChanged qtbot.wait(stepDelay) + nwGUI.rebuildIndex() + qtbot.wait(stepDelay) # Open and view the edited document nwGUI.setFocus(3) @@ -148,16 +232,45 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): qtbot.wait(stepDelay) assert nwGUI.saveProject() assert nwGUI.closeDocViewer() + qtbot.wait(stepDelay) # Check the files projFile = path.join(nwTempGUI,"nwProject.nwx") assert cmpFiles(projFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2]) + sceneFile = path.join(nwTempGUI,"data_0","2d20bbd7e394_main.nwd") + assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_2d20bbd7e394_main.nwd")) + sceneFile = path.join(nwTempGUI,"data_2","fca346db6561_main.nwd") + assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_fca346db6561_main.nwd")) sceneFile = path.join(nwTempGUI,"data_3","1489056e0916_main.nwd") assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd")) + sceneFile = path.join(nwTempGUI,"data_7","688b6ef52555_main.nwd") + assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_688b6ef52555_main.nwd")) nwGUI.closeMain() # qtbot.stopForInteraction() +@pytest.mark.gui +def testTimeLineView(qtbot, nwTempGUI, nwRef): + nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + # Create new, save, open project + nwGUI.theProject.handleSeed = 42 + assert nwGUI.openProject(nwTempGUI) + qtbot.wait(stepDelay) + + timeLine = GuiTimeLineView(nwGUI, nwGUI.theProject, nwGUI.theIndex) + qtbot.addWidget(timeLine) + + assert timeLine.numRows == 4 + assert timeLine.numCols == 3 + + # qtbot.stopForInteraction() + nwGUI.closeMain() + @pytest.mark.gui def testProjectEditor(qtbot, nwTempGUI, nwRef): nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI]) diff --git a/tests/test_project.py b/tests/test_project.py index 9ef638c8..baeaf1a6 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -96,4 +96,24 @@ def testIndexScanThis(nwTempProj): assert str(theBits) == "['@tag', 'this', 'and this']" assert str(thePos) == "[0, 6, 12]" - # assert False +@pytest.mark.project +def testBuildIndex(nwTempProj): + projFile = path.join(nwTempProj,"nwProject.nwx") + assert theProject.openProject(projFile) + + theIndex = NWIndex(theProject,theMain) + tHandle = "31489056e0916" + + theIndex.scanText(tHandle, ( + "# Novel\n\n" + "## Chapter\n\n" + "### Scene\n\n" + "#### Section\n\n" + "@pov: John\n" + "@char: Jane\n" + "@location: Somewhere\n" + )) + + assert theIndex.buildNovelList() + assert str(theIndex.novelList) == "[[1, 1, 'Novel', 'SCENE'], [3, 2, 'Chapter', 'SCENE'], [5, 3, 'Scene', 'SCENE'], [7, 4, 'Section', 'SCENE']]" + assert str(theIndex.novelOrder) == "['31489056e0916:1', '31489056e0916:3', '31489056e0916:5', '31489056e0916:7']" From 9642b8e91f5467575ecf03d29ae3fb34e6ea7d17 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sat, 1 Jun 2019 21:07:08 +0200 Subject: [PATCH 21/23] Addded a reference file for the json index too. --- tests/reference/gui/1_tagsIndex.json | 1 + tests/test_gui.py | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 tests/reference/gui/1_tagsIndex.json diff --git a/tests/reference/gui/1_tagsIndex.json b/tests/reference/gui/1_tagsIndex.json new file mode 100644 index 00000000..57b2ed92 --- /dev/null +++ b/tests/reference/gui/1_tagsIndex.json @@ -0,0 +1 @@ +{"tagIndex": {"Jane": [3, "2fca346db6561", "CHARACTER"], "MainPlot": [3, "02d20bbd7e394", "PLOT"], "Home": [3, "7688b6ef52555", "WORLD"]}, "refIndex": {"31489056e0916": [[5, "@pov", "Jane", 3], [6, "@plot", "MainPlot", 3], [11, "@pov", "Jane", 8], [12, "@plot", "MainPlot", 8], [13, "@location", "Home", 8], [17, "@char", "Jane", 15]]}, "novelIndex": {"31489056e0916": [[1, 1, "Novel", "SCENE"], [3, 2, "Chapter", "SCENE"], [8, 3, "Scene", "SCENE"], [15, 4, "Some Section", "SCENE"]]}} \ No newline at end of file diff --git a/tests/test_gui.py b/tests/test_gui.py index c4857ab8..02b6bef1 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -235,16 +235,18 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): qtbot.wait(stepDelay) # Check the files - projFile = path.join(nwTempGUI,"nwProject.nwx") - assert cmpFiles(projFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2]) - sceneFile = path.join(nwTempGUI,"data_0","2d20bbd7e394_main.nwd") - assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_2d20bbd7e394_main.nwd")) - sceneFile = path.join(nwTempGUI,"data_2","fca346db6561_main.nwd") - assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_fca346db6561_main.nwd")) - sceneFile = path.join(nwTempGUI,"data_3","1489056e0916_main.nwd") - assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd")) - sceneFile = path.join(nwTempGUI,"data_7","688b6ef52555_main.nwd") - assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_688b6ef52555_main.nwd")) + refFile = path.join(nwTempGUI,"nwProject.nwx") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2]) + refFile = path.join(nwTempGUI,"data_0","2d20bbd7e394_main.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_2d20bbd7e394_main.nwd")) + refFile = path.join(nwTempGUI,"data_2","fca346db6561_main.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_fca346db6561_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_7","688b6ef52555_main.nwd") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_688b6ef52555_main.nwd")) + refFile = path.join(nwTempGUI,"meta","tagsIndex.json") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_tagsIndex.json")) nwGUI.closeMain() # qtbot.stopForInteraction() From fa52baa1099c542806253d5a0d34dc5ada8ba1f0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sat, 1 Jun 2019 21:13:24 +0200 Subject: [PATCH 22/23] Remove the delay that helps to show the index build progress bar. --- nw/gui/winmain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index a7ad1f2c..a4cbec70 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -421,7 +421,6 @@ class GuiMain(QMainWindow): # Build tag index self.theIndex.scanText(tHandle, theText) - time.sleep(0.05) nDone += 1 if dlgProg.wasCanceled(): break From 3e7f3609af701c3f66ac9ba9de1c666c2650ce57 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sat, 1 Jun 2019 21:19:33 +0200 Subject: [PATCH 23/23] Only compare tagsindex if python version >= 3.6 --- tests/test_gui.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_gui.py b/tests/test_gui.py index 02b6bef1..3cecee17 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -2,7 +2,7 @@ """novelWriter Main GUI Class Tester """ -import nw, pytest +import nw, pytest, sys from nwtools import * from os import path, unlink @@ -245,8 +245,9 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): assert cmpFiles(refFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd")) refFile = path.join(nwTempGUI,"data_7","688b6ef52555_main.nwd") assert cmpFiles(refFile, path.join(nwRef,"gui","1_688b6ef52555_main.nwd")) - refFile = path.join(nwTempGUI,"meta","tagsIndex.json") - assert cmpFiles(refFile, path.join(nwRef,"gui","1_tagsIndex.json")) + if sys.version_info[0] >= 3 and sys.version_info[1] >= 6: + refFile = path.join(nwTempGUI,"meta","tagsIndex.json") + assert cmpFiles(refFile, path.join(nwRef,"gui","1_tagsIndex.json")) nwGUI.closeMain() # qtbot.stopForInteraction()