From 231b0b8c53be147480fbcd06f421d339f4fab12a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 10 Nov 2019 17:09:35 +0100 Subject: [PATCH 01/28] Added synopsis keyword and a meta file and dictiopnaries to hold the data --- nw/constants/constants.py | 1 + nw/gui/tools/dochighlight.py | 7 ++ nw/guimain.py | 10 +-- nw/project/index.py | 86 ++++++++++++++++--- .../sampleNovel/data_6/a2d6d5f4f401_main.nwd | 2 +- sample/sampleNovel/nwProject.nwx | 16 ++-- 6 files changed, 96 insertions(+), 26 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index a0325482..d43167a3 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -26,6 +26,7 @@ class nwFiles(): PROJ_DICT = "wordlist.txt" SESS_INFO = "sessionInfo.log" INDEX_FILE = "tagsIndex.json" + META_FILE = "projectMeta.json" EXPORT_OPT = "exportOptions.json" TLINE_OPT = "timelineOptions.json" SLOG_OPT = "sessionLogOptions.json" diff --git a/nw/gui/tools/dochighlight.py b/nw/gui/tools/dochighlight.py index e8ac2b04..f3b0a11b 100644 --- a/nw/gui/tools/dochighlight.py +++ b/nw/gui/tools/dochighlight.py @@ -140,6 +140,13 @@ class GuiDocHighlighter(QSyntaxHighlighter): 0 : self.hStyles["hidden"], } )) + self.hRules.append(( + r"^(%)(synopsis:\s+)(.*)$", { + 1 : self.hStyles["hidden"], + 2 : self.hStyles["keyword"], + 3 : self.hStyles["hidden"], + } + )) # Trailing Spaces, 2+ self.hRules.append(( diff --git a/nw/guimain.py b/nw/guimain.py index 5d6557c5..4053d0c0 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -523,17 +523,17 @@ class GuiMain(QMainWindow): theDoc = NWDoc(self.theProject, self) theText = theDoc.openDocument(tHandle, False) - # Run Word Count - cC, wC, pC = countWords(theText) + # Build tag index + self.theIndex.scanText(tHandle, theText) + + # Get Word Counts + cC, wC, pC = self.theIndex.getCounts(tHandle) tItem.setCharCount(cC) tItem.setWordCount(wC) tItem.setParaCount(pC) self.treeView.propagateCount(tHandle, wC) self.treeView.projectWordCount() - # Build tag index - self.theIndex.scanText(tHandle, theText) - nDone += 1 if dlgProg.wasCanceled(): break diff --git a/nw/project/index.py b/nw/project/index.py index dc5edb75..382abbae 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -19,6 +19,7 @@ from os import path from nw.constants import ( nwFiles, nwKeyWords, nwItemType, nwItemClass, nwAlert ) +from nw.tools import countWords logger = logging.getLogger(__name__) @@ -60,6 +61,10 @@ class NWIndex(): self.novelIndex = {} self.noteIndex = {} + # Meta Data + self.textCounts = {} + self.fileSynopsis = {} + # Lists self.novelList = [] @@ -70,10 +75,12 @@ class NWIndex(): ## def clearIndex(self): - self.tagIndex = {} - self.refIndex = {} - self.novelIndex = {} - self.noteIndex = {} + self.tagIndex = {} + self.refIndex = {} + self.novelIndex = {} + self.noteIndex = {} + self.textCounts = {} + self.fileSynopsis = {} return def deleteHandle(self, tHandle): @@ -101,7 +108,10 @@ class NWIndex(): """ theData = {} + loadsOK = False indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) + metaFile = path.join(self.theProject.projMeta, nwFiles.META_FILE) + if path.isfile(indexFile): logger.debug("Loading index file") try: @@ -122,11 +132,29 @@ class NWIndex(): if "noteIndex" in theData.keys(): self.noteIndex = theData["noteIndex"] - self.checkIndex() + loadsOK = True - return True + if path.isfile(indexFile): + logger.debug("Loading meta file") + try: + with open(metaFile,mode="r",encoding="utf8") as inFile: + theJson = inFile.read() + theData = json.loads(theJson) + except Exception as e: + logger.error("Failed to load meta file") + logger.error(str(e)) + return False - return False + if "textCounts" in theData.keys(): + self.textCounts = theData["textCounts"] + if "fileSynopsis" in theData.keys(): + self.fileSynopsis = theData["fileSynopsis"] + + loadsOK &= True + + self.checkIndex() + + return loadsOK def saveIndex(self): """Save the current index as a json file in the project meta @@ -134,11 +162,14 @@ class NWIndex(): """ indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) - logger.debug("Saving index file") + metaFile = path.join(self.theProject.projMeta, nwFiles.META_FILE) + + logger.debug("Saving index and meta files") if self.mainConf.debugInfo: nIndent = 2 else: nIndent = None + try: with open(indexFile,mode="w+",encoding="utf8") as outFile: outFile.write(json.dumps({ @@ -152,6 +183,17 @@ class NWIndex(): logger.error(str(e)) return False + try: + with open(metaFile,mode="w+",encoding="utf8") as outFile: + outFile.write(json.dumps({ + "textCounts" : self.textCounts, + "fileSynopsis" : self.fileSynopsis, + }, indent=nIndent)) + except Exception as e: + logger.error("Failed to save meta file") + logger.error(str(e)) + return False + return True def checkIndex(self): @@ -180,6 +222,10 @@ class NWIndex(): if len(tEntry) != 4: self.indexBroken = True + for tHandle in self.textCounts: + if len(self.textCounts[tHandle]) != 3: + self.indexBroken = True + if self.indexBroken: self.clearIndex() self.theParent.makeAlert( @@ -230,17 +276,23 @@ class NWIndex(): nLine = 0 nTitle = 0 for aLine in theText.splitlines(): - aLine = aLine.strip() + aLine = aLine nLine += 1 - nChar = len(aLine) + nChar = len(aLine.strip()) if nChar == 0: continue - if aLine[0] == "#": + if aLine.startswith(r"#"): isTitle = self.indexTitle(tHandle, isNovel, aLine, nLine, itemLayout) if isTitle: nTitle = nLine - elif aLine[0] == "@": + elif aLine.startswith(r"@"): self.indexNoteRef(tHandle, aLine, nLine, nTitle) self.indexTag(tHandle, aLine, nLine, itemClass) + elif aLine.startswith(r"%synopsis:"): + self.fileSynopsis[tHandle] = aLine[10:].strip() + + # Run word counter + cC, wC, pC = countWords(theText) + self.textCounts[tHandle] = [cC, wC, pC] return True @@ -386,6 +438,16 @@ class NWIndex(): # Extract Data ## + def getCounts(self, tHandle): + cC = 0 + wC = 0 + pC = 0 + if tHandle in self.textCounts: + cC = self.textCounts[tHandle][0] + wC = self.textCounts[tHandle][1] + pC = self.textCounts[tHandle][2] + return cC, wC, pC + def buildNovelList(self): """Build a list of the content of the novel. """ diff --git a/sample/sampleNovel/data_6/a2d6d5f4f401_main.nwd b/sample/sampleNovel/data_6/a2d6d5f4f401_main.nwd index ee002568..cee10674 100644 --- a/sample/sampleNovel/data_6/a2d6d5f4f401_main.nwd +++ b/sample/sampleNovel/data_6/a2d6d5f4f401_main.nwd @@ -3,4 +3,4 @@ @pov: Jane @location: Earth -% We can add a chapter file, but keep the scene files separate. In the chapter file we can set the meta data that applies to the whole chapter if we wish. \ No newline at end of file +%synopsis: We can add a chapter file, but keep the scene files separate. In the chapter file we can set the meta data that applies to the whole chapter if we wish to. \ No newline at end of file diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 63b80110..d98b72a4 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -9,9 +9,9 @@ True - 96b68994dfa3d + 6a2d6d5f4f401 b3e74dbc1f584 - 855 + 849 B E @@ -70,7 +70,7 @@ 12 3 0 - 45 + 211 Making a Scene @@ -118,7 +118,7 @@ 1692 313 6 - 216 + 144 Chapter Two @@ -239,9 +239,9 @@ New False SCENE - 30 - 6 - 1 + 0 + 0 + 0 36 From 7ed60ab97bf54baaacddd1081044d969a468e7b9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Nov 2019 14:43:23 +0100 Subject: [PATCH 02/28] Added outline tab and started populating outline tree --- nw/gui/__init__.py | 2 + nw/gui/elements/__init__.py | 2 + nw/gui/elements/outline.py | 112 ++++++++++++++++++++++++++++++++++++ nw/guimain.py | 30 ++++++++-- 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 nw/gui/elements/outline.py diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index b8196cc2..4c215058 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -20,6 +20,7 @@ from nw.gui.elements.doceditor import GuiDocEditor from nw.gui.elements.doctree import GuiDocTree from nw.gui.elements.docviewer import GuiDocViewer from nw.gui.elements.noticebar import GuiNoticeBar +from nw.gui.elements.outline import GuiProjectOutline from nw.gui.elements.searchbar import GuiSearchBar from nw.gui.elements.viewdetails import GuiDocViewDetails @@ -43,6 +44,7 @@ __all__ = [ "GuiDocTree", "GuiDocViewer", "GuiNoticeBar", + "GuiProjectOutline", "GuiSearchBar", "GuiDocViewDetails", "GuiDocHighlighter", diff --git a/nw/gui/elements/__init__.py b/nw/gui/elements/__init__.py index 572473ee..1d885de0 100644 --- a/nw/gui/elements/__init__.py +++ b/nw/gui/elements/__init__.py @@ -5,6 +5,7 @@ from nw.gui.elements.doceditor import GuiDocEditor from nw.gui.elements.doctree import GuiDocTree from nw.gui.elements.docviewer import GuiDocViewer from nw.gui.elements.noticebar import GuiNoticeBar +from nw.gui.elements.outline import GuiProjectOutline from nw.gui.elements.searchbar import GuiSearchBar from nw.gui.elements.viewdetails import GuiDocViewDetails @@ -14,6 +15,7 @@ __all__ = [ "GuiDocTree", "GuiDocViewer", "GuiNoticeBar", + "GuiProjectOutline", "GuiSearchBar", "GuiDocViewDetails", ] diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py new file mode 100644 index 00000000..166d6fef --- /dev/null +++ b/nw/gui/elements/outline.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +"""novelWriter GUI Project Outline + + novelWriter – GUI Project Outline +=================================== + Class holding the project outline view + + File History: + Created: 2019-11-16 [0.4.1] + +""" + +import logging +import nw + +from os import path + +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem +) + +from nw.constants import nwItemLayout + +logger = logging.getLogger(__name__) + +class GuiProjectOutline(QWidget): + + def __init__(self, theParent, theProject): + QWidget.__init__(self, theParent) + + logger.debug("Initialising ProjectOutline ...") + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theProject = theProject + self.theIndex = self.theParent.theIndex + + self.showWords = True + self.showSynopsis = True + self.showFilePath = False + + self.outerBox = QVBoxLayout() + self.mainTree = QTreeWidget() + + self.outerBox.addWidget(self.mainTree) + self.outerBox.setContentsMargins(0,0,0,0) + self.setLayout(self.outerBox) + + logger.debug("ProjectOutline initialisation complete") + + return + + def populateTree(self): + + self.mainTree.clear() + self.mainTree.setHeaderLabels(["Title","Level","Document","Line"]) + + currTitle = None + currChapter = None + currScene = None + + for tHandle in self.theProject.treeOrder: + + if tHandle not in self.theIndex.novelIndex: + continue + + nwItem = self.theProject.getItem(tHandle) + if nwItem.itemLayout == nwItemLayout.NOTE: + continue + + for tEntry in self.theIndex.novelIndex[tHandle]: + newItem = QTreeWidgetItem([""]*4) + newItem.setText(0, tEntry[2]) + newItem.setText(1, str(tEntry[1])) + newItem.setText(2, nwItem.itemName) + newItem.setText(3, str(tEntry[0])) + + if tEntry[1] == 1: + currTitle = newItem + self.mainTree.addTopLevelItem(newItem) + elif tEntry[1] == 2: + if currTitle is None: + self.mainTree.addTopLevelItem(newItem) + else: + currTitle.addChild(newItem) + currChapter = newItem + elif tEntry[1] == 3: + if currChapter is None: + if currTitle is None: + self.mainTree.addTopLevelItem(newItem) + else: + currTitle.addChild(newItem) + else: + currChapter.addChild(newItem) + currScene = newItem + elif tEntry[1] == 4: + if currScene is None: + if currChapter is None: + if currTitle is None: + self.mainTree.addTopLevelItem(newItem) + else: + currTitle.addChild(newItem) + else: + currChapter.addChild(newItem) + else: + currScene.addChild(newItem) + + newItem.setExpanded(True) + + return + +# END Class GuiProjectOutline diff --git a/nw/guimain.py b/nw/guimain.py index 0156877e..74f41f91 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -20,14 +20,14 @@ from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QIcon, QPixmap, QColor from PyQt5.QtWidgets import ( qApp, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QShortcut, - QMessageBox, QProgressDialog, QDialog + QMessageBox, QProgressDialog, QDialog, QTabWidget ) from nw.gui import ( GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails, GuiConfigEditor, GuiProjectEditor, GuiExport, - GuiItemEditor, GuiTimeLineView, GuiSessionLogView + GuiItemEditor, GuiTimeLineView, GuiSessionLogView, GuiProjectOutline ) from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup from nw.tools import countWords @@ -75,6 +75,7 @@ class GuiMain(QMainWindow): self.searchBar = GuiSearchBar(self) self.treeMeta = GuiDocDetails(self, self.theProject) self.treeView = GuiDocTree(self, self.theProject) + self.projView = GuiProjectOutline(self, self.theProject) self.mainMenu = GuiMainMenu(self, self.theProject) # Minor Gui Elements @@ -109,19 +110,29 @@ class GuiMain(QMainWindow): self.splitView.addWidget(self.editPane) self.splitView.addWidget(self.viewPane) + self.tabWidget = QTabWidget() + self.tabWidget.setTabPosition(QTabWidget.East) + self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}") + self.tabWidget.addTab(self.splitView, "Editor") + self.tabWidget.addTab(self.projView, "Outline") + self.tabWidget.currentChanged.connect(self._mainTabChanged) + self.splitMain = QSplitter(Qt.Horizontal) self.splitMain.setContentsMargins(4,4,4,4) self.splitMain.addWidget(self.treePane) - self.splitMain.addWidget(self.splitView) + self.splitMain.addWidget(self.tabWidget) self.splitMain.setSizes(self.mainConf.mainPanePos) self.setCentralWidget(self.splitMain) self.idxTree = self.splitMain.indexOf(self.treePane) - self.idxMain = self.splitMain.indexOf(self.splitView) + self.idxMain = self.splitMain.indexOf(self.tabWidget) self.idxEditor = self.splitView.indexOf(self.editPane) self.idxViewer = self.splitView.indexOf(self.viewPane) + self.idxTabEdit = self.tabWidget.indexOf(self.splitView) + self.idxTabProj = self.tabWidget.indexOf(self.projView) + self.splitMain.setCollapsible(self.idxTree, False) self.splitMain.setCollapsible(self.idxMain, False) self.splitView.setCollapsible(self.idxEditor, False) @@ -876,4 +887,15 @@ class GuiMain(QMainWindow): self.toggleZenMode() return + def _mainTabChanged(self, tabIndex): + """Activated when the main window tab is changed. + """ + if tabIndex == self.idxTabEdit: + logger.verbose("Editor tab activated") + elif tabIndex == self.idxTabProj: + logger.verbose("Project outline tab activated") + if self.hasProject: + self.projView.populateTree() + return + # END Class GuiMain From 2b3e95ce733c9b4c055bf5ae239b9062020cfe1c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Nov 2019 14:46:20 +0100 Subject: [PATCH 03/28] Fixed merge conflict in sample project --- sample/sampleNovel/nwProject.nwx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index cadf7d26..2da17d79 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -9,9 +9,9 @@ True - 636b6aa9b697b + 88706ddc78b1b b3e74dbc1f584 - 875 + 869 B E @@ -33,7 +33,7 @@ Main - + Novel ROOT @@ -70,7 +70,7 @@ 12 3 0 - 211 + 212 Making a Scene @@ -118,11 +118,7 @@ 1692 313 6 -<<<<<<< HEAD - 144 -======= 530 ->>>>>>> master Chapter Two From d09fb50bfcdb74682aa6b1644a33675ddf0682f6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Nov 2019 16:29:38 +0100 Subject: [PATCH 04/28] Restructured the index a bit, and added more columns to the outline --- nw/gui/elements/outline.py | 61 ++++++++++++++++--- nw/project/index.py | 116 ++++++++++++++++++++++++++----------- 2 files changed, 137 insertions(+), 40 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 166d6fef..6fbe3d21 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -15,6 +15,7 @@ import nw from os import path +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem ) @@ -25,6 +26,31 @@ logger = logging.getLogger(__name__) class GuiProjectOutline(QWidget): + I_TITLE = 0 + I_LEVEL = 1 + I_LABEL = 2 + I_LINE = 3 + I_WORDS = 4 + I_CHARS = 5 + I_PARAS = 6 + I_SYNOP = 7 + + COL_ORDER = [ + I_TITLE, I_LEVEL, I_LABEL, I_LINE, + I_WORDS, I_CHARS, I_PARAS, I_SYNOP, + ] + + COL_LABELS = { + I_TITLE : "Title", + I_LEVEL : "Level", + I_LABEL : "Document", + I_LINE : "Line", + I_WORDS : "Words", + I_CHARS : "Chars", + I_PARAS : "Pars", + I_SYNOP : "Synopsis", + } + def __init__(self, theParent, theProject): QWidget.__init__(self, theParent) @@ -39,8 +65,9 @@ class GuiProjectOutline(QWidget): self.showSynopsis = True self.showFilePath = False - self.outerBox = QVBoxLayout() - self.mainTree = QTreeWidget() + self.outerBox = QVBoxLayout() + self.mainTree = QTreeWidget() + self.treeOrder = self.COL_ORDER self.outerBox.addWidget(self.mainTree) self.outerBox.setContentsMargins(0,0,0,0) @@ -52,8 +79,17 @@ class GuiProjectOutline(QWidget): def populateTree(self): + theLabels = [] + for n in self.treeOrder: + theLabels.append(self.COL_LABELS[n]) + self.mainTree.clear() - self.mainTree.setHeaderLabels(["Title","Level","Document","Line"]) + self.mainTree.setHeaderLabels(theLabels) + + treeHead = self.mainTree.headerItem() + treeHead.setTextAlignment(self.I_CHARS,Qt.AlignRight) + treeHead.setTextAlignment(self.I_WORDS,Qt.AlignRight) + treeHead.setTextAlignment(self.I_PARAS,Qt.AlignRight) currTitle = None currChapter = None @@ -69,11 +105,22 @@ class GuiProjectOutline(QWidget): continue for tEntry in self.theIndex.novelIndex[tHandle]: + + theLine = str(tEntry[0]) + newItem = QTreeWidgetItem([""]*4) - newItem.setText(0, tEntry[2]) - newItem.setText(1, str(tEntry[1])) - newItem.setText(2, nwItem.itemName) - newItem.setText(3, str(tEntry[0])) + newItem.setText(self.I_TITLE, tEntry[2]) + newItem.setText(self.I_LEVEL, str(tEntry[1])) + newItem.setText(self.I_LABEL, nwItem.itemName) + newItem.setText(self.I_LINE, theLine) + + cC, wC, pC = self.theIndex.getCounts(tHandle, theLine) + newItem.setText(self.I_CHARS, str(cC)) + newItem.setText(self.I_WORDS, str(wC)) + newItem.setText(self.I_PARAS, str(pC)) + newItem.setTextAlignment(self.I_CHARS,Qt.AlignRight) + newItem.setTextAlignment(self.I_WORDS,Qt.AlignRight) + newItem.setTextAlignment(self.I_PARAS,Qt.AlignRight) if tEntry[1] == 1: currTitle = newItem diff --git a/nw/project/index.py b/nw/project/index.py index 382abbae..e9bb2661 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -62,8 +62,9 @@ class NWIndex(): self.noteIndex = {} # Meta Data + self.fileCounts = {} self.textCounts = {} - self.fileSynopsis = {} + self.textSynopsis = {} # Lists self.novelList = [] @@ -79,8 +80,9 @@ class NWIndex(): self.refIndex = {} self.novelIndex = {} self.noteIndex = {} + self.fileCounts = {} self.textCounts = {} - self.fileSynopsis = {} + self.textSynopsis = {} return def deleteHandle(self, tHandle): @@ -145,10 +147,12 @@ class NWIndex(): logger.error(str(e)) return False + if "fileCounts" in theData.keys(): + self.fileCounts = theData["fileCounts"] if "textCounts" in theData.keys(): self.textCounts = theData["textCounts"] - if "fileSynopsis" in theData.keys(): - self.fileSynopsis = theData["fileSynopsis"] + if "textSynopsis" in theData.keys(): + self.textSynopsis = theData["textSynopsis"] loadsOK &= True @@ -186,8 +190,9 @@ class NWIndex(): try: with open(metaFile,mode="w+",encoding="utf8") as outFile: outFile.write(json.dumps({ + "fileCounts" : self.fileCounts, "textCounts" : self.textCounts, - "fileSynopsis" : self.fileSynopsis, + "textSynopsis" : self.textSynopsis, }, indent=nIndent)) except Exception as e: logger.error("Failed to save meta file") @@ -203,28 +208,37 @@ class NWIndex(): self.indexBroken = False - for tTag in self.tagIndex: - if len(self.tagIndex[tTag]) != 3: - self.indexBroken = True - - for tHandle in self.refIndex: - for tEntry in self.refIndex[tHandle]: - if len(tEntry) != 4: + try: + for tTag in self.tagIndex: + if len(self.tagIndex[tTag]) != 3: self.indexBroken = True - for tHandle in self.novelIndex: - for tEntry in self.novelIndex[tHandle]: - if len(tEntry) != 4: + for tHandle in self.refIndex: + for tEntry in self.refIndex[tHandle]: + if len(tEntry) != 4: + self.indexBroken = True + + for tHandle in self.novelIndex: + for tEntry in self.novelIndex[tHandle]: + if len(tEntry) != 4: + self.indexBroken = True + + for tHandle in self.noteIndex: + for tEntry in self.noteIndex[tHandle]: + if len(tEntry) != 4: + self.indexBroken = True + + for tHandle in self.fileCounts: + if len(self.fileCounts[tHandle]) != 3: self.indexBroken = True - for tHandle in self.noteIndex: - for tEntry in self.noteIndex[tHandle]: - if len(tEntry) != 4: - self.indexBroken = True + for tHandle in self.textCounts: + for tLine in self.textCounts[tHandle]: + if len(self.textCounts[tHandle][tLine]) != 3: + self.indexBroken = True - for tHandle in self.textCounts: - if len(self.textCounts[tHandle]) != 3: - self.indexBroken = True + except: + self.indexBroken = True if self.indexBroken: self.clearIndex() @@ -275,24 +289,47 @@ class NWIndex(): nLine = 0 nTitle = 0 - for aLine in theText.splitlines(): + theSynopsis = {} + theCounts = {} + theLines = theText.splitlines() + for aLine in theLines: aLine = aLine nLine += 1 nChar = len(aLine.strip()) if nChar == 0: continue + if aLine.startswith(r"#"): isTitle = self.indexTitle(tHandle, isNovel, aLine, nLine, itemLayout) - if isTitle: + if isTitle and nLine > 0: + # Count words in the previous section, before we tag + # the new title location + if nTitle > 0: + lastText = "\n".join(theLines[nTitle-1:nLine-1]) + cC, wC, pC = countWords(lastText) + theCounts[str(nTitle)] = [cC, wC, pC] nTitle = nLine + elif aLine.startswith(r"@"): self.indexNoteRef(tHandle, aLine, nLine, nTitle) self.indexTag(tHandle, aLine, nLine, itemClass) - elif aLine.startswith(r"%synopsis:"): - self.fileSynopsis[tHandle] = aLine[10:].strip() - # Run word counter + elif aLine.startswith(r"%synopsis:"): + theSynopsis[str(nTitle)] = aLine[10:].strip() + + # Count words for remaining text after last heading + if nTitle > 0: + lastText = "\n".join(theLines[nTitle-1:]) + cC, wC, pC = countWords(lastText) + theCounts[str(nTitle)] = [cC, wC, pC] + + if theSynopsis: + self.textSynopsis[tHandle] = theSynopsis + if theCounts: + self.textCounts[tHandle] = theCounts + + # Run word counter for whole text cC, wC, pC = countWords(theText) - self.textCounts[tHandle] = [cC, wC, pC] + self.fileCounts[tHandle] = [cC, wC, pC] return True @@ -438,14 +475,27 @@ class NWIndex(): # Extract Data ## - def getCounts(self, tHandle): + def getCounts(self, tHandle, tLine=None): + """Returns the counts for a file, or a section of a file + starting at line tLine. + """ + cC = 0 wC = 0 pC = 0 - if tHandle in self.textCounts: - cC = self.textCounts[tHandle][0] - wC = self.textCounts[tHandle][1] - pC = self.textCounts[tHandle][2] + + if tLine is None: + if tHandle in self.fileCounts: + cC = self.fileCounts[tHandle][0] + wC = self.fileCounts[tHandle][1] + pC = self.fileCounts[tHandle][2] + else: + if tHandle in self.textCounts: + if tLine in self.textCounts[tHandle]: + cC = self.textCounts[tHandle][tLine][0] + wC = self.textCounts[tHandle][tLine][1] + pC = self.textCounts[tHandle][tLine][2] + return cC, wC, pC def buildNovelList(self): From 19ebedd8b355c161e08e5f9cb7244edfc769035f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Nov 2019 17:23:32 +0100 Subject: [PATCH 05/28] Most columns now available in outline --- nw/gui/elements/outline.py | 172 +++++++++++++++++++++++++------------ nw/project/index.py | 29 +++++-- 2 files changed, 141 insertions(+), 60 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 6fbe3d21..75e4f396 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -20,35 +20,53 @@ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem ) -from nw.constants import nwItemLayout +from nw.constants import nwItemLayout, nwKeyWords, nwLabels logger = logging.getLogger(__name__) class GuiProjectOutline(QWidget): - I_TITLE = 0 - I_LEVEL = 1 - I_LABEL = 2 - I_LINE = 3 - I_WORDS = 4 - I_CHARS = 5 - I_PARAS = 6 - I_SYNOP = 7 + I_TITLE = 0 + I_LEVEL = 1 + I_LABEL = 2 + I_LINE = 3 + I_WCOUNT = 4 + I_CCOUNT = 5 + I_PCOUNT = 6 + I_SYNOP = 7 + I_POV = 8 + I_CHAR = 9 + I_PLOT = 10 + I_TIME = 11 + I_WORLD = 12 + I_OBJECT = 13 + I_ENTITY = 14 + I_CUSTOM = 15 COL_ORDER = [ - I_TITLE, I_LEVEL, I_LABEL, I_LINE, - I_WORDS, I_CHARS, I_PARAS, I_SYNOP, + I_TITLE, I_LEVEL, I_LABEL, I_LINE, + I_WCOUNT, I_CCOUNT, I_PCOUNT, I_SYNOP, + I_POV, I_CHAR, I_PLOT, I_TIME, + I_WORLD, I_OBJECT, I_ENTITY, I_CUSTOM, ] COL_LABELS = { - I_TITLE : "Title", - I_LEVEL : "Level", - I_LABEL : "Document", - I_LINE : "Line", - I_WORDS : "Words", - I_CHARS : "Chars", - I_PARAS : "Pars", - I_SYNOP : "Synopsis", + I_TITLE : "Title", + I_LEVEL : "Level", + I_LABEL : "Document", + I_LINE : "Line", + I_WCOUNT : "Words", + I_CCOUNT : "Chars", + I_PCOUNT : "Pars", + I_SYNOP : "Synopsis", + I_POV : nwLabels.KEY_NAME[nwKeyWords.POV_KEY], + I_CHAR : nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY], + I_PLOT : nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY], + I_TIME : nwLabels.KEY_NAME[nwKeyWords.TIME_KEY], + I_WORLD : nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY], + I_OBJECT : nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY], + I_ENTITY : nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY], + I_CUSTOM : nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY], } def __init__(self, theParent, theProject): @@ -82,14 +100,14 @@ class GuiProjectOutline(QWidget): theLabels = [] for n in self.treeOrder: theLabels.append(self.COL_LABELS[n]) - + self.mainTree.clear() self.mainTree.setHeaderLabels(theLabels) - + treeHead = self.mainTree.headerItem() - treeHead.setTextAlignment(self.I_CHARS,Qt.AlignRight) - treeHead.setTextAlignment(self.I_WORDS,Qt.AlignRight) - treeHead.setTextAlignment(self.I_PARAS,Qt.AlignRight) + treeHead.setTextAlignment(self.I_CCOUNT,Qt.AlignRight) + treeHead.setTextAlignment(self.I_WCOUNT,Qt.AlignRight) + treeHead.setTextAlignment(self.I_PCOUNT,Qt.AlignRight) currTitle = None currChapter = None @@ -106,54 +124,102 @@ class GuiProjectOutline(QWidget): for tEntry in self.theIndex.novelIndex[tHandle]: - theLine = str(tEntry[0]) - - newItem = QTreeWidgetItem([""]*4) - newItem.setText(self.I_TITLE, tEntry[2]) - newItem.setText(self.I_LEVEL, str(tEntry[1])) - newItem.setText(self.I_LABEL, nwItem.itemName) - newItem.setText(self.I_LINE, theLine) - - cC, wC, pC = self.theIndex.getCounts(tHandle, theLine) - newItem.setText(self.I_CHARS, str(cC)) - newItem.setText(self.I_WORDS, str(wC)) - newItem.setText(self.I_PARAS, str(pC)) - newItem.setTextAlignment(self.I_CHARS,Qt.AlignRight) - newItem.setTextAlignment(self.I_WORDS,Qt.AlignRight) - newItem.setTextAlignment(self.I_PARAS,Qt.AlignRight) + nTitle = str(tEntry[0]) + tTitle = tEntry[2] + tLevel = str(tEntry[1]) + tLabel = nwItem.itemName + tItem = self._createTreeItem(tHandle, nTitle, tTitle, tLevel, tLabel) if tEntry[1] == 1: - currTitle = newItem - self.mainTree.addTopLevelItem(newItem) + currTitle = tItem + self.mainTree.addTopLevelItem(tItem) elif tEntry[1] == 2: if currTitle is None: - self.mainTree.addTopLevelItem(newItem) + self.mainTree.addTopLevelItem(tItem) else: - currTitle.addChild(newItem) - currChapter = newItem + currTitle.addChild(tItem) + currChapter = tItem elif tEntry[1] == 3: if currChapter is None: if currTitle is None: - self.mainTree.addTopLevelItem(newItem) + self.mainTree.addTopLevelItem(tItem) else: - currTitle.addChild(newItem) + currTitle.addChild(tItem) else: - currChapter.addChild(newItem) - currScene = newItem + currChapter.addChild(tItem) + currScene = tItem elif tEntry[1] == 4: if currScene is None: if currChapter is None: if currTitle is None: - self.mainTree.addTopLevelItem(newItem) + self.mainTree.addTopLevelItem(tItem) else: - currTitle.addChild(newItem) + currTitle.addChild(tItem) else: - currChapter.addChild(newItem) + currChapter.addChild(tItem) else: - currScene.addChild(newItem) + currScene.addChild(tItem) - newItem.setExpanded(True) + tItem.setExpanded(True) return + ## + # Internal Functions + ## + + def _createTreeItem(self, tHandle, nTitle, tTitle, tLevel, tLabel): + + newItem = QTreeWidgetItem() + newItem.setText(self.I_TITLE, tTitle) + newItem.setText(self.I_LEVEL, tLevel) + newItem.setText(self.I_LABEL, tLabel) + newItem.setText(self.I_LINE, nTitle) + + cC, wC, pC = self.theIndex.getCounts(tHandle, nTitle) + newItem.setText(self.I_CCOUNT, str(cC)) + newItem.setText(self.I_WCOUNT, str(wC)) + newItem.setText(self.I_PCOUNT, str(pC)) + newItem.setTextAlignment(self.I_CCOUNT,Qt.AlignRight) + newItem.setTextAlignment(self.I_WCOUNT,Qt.AlignRight) + newItem.setTextAlignment(self.I_PCOUNT,Qt.AlignRight) + + povList = [] + charList = [] + plotList = [] + timeList = [] + worldList = [] + objectList = [] + entityList = [] + customList = [] + for tKey, tTag in self.theIndex.getReferences(tHandle, nTitle): + if tKey == nwKeyWords.POV_KEY: + povList.append(tTag) + elif tKey == nwKeyWords.CHAR_KEY: + charList.append(tTag) + elif tKey == nwKeyWords.PLOT_KEY: + plotList.append(tTag) + elif tKey == nwKeyWords.TIME_KEY: + timeList.append(tTag) + elif tKey == nwKeyWords.WORLD_KEY: + worldList.append(tTag) + elif tKey == nwKeyWords.OBJECT_KEY: + objectList.append(tTag) + elif tKey == nwKeyWords.ENTITY_KEY: + entityList.append(tTag) + elif tKey == nwKeyWords.CUSTOM_KEY: + customList.append(tTag) + + newItem.setText(self.I_POV, ", ".join(povList)) + newItem.setText(self.I_CHAR, ", ".join(charList)) + newItem.setText(self.I_PLOT, ", ".join(plotList)) + newItem.setText(self.I_TIME, ", ".join(timeList)) + newItem.setText(self.I_WORLD, ", ".join(worldList)) + newItem.setText(self.I_OBJECT, ", ".join(objectList)) + newItem.setText(self.I_ENTITY, ", ".join(entityList)) + newItem.setText(self.I_CUSTOM, ", ".join(customList)) + + return newItem + + # END Class GuiProjectOutline diff --git a/nw/project/index.py b/nw/project/index.py index e9bb2661..0378adc2 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -475,29 +475,44 @@ class NWIndex(): # Extract Data ## - def getCounts(self, tHandle, tLine=None): + def getCounts(self, tHandle, nTitle=None): """Returns the counts for a file, or a section of a file - starting at line tLine. + starting at title nTitle. """ cC = 0 wC = 0 pC = 0 - if tLine is None: + if nTitle is None: if tHandle in self.fileCounts: cC = self.fileCounts[tHandle][0] wC = self.fileCounts[tHandle][1] pC = self.fileCounts[tHandle][2] else: if tHandle in self.textCounts: - if tLine in self.textCounts[tHandle]: - cC = self.textCounts[tHandle][tLine][0] - wC = self.textCounts[tHandle][tLine][1] - pC = self.textCounts[tHandle][tLine][2] + if nTitle in self.textCounts[tHandle]: + cC = self.textCounts[tHandle][nTitle][0] + wC = self.textCounts[tHandle][nTitle][1] + pC = self.textCounts[tHandle][nTitle][2] return cC, wC, pC + def getReferences(self, tHandle, tTitle=None): + """Extract all references made in a file, and optionally title + section. tTitle must be a string. + """ + + theRefs = [] + if tHandle not in self.refIndex: + return theRefs + + for nLine, tKey, tTag, nTitle in self.refIndex[tHandle]: + if tTitle is None or tTitle == str(nTitle): + theRefs.append((tKey, tTag)) + + return theRefs + def buildNovelList(self): """Build a list of the content of the novel. """ From 816c708853a8f282ba18093d79da1e2b0f95f6a8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Nov 2019 13:09:21 +0100 Subject: [PATCH 06/28] Remove timeline view so we can clean up the indexing class --- nw/gui/__init__.py | 2 - nw/gui/dialogs/__init__.py | 2 - nw/gui/dialogs/timelineview.py | 273 --------------------------------- nw/gui/elements/viewdetails.py | 2 +- nw/gui/mainmenu.py | 10 -- nw/guimain.py | 9 +- nw/project/index.py | 88 +++-------- tests/test_project.py | 22 --- 8 files changed, 20 insertions(+), 388 deletions(-) delete mode 100644 nw/gui/dialogs/timelineview.py diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 4c215058..680c5010 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -12,7 +12,6 @@ from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor from nw.gui.dialogs.sessionlog import GuiSessionLogView -from nw.gui.dialogs.timelineview import GuiTimeLineView # GUI Elements from nw.gui.elements.docdetails import GuiDocDetails @@ -38,7 +37,6 @@ __all__ = [ "GuiItemEditor", "GuiProjectEditor", "GuiSessionLogView", - "GuiTimeLineView", "GuiDocDetails", "GuiDocEditor", "GuiDocTree", diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py index fac299ba..9374ff13 100644 --- a/nw/gui/dialogs/__init__.py +++ b/nw/gui/dialogs/__init__.py @@ -5,7 +5,6 @@ from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor from nw.gui.dialogs.sessionlog import GuiSessionLogView -from nw.gui.dialogs.timelineview import GuiTimeLineView __all__ = [ "GuiConfigEditor", @@ -13,5 +12,4 @@ __all__ = [ "GuiItemEditor", "GuiProjectEditor", "GuiSessionLogView", - "GuiTimeLineView", ] diff --git a/nw/gui/dialogs/timelineview.py b/nw/gui/dialogs/timelineview.py deleted file mode 100644 index dab545fc..00000000 --- a/nw/gui/dialogs/timelineview.py +++ /dev/null @@ -1,273 +0,0 @@ -# -*- 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 os import path - -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QColor, QPixmap -from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QLabel, - QDialogButtonBox, QPushButton, QHeaderView, QGridLayout, QGroupBox, - QCheckBox -) - -from nw.constants import nwFiles, nwItemClass -from nw.tools import OptLastState - -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.optState = TimeLineLastState(self.theProject,nwFiles.TLINE_OPT) - self.optState.loadSettings() - - self.theMatrix = {} - self.numRows = 0 - self.numCols = 0 - - self.outerBox = QVBoxLayout() - self.filterBox = QVBoxLayout() - self.centreBox = QHBoxLayout() - self.bottomBox = QHBoxLayout() - - self.setWindowTitle("Timeline View") - self.setMinimumWidth(700) - self.setMinimumHeight(400) - - winWidth = self.optState.validIntRange( - self.optState.getSetting("winWidth"), 700, 10000, 700 - ) - winHeight = self.optState.validIntRange( - self.optState.getSetting("winHeight"), 400, 10000, 400 - ) - self.resize(winWidth,winHeight) - - # TimeLine Table - 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) - - # Option Box - self.optFilter = QGroupBox("Include Tags", self) - self.optFilterGrid = QGridLayout(self) - self.optFilter.setLayout(self.optFilterGrid) - - self.filterPlot = QCheckBox("Plot tags", self) - self.filterPlot.setChecked(self.optState.getSetting("fPlot")) - self.filterPlot.stateChanged.connect(self._filterChange) - - self.filterChar = QCheckBox("Character tags", self) - self.filterChar.setChecked(self.optState.getSetting("fChar")) - self.filterChar.stateChanged.connect(self._filterChange) - - self.filterWorld = QCheckBox("Location tags", self) - self.filterWorld.setChecked(self.optState.getSetting("fWorld")) - self.filterWorld.stateChanged.connect(self._filterChange) - - self.filterTime = QCheckBox("Timeline tags", self) - self.filterTime.setChecked(self.optState.getSetting("fTime")) - self.filterTime.stateChanged.connect(self._filterChange) - - self.filterObject = QCheckBox("Object tags", self) - self.filterObject.setChecked(self.optState.getSetting("fObject")) - self.filterObject.stateChanged.connect(self._filterChange) - - self.filterCustom = QCheckBox("Custom tags", self) - self.filterCustom.setChecked(self.optState.getSetting("fCustom")) - self.filterCustom.stateChanged.connect(self._filterChange) - - self.optFilterGrid.addWidget(self.filterPlot, 0, 1) - self.optFilterGrid.addWidget(self.filterChar, 1, 1) - self.optFilterGrid.addWidget(self.filterWorld, 2, 1) - self.optFilterGrid.addWidget(self.filterTime, 3, 1) - self.optFilterGrid.addWidget(self.filterObject, 4, 1) - self.optFilterGrid.addWidget(self.filterCustom, 5, 1) - - self.optHide = QGroupBox("Filters", self) - self.optHideGrid = QGridLayout(self) - self.optHide.setLayout(self.optHideGrid) - - self.hideUnused = QCheckBox("Hide unused", self) - self.hideUnused.setChecked(self.optState.getSetting("hUnused")) - self.hideUnused.stateChanged.connect(self._filterChange) - - self.optHideGrid.addWidget(self.hideUnused, 0, 1) - - # Button Box - 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.bottomBox.addWidget(self.btnRebuild) - self.bottomBox.addWidget(self.btnRefresh) - self.bottomBox.addStretch() - self.bottomBox.addWidget(self.buttonBox) - - # Assemble - self.filterBox.addWidget(self.optFilter) - self.filterBox.addWidget(self.optHide) - self.filterBox.addStretch() - self.centreBox.addWidget(self.mainTable) - self.centreBox.addLayout(self.filterBox) - self.outerBox.addLayout(self.centreBox) - self.outerBox.addLayout(self.bottomBox) - self.setLayout(self.outerBox) - - self._buildNovelList() - self.buttonBox.setFocus() - - self.show() - - logger.debug("TimeLineView initialisation complete") - - return - - def _buildNovelList(self): - - self.mainTable.clear() - self.theIndex.buildNovelList() - - self.numRows = len(self.theIndex.novelList) - self.mainTable.setRowCount(self.numRows) - - theFilters = {} - theFilters["exClass"] = [] - theFilters["hUnused"] = self.hideUnused.isChecked() - - if not self.filterPlot.isChecked(): - theFilters["exClass"].append(nwItemClass.PLOT) - if not self.filterChar.isChecked(): - theFilters["exClass"].append(nwItemClass.CHARACTER) - if not self.filterWorld.isChecked(): - theFilters["exClass"].append(nwItemClass.WORLD) - if not self.filterTime.isChecked(): - theFilters["exClass"].append(nwItemClass.TIMELINE) - if not self.filterObject.isChecked(): - theFilters["exClass"].append(nwItemClass.OBJECT) - if not self.filterCustom.isChecked(): - theFilters["exClass"].append(nwItemClass.CUSTOM) - - for n in range(len(self.theIndex.novelList)): - iDepth = self.theIndex.novelList[n][1] - iTitle = self.theIndex.novelList[n][2] - newItem = QTableWidgetItem("%s%s " % (" "*iDepth,iTitle)) - self.mainTable.setVerticalHeaderItem(n, newItem) - - theMap = self.theIndex.buildTagNovelMap(self.theIndex.tagIndex.keys(), theFilters) - self.numCols = len(theMap.keys()) - self.mainTable.setColumnCount(self.numCols) - - nCol = 0 - for theTag, theCols in theMap.items(): - newItem = QTableWidgetItem(" %s " % theTag) - self.mainTable.setHorizontalHeaderItem(nCol, newItem) - 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) - lblNew.setAttribute(Qt.WA_TranslucentBackground) - self.mainTable.setCellWidget(n, nCol, lblNew) - elif theCols[n] == 2: - pxNew = QPixmap(10,10) - 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 - - return - - def _doClose(self): - - logger.verbose("GuiTimeLineView close button clicked") - - winWidth = self.width() - winHeight = self.height() - fPlot = self.filterPlot.isChecked() - fChar = self.filterChar.isChecked() - fWorld = self.filterWorld.isChecked() - fTime = self.filterTime.isChecked() - fObject = self.filterObject.isChecked() - fCustom = self.filterCustom.isChecked() - hUnused = self.hideUnused.isChecked() - - self.optState.setSetting("winWidth", winWidth) - self.optState.setSetting("winHeight",winHeight) - self.optState.setSetting("fPlot", fPlot) - self.optState.setSetting("fChar", fChar) - self.optState.setSetting("fWorld", fWorld) - self.optState.setSetting("fTime", fTime) - self.optState.setSetting("fObject", fObject) - self.optState.setSetting("fCustom", fCustom) - self.optState.setSetting("hUnused", hUnused) - - self.optState.saveSettings() - self.close() - - return - - def _filterChange(self, checkState): - self._buildNovelList() - return - -# END Class GuiTimeLineView - -class TimeLineLastState(OptLastState): - - def __init__(self, theProject, theFile): - OptLastState.__init__(self, theProject, theFile) - self.theState = { - "winWidth" : 700, - "winHeight" : 400, - "fPlot" : True, - "fChar" : True, - "fWorld" : True, - "fTime" : True, - "fObject" : True, - "fCustom" : True, - "hUnused" : True, - } - self.stringOpt = () - self.boolOpt = ("fPlot","fChar","fWorld","fTime","fObject","fCustom","hUnused") - self.intOpt = ("winWidth","winHeight") - return - -# END Class TimeLineLastState diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/elements/viewdetails.py index 2a0c9a29..b92edd01 100644 --- a/nw/gui/elements/viewdetails.py +++ b/nw/gui/elements/viewdetails.py @@ -87,7 +87,7 @@ class GuiDocViewDetails(QWidget): if self.isSticky.isChecked(): return - theRefs = self.theParent.theIndex.buildReferenceList(tHandle) + theRefs = self.theParent.theIndex.getBackReferenceList(tHandle) theList = [] for tHandle in theRefs: tItem = self.theProject.getItem(tHandle) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 179a6077..0c5ac3f4 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -416,16 +416,6 @@ class GuiMainMenu(QMenuBar): self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode) self.viewMenu.addAction(self.aFullScreen) - # View > Separator - self.viewMenu.addSeparator() - - # View > Project Timeline - self.aViewTimeLine = QAction("Show Project Timeline", self) - self.aViewTimeLine.setStatusTip("Open the project timeline window") - self.aViewTimeLine.setShortcut("Ctrl+T") - self.aViewTimeLine.triggered.connect(self.theParent.showTimeLineDialog) - self.viewMenu.addAction(self.aViewTimeLine) - return def _buildEditMenu(self): diff --git a/nw/guimain.py b/nw/guimain.py index 74f41f91..177983bf 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -27,7 +27,7 @@ from nw.gui import ( GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails, GuiConfigEditor, GuiProjectEditor, GuiExport, - GuiItemEditor, GuiTimeLineView, GuiSessionLogView, GuiProjectOutline + GuiItemEditor, GuiSessionLogView, GuiProjectOutline ) from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup from nw.tools import countWords @@ -619,12 +619,6 @@ class GuiMain(QMainWindow): dlgExport.exec_() return True - def showTimeLineDialog(self): - if self.hasProject: - dlgTLine = GuiTimeLineView(self, self.theProject, self.theIndex) - dlgTLine.exec_() - return True - def showSessionLogDialog(self): if self.hasProject: dlgTLine = GuiSessionLogView(self, self.theProject) @@ -778,7 +772,6 @@ class GuiMain(QMainWindow): self.addAction(self.mainMenu.aFileDetails) self.addAction(self.mainMenu.aZenMode) self.addAction(self.mainMenu.aFullScreen) - self.addAction(self.mainMenu.aViewTimeLine) self.addAction(self.mainMenu.aEditUndo) self.addAction(self.mainMenu.aEditRedo) self.addAction(self.mainMenu.aEditCut) diff --git a/nw/project/index.py b/nw/project/index.py index 0378adc2..f0e93fbc 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -17,7 +17,7 @@ import nw from os import path from nw.constants import ( - nwFiles, nwKeyWords, nwItemType, nwItemClass, nwAlert + nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert ) from nw.tools import countWords @@ -66,9 +66,6 @@ class NWIndex(): self.textCounts = {} self.textSynopsis = {} - # Lists - self.novelList = [] - return ## @@ -261,23 +258,28 @@ class NWIndex(): """ theItem = self.theProject.getItem(tHandle) - if theItem is None: return False - if theItem.itemType != nwItemType.FILE: return False - if theItem.parHandle == self.theProject.trashRoot: return False + if theItem is None: + return False + if theItem.itemType != nwItemType.FILE: + return False + if theItem.parHandle == self.theProject.trashRoot: + return False + if theItem.itemLayout == nwItemLayout.NO_LAYOUT: + return False + itemClass = theItem.itemClass itemLayout = theItem.itemLayout logger.debug("Indexing item with handle %s" % tHandle) # Check file type, and reset its old index - if itemClass == nwItemClass.NOVEL: - self.novelIndex[tHandle] = [] - self.refIndex[tHandle] = [] - isNovel = True - else: + self.refIndex[tHandle] = [] + if itemLayout == nwItemLayout.NOTE: self.noteIndex[tHandle] = [] - self.refIndex[tHandle] = [] isNovel = False + else: + self.novelIndex[tHandle] = [] + isNovel = True # Also clear references to file in tag index clearTags = [] @@ -296,7 +298,8 @@ class NWIndex(): aLine = aLine nLine += 1 nChar = len(aLine.strip()) - if nChar == 0: continue + if nChar == 0: + continue if aLine.startswith(r"#"): isTitle = self.indexTitle(tHandle, isNovel, aLine, nLine, itemLayout) @@ -513,20 +516,7 @@ class NWIndex(): return theRefs - def buildNovelList(self): - """Build a list of the content of the novel. - """ - 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 buildReferenceList(self, tHandle): + def getBackReferenceList(self, tHandle): """Build a list of files referring back to our file, specified by tHandle. """ @@ -560,46 +550,4 @@ class NWIndex(): return theRef[1], theRef[0] return None, 0 - def buildTagNovelMap(self, theTags, theFilters=None): - """Build a two-dimensional map of all titles of the novel and - which tags they link to from the various meta tags. This map is - used to display the timeline view. - """ - - tagMap = {} - tagClass = {} - exClass = [] - - if theFilters is not None: - if "exClass" in theFilters.keys(): - exClass = theFilters["exClass"] - - for theTag in theTags: - try: - tagClass[theTag] = nwItemClass[self.tagIndex[theTag][2]] - except: - logger.error("Could not map '%s' to nwItemClass" % self.tagIndex[theTag][2]) - tagClass[theTag] = None - if tagClass[theTag] not in exClass: - tagMap[theTag] = [0]*len(self.novelOrder) - - 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)) - - if theFilters["hUnused"]: - tagMapFiltered = {} - for theTag in tagMap.keys(): - if sum(tagMap[theTag]) > 0: - tagMapFiltered[theTag] = tagMap[theTag] - return tagMapFiltered - - return tagMap - # END Class NWIndex diff --git a/tests/test_project.py b/tests/test_project.py index 907d0e9f..41b5246a 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -95,25 +95,3 @@ def testIndexScanThis(nwTempProj): assert isValid assert str(theBits) == "['@tag', 'this', 'and this']" assert str(thePos) == "[0, 6, 12]" - -@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 6500d23bd7f2c3c202aab218dc7288b60edc5036 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Nov 2019 14:05:09 +0100 Subject: [PATCH 07/28] Cleaned up index, and fixed data extraction --- nw/gui/elements/outline.py | 32 +++++++-------- nw/project/index.py | 82 +++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 55 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 75e4f396..33fd5469 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -118,28 +118,24 @@ class GuiProjectOutline(QWidget): if tHandle not in self.theIndex.novelIndex: continue - nwItem = self.theProject.getItem(tHandle) - if nwItem.itemLayout == nwItemLayout.NOTE: - continue - - for tEntry in self.theIndex.novelIndex[tHandle]: - - nTitle = str(tEntry[0]) - tTitle = tEntry[2] - tLevel = str(tEntry[1]) + for sTitle in self.theIndex.novelIndex[tHandle]: + + nwItem = self.theProject.getItem(tHandle) + tTitle = self.theIndex.novelIndex[tHandle][sTitle][1] + tLevel = self.theIndex.novelIndex[tHandle][sTitle][0] tLabel = nwItem.itemName - tItem = self._createTreeItem(tHandle, nTitle, tTitle, tLevel, tLabel) + tItem = self._createTreeItem(tHandle, sTitle, tTitle, tLevel, tLabel) - if tEntry[1] == 1: + if tLevel == "H1": currTitle = tItem self.mainTree.addTopLevelItem(tItem) - elif tEntry[1] == 2: + elif tLevel == "H2": if currTitle is None: self.mainTree.addTopLevelItem(tItem) else: currTitle.addChild(tItem) currChapter = tItem - elif tEntry[1] == 3: + elif tLevel == "H3": if currChapter is None: if currTitle is None: self.mainTree.addTopLevelItem(tItem) @@ -148,7 +144,7 @@ class GuiProjectOutline(QWidget): else: currChapter.addChild(tItem) currScene = tItem - elif tEntry[1] == 4: + elif tLevel == "H4": if currScene is None: if currChapter is None: if currTitle is None: @@ -168,15 +164,15 @@ class GuiProjectOutline(QWidget): # Internal Functions ## - def _createTreeItem(self, tHandle, nTitle, tTitle, tLevel, tLabel): + def _createTreeItem(self, tHandle, sTitle, tTitle, tLevel, tLabel): newItem = QTreeWidgetItem() newItem.setText(self.I_TITLE, tTitle) newItem.setText(self.I_LEVEL, tLevel) newItem.setText(self.I_LABEL, tLabel) - newItem.setText(self.I_LINE, nTitle) + newItem.setText(self.I_LINE, sTitle) - cC, wC, pC = self.theIndex.getCounts(tHandle, nTitle) + cC, wC, pC = self.theIndex.getCounts(tHandle, sTitle) newItem.setText(self.I_CCOUNT, str(cC)) newItem.setText(self.I_WCOUNT, str(wC)) newItem.setText(self.I_PCOUNT, str(pC)) @@ -192,7 +188,7 @@ class GuiProjectOutline(QWidget): objectList = [] entityList = [] customList = [] - for tKey, tTag in self.theIndex.getReferences(tHandle, nTitle): + for tKey, tTag in self.theIndex.getReferences(tHandle, sTitle): if tKey == nwKeyWords.POV_KEY: povList.append(tTag) elif tKey == nwKeyWords.CHAR_KEY: diff --git a/nw/project/index.py b/nw/project/index.py index f0e93fbc..ef4f7abf 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -37,14 +37,14 @@ class NWIndex(): nwKeyWords.CUSTOM_KEY ] TAG_CLASS = { - nwKeyWords.CHAR_KEY : [nwItemClass.CHARACTER, 1], - nwKeyWords.POV_KEY : [nwItemClass.CHARACTER, 2], - nwKeyWords.PLOT_KEY : [nwItemClass.PLOT, 1], - nwKeyWords.TIME_KEY : [nwItemClass.TIMELINE, 1], - nwKeyWords.WORLD_KEY : [nwItemClass.WORLD, 1], - nwKeyWords.OBJECT_KEY : [nwItemClass.OBJECT, 1], - nwKeyWords.ENTITY_KEY : [nwItemClass.ENTITY, 1], - nwKeyWords.CUSTOM_KEY : [nwItemClass.CUSTOM, 1], + nwKeyWords.CHAR_KEY : nwItemClass.CHARACTER, + nwKeyWords.POV_KEY : nwItemClass.CHARACTER, + nwKeyWords.PLOT_KEY : nwItemClass.PLOT, + nwKeyWords.TIME_KEY : nwItemClass.TIMELINE, + nwKeyWords.WORLD_KEY : nwItemClass.WORLD, + nwKeyWords.OBJECT_KEY : nwItemClass.OBJECT, + nwKeyWords.ENTITY_KEY : nwItemClass.ENTITY, + nwKeyWords.CUSTOM_KEY : nwItemClass.CUSTOM, } def __init__(self, theProject, theParent): @@ -211,18 +211,19 @@ class NWIndex(): self.indexBroken = True for tHandle in self.refIndex: - for tEntry in self.refIndex[tHandle]: - if len(tEntry) != 4: - self.indexBroken = True + for sTitle in self.refIndex[tHandle]: + for tEntry in self.refIndex[tHandle][sTitle]: + if len(tEntry) != 3: + self.indexBroken = True for tHandle in self.novelIndex: - for tEntry in self.novelIndex[tHandle]: - if len(tEntry) != 4: + for sLine in self.novelIndex[tHandle]: + if len(self.novelIndex[tHandle][sLine]) != 3: self.indexBroken = True for tHandle in self.noteIndex: - for tEntry in self.noteIndex[tHandle]: - if len(tEntry) != 4: + for sLine in self.noteIndex[tHandle]: + if len(self.noteIndex[tHandle][sLine]) != 3: self.indexBroken = True for tHandle in self.fileCounts: @@ -240,7 +241,7 @@ class NWIndex(): if self.indexBroken: self.clearIndex() self.theParent.makeAlert( - "The project index loaded from cache contains errors. Triggering Rebuild Index.", + "The index loaded from project cache contains errors. Rebuilding index.", nwAlert.WARN ) @@ -273,12 +274,12 @@ class NWIndex(): logger.debug("Indexing item with handle %s" % tHandle) # Check file type, and reset its old index - self.refIndex[tHandle] = [] + self.refIndex[tHandle] = {} if itemLayout == nwItemLayout.NOTE: - self.noteIndex[tHandle] = [] + self.noteIndex[tHandle] = {} isNovel = False else: - self.novelIndex[tHandle] = [] + self.novelIndex[tHandle] = {} isNovel = True # Also clear references to file in tag index @@ -309,7 +310,7 @@ class NWIndex(): if nTitle > 0: lastText = "\n".join(theLines[nTitle-1:nLine-1]) cC, wC, pC = countWords(lastText) - theCounts[str(nTitle)] = [cC, wC, pC] + theCounts["T%d" % nTitle] = [cC, wC, pC] nTitle = nLine elif aLine.startswith(r"@"): @@ -317,14 +318,14 @@ class NWIndex(): self.indexTag(tHandle, aLine, nLine, itemClass) elif aLine.startswith(r"%synopsis:"): - theSynopsis[str(nTitle)] = aLine[10:].strip() + theSynopsis["T%d" % nTitle] = aLine[10:].strip() # Count words for remaining text after last heading if nTitle > 0: lastText = "\n".join(theLines[nTitle-1:]) cC, wC, pC = countWords(lastText) - theCounts[str(nTitle)] = [cC, wC, pC] - + theCounts["T%d" % nTitle] = [cC, wC, pC] + if theSynopsis: self.textSynopsis[tHandle] = theSynopsis if theCounts: @@ -342,27 +343,30 @@ class NWIndex(): """ if aLine.startswith("# "): - hDepth = 1 + hDepth = "H1" hText = aLine[2:].strip() elif aLine.startswith("## "): - hDepth = 2 + hDepth = "H2" hText = aLine[3:].strip() elif aLine.startswith("### "): - hDepth = 3 + hDepth = "H3" hText = aLine[4:].strip() elif aLine.startswith("#### "): - hDepth = 4 + hDepth = "H4" hText = aLine[5:].strip() else: return False + sTitle = "T%d" % nLine + self.refIndex[tHandle][sTitle] = [] + if hText != "": if isNovel: if tHandle in self.novelIndex: - self.novelIndex[tHandle].append([nLine, hDepth, hText, itemLayout.name]) + self.novelIndex[tHandle][sTitle] = [hDepth, hText, itemLayout.name] else: if tHandle in self.noteIndex: - self.noteIndex[tHandle].append([nLine, hDepth, hText, itemLayout.name]) + self.noteIndex[tHandle][sTitle] = [hDepth, hText, itemLayout.name] return True @@ -375,9 +379,11 @@ class NWIndex(): if not isValid or len(theBits) == 0: return False + sTitle = "T%d" % nTitle + if theBits[0] != nwKeyWords.TAG_KEY: for aVal in theBits[1:]: - self.refIndex[tHandle].append([nLine, theBits[0], aVal, nTitle]) + self.refIndex[tHandle][sTitle].append([nLine, theBits[0], aVal]) return True @@ -470,7 +476,7 @@ class NWIndex(): # If we're still here, we better check that the references exist for n in range(1,nBits): if theBits[n] in self.tagIndex: - isGood[n] = self.TAG_CLASS[theBits[0]][0].name == self.tagIndex[theBits[n]][2] + isGood[n] = self.TAG_CLASS[theBits[0]].name == self.tagIndex[theBits[n]][2] return isGood @@ -510,9 +516,10 @@ class NWIndex(): if tHandle not in self.refIndex: return theRefs - for nLine, tKey, tTag, nTitle in self.refIndex[tHandle]: - if tTitle is None or tTitle == str(nTitle): - theRefs.append((tKey, tTag)) + for sTitle in self.refIndex[tHandle]: + for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]: + if tTitle is None or tTitle == sTitle: + theRefs.append((tKey, tTag)) return theRefs @@ -535,9 +542,10 @@ class NWIndex(): if theTag is not None: for tHandle in self.refIndex: - for nLine, tKey, tTag, nTitle in self.refIndex[tHandle]: - if tTag == theTag: - theRefs[tHandle] = nLine + for sTitle in self.refIndex[tHandle]: + for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]: + if tTag == theTag: + theRefs[tHandle] = nLine return theRefs From 612c2ef3283e8ec35eb09cbd1bbf4a6c7f801079 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Nov 2019 15:45:38 +0100 Subject: [PATCH 08/28] Merged the indices so we have a minimum of dictionaries --- nw/constants/constants.py | 1 - nw/gui/elements/outline.py | 17 +-- nw/project/index.py | 206 ++++++++++++++++++++----------------- 3 files changed, 120 insertions(+), 104 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index d43167a3..a0325482 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -26,7 +26,6 @@ class nwFiles(): PROJ_DICT = "wordlist.txt" SESS_INFO = "sessionInfo.log" INDEX_FILE = "tagsIndex.json" - META_FILE = "projectMeta.json" EXPORT_OPT = "exportOptions.json" TLINE_OPT = "timelineOptions.json" SLOG_OPT = "sessionLogOptions.json" diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 33fd5469..fa1961db 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -83,9 +83,9 @@ class GuiProjectOutline(QWidget): self.showSynopsis = True self.showFilePath = False - self.outerBox = QVBoxLayout() - self.mainTree = QTreeWidget() - self.treeOrder = self.COL_ORDER + self.outerBox = QVBoxLayout() + self.mainTree = QTreeWidget() + self.treeCols = self.COL_ORDER self.outerBox.addWidget(self.mainTree) self.outerBox.setContentsMargins(0,0,0,0) @@ -98,7 +98,7 @@ class GuiProjectOutline(QWidget): def populateTree(self): theLabels = [] - for n in self.treeOrder: + for n in self.treeCols: theLabels.append(self.COL_LABELS[n]) self.mainTree.clear() @@ -113,16 +113,18 @@ class GuiProjectOutline(QWidget): currChapter = None currScene = None + theTitles = self.theIndex.getNovelStructure() + for tHandle in self.theProject.treeOrder: if tHandle not in self.theIndex.novelIndex: continue for sTitle in self.theIndex.novelIndex[tHandle]: - + nwItem = self.theProject.getItem(tHandle) - tTitle = self.theIndex.novelIndex[tHandle][sTitle][1] - tLevel = self.theIndex.novelIndex[tHandle][sTitle][0] + tTitle = self.theIndex.novelIndex[tHandle][sTitle]["title"] + tLevel = self.theIndex.novelIndex[tHandle][sTitle]["level"] tLabel = nwItem.itemName tItem = self._createTreeItem(tHandle, sTitle, tTitle, tLevel, tLabel) @@ -217,5 +219,4 @@ class GuiProjectOutline(QWidget): return newItem - # END Class GuiProjectOutline diff --git a/nw/project/index.py b/nw/project/index.py index ef4f7abf..a7a82b9d 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -15,6 +15,7 @@ import json import nw from os import path +from time import time from nw.constants import ( nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert @@ -56,15 +57,13 @@ class NWIndex(): self.indexBroken = False # Indices - self.tagIndex = {} - self.refIndex = {} - self.novelIndex = {} - self.noteIndex = {} + self.tagIndex = None + self.refIndex = None + self.novelIndex = None + self.noteIndex = None + self.textCounts = None - # Meta Data - self.fileCounts = {} - self.textCounts = {} - self.textSynopsis = {} + self.clearIndex() return @@ -73,13 +72,11 @@ class NWIndex(): ## def clearIndex(self): - self.tagIndex = {} - self.refIndex = {} - self.novelIndex = {} - self.noteIndex = {} - self.fileCounts = {} - self.textCounts = {} - self.textSynopsis = {} + self.tagIndex = {} + self.refIndex = {} + self.novelIndex = {} + self.noteIndex = {} + self.textCounts = {} return def deleteHandle(self, tHandle): @@ -95,6 +92,7 @@ class NWIndex(): self.refIndex.pop(tHandle, None) self.novelIndex.pop(tHandle, None) self.noteIndex.pop(tHandle, None) + self.textCounts.pop(tHandle, None) return @@ -106,10 +104,8 @@ class NWIndex(): """Load index from last session from the project meta folder. """ - theData = {} - loadsOK = False + theData = {} indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) - metaFile = path.join(self.theProject.projMeta, nwFiles.META_FILE) if path.isfile(indexFile): logger.debug("Loading index file") @@ -130,40 +126,19 @@ class NWIndex(): self.novelIndex = theData["novelIndex"] if "noteIndex" in theData.keys(): self.noteIndex = theData["noteIndex"] - - loadsOK = True - - if path.isfile(indexFile): - logger.debug("Loading meta file") - try: - with open(metaFile,mode="r",encoding="utf8") as inFile: - theJson = inFile.read() - theData = json.loads(theJson) - except Exception as e: - logger.error("Failed to load meta file") - logger.error(str(e)) - return False - - if "fileCounts" in theData.keys(): - self.fileCounts = theData["fileCounts"] if "textCounts" in theData.keys(): self.textCounts = theData["textCounts"] - if "textSynopsis" in theData.keys(): - self.textSynopsis = theData["textSynopsis"] - - loadsOK &= True self.checkIndex() - return loadsOK + return True def saveIndex(self): """Save the current index as a json file in the project meta - folder. + data folder. """ indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) - metaFile = path.join(self.theProject.projMeta, nwFiles.META_FILE) logger.debug("Saving index and meta files") if self.mainConf.debugInfo: @@ -178,24 +153,13 @@ class NWIndex(): "refIndex" : self.refIndex, "novelIndex" : self.novelIndex, "noteIndex" : self.noteIndex, + "textCounts" : self.textCounts, }, indent=nIndent)) except Exception as e: logger.error("Failed to save index file") logger.error(str(e)) return False - try: - with open(metaFile,mode="w+",encoding="utf8") as outFile: - outFile.write(json.dumps({ - "fileCounts" : self.fileCounts, - "textCounts" : self.textCounts, - "textSynopsis" : self.textSynopsis, - }, indent=nIndent)) - except Exception as e: - logger.error("Failed to save meta file") - logger.error(str(e)) - return False - return True def checkIndex(self): @@ -212,28 +176,23 @@ class NWIndex(): for tHandle in self.refIndex: for sTitle in self.refIndex[tHandle]: - for tEntry in self.refIndex[tHandle][sTitle]: + for tEntry in self.refIndex[tHandle][sTitle]["tags"]: if len(tEntry) != 3: self.indexBroken = True for tHandle in self.novelIndex: for sLine in self.novelIndex[tHandle]: - if len(self.novelIndex[tHandle][sLine]) != 3: + if len(self.novelIndex[tHandle][sLine].keys()) != 8: self.indexBroken = True for tHandle in self.noteIndex: for sLine in self.noteIndex[tHandle]: - if len(self.noteIndex[tHandle][sLine]) != 3: + if len(self.noteIndex[tHandle][sLine].keys()) != 8: self.indexBroken = True - for tHandle in self.fileCounts: - if len(self.fileCounts[tHandle]) != 3: - self.indexBroken = True - for tHandle in self.textCounts: - for tLine in self.textCounts[tHandle]: - if len(self.textCounts[tHandle][tLine]) != 3: - self.indexBroken = True + if len(self.textCounts[tHandle]) != 3: + self.indexBroken = True except: self.indexBroken = True @@ -292,9 +251,8 @@ class NWIndex(): nLine = 0 nTitle = 0 - theSynopsis = {} - theCounts = {} - theLines = theText.splitlines() + sTitle = None + theLines = theText.splitlines() for aLine in theLines: aLine = aLine nLine += 1 @@ -305,12 +263,9 @@ class NWIndex(): if aLine.startswith(r"#"): isTitle = self.indexTitle(tHandle, isNovel, aLine, nLine, itemLayout) if isTitle and nLine > 0: - # Count words in the previous section, before we tag - # the new title location if nTitle > 0: lastText = "\n".join(theLines[nTitle-1:nLine-1]) - cC, wC, pC = countWords(lastText) - theCounts["T%d" % nTitle] = [cC, wC, pC] + self.indexWordCounts(tHandle, isNovel, lastText, nTitle) nTitle = nLine elif aLine.startswith(r"@"): @@ -318,22 +273,17 @@ class NWIndex(): self.indexTag(tHandle, aLine, nLine, itemClass) elif aLine.startswith(r"%synopsis:"): - theSynopsis["T%d" % nTitle] = aLine[10:].strip() + if nTitle > 0: + self.indexSynopsis(tHandle, isNovel, aLine[10:].strip(), nTitle) # Count words for remaining text after last heading if nTitle > 0: - lastText = "\n".join(theLines[nTitle-1:]) - cC, wC, pC = countWords(lastText) - theCounts["T%d" % nTitle] = [cC, wC, pC] - - if theSynopsis: - self.textSynopsis[tHandle] = theSynopsis - if theCounts: - self.textCounts[tHandle] = theCounts + lastText = "\n".join(theLines[nTitle-1:nLine-1]) + self.indexWordCounts(tHandle, isNovel, lastText, nTitle) # Run word counter for whole text cC, wC, pC = countWords(theText) - self.fileCounts[tHandle] = [cC, wC, pC] + self.textCounts[tHandle] = [cC, wC, pC] return True @@ -358,18 +308,64 @@ class NWIndex(): return False sTitle = "T%d" % nLine - self.refIndex[tHandle][sTitle] = [] + self.refIndex[tHandle][sTitle] = { + "tags" : [], + "updated" : time(), + } + theData = { + "level" : hDepth, + "title" : hText, + "layout" : itemLayout.name, + "synopsis" : "", + "cCount" : 0, + "wCount" : 0, + "pCount" : 0, + "updated" : time(), + } if hText != "": if isNovel: if tHandle in self.novelIndex: - self.novelIndex[tHandle][sTitle] = [hDepth, hText, itemLayout.name] + self.novelIndex[tHandle][sTitle] = theData else: if tHandle in self.noteIndex: - self.noteIndex[tHandle][sTitle] = [hDepth, hText, itemLayout.name] + self.noteIndex[tHandle][sTitle] = theData return True + def indexWordCounts(self, tHandle, isNovel, theText, nTitle): + cC, wC, pC = countWords(theText) + sTitle = "T%d" % nTitle + if isNovel: + if tHandle in self.novelIndex: + if sTitle in self.novelIndex[tHandle]: + self.novelIndex[tHandle][sTitle]["cCount"] = cC + self.novelIndex[tHandle][sTitle]["wCount"] = wC + self.novelIndex[tHandle][sTitle]["pCount"] = pC + self.novelIndex[tHandle][sTitle]["updated"] = time() + else: + if tHandle in self.noteIndex: + if sTitle in self.noteIndex[tHandle]: + self.noteIndex[tHandle][sTitle]["cCount"] = cC + self.noteIndex[tHandle][sTitle]["wCount"] = wC + self.noteIndex[tHandle][sTitle]["pCount"] = pC + self.noteIndex[tHandle][sTitle]["updated"] = time() + return + + def indexSynopsis(self, tHandle, isNovel, theText, nTitle): + sTitle = "T%d" % nTitle + if isNovel: + if tHandle in self.novelIndex: + if sTitle in self.novelIndex[tHandle]: + self.novelIndex[tHandle][sTitle]["synopsis"] = theText + self.novelIndex[tHandle][sTitle]["updated"] = time() + else: + if tHandle in self.noteIndex: + if sTitle in self.noteIndex[tHandle]: + self.noteIndex[tHandle][sTitle]["synopsis"] = theText + self.noteIndex[tHandle][sTitle]["updated"] = time() + return + def indexNoteRef(self, tHandle, aLine, nLine, nTitle): """Validate and save the information about a reference to a tag in another file. @@ -383,7 +379,7 @@ class NWIndex(): if theBits[0] != nwKeyWords.TAG_KEY: for aVal in theBits[1:]: - self.refIndex[tHandle][sTitle].append([nLine, theBits[0], aVal]) + self.refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal]) return True @@ -484,6 +480,21 @@ class NWIndex(): # Extract Data ## + def getNovelStructure(self): + """Builds a list of all titles in the novel, in the correct + order as they appear in the tree view and in the respective + document files, but skipping all note files. + """ + + theStructure = [] + for tHandle in self.theProject.treeOrder: + if tHandle not in self.novelIndex: + continue + for sTitle in sorted(self.novelIndex[tHandle].keys()): + theStructure.append("%s:%s" % (tHandle, sTitle)) + + return theStructure + def getCounts(self, tHandle, nTitle=None): """Returns the counts for a file, or a section of a file starting at title nTitle. @@ -494,16 +505,21 @@ class NWIndex(): pC = 0 if nTitle is None: - if tHandle in self.fileCounts: - cC = self.fileCounts[tHandle][0] - wC = self.fileCounts[tHandle][1] - pC = self.fileCounts[tHandle][2] - else: if tHandle in self.textCounts: - if nTitle in self.textCounts[tHandle]: - cC = self.textCounts[tHandle][nTitle][0] - wC = self.textCounts[tHandle][nTitle][1] - pC = self.textCounts[tHandle][nTitle][2] + cC = self.textCounts[tHandle][0] + wC = self.textCounts[tHandle][1] + pC = self.textCounts[tHandle][2] + else: + if tHandle in self.novelIndex: + if nTitle in self.novelIndex[tHandle]: + cC = self.novelIndex[tHandle][nTitle]["cCount"] + wC = self.novelIndex[tHandle][nTitle]["wCount"] + pC = self.novelIndex[tHandle][nTitle]["pCount"] + elif tHandle in self.noteIndex: + if nTitle in self.noteIndex[tHandle]: + cC = self.noteIndex[tHandle][nTitle]["cCount"] + wC = self.noteIndex[tHandle][nTitle]["wCount"] + pC = self.noteIndex[tHandle][nTitle]["pCount"] return cC, wC, pC @@ -517,7 +533,7 @@ class NWIndex(): return theRefs for sTitle in self.refIndex[tHandle]: - for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]: + for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]: if tTitle is None or tTitle == sTitle: theRefs.append((tKey, tTag)) @@ -543,7 +559,7 @@ class NWIndex(): if theTag is not None: for tHandle in self.refIndex: for sTitle in self.refIndex[tHandle]: - for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]: + for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]: if tTag == theTag: theRefs[tHandle] = nLine From d1417ceea0ce5652504ba686252da304e17547f8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Nov 2019 16:08:55 +0100 Subject: [PATCH 09/28] Reworked the way the outline is built with the new index in place --- nw/gui/elements/outline.py | 128 ++++++++++++++++--------------------- nw/project/index.py | 17 +++-- 2 files changed, 67 insertions(+), 78 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index fa1961db..a2f5cf51 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -14,6 +14,7 @@ import logging import nw from os import path +from time import time from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( @@ -83,9 +84,11 @@ class GuiProjectOutline(QWidget): self.showSynopsis = True self.showFilePath = False - self.outerBox = QVBoxLayout() - self.mainTree = QTreeWidget() - self.treeCols = self.COL_ORDER + self.outerBox = QVBoxLayout() + self.mainTree = QTreeWidget() + self.treeCols = self.COL_ORDER + self.lastBuild = 0 + self.treeMap = {} self.outerBox.addWidget(self.mainTree) self.outerBox.setContentsMargins(0,0,0,0) @@ -113,31 +116,41 @@ class GuiProjectOutline(QWidget): currChapter = None currScene = None - theTitles = self.theIndex.getNovelStructure() + for titleKey in self.theIndex.getNovelStructure(): - for tHandle in self.theProject.treeOrder: + tHandle = titleKey[:13] + sTitle = titleKey[14:] if tHandle not in self.theIndex.novelIndex: continue + if sTitle not in self.theIndex.novelIndex[tHandle]: + continue - for sTitle in self.theIndex.novelIndex[tHandle]: + tLevel = self.theIndex.novelIndex[tHandle][sTitle]["level"] + tTime = self.theIndex.novelIndex[tHandle][sTitle]["updated"] + tItem = self._createTreeItem(tHandle, sTitle) + self.treeMap[titleKey] = tItem - nwItem = self.theProject.getItem(tHandle) - tTitle = self.theIndex.novelIndex[tHandle][sTitle]["title"] - tLevel = self.theIndex.novelIndex[tHandle][sTitle]["level"] - tLabel = nwItem.itemName - tItem = self._createTreeItem(tHandle, sTitle, tTitle, tLevel, tLabel) - - if tLevel == "H1": - currTitle = tItem + if tLevel == "H1": + currTitle = tItem + self.mainTree.addTopLevelItem(tItem) + elif tLevel == "H2": + if currTitle is None: self.mainTree.addTopLevelItem(tItem) - elif tLevel == "H2": + else: + currTitle.addChild(tItem) + currChapter = tItem + elif tLevel == "H3": + if currChapter is None: if currTitle is None: self.mainTree.addTopLevelItem(tItem) else: currTitle.addChild(tItem) - currChapter = tItem - elif tLevel == "H3": + else: + currChapter.addChild(tItem) + currScene = tItem + elif tLevel == "H4": + if currScene is None: if currChapter is None: if currTitle is None: self.mainTree.addTopLevelItem(tItem) @@ -145,20 +158,12 @@ class GuiProjectOutline(QWidget): currTitle.addChild(tItem) else: currChapter.addChild(tItem) - currScene = tItem - elif tLevel == "H4": - if currScene is None: - if currChapter is None: - if currTitle is None: - self.mainTree.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - else: - currChapter.addChild(tItem) - else: - currScene.addChild(tItem) + else: + currScene.addChild(tItem) - tItem.setExpanded(True) + tItem.setExpanded(True) + + self.lastBuild = time() return @@ -166,56 +171,33 @@ class GuiProjectOutline(QWidget): # Internal Functions ## - def _createTreeItem(self, tHandle, sTitle, tTitle, tLevel, tLabel): + def _createTreeItem(self, tHandle, sTitle): + + nwItem = self.theProject.getItem(tHandle) + novIdx = self.theIndex.novelIndex[tHandle][sTitle] newItem = QTreeWidgetItem() - newItem.setText(self.I_TITLE, tTitle) - newItem.setText(self.I_LEVEL, tLevel) - newItem.setText(self.I_LABEL, tLabel) - newItem.setText(self.I_LINE, sTitle) + newItem.setText(self.I_TITLE, novIdx["title"]) + newItem.setText(self.I_LEVEL, novIdx["level"]) + newItem.setText(self.I_LABEL, nwItem.itemName) + newItem.setText(self.I_LINE, sTitle[1:]) - cC, wC, pC = self.theIndex.getCounts(tHandle, sTitle) - newItem.setText(self.I_CCOUNT, str(cC)) - newItem.setText(self.I_WCOUNT, str(wC)) - newItem.setText(self.I_PCOUNT, str(pC)) + newItem.setText(self.I_CCOUNT, str(novIdx["cCount"])) + newItem.setText(self.I_WCOUNT, str(novIdx["wCount"])) + newItem.setText(self.I_PCOUNT, str(novIdx["pCount"])) newItem.setTextAlignment(self.I_CCOUNT,Qt.AlignRight) newItem.setTextAlignment(self.I_WCOUNT,Qt.AlignRight) newItem.setTextAlignment(self.I_PCOUNT,Qt.AlignRight) - povList = [] - charList = [] - plotList = [] - timeList = [] - worldList = [] - objectList = [] - entityList = [] - customList = [] - for tKey, tTag in self.theIndex.getReferences(tHandle, sTitle): - if tKey == nwKeyWords.POV_KEY: - povList.append(tTag) - elif tKey == nwKeyWords.CHAR_KEY: - charList.append(tTag) - elif tKey == nwKeyWords.PLOT_KEY: - plotList.append(tTag) - elif tKey == nwKeyWords.TIME_KEY: - timeList.append(tTag) - elif tKey == nwKeyWords.WORLD_KEY: - worldList.append(tTag) - elif tKey == nwKeyWords.OBJECT_KEY: - objectList.append(tTag) - elif tKey == nwKeyWords.ENTITY_KEY: - entityList.append(tTag) - elif tKey == nwKeyWords.CUSTOM_KEY: - customList.append(tTag) - - newItem.setText(self.I_POV, ", ".join(povList)) - newItem.setText(self.I_CHAR, ", ".join(charList)) - newItem.setText(self.I_PLOT, ", ".join(plotList)) - newItem.setText(self.I_TIME, ", ".join(timeList)) - newItem.setText(self.I_WORLD, ", ".join(worldList)) - newItem.setText(self.I_OBJECT, ", ".join(objectList)) - newItem.setText(self.I_ENTITY, ", ".join(entityList)) - newItem.setText(self.I_CUSTOM, ", ".join(customList)) + theRefs = self.theIndex.getReferences(tHandle, sTitle) + newItem.setText(self.I_POV, ", ".join(theRefs[nwKeyWords.POV_KEY])) + newItem.setText(self.I_CHAR, ", ".join(theRefs[nwKeyWords.CHAR_KEY])) + newItem.setText(self.I_PLOT, ", ".join(theRefs[nwKeyWords.PLOT_KEY])) + newItem.setText(self.I_TIME, ", ".join(theRefs[nwKeyWords.TIME_KEY])) + newItem.setText(self.I_WORLD, ", ".join(theRefs[nwKeyWords.WORLD_KEY])) + newItem.setText(self.I_OBJECT, ", ".join(theRefs[nwKeyWords.OBJECT_KEY])) + newItem.setText(self.I_ENTITY, ", ".join(theRefs[nwKeyWords.ENTITY_KEY])) + newItem.setText(self.I_CUSTOM, ", ".join(theRefs[nwKeyWords.CUSTOM_KEY])) return newItem diff --git a/nw/project/index.py b/nw/project/index.py index a7a82b9d..912ced7c 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -528,14 +528,21 @@ class NWIndex(): section. tTitle must be a string. """ - theRefs = [] + theRefs = {} + for tKey in self.TAG_CLASS: + theRefs[tKey] = [] + if tHandle not in self.refIndex: return theRefs - for sTitle in self.refIndex[tHandle]: - for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]: - if tTitle is None or tTitle == sTitle: - theRefs.append((tKey, tTag)) + try: + for sTitle in self.refIndex[tHandle]: + for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]: + if tTitle is None or tTitle == sTitle: + theRefs[tKey].append(tTag) + except Exception as e: + logger.error("Failed to generate reference list") + logger.error(str(e)) return theRefs From d533d91adc941b4f25b7b85534c6472b184d9f7d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Nov 2019 16:11:26 +0100 Subject: [PATCH 10/28] Added a sanity check on the titleKey --- nw/gui/elements/outline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index a2f5cf51..9c3937de 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -118,6 +118,9 @@ class GuiProjectOutline(QWidget): for titleKey in self.theIndex.getNovelStructure(): + if len(titleKey) < 15: + continue + tHandle = titleKey[:13] sTitle = titleKey[14:] From cff0d86d9057f8eaa2de16656d371304b61ae71a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Nov 2019 16:23:18 +0100 Subject: [PATCH 11/28] Some minor imrpovements --- nw/gui/elements/outline.py | 4 ++-- nw/project/index.py | 33 ++++++++++++++++----------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 9c3937de..dc0af7c9 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -60,7 +60,7 @@ class GuiProjectOutline(QWidget): I_CCOUNT : "Chars", I_PCOUNT : "Pars", I_SYNOP : "Synopsis", - I_POV : nwLabels.KEY_NAME[nwKeyWords.POV_KEY], + I_POV : "POV", I_CHAR : nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY], I_PLOT : nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY], I_TIME : nwLabels.KEY_NAME[nwKeyWords.TIME_KEY], @@ -118,7 +118,7 @@ class GuiProjectOutline(QWidget): for titleKey in self.theIndex.getNovelStructure(): - if len(titleKey) < 15: + if len(titleKey) < 16: continue tHandle = titleKey[:13] diff --git a/nw/project/index.py b/nw/project/index.py index 912ced7c..63f90eab 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -140,7 +140,7 @@ class NWIndex(): indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) - logger.debug("Saving index and meta files") + logger.debug("Saving index file") if self.mainConf.debugInfo: nIndent = 2 else: @@ -251,7 +251,6 @@ class NWIndex(): nLine = 0 nTitle = 0 - sTitle = None theLines = theText.splitlines() for aLine in theLines: aLine = aLine @@ -495,7 +494,7 @@ class NWIndex(): return theStructure - def getCounts(self, tHandle, nTitle=None): + def getCounts(self, tHandle, sTitle=None): """Returns the counts for a file, or a section of a file starting at title nTitle. """ @@ -504,28 +503,28 @@ class NWIndex(): wC = 0 pC = 0 - if nTitle is None: + if sTitle is None: if tHandle in self.textCounts: cC = self.textCounts[tHandle][0] wC = self.textCounts[tHandle][1] pC = self.textCounts[tHandle][2] else: if tHandle in self.novelIndex: - if nTitle in self.novelIndex[tHandle]: - cC = self.novelIndex[tHandle][nTitle]["cCount"] - wC = self.novelIndex[tHandle][nTitle]["wCount"] - pC = self.novelIndex[tHandle][nTitle]["pCount"] + if sTitle in self.novelIndex[tHandle]: + cC = self.novelIndex[tHandle][sTitle]["cCount"] + wC = self.novelIndex[tHandle][sTitle]["wCount"] + pC = self.novelIndex[tHandle][sTitle]["pCount"] elif tHandle in self.noteIndex: - if nTitle in self.noteIndex[tHandle]: - cC = self.noteIndex[tHandle][nTitle]["cCount"] - wC = self.noteIndex[tHandle][nTitle]["wCount"] - pC = self.noteIndex[tHandle][nTitle]["pCount"] + if sTitle in self.noteIndex[tHandle]: + cC = self.noteIndex[tHandle][sTitle]["cCount"] + wC = self.noteIndex[tHandle][sTitle]["wCount"] + pC = self.noteIndex[tHandle][sTitle]["pCount"] return cC, wC, pC - def getReferences(self, tHandle, tTitle=None): + def getReferences(self, tHandle, sTitle=None): """Extract all references made in a file, and optionally title - section. tTitle must be a string. + section. sTitle must be a string. """ theRefs = {} @@ -536,9 +535,9 @@ class NWIndex(): return theRefs try: - for sTitle in self.refIndex[tHandle]: - for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]: - if tTitle is None or tTitle == sTitle: + for refTitle in self.refIndex[tHandle]: + for nLine, tKey, tTag in self.refIndex[tHandle][refTitle]["tags"]: + if sTitle is None or sTitle == refTitle: theRefs[tKey].append(tTag) except Exception as e: logger.error("Failed to generate reference list") From dd96432c6efa2b71286843e29c3723955631e6ce Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Nov 2019 16:30:33 +0100 Subject: [PATCH 12/28] Deleted tests from removed code --- tests/reference/gui/1_tagsIndex.json | 1 - tests/test_gui.py | 26 -------------------------- 2 files changed, 27 deletions(-) delete mode 100644 tests/reference/gui/1_tagsIndex.json diff --git a/tests/reference/gui/1_tagsIndex.json b/tests/reference/gui/1_tagsIndex.json deleted file mode 100644 index 8c365f5c..00000000 --- a/tests/reference/gui/1_tagsIndex.json +++ /dev/null @@ -1 +0,0 @@ -{"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]], "2fca346db6561": [], "02d20bbd7e394": [], "7688b6ef52555": []}, "novelIndex": {"31489056e0916": [[1, 1, "Novel", "SCENE"], [3, 2, "Chapter", "SCENE"], [8, 3, "Scene", "SCENE"], [15, 4, "Some Section", "SCENE"]]}, "noteIndex": {"2fca346db6561": [[1, 1, "Jane Doe", "NOTE"]], "02d20bbd7e394": [[1, 1, "Main Plot", "NOTE"]], "7688b6ef52555": [[1, 1, "Main Location", "NOTE"]]}} \ No newline at end of file diff --git a/tests/test_gui.py b/tests/test_gui.py index 89121ace..d5633b0e 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -9,7 +9,6 @@ from os import path, unlink from PyQt5.QtCore import Qt from nw.gui.dialogs.projecteditor import GuiProjectEditor -from nw.gui.dialogs.timelineview import GuiTimeLineView from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.constants import * @@ -248,35 +247,10 @@ 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")) - 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() -@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]) From f9a06f34256a7072a49e0e8eeea4ac6d85482b4e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 20 Nov 2019 19:09:32 +0100 Subject: [PATCH 13/28] Fix the highlighting of synopsis tag --- nw/assets/themes/syntax/default_dark.conf | 1 + nw/assets/themes/syntax/default_light.conf | 1 + nw/assets/themes/syntax/light_owl.conf | 1 + nw/assets/themes/syntax/night_owl.conf | 1 + nw/assets/themes/syntax/tomorrow.conf | 1 + nw/assets/themes/syntax/tomorrow_night.conf | 1 + nw/assets/themes/syntax/tomorrow_night_blue.conf | 1 + nw/assets/themes/syntax/tomorrow_night_bright.conf | 1 + nw/assets/themes/syntax/tomorrow_night_eighties.conf | 1 + nw/gui/theme.py | 2 ++ nw/gui/tools/dochighlight.py | 8 +++++++- 11 files changed, 18 insertions(+), 1 deletion(-) diff --git a/nw/assets/themes/syntax/default_dark.conf b/nw/assets/themes/syntax/default_dark.conf index 8d1cd6b4..b5658bfc 100644 --- a/nw/assets/themes/syntax/default_dark.conf +++ b/nw/assets/themes/syntax/default_dark.conf @@ -17,3 +17,4 @@ value = 184, 200, 0 spellcheckline = 200, 46, 0 tagerror = 46, 200, 0 replacetag = 0, 184, 46 +modifier = 200, 120, 0 diff --git a/nw/assets/themes/syntax/default_light.conf b/nw/assets/themes/syntax/default_light.conf index 709a1ab2..07eea504 100644 --- a/nw/assets/themes/syntax/default_light.conf +++ b/nw/assets/themes/syntax/default_light.conf @@ -17,3 +17,4 @@ value = 50, 150, 50 spellcheckline = 200, 0, 0 tagerror = 0, 150, 0 replacetag = 0, 150, 0 +modifier = 150, 110, 30 diff --git a/nw/assets/themes/syntax/light_owl.conf b/nw/assets/themes/syntax/light_owl.conf index 8f6b54a5..788cea2c 100644 --- a/nw/assets/themes/syntax/light_owl.conf +++ b/nw/assets/themes/syntax/light_owl.conf @@ -40,3 +40,4 @@ value = 150, 74, 193 spellcheckline = 222, 61, 58 tagerror = 8, 145, 106 replacetag = 42, 162, 152 +modifier = 224, 175, 5 diff --git a/nw/assets/themes/syntax/night_owl.conf b/nw/assets/themes/syntax/night_owl.conf index 32d22ddf..33fbf47e 100644 --- a/nw/assets/themes/syntax/night_owl.conf +++ b/nw/assets/themes/syntax/night_owl.conf @@ -40,3 +40,4 @@ value = 199, 146, 234 spellcheckline = 247, 140, 108 tagerror = 173, 219, 103 replacetag = 127, 219, 202 +modifier = 236, 196, 141 diff --git a/nw/assets/themes/syntax/tomorrow.conf b/nw/assets/themes/syntax/tomorrow.conf index 909e9d65..26435950 100644 --- a/nw/assets/themes/syntax/tomorrow.conf +++ b/nw/assets/themes/syntax/tomorrow.conf @@ -40,3 +40,4 @@ value = 137, 89, 168 spellcheckline = 240, 40, 41 tagerror = 113, 140, 0 replacetag = 62, 153, 159 +modifier = 245, 135, 31 diff --git a/nw/assets/themes/syntax/tomorrow_night.conf b/nw/assets/themes/syntax/tomorrow_night.conf index 09966ff0..817f255a 100644 --- a/nw/assets/themes/syntax/tomorrow_night.conf +++ b/nw/assets/themes/syntax/tomorrow_night.conf @@ -40,3 +40,4 @@ value = 178, 148, 187 spellcheckline = 204, 102, 102 tagerror = 181, 189, 104 replacetag = 138, 190, 183 +modifier = 222, 147, 95 diff --git a/nw/assets/themes/syntax/tomorrow_night_blue.conf b/nw/assets/themes/syntax/tomorrow_night_blue.conf index d05a757b..e1c36135 100644 --- a/nw/assets/themes/syntax/tomorrow_night_blue.conf +++ b/nw/assets/themes/syntax/tomorrow_night_blue.conf @@ -40,3 +40,4 @@ value = 235, 187, 255 spellcheckline = 255, 157, 164 tagerror = 209, 241, 169 replacetag = 153, 255, 255 +modifier = 255, 197, 143 diff --git a/nw/assets/themes/syntax/tomorrow_night_bright.conf b/nw/assets/themes/syntax/tomorrow_night_bright.conf index 0d9c6c3a..1c4122e3 100644 --- a/nw/assets/themes/syntax/tomorrow_night_bright.conf +++ b/nw/assets/themes/syntax/tomorrow_night_bright.conf @@ -40,3 +40,4 @@ value = 195, 151, 216 spellcheckline = 213, 78, 83 tagerror = 185, 202, 74 replacetag = 112, 192, 177 +modifier = 231, 140, 69 diff --git a/nw/assets/themes/syntax/tomorrow_night_eighties.conf b/nw/assets/themes/syntax/tomorrow_night_eighties.conf index 5bd0c222..e75ffe31 100644 --- a/nw/assets/themes/syntax/tomorrow_night_eighties.conf +++ b/nw/assets/themes/syntax/tomorrow_night_eighties.conf @@ -40,3 +40,4 @@ value = 204, 153, 204 spellcheckline = 242, 119, 122 tagerror = 153, 204, 153 replacetag = 102, 204, 204 +modifier = 249, 145, 57 diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 8eb74502..e200361c 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -78,6 +78,7 @@ class GuiTheme: self.colSpell = [ 0, 0, 0] self.colTagErr = [ 0, 0, 0] self.colRepTag = [ 0, 0, 0] + self.colMod = [ 0, 0, 0] # Changeable Settings self.guiTheme = None @@ -217,6 +218,7 @@ class GuiTheme: self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline") self.colTagErr = self._loadColour(confParser, cnfSec, "tagerror") self.colRepTag = self._loadColour(confParser, cnfSec, "replacetag") + self.colMod = self._loadColour(confParser, cnfSec, "modifier") logger.info("Loaded syntax theme '%s'" % self.guiSyntax) diff --git a/nw/gui/tools/dochighlight.py b/nw/gui/tools/dochighlight.py index f20aece1..dc679c0a 100644 --- a/nw/gui/tools/dochighlight.py +++ b/nw/gui/tools/dochighlight.py @@ -75,6 +75,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.colSpell = QColor(*self.theTheme.colSpell) self.colTagErr = QColor(*self.theTheme.colTagErr) self.colRepTag = QColor(*self.theTheme.colRepTag) + self.colMod = QColor(*self.theTheme.colMod) self.colTrail = QColor(*self.theTheme.colEmph,64) self.hStyles = { @@ -98,6 +99,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): "replace" : self._makeFormat(self.colRepTag), "hidden" : self._makeFormat(self.colComm), "keyword" : self._makeFormat(self.colKey), + "modifier" : self._makeFormat(self.colMod), "value" : self._makeFormat(self.colVal), } @@ -250,7 +252,11 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.setFormat(4, len(theText), self.hStyles["header4"]) elif theText.startswith("%"): # Comments - self.setFormat(0, len(theText), self.hStyles["hidden"]) + if theText.startswith("%synopsis:"): + self.setFormat(0, 10, self.hStyles["modifier"]) + self.setFormat(10, len(theText), self.hStyles["hidden"]) + else: + self.setFormat(0, len(theText), self.hStyles["hidden"]) else: # Text Paragraph for rX, xFmt in self.rxRules: From 4739959c4ead79ab7e558d1420b9e8893cf2438e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 20 Nov 2019 21:17:43 +0100 Subject: [PATCH 14/28] Visible columns and column width in outline is now saved and loaded --- nw/constants/constants.py | 18 +++--- nw/gui/dialogs/export.py | 2 +- nw/gui/elements/outline.py | 124 +++++++++++++++++++++++++++---------- nw/guimain.py | 2 + nw/tools/optlaststate.py | 24 +++++++ 5 files changed, 128 insertions(+), 42 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index a0325482..acc63977 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -20,15 +20,15 @@ class nwConst(): class nwFiles(): - APP_ICON = "novelWriter.svg" - PROJ_FILE = "nwProject.nwx" - PROJ_COUNT = "projCount.txt" - PROJ_DICT = "wordlist.txt" - SESS_INFO = "sessionInfo.log" - INDEX_FILE = "tagsIndex.json" - EXPORT_OPT = "exportOptions.json" - TLINE_OPT = "timelineOptions.json" - SLOG_OPT = "sessionLogOptions.json" + APP_ICON = "novelWriter.svg" + PROJ_FILE = "nwProject.nwx" + PROJ_COUNT = "projCount.txt" + PROJ_DICT = "wordlist.txt" + SESS_INFO = "sessionInfo.log" + INDEX_FILE = "tagsIndex.json" + EXPORT_OPT = "exportOptions.json" + OUTLINE_OPT = "outlineOptions.json" + SLOG_OPT = "sessionLogOptions.json" # END Class nwFiles diff --git a/nw/gui/dialogs/export.py b/nw/gui/dialogs/export.py index 8d580f87..16f72484 100644 --- a/nw/gui/dialogs/export.py +++ b/nw/gui/dialogs/export.py @@ -683,7 +683,7 @@ class ExportLastState(OptLastState): def __init__(self, theProject, theFile): OptLastState.__init__(self, theProject, theFile) - self.theState = { + self.theState = { "wNovel" : True, "wNotes" : False, "eFormat" : 1, diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index dc0af7c9..403c20a0 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -16,12 +16,13 @@ import nw from os import path from time import time -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QByteArray from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem ) -from nw.constants import nwItemLayout, nwKeyWords, nwLabels +from nw.tools import OptLastState +from nw.constants import nwItemLayout, nwKeyWords, nwLabels, nwFiles logger = logging.getLogger(__name__) @@ -44,12 +45,7 @@ class GuiProjectOutline(QWidget): I_ENTITY = 14 I_CUSTOM = 15 - COL_ORDER = [ - I_TITLE, I_LEVEL, I_LABEL, I_LINE, - I_WCOUNT, I_CCOUNT, I_PCOUNT, I_SYNOP, - I_POV, I_CHAR, I_PLOT, I_TIME, - I_WORLD, I_OBJECT, I_ENTITY, I_CUSTOM, - ] + COL_MAX = 15 COL_LABELS = { I_TITLE : "Title", @@ -79,6 +75,7 @@ class GuiProjectOutline(QWidget): self.theParent = theParent self.theProject = theProject self.theIndex = self.theParent.theIndex + self.optState = OutlineLastState(self.theProject,nwFiles.OUTLINE_OPT) self.showWords = True self.showSynopsis = True @@ -86,9 +83,18 @@ class GuiProjectOutline(QWidget): self.outerBox = QVBoxLayout() self.mainTree = QTreeWidget() - self.treeCols = self.COL_ORDER self.lastBuild = 0 self.treeMap = {} + self.treeCols = { + "order" : [ + self.I_TITLE, self.I_LABEL, + self.I_WCOUNT, self.I_POV, + self.I_CHAR, self.I_PLOT, + self.I_WORLD, self.I_SYNOP + ], + "width" : [150, 100, 80, 100, 100, 100, 100, 300], + } + self.colIndex = {} self.outerBox.addWidget(self.mainTree) self.outerBox.setContentsMargins(0,0,0,0) @@ -98,19 +104,57 @@ class GuiProjectOutline(QWidget): return + def saveHeaderState(self): + + colW = [] + for iCol in range(self.mainTree.columnCount()): + colW.append(self.mainTree.columnWidth(iCol)) + + self.treeCols["width"] = colW + self.optState.setSetting("headState", self.treeCols) + self.optState.saveSettings() + return + + def loadHeaderState(self): + self.optState.loadSettings() + treeCols = self.optState.getSetting("headState") + + if "order" not in treeCols.keys(): return + if not isinstance(treeCols["order"], list): return + if len(treeCols["order"]) == 0: return + + self.treeCols["order"] = [] + for colID in treeCols["order"]: + if colID >= 0 and colID <= self.COL_MAX: + self.treeCols["order"].append(colID) + + if "width" in treeCols.keys(): + if isinstance(treeCols["width"],list): + self.treeCols["width"] = treeCols["width"] + + return + def populateTree(self): + self.loadHeaderState() + theLabels = [] - for n in self.treeCols: + for i, n in enumerate(self.treeCols["order"]): theLabels.append(self.COL_LABELS[n]) + self.colIndex[n] = i self.mainTree.clear() self.mainTree.setHeaderLabels(theLabels) + for n, colW in enumerate(self.treeCols["width"]): + self.mainTree.setColumnWidth(n,colW) treeHead = self.mainTree.headerItem() - treeHead.setTextAlignment(self.I_CCOUNT,Qt.AlignRight) - treeHead.setTextAlignment(self.I_WCOUNT,Qt.AlignRight) - treeHead.setTextAlignment(self.I_PCOUNT,Qt.AlignRight) + if self.I_CCOUNT in self.colIndex: + treeHead.setTextAlignment(self.colIndex[self.I_CCOUNT],Qt.AlignRight) + if self.I_WCOUNT in self.colIndex: + treeHead.setTextAlignment(self.colIndex[self.I_WCOUNT],Qt.AlignRight) + if self.I_PCOUNT in self.colIndex: + treeHead.setTextAlignment(self.colIndex[self.I_PCOUNT],Qt.AlignRight) currTitle = None currChapter = None @@ -180,28 +224,44 @@ class GuiProjectOutline(QWidget): novIdx = self.theIndex.novelIndex[tHandle][sTitle] newItem = QTreeWidgetItem() - newItem.setText(self.I_TITLE, novIdx["title"]) - newItem.setText(self.I_LEVEL, novIdx["level"]) - newItem.setText(self.I_LABEL, nwItem.itemName) - newItem.setText(self.I_LINE, sTitle[1:]) - - newItem.setText(self.I_CCOUNT, str(novIdx["cCount"])) - newItem.setText(self.I_WCOUNT, str(novIdx["wCount"])) - newItem.setText(self.I_PCOUNT, str(novIdx["pCount"])) - newItem.setTextAlignment(self.I_CCOUNT,Qt.AlignRight) - newItem.setTextAlignment(self.I_WCOUNT,Qt.AlignRight) - newItem.setTextAlignment(self.I_PCOUNT,Qt.AlignRight) + self._setItemText(newItem, self.I_TITLE, novIdx["title"]) + self._setItemText(newItem, self.I_LEVEL, novIdx["level"]) + self._setItemText(newItem, self.I_LABEL, nwItem.itemName) + self._setItemText(newItem, self.I_LINE, sTitle[1:]) + self._setItemText(newItem, self.I_SYNOP, novIdx["synopsis"]) + self._setItemText(newItem, self.I_CCOUNT, str(novIdx["cCount"]), True) + self._setItemText(newItem, self.I_WCOUNT, str(novIdx["wCount"]), True) + self._setItemText(newItem, self.I_PCOUNT, str(novIdx["pCount"]), True) theRefs = self.theIndex.getReferences(tHandle, sTitle) - newItem.setText(self.I_POV, ", ".join(theRefs[nwKeyWords.POV_KEY])) - newItem.setText(self.I_CHAR, ", ".join(theRefs[nwKeyWords.CHAR_KEY])) - newItem.setText(self.I_PLOT, ", ".join(theRefs[nwKeyWords.PLOT_KEY])) - newItem.setText(self.I_TIME, ", ".join(theRefs[nwKeyWords.TIME_KEY])) - newItem.setText(self.I_WORLD, ", ".join(theRefs[nwKeyWords.WORLD_KEY])) - newItem.setText(self.I_OBJECT, ", ".join(theRefs[nwKeyWords.OBJECT_KEY])) - newItem.setText(self.I_ENTITY, ", ".join(theRefs[nwKeyWords.ENTITY_KEY])) - newItem.setText(self.I_CUSTOM, ", ".join(theRefs[nwKeyWords.CUSTOM_KEY])) + self._setItemText(newItem, self.I_POV, ", ".join(theRefs[nwKeyWords.POV_KEY])) + self._setItemText(newItem, self.I_CHAR, ", ".join(theRefs[nwKeyWords.CHAR_KEY])) + self._setItemText(newItem, self.I_PLOT, ", ".join(theRefs[nwKeyWords.PLOT_KEY])) + self._setItemText(newItem, self.I_TIME, ", ".join(theRefs[nwKeyWords.TIME_KEY])) + self._setItemText(newItem, self.I_WORLD, ", ".join(theRefs[nwKeyWords.WORLD_KEY])) + self._setItemText(newItem, self.I_OBJECT, ", ".join(theRefs[nwKeyWords.OBJECT_KEY])) + self._setItemText(newItem, self.I_ENTITY, ", ".join(theRefs[nwKeyWords.ENTITY_KEY])) + self._setItemText(newItem, self.I_CUSTOM, ", ".join(theRefs[nwKeyWords.CUSTOM_KEY])) return newItem + def _setItemText(self, tItem, colID, theText, rAlign=False): + if colID in self.colIndex: + tItem.setText(self.colIndex[colID], theText) + if rAlign: + tItem.setTextAlignment(self.colIndex[colID],Qt.AlignRight) + return + # END Class GuiProjectOutline + +class OutlineLastState(OptLastState): + + def __init__(self, theProject, theFile): + OptLastState.__init__(self, theProject, theFile) + self.theState = { + "headState" : {}, + } + self.dictOpt = ("headState") + return + +# END Class OutlineLastState diff --git a/nw/guimain.py b/nw/guimain.py index c6535eb5..acdeb131 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -679,6 +679,8 @@ class GuiMain(QMainWindow): return False logger.info("Exiting %s" % nw.__package__) + self.projView.saveHeaderState() + self.closeProject(True) self.mainConf.setTreeColWidths(self.treeView.getColumnSizes()) diff --git a/nw/tools/optlaststate.py b/nw/tools/optlaststate.py index 0df6ff68..bce89f00 100644 --- a/nw/tools/optlaststate.py +++ b/nw/tools/optlaststate.py @@ -28,12 +28,21 @@ class OptLastState(): self.theState = {} self.stringOpt = () self.boolOpt = () + self.dictOpt = () self.intOpt = () return def loadSettings(self): + + if self.theProject.projMeta is None or self.theFile is None: + logger.error("Cannot load file '%s' to path '%s'" % ( + str(self.theFile), str(self.theProject.projMeta) + )) + return False + stateFile = path.join(self.theProject.projMeta,self.theFile) theState = {} + if path.isfile(stateFile): logger.debug("Loading options file") try: @@ -44,11 +53,20 @@ class OptLastState(): logger.error("Failed to load options file") logger.error(str(e)) return False + for anOpt in theState: self.theState[anOpt] = theState[anOpt] + return True def saveSettings(self): + + if self.theProject.projMeta is None or self.theFile is None: + logger.error("Cannot save file '%s' to path '%s'" % ( + str(self.theFile), str(self.theProject.projMeta) + )) + return False + stateFile = path.join(self.theProject.projMeta,self.theFile) logger.debug("Saving options file") try: @@ -58,6 +76,7 @@ class OptLastState(): logger.error("Failed to save options file") logger.error(str(e)) return False + return True def setSetting(self, setName, setValue): @@ -72,6 +91,11 @@ class OptLastState(): return checkString(self.theState[setName],self.theState[setName],False) elif setName in self.boolOpt: return checkBool(self.theState[setName],self.theState[setName],False) + elif setName in self.dictOpt: + if isinstance(self.theState[setName], dict): + return self.theState[setName] + else: + return {} elif setName in self.intOpt: return checkInt(self.theState[setName],self.theState[setName],False) return None From 1eb6b59c32e247a4ca9d8b7758fd525e82c9be1b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 10 Dec 2019 19:05:57 +0100 Subject: [PATCH 15/28] Allow tags in files with no title --- nw/project/index.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nw/project/index.py b/nw/project/index.py index 63f90eab..499d5765 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -233,7 +233,12 @@ class NWIndex(): logger.debug("Indexing item with handle %s" % tHandle) # Check file type, and reset its old index + # Also add an entry for T0 in case the file has no title self.refIndex[tHandle] = {} + self.refIndex[tHandle]["T0"] = { + "tags" : [], + "updated" : time(), + } if itemLayout == nwItemLayout.NOTE: self.noteIndex[tHandle] = {} isNovel = False @@ -375,6 +380,9 @@ class NWIndex(): return False sTitle = "T%d" % nTitle + if sTitle not in self.refIndex[tHandle]: + logger.error("Cannot save tags to file %s, no title %s" % (tHandle, sTitle)) + return False if theBits[0] != nwKeyWords.TAG_KEY: for aVal in theBits[1:]: From fdda0ddc748554ee7995942e9429df5f16ed856a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 27 Feb 2020 21:54:26 +0100 Subject: [PATCH 16/28] Fixed a few issues with loading and saving of outline settings, and fixed zen mode --- nw/gui/elements/outline.py | 46 +++++++++++++++++++++++++++----------- nw/guimain.py | 9 ++++++-- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 0be50914..ed10b1ee 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -21,7 +21,7 @@ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem ) -from nw.constants import nwItemLayout, nwKeyWords, nwLabels, nwFiles +from nw.constants import nwKeyWords, nwLabels logger = logging.getLogger(__name__) @@ -76,9 +76,7 @@ class GuiProjectOutline(QWidget): self.theIndex = self.theParent.theIndex self.optState = self.theProject.optState - self.showWords = True - self.showSynopsis = True - self.showFilePath = False + self.firstView = True self.outerBox = QVBoxLayout() self.mainTree = QTreeWidget() @@ -103,7 +101,35 @@ class GuiProjectOutline(QWidget): return - def saveHeaderState(self): + def refreshTree(self): + """Called whenever the Outline tab is activated and controls + what data to load, and if necessary, force a rebuild of the + tree. + """ + + if self.firstView: + self._loadHeaderState() + self._populateTree() + + self.firstView = False + + return + + def closeOutline(self): + """Called before a project is closed. + """ + + self._saveHeaderState() + self.mainTree.clear() + self.firstView = True + + return + + ## + # Internal Functions + ## + + def _saveHeaderState(self): colW = [] for iCol in range(self.mainTree.columnCount()): @@ -114,7 +140,7 @@ class GuiProjectOutline(QWidget): self.optState.saveSettings() return - def loadHeaderState(self): + def _loadHeaderState(self): treeCols = self.optState.getValue("GuiProjectOutline", "headState", {}) @@ -133,9 +159,7 @@ class GuiProjectOutline(QWidget): return - def populateTree(self): - - self.loadHeaderState() + def _populateTree(self): theLabels = [] for i, n in enumerate(self.treeCols["order"]): @@ -213,10 +237,6 @@ class GuiProjectOutline(QWidget): return - ## - # Internal Functions - ## - def _createTreeItem(self, tHandle, sTitle): nwItem = self.theProject.getItem(tHandle) diff --git a/nw/guimain.py b/nw/guimain.py index 33c883b8..460f096e 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -307,6 +307,7 @@ class GuiMain(QMainWindow): if saveOK: self.closeDocument() + self.projView.closeOutline() self.theProject.closeProject() self.theIndex.clearIndex() self.clearGUI() @@ -329,6 +330,9 @@ class GuiMain(QMainWindow): if not self.closeProject(): return False + # Switch main tab to editor view + self.tabWidget.setCurrentWidget(self.splitView) + # Try to open the project if not self.theProject.openProject(projFile): if self.theProject.lockedBy is not None: @@ -773,7 +777,6 @@ class GuiMain(QMainWindow): logger.info("Exiting %s" % nw.__package__) if self.hasProject: - self.projView.saveHeaderState() self.closeProject(True) self.mainConf.setTreeColWidths(self.treeView.getColumnSizes()) @@ -824,6 +827,7 @@ class GuiMain(QMainWindow): self.isZenMode = not self.isZenMode if self.isZenMode: logger.debug("Activating Zen mode") + self.tabWidget.setCurrentWidget(self.splitView) else: logger.debug("Deactivating Zen mode") @@ -831,6 +835,7 @@ class GuiMain(QMainWindow): self.treePane.setVisible(isVisible) self.statusBar.setVisible(isVisible) self.mainMenu.setVisible(isVisible) + self.tabWidget.tabBar().setVisible(isVisible) if self.viewPane.isVisible(): self.viewPane.setVisible(False) @@ -995,7 +1000,7 @@ class GuiMain(QMainWindow): elif tabIndex == self.idxTabProj: logger.verbose("Project outline tab activated") if self.hasProject: - self.projView.populateTree() + self.projView.refreshTree() return # END Class GuiMain From d8a44e63e715622038e032f8e74b2ded1c54915d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 27 Feb 2020 22:29:57 +0100 Subject: [PATCH 17/28] Some minor cleanup in the outline class --- nw/gui/elements/outline.py | 40 +++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index ed10b1ee..35c364de 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -129,20 +129,12 @@ class GuiProjectOutline(QWidget): # Internal Functions ## - def _saveHeaderState(self): - - colW = [] - for iCol in range(self.mainTree.columnCount()): - colW.append(self.mainTree.columnWidth(iCol)) - - self.treeCols["width"] = colW - self.optState.setValue("GuiProjectOutline", "headState", self.treeCols) - self.optState.saveSettings() - return - def _loadHeaderState(self): + """Load the state of the main tree header, that is, column order + and column width. + """ - treeCols = self.optState.getValue("GuiProjectOutline", "headState", {}) + treeCols = self.optState.getValue("GuiProjectOutline", "headerState", self.treeCols) if "order" not in treeCols.keys(): return if not isinstance(treeCols["order"], list): return @@ -159,7 +151,24 @@ class GuiProjectOutline(QWidget): return + def _saveHeaderState(self): + """Save the state of the main tree header, that is, column order + and column width. + """ + + colW = [] + for iCol in range(self.mainTree.columnCount()): + colW.append(self.mainTree.columnWidth(iCol)) + + self.treeCols["width"] = colW + self.optState.setValue("GuiProjectOutline", "headerState", self.treeCols) + self.optState.saveSettings() + + return + def _populateTree(self): + """Build the tree based on the project index. + """ theLabels = [] for i, n in enumerate(self.treeCols["order"]): @@ -238,6 +247,8 @@ class GuiProjectOutline(QWidget): return def _createTreeItem(self, tHandle, sTitle): + """Populate a tree item with all the column values. + """ nwItem = self.theProject.getItem(tHandle) novIdx = self.theIndex.novelIndex[tHandle][sTitle] @@ -265,10 +276,13 @@ class GuiProjectOutline(QWidget): return newItem def _setItemText(self, tItem, colID, theText, rAlign=False): + """Set the correct text in the correct column, and if necessary, + right align it. + """ if colID in self.colIndex: tItem.setText(self.colIndex[colID], theText) if rAlign: - tItem.setTextAlignment(self.colIndex[colID],Qt.AlignRight) + tItem.setTextAlignment(self.colIndex[colID], Qt.AlignRight) return # END Class GuiProjectOutline From c9657a351531dafeb57326fbf6ef0e0ba5fffb5f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 7 Mar 2020 17:10:35 +0100 Subject: [PATCH 18/28] Outline class should just be a sunclass of the central QTreeWidget, like other main GUI classes --- nw/gui/elements/outline.py | 55 +++++++++++++++++++++++--------------- nw/guimain.py | 9 ++++--- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 35c364de..2cffb59e 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -18,14 +18,15 @@ from time import time from PyQt5.QtCore import Qt, QByteArray from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem + QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, + QAbstractItemView ) from nw.constants import nwKeyWords, nwLabels logger = logging.getLogger(__name__) -class GuiProjectOutline(QWidget): +class GuiProjectOutline(QTreeWidget): I_TITLE = 0 I_LEVEL = 1 @@ -66,7 +67,7 @@ class GuiProjectOutline(QWidget): } def __init__(self, theParent, theProject): - QWidget.__init__(self, theParent) + QTreeWidget.__init__(self, theParent) logger.debug("Initialising ProjectOutline ...") @@ -77,10 +78,18 @@ class GuiProjectOutline(QWidget): self.optState = self.theProject.optState self.firstView = True - - self.outerBox = QVBoxLayout() - self.mainTree = QTreeWidget() self.lastBuild = 0 + + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setSelectionMode(QAbstractItemView.SingleSelection) + self.setExpandsOnDoubleClick(False) + self.setDragEnabled(False) + self.itemDoubleClicked.connect(self._treeDoubleClick) + + # self.mainHead = self.header() + # self.mainHead.setContextMenuPolicy(Qt.CustomContextMenu) + # self.mainHead. + self.treeMap = {} self.treeCols = { "order" : [ @@ -93,10 +102,6 @@ class GuiProjectOutline(QWidget): } self.colIndex = {} - self.outerBox.addWidget(self.mainTree) - self.outerBox.setContentsMargins(0,0,0,0) - self.setLayout(self.outerBox) - logger.debug("ProjectOutline initialisation complete") return @@ -120,11 +125,19 @@ class GuiProjectOutline(QWidget): """ self._saveHeaderState() - self.mainTree.clear() + self.clear() self.firstView = True return + ## + # Slots + ## + + def _treeDoubleClick(self, tItem, tCol): + print(tItem, tCol) + return + ## # Internal Functions ## @@ -157,8 +170,8 @@ class GuiProjectOutline(QWidget): """ colW = [] - for iCol in range(self.mainTree.columnCount()): - colW.append(self.mainTree.columnWidth(iCol)) + for iCol in range(self.columnCount()): + colW.append(self.columnWidth(iCol)) self.treeCols["width"] = colW self.optState.setValue("GuiProjectOutline", "headerState", self.treeCols) @@ -175,12 +188,12 @@ class GuiProjectOutline(QWidget): theLabels.append(self.COL_LABELS[n]) self.colIndex[n] = i - self.mainTree.clear() - self.mainTree.setHeaderLabels(theLabels) + self.clear() + self.setHeaderLabels(theLabels) for n, colW in enumerate(self.treeCols["width"]): - self.mainTree.setColumnWidth(n,colW) + self.setColumnWidth(n,colW) - treeHead = self.mainTree.headerItem() + treeHead = self.headerItem() if self.I_CCOUNT in self.colIndex: treeHead.setTextAlignment(self.colIndex[self.I_CCOUNT],Qt.AlignRight) if self.I_WCOUNT in self.colIndex: @@ -212,17 +225,17 @@ class GuiProjectOutline(QWidget): if tLevel == "H1": currTitle = tItem - self.mainTree.addTopLevelItem(tItem) + self.addTopLevelItem(tItem) elif tLevel == "H2": if currTitle is None: - self.mainTree.addTopLevelItem(tItem) + self.addTopLevelItem(tItem) else: currTitle.addChild(tItem) currChapter = tItem elif tLevel == "H3": if currChapter is None: if currTitle is None: - self.mainTree.addTopLevelItem(tItem) + self.addTopLevelItem(tItem) else: currTitle.addChild(tItem) else: @@ -232,7 +245,7 @@ class GuiProjectOutline(QWidget): if currScene is None: if currChapter is None: if currTitle is None: - self.mainTree.addTopLevelItem(tItem) + self.addTopLevelItem(tItem) else: currTitle.addChild(tItem) else: diff --git a/nw/guimain.py b/nw/guimain.py index 460f096e..619cc372 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -112,11 +112,14 @@ class GuiMain(QMainWindow): self.splitView.addWidget(self.editPane) self.splitView.addWidget(self.viewPane) + self.splitOutline = QSplitter(Qt.Vertical) + self.splitOutline.addWidget(self.projView) + self.tabWidget = QTabWidget() self.tabWidget.setTabPosition(QTabWidget.East) self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}") - self.tabWidget.addTab(self.splitView, "Editor") - self.tabWidget.addTab(self.projView, "Outline") + self.tabWidget.addTab(self.splitView, "Editor") + self.tabWidget.addTab(self.splitOutline, "Outline") self.tabWidget.currentChanged.connect(self._mainTabChanged) self.splitMain = QSplitter(Qt.Horizontal) @@ -134,7 +137,7 @@ class GuiMain(QMainWindow): self.idxViewer = self.splitView.indexOf(self.viewPane) self.idxTabEdit = self.tabWidget.indexOf(self.splitView) - self.idxTabProj = self.tabWidget.indexOf(self.projView) + self.idxTabProj = self.tabWidget.indexOf(self.splitOutline) self.splitMain.setCollapsible(self.idxTree, False) self.splitMain.setCollapsible(self.idxMain, False) From 32c6d80efbe9896d6cd255500479ff34791ddea9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 10 Mar 2020 23:18:58 +0100 Subject: [PATCH 19/28] Restructured the way column state is preserved in the Outline tree --- nw/constants/__init__.py | 2 +- nw/gui/elements/outline.py | 235 +++++++++++++++++++++---------------- 2 files changed, 134 insertions(+), 103 deletions(-) diff --git a/nw/constants/__init__.py b/nw/constants/__init__.py index 27c14997..769c50d5 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -22,4 +22,4 @@ __all__ = [ "nwItemClass", "nwItemLayout", "nwItemType", -] \ No newline at end of file +] diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 2cffb59e..b9f209e6 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -15,10 +15,11 @@ import nw from os import path from time import time +from enum import Enum from PyQt5.QtCore import Qt, QByteArray from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, + QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView ) @@ -26,44 +27,46 @@ from nw.constants import nwKeyWords, nwLabels logger = logging.getLogger(__name__) +class HCols(Enum): + + TITLE = 0 + LEVEL = 1 + LABEL = 2 + LINE = 3 + WCOUNT = 4 + CCOUNT = 5 + PCOUNT = 6 + SYNOP = 7 + POV = 8 + CHAR = 9 + PLOT = 10 + TIME = 11 + WORLD = 12 + OBJECT = 13 + ENTITY = 14 + CUSTOM = 15 + +# END Enum HCols + class GuiProjectOutline(QTreeWidget): - I_TITLE = 0 - I_LEVEL = 1 - I_LABEL = 2 - I_LINE = 3 - I_WCOUNT = 4 - I_CCOUNT = 5 - I_PCOUNT = 6 - I_SYNOP = 7 - I_POV = 8 - I_CHAR = 9 - I_PLOT = 10 - I_TIME = 11 - I_WORLD = 12 - I_OBJECT = 13 - I_ENTITY = 14 - I_CUSTOM = 15 - - COL_MAX = 15 - COL_LABELS = { - I_TITLE : "Title", - I_LEVEL : "Level", - I_LABEL : "Document", - I_LINE : "Line", - I_WCOUNT : "Words", - I_CCOUNT : "Chars", - I_PCOUNT : "Pars", - I_SYNOP : "Synopsis", - I_POV : "POV", - I_CHAR : nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY], - I_PLOT : nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY], - I_TIME : nwLabels.KEY_NAME[nwKeyWords.TIME_KEY], - I_WORLD : nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY], - I_OBJECT : nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY], - I_ENTITY : nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY], - I_CUSTOM : nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY], + HCols.TITLE : "Title", + HCols.LEVEL : "Level", + HCols.LABEL : "Document", + HCols.LINE : "Line", + HCols.WCOUNT : "Words", + HCols.CCOUNT : "Chars", + HCols.PCOUNT : "Pars", + HCols.POV : "POV", + HCols.CHAR : nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY], + HCols.PLOT : nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY], + HCols.TIME : nwLabels.KEY_NAME[nwKeyWords.TIME_KEY], + HCols.WORLD : nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY], + HCols.OBJECT : nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY], + HCols.ENTITY : nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY], + HCols.CUSTOM : nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY], + HCols.SYNOP : "Synopsis", } def __init__(self, theParent, theProject): @@ -76,6 +79,7 @@ class GuiProjectOutline(QTreeWidget): self.theProject = theProject self.theIndex = self.theParent.theIndex self.optState = self.theProject.optState + self.headerMenu = GuiOutlineHeaderMenu(self) self.firstView = True self.lastBuild = 0 @@ -86,21 +90,16 @@ class GuiProjectOutline(QTreeWidget): self.setDragEnabled(False) self.itemDoubleClicked.connect(self._treeDoubleClick) - # self.mainHead = self.header() - # self.mainHead.setContextMenuPolicy(Qt.CustomContextMenu) - # self.mainHead. + self.treeHead = self.header() + self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu) + self.treeHead.customContextMenuRequested.connect(self._headerRightClick) + self.treeHead.sectionMoved.connect(self._columnMoved) self.treeMap = {} - self.treeCols = { - "order" : [ - self.I_TITLE, self.I_LABEL, - self.I_WCOUNT, self.I_POV, - self.I_CHAR, self.I_PLOT, - self.I_WORLD, self.I_SYNOP - ], - "width" : [150, 100, 80, 100, 100, 100, 100, 300], - } - self.colIndex = {} + self.treeOrder = self.COL_LABELS.keys() + self.treeNCols = len(self.treeOrder) + self.treeWidth = [150]*self.treeNCols + self.colIndex = {} logger.debug("ProjectOutline initialisation complete") @@ -123,11 +122,9 @@ class GuiProjectOutline(QTreeWidget): def closeOutline(self): """Called before a project is closed. """ - self._saveHeaderState() self.clear() self.firstView = True - return ## @@ -138,6 +135,22 @@ class GuiProjectOutline(QTreeWidget): print(tItem, tCol) return + def _headerRightClick(self, clickPos): + print(clickPos) + globPos = self.mapToGlobal(clickPos) + print(globPos) + return + + def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx): + """Make sure the order and width read from settings file, or + with default values, is kept up-to-date when columns are moved + around. Otherwise, the original order will be restored on a tree + rebuild. + """ + self.treeOrder.insert(newVisualIdx, self.treeOrder.pop(oldVisualIdx)) + self.treeWidth.insert(newVisualIdx, self.treeWidth.pop(oldVisualIdx)) + return + ## # Internal Functions ## @@ -147,20 +160,35 @@ class GuiProjectOutline(QTreeWidget): and column width. """ - treeCols = self.optState.getValue("GuiProjectOutline", "headerState", self.treeCols) + # Load whatever we saved last time, regardless of wether it + # contains the correct names or number of columns. + keysOrder = self.COL_LABELS.keys() + tempOrder = self.optState.getValue("GuiProjectOutline", "headerOrder", keysOrder) + treeOrder = [] + for hName in tempOrder: + for hItem in HCols: + if hItem.name == hName: + treeOrder.append(hItem) - if "order" not in treeCols.keys(): return - if not isinstance(treeCols["order"], list): return - if len(treeCols["order"]) == 0: return + # Add columns that were not in tempOrder to treeOrder, but in + # the default column order. + for cItem in keysOrder: + if cItem not in treeOrder: + treeOrder.append(cItem) - self.treeCols["order"] = [] - for colID in treeCols["order"]: - if colID >= 0 and colID <= self.COL_MAX: - self.treeCols["order"].append(colID) + # Check that we now have a complete list, and only if so, save + # the order loaded from file. Otherwise, we keep the default. + if len(treeOrder) == self.treeNCols: + self.treeOrder = treeOrder + else: + logger.error("Failed to extract outline column order from previous session") + logger.error("Column count doesn't match %d != %d" % (len(treeOrder), self.treeNCols)) - if "width" in treeCols.keys(): - if isinstance(treeCols["width"],list): - self.treeCols["width"] = treeCols["width"] + # The columns widths we just fill whatever we've got, and append + # the rest with defaults, and truncate to desired length. + tempWidth = self.optState.getValue("GuiProjectOutline", "headerWidth", []) + treeWidth = [int(w) for w in tempWidth] + self.treeWidth = (treeWidth + self.treeWidth)[0:self.treeNCols] return @@ -169,12 +197,15 @@ class GuiProjectOutline(QTreeWidget): and column width. """ - colW = [] + treeWidth = [] + treeOrder = [] for iCol in range(self.columnCount()): - colW.append(self.columnWidth(iCol)) + treeOrder.append(self.treeOrder[iCol].name) + iLog = self.treeHead.logicalIndex(iCol) + treeWidth.append(self.columnWidth(iLog)) - self.treeCols["width"] = colW - self.optState.setValue("GuiProjectOutline", "headerState", self.treeCols) + self.optState.setValue("GuiProjectOutline", "headerOrder", treeOrder) + self.optState.setValue("GuiProjectOutline", "headerWidth", treeWidth) self.optState.saveSettings() return @@ -184,22 +215,19 @@ class GuiProjectOutline(QTreeWidget): """ theLabels = [] - for i, n in enumerate(self.treeCols["order"]): - theLabels.append(self.COL_LABELS[n]) - self.colIndex[n] = i + for i, hItem in enumerate(self.treeOrder): + theLabels.append(self.COL_LABELS[hItem]) + self.colIndex[hItem] = i self.clear() self.setHeaderLabels(theLabels) - for n, colW in enumerate(self.treeCols["width"]): + for n, colW in enumerate(self.treeWidth): self.setColumnWidth(n,colW) - treeHead = self.headerItem() - if self.I_CCOUNT in self.colIndex: - treeHead.setTextAlignment(self.colIndex[self.I_CCOUNT],Qt.AlignRight) - if self.I_WCOUNT in self.colIndex: - treeHead.setTextAlignment(self.colIndex[self.I_WCOUNT],Qt.AlignRight) - if self.I_PCOUNT in self.colIndex: - treeHead.setTextAlignment(self.colIndex[self.I_PCOUNT],Qt.AlignRight) + headItem = self.headerItem() + headItem.setTextAlignment(self.colIndex[HCols.CCOUNT],Qt.AlignRight) + headItem.setTextAlignment(self.colIndex[HCols.WCOUNT],Qt.AlignRight) + headItem.setTextAlignment(self.colIndex[HCols.PCOUNT],Qt.AlignRight) currTitle = None currChapter = None @@ -267,35 +295,38 @@ class GuiProjectOutline(QTreeWidget): novIdx = self.theIndex.novelIndex[tHandle][sTitle] newItem = QTreeWidgetItem() - self._setItemText(newItem, self.I_TITLE, novIdx["title"]) - self._setItemText(newItem, self.I_LEVEL, novIdx["level"]) - self._setItemText(newItem, self.I_LABEL, nwItem.itemName) - self._setItemText(newItem, self.I_LINE, sTitle[1:]) - self._setItemText(newItem, self.I_SYNOP, novIdx["synopsis"]) - self._setItemText(newItem, self.I_CCOUNT, str(novIdx["cCount"]), True) - self._setItemText(newItem, self.I_WCOUNT, str(novIdx["wCount"]), True) - self._setItemText(newItem, self.I_PCOUNT, str(novIdx["pCount"]), True) + + newItem.setText(self.colIndex[HCols.TITLE], novIdx["title"]) + newItem.setText(self.colIndex[HCols.LEVEL], novIdx["level"]) + newItem.setText(self.colIndex[HCols.LABEL], nwItem.itemName) + newItem.setText(self.colIndex[HCols.LINE], sTitle[1:]) + newItem.setText(self.colIndex[HCols.SYNOP], novIdx["synopsis"]) + newItem.setText(self.colIndex[HCols.CCOUNT], str(novIdx["cCount"])) + newItem.setText(self.colIndex[HCols.WCOUNT], str(novIdx["wCount"])) + newItem.setText(self.colIndex[HCols.PCOUNT], str(novIdx["pCount"])) + newItem.setTextAlignment(self.colIndex[HCols.CCOUNT], Qt.AlignRight) + newItem.setTextAlignment(self.colIndex[HCols.WCOUNT], Qt.AlignRight) + newItem.setTextAlignment(self.colIndex[HCols.PCOUNT], Qt.AlignRight) theRefs = self.theIndex.getReferences(tHandle, sTitle) - self._setItemText(newItem, self.I_POV, ", ".join(theRefs[nwKeyWords.POV_KEY])) - self._setItemText(newItem, self.I_CHAR, ", ".join(theRefs[nwKeyWords.CHAR_KEY])) - self._setItemText(newItem, self.I_PLOT, ", ".join(theRefs[nwKeyWords.PLOT_KEY])) - self._setItemText(newItem, self.I_TIME, ", ".join(theRefs[nwKeyWords.TIME_KEY])) - self._setItemText(newItem, self.I_WORLD, ", ".join(theRefs[nwKeyWords.WORLD_KEY])) - self._setItemText(newItem, self.I_OBJECT, ", ".join(theRefs[nwKeyWords.OBJECT_KEY])) - self._setItemText(newItem, self.I_ENTITY, ", ".join(theRefs[nwKeyWords.ENTITY_KEY])) - self._setItemText(newItem, self.I_CUSTOM, ", ".join(theRefs[nwKeyWords.CUSTOM_KEY])) + newItem.setText(self.colIndex[HCols.POV], ", ".join(theRefs[nwKeyWords.POV_KEY])) + newItem.setText(self.colIndex[HCols.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY])) + newItem.setText(self.colIndex[HCols.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY])) + newItem.setText(self.colIndex[HCols.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY])) + newItem.setText(self.colIndex[HCols.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY])) + newItem.setText(self.colIndex[HCols.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY])) + newItem.setText(self.colIndex[HCols.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY])) + newItem.setText(self.colIndex[HCols.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY])) return newItem - def _setItemText(self, tItem, colID, theText, rAlign=False): - """Set the correct text in the correct column, and if necessary, - right align it. - """ - if colID in self.colIndex: - tItem.setText(self.colIndex[colID], theText) - if rAlign: - tItem.setTextAlignment(self.colIndex[colID], Qt.AlignRight) +# END Class GuiProjectOutline + +class GuiOutlineHeaderMenu(QMenu): + + def __init__(self, theParent): + QMenu.__init__(self, theParent) + return -# END Class GuiProjectOutline +# END Class GuiOutlineHeaderMenu From 61ed3ee721d9b7cdce0892c006f3a2940b72eb9f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 9 Apr 2020 19:46:10 +0200 Subject: [PATCH 20/28] Added Outline enums and column labels to constants submodule --- nw/constants/__init__.py | 3 ++- nw/constants/constants.py | 20 +++++++++++++++++++- nw/constants/enum.py | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/nw/constants/__init__.py b/nw/constants/__init__.py index 769c50d5..eeab86cc 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -4,7 +4,7 @@ from nw.constants.constants import ( nwConst, nwFiles, nwKeyWords, nwLabels, nwDependencies, nwQuotes, nwUnicode ) from nw.constants.enum import ( - nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType + nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline ) __all__ = [ @@ -22,4 +22,5 @@ __all__ = [ "nwItemClass", "nwItemLayout", "nwItemType", + "nwOutline", ] diff --git a/nw/constants/constants.py b/nw/constants/constants.py index b39830bc..9e9adefc 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -10,7 +10,7 @@ """ -from nw.constants.enum import nwItemClass, nwItemLayout +from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline class nwConst(): @@ -104,6 +104,24 @@ class nwLabels(): nwKeyWords.ENTITY_KEY : "Entities", nwKeyWords.CUSTOM_KEY : "Custom", } + OUTLINE_COLS = { + nwOutline.TITLE : "Title", + nwOutline.LEVEL : "Level", + nwOutline.LABEL : "Document", + nwOutline.LINE : "Line", + nwOutline.CCOUNT : "Chars", + nwOutline.WCOUNT : "Words", + nwOutline.PCOUNT : "Pars", + nwOutline.POV : "POV", + nwOutline.CHAR : KEY_NAME[nwKeyWords.CHAR_KEY], + nwOutline.PLOT : KEY_NAME[nwKeyWords.PLOT_KEY], + nwOutline.TIME : KEY_NAME[nwKeyWords.TIME_KEY], + nwOutline.WORLD : KEY_NAME[nwKeyWords.WORLD_KEY], + nwOutline.OBJECT : KEY_NAME[nwKeyWords.OBJECT_KEY], + nwOutline.ENTITY : KEY_NAME[nwKeyWords.ENTITY_KEY], + nwOutline.CUSTOM : KEY_NAME[nwKeyWords.CUSTOM_KEY], + nwOutline.SYNOP : "Synopsis", + } # END Class nwLabels diff --git a/nw/constants/enum.py b/nw/constants/enum.py index 9d47581e..4c484a9a 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -88,3 +88,24 @@ class nwAlert(Enum): BUG = 3 # END Enum nwAlert + +class nwOutline(Enum): + + TITLE = 0 + LEVEL = 1 + LABEL = 2 + LINE = 3 + CCOUNT = 4 + WCOUNT = 5 + PCOUNT = 6 + POV = 7 + CHAR = 8 + PLOT = 9 + TIME = 10 + WORLD = 11 + OBJECT = 12 + ENTITY = 13 + CUSTOM = 14 + SYNOP = 15 + +# END Enum nwOutline From ff69667ab5283381f64e4a4e21c449282a89c14c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 9 Apr 2020 19:46:56 +0200 Subject: [PATCH 21/28] Added tab for Outline settings to config dialog --- nw/gui/dialogs/configeditor.py | 79 ++++++++++++++++++++++++++++++--- nw/gui/dialogs/projecteditor.py | 6 +-- 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/configeditor.py index 06dc5b92..87367004 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/configeditor.py @@ -20,7 +20,8 @@ from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( QDialog, QHBoxLayout, QVBoxLayout, QLineEdit, QLabel, QWidget, QTabWidget, QDialogButtonBox, QSpinBox, QGroupBox, QComboBox, QMessageBox, QCheckBox, - QGridLayout, QFontComboBox, QPushButton, QFileDialog + QGridLayout, QFontComboBox, QPushButton, QFileDialog, QListWidget, + QSizePolicy, QRadioButton, QButtonGroup ) from nw.tools import NWSpellCheck, NWSpellSimple, NWSpellEnchant @@ -44,12 +45,14 @@ class GuiConfigEditor(QDialog): self.setWindowTitle("Preferences") self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64)) - self.tabMain = GuiConfigEditGeneral(self.theParent) - self.tabEditor = GuiConfigEditEditor(self.theParent) + self.tabMain = GuiConfigEditGeneral(self.theParent) + self.tabEditor = GuiConfigEditEditor(self.theParent) + self.tabOutline = GuiConfigEditOutline(self.theParent) self.tabWidget = QTabWidget() - self.tabWidget.addTab(self.tabMain, "General") + self.tabWidget.addTab(self.tabMain, "General") self.tabWidget.addTab(self.tabEditor, "Editor") + self.tabWidget.addTab(self.tabOutline, "Outline") self.setLayout(self.outerBox) self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) @@ -79,11 +82,15 @@ class GuiConfigEditor(QDialog): validEntries = True needsRestart = False - retA, retB = self.tabMain.saveValues() + retA, retB = self.tabMain.saveValues() validEntries &= retA needsRestart |= retB - retA, retB = self.tabEditor.saveValues() + retA, retB = self.tabEditor.saveValues() + validEntries &= retA + needsRestart |= retB + + retA, retB = self.tabOutline.saveValues() validEntries &= retA needsRestart |= retB @@ -661,3 +668,63 @@ class GuiConfigEditEditor(QWidget): return False # END Class GuiConfigEditEditor + +class GuiConfigEditOutline(QWidget): + + def __init__(self, theParent): + QWidget.__init__(self, theParent) + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.outerBox = QVBoxLayout() + self.innerBox = QHBoxLayout() + + self.selectBox = QVBoxLayout() + self.selectList = QListWidget() + self.selectList.setMaximumWidth(180) + self.selectBox.addWidget(QLabel("Selected")) + self.selectBox.addWidget(self.selectList) + + self.availBox = QVBoxLayout() + self.availList = QListWidget() + self.availList.setMaximumWidth(180) + self.availBox.addWidget(QLabel("Available")) + self.availBox.addWidget(self.availList) + + self.buttonBox = QVBoxLayout() + + self.upButton = QPushButton("Move Up") + self.addButton = QPushButton("< Add") + self.delButton = QPushButton("Remove >") + self.downButton = QPushButton("Move Down") + + self.buttonBox.addStretch(1) + self.buttonBox.addWidget(self.upButton) + self.buttonBox.addWidget(self.addButton) + self.buttonBox.addWidget(self.delButton) + self.buttonBox.addWidget(self.downButton) + self.buttonBox.addStretch(1) + + self.projectOnly = QCheckBox("Use these settings for the current project only", self) + + # Assemble + self.innerBox.addLayout(self.selectBox) + self.innerBox.addStretch(1) + self.innerBox.addLayout(self.buttonBox) + self.innerBox.addStretch(1) + self.innerBox.addLayout(self.availBox) + + self.outerBox.addLayout(self.innerBox) + self.outerBox.addWidget(self.projectOnly) + + self.setLayout(self.outerBox) + + return + + def saveValues(self): + + validEntries = True + + return validEntries, False + +# END Class GuiConfigEditOutline diff --git a/nw/gui/dialogs/projecteditor.py b/nw/gui/dialogs/projecteditor.py index 3b8c0114..775462cc 100644 --- a/nw/gui/dialogs/projecteditor.py +++ b/nw/gui/dialogs/projecteditor.py @@ -162,10 +162,10 @@ class GuiProjectEditStatus(QWidget): self.colChanged = False self.selColour = None - self.mainBox = QHBoxLayout() - self.mainForm = QVBoxLayout() + self.mainBox = QHBoxLayout() + self.mainForm = QVBoxLayout() - self.listBox = QListWidget() + self.listBox = QListWidget() self.listBox.setDragDropMode(QAbstractItemView.InternalMove) self.listBox.itemSelectionChanged.connect(self._selectedItem) self.listBox.model().rowsMoved.connect(self._rowsMoved) From 15ade8d28cf2e8d0b402cdd42bdde812d55a35ec Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 9 Apr 2020 19:48:09 +0200 Subject: [PATCH 22/28] Various changes and fixes to the outline class --- nw/gui/elements/outline.py | 183 ++++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 82 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index b9f209e6..1ff1d2a2 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -23,50 +23,29 @@ from PyQt5.QtWidgets import ( QAbstractItemView ) -from nw.constants import nwKeyWords, nwLabels +from nw.constants import nwKeyWords, nwLabels, nwOutline logger = logging.getLogger(__name__) -class HCols(Enum): - - TITLE = 0 - LEVEL = 1 - LABEL = 2 - LINE = 3 - WCOUNT = 4 - CCOUNT = 5 - PCOUNT = 6 - SYNOP = 7 - POV = 8 - CHAR = 9 - PLOT = 10 - TIME = 11 - WORLD = 12 - OBJECT = 13 - ENTITY = 14 - CUSTOM = 15 - -# END Enum HCols - class GuiProjectOutline(QTreeWidget): - COL_LABELS = { - HCols.TITLE : "Title", - HCols.LEVEL : "Level", - HCols.LABEL : "Document", - HCols.LINE : "Line", - HCols.WCOUNT : "Words", - HCols.CCOUNT : "Chars", - HCols.PCOUNT : "Pars", - HCols.POV : "POV", - HCols.CHAR : nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY], - HCols.PLOT : nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY], - HCols.TIME : nwLabels.KEY_NAME[nwKeyWords.TIME_KEY], - HCols.WORLD : nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY], - HCols.OBJECT : nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY], - HCols.ENTITY : nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY], - HCols.CUSTOM : nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY], - HCols.SYNOP : "Synopsis", + COL_DEF = { + nwOutline.TITLE : (200, True, nwLabels.OUTLINE_COLS[nwOutline.TITLE]), + nwOutline.LEVEL : ( 40, False, nwLabels.OUTLINE_COLS[nwOutline.LEVEL]), + nwOutline.LABEL : (150, True, nwLabels.OUTLINE_COLS[nwOutline.LABEL]), + nwOutline.LINE : ( 40, False, nwLabels.OUTLINE_COLS[nwOutline.LINE]), + nwOutline.CCOUNT : ( 50, False, nwLabels.OUTLINE_COLS[nwOutline.CCOUNT]), + nwOutline.WCOUNT : ( 50, True, nwLabels.OUTLINE_COLS[nwOutline.WCOUNT]), + nwOutline.PCOUNT : ( 50, True, nwLabels.OUTLINE_COLS[nwOutline.PCOUNT]), + nwOutline.POV : (100, True, nwLabels.OUTLINE_COLS[nwOutline.POV]), + nwOutline.CHAR : (100, True, nwLabels.OUTLINE_COLS[nwOutline.CHAR]), + nwOutline.PLOT : (100, True, nwLabels.OUTLINE_COLS[nwOutline.PLOT]), + nwOutline.TIME : (100, False, nwLabels.OUTLINE_COLS[nwOutline.TIME]), + nwOutline.WORLD : (100, True, nwLabels.OUTLINE_COLS[nwOutline.WORLD]), + nwOutline.OBJECT : (100, False, nwLabels.OUTLINE_COLS[nwOutline.OBJECT]), + nwOutline.ENTITY : (100, False, nwLabels.OUTLINE_COLS[nwOutline.ENTITY]), + nwOutline.CUSTOM : (100, False, nwLabels.OUTLINE_COLS[nwOutline.CUSTOM]), + nwOutline.SYNOP : (200, True, nwLabels.OUTLINE_COLS[nwOutline.SYNOP]), } def __init__(self, theParent, theProject): @@ -79,7 +58,7 @@ class GuiProjectOutline(QTreeWidget): self.theProject = theProject self.theIndex = self.theParent.theIndex self.optState = self.theProject.optState - self.headerMenu = GuiOutlineHeaderMenu(self) + self.headerMenu = GuiOutlineHeaderMenu(self, self.COL_DEF, nwOutline.TITLE) self.firstView = True self.lastBuild = 0 @@ -96,11 +75,17 @@ class GuiProjectOutline(QTreeWidget): self.treeHead.sectionMoved.connect(self._columnMoved) self.treeMap = {} - self.treeOrder = self.COL_LABELS.keys() + self.treeOrder = self.COL_DEF.keys() self.treeNCols = len(self.treeOrder) - self.treeWidth = [150]*self.treeNCols + self.colWidth = [150]*self.treeNCols + self.colHidden = [False]*self.treeNCols self.colIndex = {} + # Set defaults + for hItem in self.treeOrder: + self.colWidth[hItem.value] = self.COL_DEF[hItem][0] + self.colHidden[hItem.value] = self.COL_DEF[hItem][1] + logger.debug("ProjectOutline initialisation complete") return @@ -136,9 +121,8 @@ class GuiProjectOutline(QTreeWidget): return def _headerRightClick(self, clickPos): - print(clickPos) - globPos = self.mapToGlobal(clickPos) - print(globPos) + self.headerMenu.exec_(self.mapToGlobal(clickPos)) + print("Menu closed") return def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx): @@ -148,7 +132,7 @@ class GuiProjectOutline(QTreeWidget): rebuild. """ self.treeOrder.insert(newVisualIdx, self.treeOrder.pop(oldVisualIdx)) - self.treeWidth.insert(newVisualIdx, self.treeWidth.pop(oldVisualIdx)) + self.colWidth.insert(newVisualIdx, self.colWidth.pop(oldVisualIdx)) return ## @@ -162,11 +146,11 @@ class GuiProjectOutline(QTreeWidget): # Load whatever we saved last time, regardless of wether it # contains the correct names or number of columns. - keysOrder = self.COL_LABELS.keys() + keysOrder = self.COL_DEF.keys() tempOrder = self.optState.getValue("GuiProjectOutline", "headerOrder", keysOrder) treeOrder = [] for hName in tempOrder: - for hItem in HCols: + for hItem in nwOutline: if hItem.name == hName: treeOrder.append(hItem) @@ -184,11 +168,16 @@ class GuiProjectOutline(QTreeWidget): logger.error("Failed to extract outline column order from previous session") logger.error("Column count doesn't match %d != %d" % (len(treeOrder), self.treeNCols)) - # The columns widths we just fill whatever we've got, and append - # the rest with defaults, and truncate to desired length. - tempWidth = self.optState.getValue("GuiProjectOutline", "headerWidth", []) - treeWidth = [int(w) for w in tempWidth] - self.treeWidth = (treeWidth + self.treeWidth)[0:self.treeNCols] + # The columns widths and hidden state we just fill with whatever + # we've got, and append the rest with defaults, and truncate to + # desired length. + tmpWidth = self.optState.getValue("GuiProjectOutline", "columnWidth", []) + colWidth = [int(w) for w in tmpWidth] + self.colWidth = (colWidth + self.colWidth)[0:self.treeNCols] + + tmpHidden = self.optState.getValue("GuiProjectOutline", "columnHidden", []) + colHidden = [int(w) for w in tmpHidden] + self.colHidden = (colHidden + self.colHidden)[0:self.treeNCols] return @@ -197,15 +186,22 @@ class GuiProjectOutline(QTreeWidget): and column width. """ - treeWidth = [] + # If we haven't built the tree, there is nothing to save. + if self.lastBuild == 0: + return + treeOrder = [] + colWidth = [] + colHidden = [] for iCol in range(self.columnCount()): treeOrder.append(self.treeOrder[iCol].name) iLog = self.treeHead.logicalIndex(iCol) - treeWidth.append(self.columnWidth(iLog)) + colWidth.append(self.columnWidth(iLog)) + colHidden.append(self.isColumnHidden(iLog)) - self.optState.setValue("GuiProjectOutline", "headerOrder", treeOrder) - self.optState.setValue("GuiProjectOutline", "headerWidth", treeWidth) + self.optState.setValue("GuiProjectOutline", "headerOrder", treeOrder) + self.optState.setValue("GuiProjectOutline", "columnWidth", colWidth) + self.optState.setValue("GuiProjectOutline", "columnHidden", colHidden) self.optState.saveSettings() return @@ -216,18 +212,18 @@ class GuiProjectOutline(QTreeWidget): theLabels = [] for i, hItem in enumerate(self.treeOrder): - theLabels.append(self.COL_LABELS[hItem]) + theLabels.append(self.COL_DEF[hItem][2]) self.colIndex[hItem] = i self.clear() self.setHeaderLabels(theLabels) - for n, colW in enumerate(self.treeWidth): + for n, colW in enumerate(self.colWidth): self.setColumnWidth(n,colW) headItem = self.headerItem() - headItem.setTextAlignment(self.colIndex[HCols.CCOUNT],Qt.AlignRight) - headItem.setTextAlignment(self.colIndex[HCols.WCOUNT],Qt.AlignRight) - headItem.setTextAlignment(self.colIndex[HCols.PCOUNT],Qt.AlignRight) + headItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight) + headItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight) + headItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight) currTitle = None currChapter = None @@ -296,27 +292,27 @@ class GuiProjectOutline(QTreeWidget): newItem = QTreeWidgetItem() - newItem.setText(self.colIndex[HCols.TITLE], novIdx["title"]) - newItem.setText(self.colIndex[HCols.LEVEL], novIdx["level"]) - newItem.setText(self.colIndex[HCols.LABEL], nwItem.itemName) - newItem.setText(self.colIndex[HCols.LINE], sTitle[1:]) - newItem.setText(self.colIndex[HCols.SYNOP], novIdx["synopsis"]) - newItem.setText(self.colIndex[HCols.CCOUNT], str(novIdx["cCount"])) - newItem.setText(self.colIndex[HCols.WCOUNT], str(novIdx["wCount"])) - newItem.setText(self.colIndex[HCols.PCOUNT], str(novIdx["pCount"])) - newItem.setTextAlignment(self.colIndex[HCols.CCOUNT], Qt.AlignRight) - newItem.setTextAlignment(self.colIndex[HCols.WCOUNT], Qt.AlignRight) - newItem.setTextAlignment(self.colIndex[HCols.PCOUNT], Qt.AlignRight) + newItem.setText(self.colIndex[nwOutline.TITLE], novIdx["title"]) + newItem.setText(self.colIndex[nwOutline.LEVEL], novIdx["level"]) + newItem.setText(self.colIndex[nwOutline.LABEL], nwItem.itemName) + newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:]) + newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"]) + newItem.setText(self.colIndex[nwOutline.CCOUNT], str(novIdx["cCount"])) + newItem.setText(self.colIndex[nwOutline.WCOUNT], str(novIdx["wCount"])) + newItem.setText(self.colIndex[nwOutline.PCOUNT], str(novIdx["pCount"])) + newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight) + newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight) + newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight) theRefs = self.theIndex.getReferences(tHandle, sTitle) - newItem.setText(self.colIndex[HCols.POV], ", ".join(theRefs[nwKeyWords.POV_KEY])) - newItem.setText(self.colIndex[HCols.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY])) - newItem.setText(self.colIndex[HCols.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY])) - newItem.setText(self.colIndex[HCols.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY])) - newItem.setText(self.colIndex[HCols.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY])) - newItem.setText(self.colIndex[HCols.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY])) - newItem.setText(self.colIndex[HCols.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY])) - newItem.setText(self.colIndex[HCols.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY])) + newItem.setText(self.colIndex[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY])) + newItem.setText(self.colIndex[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY])) + newItem.setText(self.colIndex[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY])) + newItem.setText(self.colIndex[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY])) + newItem.setText(self.colIndex[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY])) + newItem.setText(self.colIndex[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY])) + newItem.setText(self.colIndex[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY])) + newItem.setText(self.colIndex[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY])) return newItem @@ -324,9 +320,32 @@ class GuiProjectOutline(QTreeWidget): class GuiOutlineHeaderMenu(QMenu): - def __init__(self, theParent): + def __init__(self, theParent, colDefault, skipCol): QMenu.__init__(self, theParent) + mnuHead = QAction("Select Columns", self) + self.addAction(mnuHead) + self.addSeparator() + + self.actionMap = {} + + for hItem in nwOutline: + if hItem == skipCol: + continue + if hItem not in colDefault: + continue + self.actionMap[hItem] = QAction(colDefault[hItem][2], self) + self.actionMap[hItem].setCheckable(True) + self.actionMap[hItem].setChecked(colDefault[hItem][1]) + self.actionMap[hItem].toggled.connect( + lambda isChecked, tItem=hItem : self._columnToggled(isChecked, tItem) + ) + self.addAction(self.actionMap[hItem]) + + return + + def _columnToggled(self, isChecked, theItem): + print(isChecked, theItem.name) return # END Class GuiOutlineHeaderMenu From 9678a49f224f1851073a75b0f8395d83864766b6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Apr 2020 17:57:40 +0200 Subject: [PATCH 23/28] Drop the Outline settings from teh main Preferences --- nw/gui/dialogs/configeditor.py | 69 +--------------------------------- 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/configeditor.py index 87367004..0f56e8dd 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/configeditor.py @@ -20,8 +20,7 @@ from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( QDialog, QHBoxLayout, QVBoxLayout, QLineEdit, QLabel, QWidget, QTabWidget, QDialogButtonBox, QSpinBox, QGroupBox, QComboBox, QMessageBox, QCheckBox, - QGridLayout, QFontComboBox, QPushButton, QFileDialog, QListWidget, - QSizePolicy, QRadioButton, QButtonGroup + QGridLayout, QFontComboBox, QPushButton, QFileDialog ) from nw.tools import NWSpellCheck, NWSpellSimple, NWSpellEnchant @@ -47,12 +46,10 @@ class GuiConfigEditor(QDialog): self.tabMain = GuiConfigEditGeneral(self.theParent) self.tabEditor = GuiConfigEditEditor(self.theParent) - self.tabOutline = GuiConfigEditOutline(self.theParent) self.tabWidget = QTabWidget() self.tabWidget.addTab(self.tabMain, "General") self.tabWidget.addTab(self.tabEditor, "Editor") - self.tabWidget.addTab(self.tabOutline, "Outline") self.setLayout(self.outerBox) self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) @@ -90,10 +87,6 @@ class GuiConfigEditor(QDialog): validEntries &= retA needsRestart |= retB - retA, retB = self.tabOutline.saveValues() - validEntries &= retA - needsRestart |= retB - if needsRestart: msgBox = QMessageBox() msgBox.information( @@ -668,63 +661,3 @@ class GuiConfigEditEditor(QWidget): return False # END Class GuiConfigEditEditor - -class GuiConfigEditOutline(QWidget): - - def __init__(self, theParent): - QWidget.__init__(self, theParent) - - self.mainConf = nw.CONFIG - self.theParent = theParent - self.outerBox = QVBoxLayout() - self.innerBox = QHBoxLayout() - - self.selectBox = QVBoxLayout() - self.selectList = QListWidget() - self.selectList.setMaximumWidth(180) - self.selectBox.addWidget(QLabel("Selected")) - self.selectBox.addWidget(self.selectList) - - self.availBox = QVBoxLayout() - self.availList = QListWidget() - self.availList.setMaximumWidth(180) - self.availBox.addWidget(QLabel("Available")) - self.availBox.addWidget(self.availList) - - self.buttonBox = QVBoxLayout() - - self.upButton = QPushButton("Move Up") - self.addButton = QPushButton("< Add") - self.delButton = QPushButton("Remove >") - self.downButton = QPushButton("Move Down") - - self.buttonBox.addStretch(1) - self.buttonBox.addWidget(self.upButton) - self.buttonBox.addWidget(self.addButton) - self.buttonBox.addWidget(self.delButton) - self.buttonBox.addWidget(self.downButton) - self.buttonBox.addStretch(1) - - self.projectOnly = QCheckBox("Use these settings for the current project only", self) - - # Assemble - self.innerBox.addLayout(self.selectBox) - self.innerBox.addStretch(1) - self.innerBox.addLayout(self.buttonBox) - self.innerBox.addStretch(1) - self.innerBox.addLayout(self.availBox) - - self.outerBox.addLayout(self.innerBox) - self.outerBox.addWidget(self.projectOnly) - - self.setLayout(self.outerBox) - - return - - def saveValues(self): - - validEntries = True - - return validEntries, False - -# END Class GuiConfigEditOutline From 0991d1b1a80be821903aa91b88ac3c0bf24cf1c3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Apr 2020 17:58:13 +0200 Subject: [PATCH 24/28] The menu for selecting columsn and storing column width now works --- nw/gui/elements/outline.py | 225 +++++++++++++++++++++++++------------ 1 file changed, 156 insertions(+), 69 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 1ff1d2a2..c6c2324c 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -29,23 +29,42 @@ logger = logging.getLogger(__name__) class GuiProjectOutline(QTreeWidget): - COL_DEF = { - nwOutline.TITLE : (200, True, nwLabels.OUTLINE_COLS[nwOutline.TITLE]), - nwOutline.LEVEL : ( 40, False, nwLabels.OUTLINE_COLS[nwOutline.LEVEL]), - nwOutline.LABEL : (150, True, nwLabels.OUTLINE_COLS[nwOutline.LABEL]), - nwOutline.LINE : ( 40, False, nwLabels.OUTLINE_COLS[nwOutline.LINE]), - nwOutline.CCOUNT : ( 50, False, nwLabels.OUTLINE_COLS[nwOutline.CCOUNT]), - nwOutline.WCOUNT : ( 50, True, nwLabels.OUTLINE_COLS[nwOutline.WCOUNT]), - nwOutline.PCOUNT : ( 50, True, nwLabels.OUTLINE_COLS[nwOutline.PCOUNT]), - nwOutline.POV : (100, True, nwLabels.OUTLINE_COLS[nwOutline.POV]), - nwOutline.CHAR : (100, True, nwLabels.OUTLINE_COLS[nwOutline.CHAR]), - nwOutline.PLOT : (100, True, nwLabels.OUTLINE_COLS[nwOutline.PLOT]), - nwOutline.TIME : (100, False, nwLabels.OUTLINE_COLS[nwOutline.TIME]), - nwOutline.WORLD : (100, True, nwLabels.OUTLINE_COLS[nwOutline.WORLD]), - nwOutline.OBJECT : (100, False, nwLabels.OUTLINE_COLS[nwOutline.OBJECT]), - nwOutline.ENTITY : (100, False, nwLabels.OUTLINE_COLS[nwOutline.ENTITY]), - nwOutline.CUSTOM : (100, False, nwLabels.OUTLINE_COLS[nwOutline.CUSTOM]), - nwOutline.SYNOP : (200, True, nwLabels.OUTLINE_COLS[nwOutline.SYNOP]), + DEF_WIDTH = { + nwOutline.TITLE : 200, + nwOutline.LEVEL : 40, + nwOutline.LABEL : 150, + nwOutline.LINE : 40, + nwOutline.CCOUNT : 50, + nwOutline.WCOUNT : 50, + nwOutline.PCOUNT : 50, + nwOutline.POV : 100, + nwOutline.CHAR : 100, + nwOutline.PLOT : 100, + nwOutline.TIME : 100, + nwOutline.WORLD : 100, + nwOutline.OBJECT : 100, + nwOutline.ENTITY : 100, + nwOutline.CUSTOM : 100, + nwOutline.SYNOP : 200, + } + + DEF_HIDDEN = { + nwOutline.TITLE : False, + nwOutline.LEVEL : True, + nwOutline.LABEL : False, + nwOutline.LINE : True, + nwOutline.CCOUNT : True, + nwOutline.WCOUNT : False, + nwOutline.PCOUNT : False, + nwOutline.POV : False, + nwOutline.CHAR : False, + nwOutline.PLOT : False, + nwOutline.TIME : True, + nwOutline.WORLD : False, + nwOutline.OBJECT : True, + nwOutline.ENTITY : True, + nwOutline.CUSTOM : True, + nwOutline.SYNOP : False, } def __init__(self, theParent, theProject): @@ -58,7 +77,7 @@ class GuiProjectOutline(QTreeWidget): self.theProject = theProject self.theIndex = self.theParent.theIndex self.optState = self.theProject.optState - self.headerMenu = GuiOutlineHeaderMenu(self, self.COL_DEF, nwOutline.TITLE) + self.headerMenu = GuiOutlineHeaderMenu(self) self.firstView = True self.lastBuild = 0 @@ -75,28 +94,45 @@ class GuiProjectOutline(QTreeWidget): self.treeHead.sectionMoved.connect(self._columnMoved) self.treeMap = {} - self.treeOrder = self.COL_DEF.keys() - self.treeNCols = len(self.treeOrder) - self.colWidth = [150]*self.treeNCols - self.colHidden = [False]*self.treeNCols + self.treeOrder = [] + self.colWidth = {} + self.colHidden = {} self.colIndex = {} + self.treeNCols = 0 - # Set defaults - for hItem in self.treeOrder: - self.colWidth[hItem.value] = self.COL_DEF[hItem][0] - self.colHidden[hItem.value] = self.COL_DEF[hItem][1] + self.initOutline() + self.headerMenu.setHiddenState(self.colHidden) logger.debug("ProjectOutline initialisation complete") return - def refreshTree(self): + def initOutline(self): + """Set the default values for the Outline tree. + """ + + self.treeOrder = [] + self.colWidth = {} + self.colHidden = {} + self.colIndex = {} + self.treeNCols = 0 + + for hItem in nwOutline: + self.treeOrder.append(hItem) + self.colWidth[hItem] = self.DEF_WIDTH[hItem] + self.colHidden[hItem] = self.DEF_HIDDEN[hItem] + + self.treeNCols = len(self.treeOrder) + + return + + def refreshTree(self, overRide=False): """Called whenever the Outline tab is activated and controls what data to load, and if necessary, force a rebuild of the tree. """ - if self.firstView: + if self.firstView or overRide: self._loadHeaderState() self._populateTree() @@ -121,18 +157,24 @@ class GuiProjectOutline(QTreeWidget): return def _headerRightClick(self, clickPos): + """Show the header column menu, and check afterwards if a + column's visibility was changed. + """ self.headerMenu.exec_(self.mapToGlobal(clickPos)) - print("Menu closed") + + hItem = self.headerMenu.toggledItem + if hItem is not None: + self.setColumnHidden(self.colIndex[hItem], not self.headerMenu.toggleState) + self.headerMenu.toggledItem = None + self.headerMenu.toggleState = None + return def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx): - """Make sure the order and width read from settings file, or - with default values, is kept up-to-date when columns are moved - around. Otherwise, the original order will be restored on a tree - rebuild. + """Make sure the order array is up to date with the actual order + of the columns. """ self.treeOrder.insert(newVisualIdx, self.treeOrder.pop(oldVisualIdx)) - self.colWidth.insert(newVisualIdx, self.colWidth.pop(oldVisualIdx)) return ## @@ -146,19 +188,19 @@ class GuiProjectOutline(QTreeWidget): # Load whatever we saved last time, regardless of wether it # contains the correct names or number of columns. - keysOrder = self.COL_DEF.keys() - tempOrder = self.optState.getValue("GuiProjectOutline", "headerOrder", keysOrder) + tempOrder = self.optState.getValue("GuiProjectOutline", "headerOrder", []) treeOrder = [] for hName in tempOrder: - for hItem in nwOutline: - if hItem.name == hName: - treeOrder.append(hItem) + try: + treeOrder.append(nwOutline[hName]) + except: + logger.warning("Ignored unknown outline column '%s'" % str(hName)) # Add columns that were not in tempOrder to treeOrder, but in # the default column order. - for cItem in keysOrder: - if cItem not in treeOrder: - treeOrder.append(cItem) + for hItem in nwOutline: + if hItem not in treeOrder: + treeOrder.append(hItem) # Check that we now have a complete list, and only if so, save # the order loaded from file. Otherwise, we keep the default. @@ -168,22 +210,31 @@ class GuiProjectOutline(QTreeWidget): logger.error("Failed to extract outline column order from previous session") logger.error("Column count doesn't match %d != %d" % (len(treeOrder), self.treeNCols)) - # The columns widths and hidden state we just fill with whatever - # we've got, and append the rest with defaults, and truncate to - # desired length. - tmpWidth = self.optState.getValue("GuiProjectOutline", "columnWidth", []) - colWidth = [int(w) for w in tmpWidth] - self.colWidth = (colWidth + self.colWidth)[0:self.treeNCols] + # We load the column widths and hidden state we find in the + # file, and leave the rest in their default state. + tmpWidth = self.optState.getValue("GuiProjectOutline", "columnWidth", {}) + for hName in tmpWidth: + try: + self.colWidth[nwOutline[hName]] = tmpWidth[hName] + except: + logger.warning("Ignored unknown outline column '%s'" % str(hName)) - tmpHidden = self.optState.getValue("GuiProjectOutline", "columnHidden", []) - colHidden = [int(w) for w in tmpHidden] - self.colHidden = (colHidden + self.colHidden)[0:self.treeNCols] + tmpHidden = self.optState.getValue("GuiProjectOutline", "columnHidden", {}) + for hName in tmpHidden: + try: + self.colHidden[nwOutline[hName]] = tmpHidden[hName] + except: + logger.warning("Ignored unknown outline column '%s'" % str(hName)) + + self.headerMenu.setHiddenState(self.colHidden) return def _saveHeaderState(self): - """Save the state of the main tree header, that is, column order - and column width. + """Save the state of the main tree header, that is, column + order, column width and column hidden state. We don't want to + save the current width of hidden columns though. This preserves + the last known width in case they're unhidden again. """ # If we haven't built the tree, there is nothing to save. @@ -191,13 +242,24 @@ class GuiProjectOutline(QTreeWidget): return treeOrder = [] - colWidth = [] - colHidden = [] + colWidth = {} + colHidden = {} + + for hItem in nwOutline: + colWidth[hItem.name] = self.colWidth[hItem] + colHidden[hItem.name] = self.colHidden[hItem] + for iCol in range(self.columnCount()): - treeOrder.append(self.treeOrder[iCol].name) + hName = self.treeOrder[iCol].name + treeOrder.append(hName) + iLog = self.treeHead.logicalIndex(iCol) - colWidth.append(self.columnWidth(iLog)) - colHidden.append(self.isColumnHidden(iLog)) + logWidth = self.columnWidth(iLog) + logHidden = self.isColumnHidden(iLog) + + colHidden[hName] = logHidden + if not logHidden and logWidth > 0: + colWidth[hName] = logWidth self.optState.setValue("GuiProjectOutline", "headerOrder", treeOrder) self.optState.setValue("GuiProjectOutline", "columnWidth", colWidth) @@ -212,13 +274,14 @@ class GuiProjectOutline(QTreeWidget): theLabels = [] for i, hItem in enumerate(self.treeOrder): - theLabels.append(self.COL_DEF[hItem][2]) + theLabels.append(nwLabels.OUTLINE_COLS[hItem]) self.colIndex[hItem] = i self.clear() self.setHeaderLabels(theLabels) - for n, colW in enumerate(self.colWidth): - self.setColumnWidth(n,colW) + for hItem in self.treeOrder: + self.setColumnWidth(self.colIndex[hItem], self.colWidth[hItem]) + self.setColumnHidden(self.colIndex[hItem], self.colHidden[hItem]) headItem = self.headerItem() headItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight) @@ -320,7 +383,7 @@ class GuiProjectOutline(QTreeWidget): class GuiOutlineHeaderMenu(QMenu): - def __init__(self, theParent, colDefault, skipCol): + def __init__(self, theParent): QMenu.__init__(self, theParent) mnuHead = QAction("Select Columns", self) @@ -328,24 +391,48 @@ class GuiOutlineHeaderMenu(QMenu): self.addSeparator() self.actionMap = {} - for hItem in nwOutline: - if hItem == skipCol: + if hItem == nwOutline.TITLE: continue - if hItem not in colDefault: - continue - self.actionMap[hItem] = QAction(colDefault[hItem][2], self) + self.actionMap[hItem] = QAction(nwLabels.OUTLINE_COLS[hItem], self) self.actionMap[hItem].setCheckable(True) - self.actionMap[hItem].setChecked(colDefault[hItem][1]) self.actionMap[hItem].toggled.connect( lambda isChecked, tItem=hItem : self._columnToggled(isChecked, tItem) ) self.addAction(self.actionMap[hItem]) + self.ignoreToggle = False + self.toggledItem = None + self.toggleState = None + + return + + def setHiddenState(self, hiddenState): + """Overwrite the checked state of the columns as the inverse of + the hidden state. Skip the TITLE column as it cannot be hidden. + """ + self.ignoreToggle = True + + for hItem in nwOutline: + if hItem == nwOutline.TITLE or hItem not in hiddenState: + continue + self.actionMap[hItem].setChecked(not hiddenState[hItem]) + + self.ignoreToggle = False + return def _columnToggled(self, isChecked, theItem): - print(isChecked, theItem.name) + """The user has toggled the visibility of a column. Record the + change, but do nothing more. + """ + if self.ignoreToggle: + return + + logger.verbose("User toggled Outline column '%s'" % theItem.name) + self.toggledItem = theItem + self.toggleState = isChecked + return # END Class GuiOutlineHeaderMenu From b11ae8ec9fec52988241914a03ebd821f60e4b55 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Apr 2020 17:58:35 +0200 Subject: [PATCH 25/28] Updated sample project xml --- sample/sampleNovel/nwProject.nwx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 448a7375..a6828145 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project From ac3adade18396d59ee6917f6ed14c85090ffcbe9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 11 Apr 2020 18:24:01 +0200 Subject: [PATCH 26/28] Some Outline class cleanup and restructuring --- nw/gui/elements/outline.py | 64 ++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index c6c2324c..02f313e2 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -13,14 +13,11 @@ import logging import nw -from os import path from time import time -from enum import Enum -from PyQt5.QtCore import Qt, QByteArray +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QMenu, QAction, - QAbstractItemView + QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView ) from nw.constants import nwKeyWords, nwLabels, nwOutline @@ -157,17 +154,9 @@ class GuiProjectOutline(QTreeWidget): return def _headerRightClick(self, clickPos): - """Show the header column menu, and check afterwards if a - column's visibility was changed. + """Show the header column menu. """ self.headerMenu.exec_(self.mapToGlobal(clickPos)) - - hItem = self.headerMenu.toggledItem - if hItem is not None: - self.setColumnHidden(self.colIndex[hItem], not self.headerMenu.toggleState) - self.headerMenu.toggledItem = None - self.headerMenu.toggleState = None - return def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx): @@ -177,6 +166,15 @@ class GuiProjectOutline(QTreeWidget): self.treeOrder.insert(newVisualIdx, self.treeOrder.pop(oldVisualIdx)) return + def _menuColumnToggled(self, isChecked, theItem): + """Receive the changes to column visibility forwarded by the + header context menu. + """ + logger.verbose("User toggled Outline column '%s'" % theItem.name) + if theItem in self.colIndex: + self.setColumnHidden(self.colIndex[theItem], not isChecked) + return + ## # Internal Functions ## @@ -187,7 +185,8 @@ class GuiProjectOutline(QTreeWidget): """ # Load whatever we saved last time, regardless of wether it - # contains the correct names or number of columns. + # contains the correct names or number of columns. The names + # must be valid though. tempOrder = self.optState.getValue("GuiProjectOutline", "headerOrder", []) treeOrder = [] for hName in tempOrder: @@ -196,8 +195,7 @@ class GuiProjectOutline(QTreeWidget): except: logger.warning("Ignored unknown outline column '%s'" % str(hName)) - # Add columns that were not in tempOrder to treeOrder, but in - # the default column order. + # Add columns that was not in the file to the treeOrder array. for hItem in nwOutline: if hItem not in treeOrder: treeOrder.append(hItem) @@ -210,8 +208,8 @@ class GuiProjectOutline(QTreeWidget): logger.error("Failed to extract outline column order from previous session") logger.error("Column count doesn't match %d != %d" % (len(treeOrder), self.treeNCols)) - # We load the column widths and hidden state we find in the - # file, and leave the rest in their default state. + # We load whatever column widths and hidden states we find in + # the file, and leave the rest in their default state. tmpWidth = self.optState.getValue("GuiProjectOutline", "columnWidth", {}) for hName in tmpWidth: try: @@ -386,6 +384,9 @@ class GuiOutlineHeaderMenu(QMenu): def __init__(self, theParent): QMenu.__init__(self, theParent) + self.theParent = theParent + self.acceptToggle = True + mnuHead = QAction("Select Columns", self) self.addAction(mnuHead) self.addSeparator() @@ -401,38 +402,33 @@ class GuiOutlineHeaderMenu(QMenu): ) self.addAction(self.actionMap[hItem]) - self.ignoreToggle = False - self.toggledItem = None - self.toggleState = None - return def setHiddenState(self, hiddenState): """Overwrite the checked state of the columns as the inverse of the hidden state. Skip the TITLE column as it cannot be hidden. """ - self.ignoreToggle = True + self.acceptToggle = False for hItem in nwOutline: if hItem == nwOutline.TITLE or hItem not in hiddenState: continue self.actionMap[hItem].setChecked(not hiddenState[hItem]) - self.ignoreToggle = False + self.acceptToggle = True return + ## + # Slots + ## + def _columnToggled(self, isChecked, theItem): - """The user has toggled the visibility of a column. Record the - change, but do nothing more. + """The user has toggled the visibility of a column. Forward the + event to the parent class only if we're accepting changes. """ - if self.ignoreToggle: - return - - logger.verbose("User toggled Outline column '%s'" % theItem.name) - self.toggledItem = theItem - self.toggleState = isChecked - + if self.acceptToggle: + self.theParent._menuColumnToggled(isChecked, theItem) return # END Class GuiOutlineHeaderMenu From 92f54af894993c791a484232e820f6c33e2cd015 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 13 Apr 2020 16:02:44 +0200 Subject: [PATCH 27/28] Added some useful menu option, and settings in project file --- nw/gui/elements/outline.py | 14 +++++++++++++- nw/gui/mainmenu.py | 31 +++++++++++++++++++++++++++++++ nw/guimain.py | 15 ++++++++++++++- nw/project/index.py | 25 +++++++++++++++++++++++++ nw/project/project.py | 11 +++++++++++ sample/sampleNovel/nwProject.nwx | 7 ++++--- 6 files changed, 98 insertions(+), 5 deletions(-) diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py index 02f313e2..26ce7855 100644 --- a/nw/gui/elements/outline.py +++ b/nw/gui/elements/outline.py @@ -129,11 +129,23 @@ class GuiProjectOutline(QTreeWidget): tree. """ + # If it's the first time, we always build if self.firstView or overRide: self._loadHeaderState() self._populateTree() + self.firstView = False + return - self.firstView = False + # If the novel index has changed since the tree was last built, + # we rebuild the tree from the updated index. + lastChange = self.theParent.theIndex.timeNovel + logger.verbose("Last outline build: %.3f" % self.lastBuild) + logger.verbose("Novel index change: %.3f" % lastChange) + + doBuild = lastChange > self.lastBuild and self.theProject.autoOutline + if doBuild or overRide: + logger.debug("Rebuilding Project Outline") + self._populateTree() return diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 7d866564..a2898263 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -69,6 +69,13 @@ class GuiMainMenu(QMenuBar): self.aSpellCheck.setChecked(theMode) return + def setAutoOutline(self, theMode): + """Set the auto outline check box to theMode. Used during + initialisation. + """ + self.aAutoOutline.setChecked(theMode) + return + ## # Menu Action ## @@ -85,6 +92,12 @@ class GuiMainMenu(QMenuBar): self.theParent.docEditor.setSpellCheck(None) return True + def _toggleAutoOutline(self, theMode): + """Toggle auto outline when the menu entry is checked. + """ + self.theProject.setAutoOutline(theMode) + return True + def _toggleViewComments(self): self.mainConf.setViewComments(self.aViewDocComments.isChecked()) self.theParent.docViewer.reloadText() @@ -646,6 +659,24 @@ class GuiMainMenu(QMenuBar): self.aRebuildIndex.triggered.connect(self.theParent.rebuildIndex) self.toolsMenu.addAction(self.aRebuildIndex) + # Tools > Rebuild Outline + self.aRebuildOutline = QAction("Rebuild Outline", self) + self.aRebuildOutline.setStatusTip("Rebuild the novel outline tree") + self.aRebuildOutline.setShortcut("F10") + self.aRebuildOutline.triggered.connect(self.theParent.rebuildOutline) + self.toolsMenu.addAction(self.aRebuildOutline) + + # Tools > Toggle Auto Build Outline + self.aAutoOutline = QAction("Auto-Update Outline", self) + self.aAutoOutline.setStatusTip("Update project outline when a novel file is changed") + self.aAutoOutline.setCheckable(True) + self.aAutoOutline.toggled.connect(self._toggleAutoOutline) + self.aAutoOutline.setShortcut("Ctrl+F10") + self.toolsMenu.addAction(self.aAutoOutline) + + # Tools > Separator + self.toolsMenu.addSeparator() + # Tools > Backup self.aBackupProject = QAction("Backup Project", self) self.aBackupProject.setStatusTip("Backup Project") diff --git a/nw/guimain.py b/nw/guimain.py index fb35e4dc..cb1331bc 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -386,6 +386,7 @@ class GuiMain(QMainWindow): self.rebuildTree() self.docEditor.setDictionaries() self.docEditor.setSpellCheck(self.theProject.spellCheck) + self.mainMenu.setAutoOutline(self.theProject.autoOutline) self.statusBar.setRefTime(self.theProject.projOpened) # Restore previously open documents, if any @@ -438,6 +439,7 @@ class GuiMain(QMainWindow): def openDocument(self, tHandle): if self.hasProject: self.closeDocument() + self.tabWidget.setCurrentWidget(self.splitView) if self.docEditor.loadText(tHandle): self.docEditor.setFocus() self.theProject.setLastEdited(tHandle) @@ -464,6 +466,9 @@ class GuiMain(QMainWindow): logger.debug("No document selected, giving up") return False + # Make sure main tab is in Editor view + self.tabWidget.setCurrentWidget(self.splitView) + if self.docViewer.loadText(tHandle) and not self.viewPane.isVisible(): bPos = self.splitMain.sizes() self.viewPane.setVisible(True) @@ -652,6 +657,14 @@ class GuiMain(QMainWindow): return True + def rebuildOutline(self): + """Force a rebuild of the Outline view. + """ + logger.verbose("Forcing a rebuild of the Project Outline") + self.tabWidget.setCurrentWidget(self.splitOutline) + self.projView.refreshTree(overRide=True) + return True + ## # Main Dialogs ## @@ -986,7 +999,7 @@ class GuiMain(QMainWindow): def _keyPressEscape(self): """When the escape key is pressed somewhere in the main window, - do the following, in order. + do the following, in order: """ if self.searchBar.isVisible(): self.searchBar.setVisible(False) diff --git a/nw/project/index.py b/nw/project/index.py index 499d5765..6e344bfe 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -63,6 +63,11 @@ class NWIndex(): self.noteIndex = None self.textCounts = None + # TimeStamps + self.timeNovel = 0 + self.timeNote = 0 + self.timeIndex = 0 + self.clearIndex() return @@ -72,14 +77,21 @@ class NWIndex(): ## def clearIndex(self): + """Clear the index dictionaries and time stamps. + """ self.tagIndex = {} self.refIndex = {} self.novelIndex = {} self.noteIndex = {} self.textCounts = {} + self.timeNovel = 0 + self.timeNote = 0 + self.timeIndex = 0 return def deleteHandle(self, tHandle): + """Delete all entries of a given document handle. + """ delTags = [] for tTag in self.tagIndex: @@ -129,6 +141,11 @@ class NWIndex(): if "textCounts" in theData.keys(): self.textCounts = theData["textCounts"] + nowTime = time() + self.timeNovel = nowTime + self.timeNote = nowTime + self.timeIndex = nowTime + self.checkIndex() return True @@ -289,6 +306,14 @@ class NWIndex(): cC, wC, pC = countWords(theText) self.textCounts[tHandle] = [cC, wC, pC] + # Update timestamps for index changes + nowTime = time() + self.timeIndex = nowTime + if isNovel: + self.timeNovel = nowTime + else: + self.timeNote = nowTime + return True def indexTitle(self, tHandle, isNovel, aLine, nLine, itemLayout): diff --git a/nw/project/project.py b/nw/project/project.py index 202491af..d6f13c9a 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -67,6 +67,7 @@ class NWProject(): # Project Settings self.spellCheck = False + self.autoOutline = True self.statusItems = None self.importItems = None self.lastEdited = None @@ -169,6 +170,7 @@ class NWProject(): self.bookAuthors = [] self.autoReplace = {} self.spellCheck = False + self.autoOutline = True self.statusItems = NWStatus() self.statusItems.addEntry("New", (100,100,100)) self.statusItems.addEntry("Note", (200, 50, 0)) @@ -299,6 +301,8 @@ class NWProject(): continue if xItem.tag == "spellCheck": self.spellCheck = checkBool(xItem.text,False) + elif xItem.tag == "autoOutline": + self.autoOutline = checkBool(xItem.text,True) elif xItem.tag == "lastEdited": self.lastEdited = checkString(xItem.text,None,True) elif xItem.tag == "lastViewed": @@ -390,6 +394,7 @@ class NWProject(): # Save Project Settings xSettings = etree.SubElement(nwXML, "settings") self._saveProjectValue(xSettings, "spellCheck", self.spellCheck) + self._saveProjectValue(xSettings, "autoOutline", self.autoOutline) self._saveProjectValue(xSettings, "lastEdited", self.lastEdited) self._saveProjectValue(xSettings, "lastViewed", self.lastViewed) self._saveProjectValue(xSettings, "lastWordCount", self.currWCount) @@ -513,6 +518,12 @@ class NWProject(): self.setProjectChanged(True) return True + def setAutoOutline(self, theMode): + if self.autoOutline != theMode: + self.autoOutline = theMode + self.setProjectChanged(True) + return True + def setTreeOrder(self, newOrder): if len(self.treeOrder) != len(newOrder): logger.warning("Size of new and old tree order does not match") diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index a6828145..53cd2fe6 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -9,7 +9,8 @@ True - 88706ddc78b1b + True + 6a2d6d5f4f401 b3e74dbc1f584 869 @@ -70,7 +71,7 @@ 12 3 0 - 212 + 15 Making a Scene From f62a8a45dbb2eb9724f139d729b0cbf675a94c75 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 13 Apr 2020 16:02:55 +0200 Subject: [PATCH 28/28] Added new option to test project files --- tests/reference/gui/0_nwProject.nwx | 1 + tests/reference/gui/1_nwProject.nwx | 1 + tests/reference/gui/2_nwProject.nwx | 1 + tests/reference/gui/3_nwProject.nwx | 1 + tests/reference/proj/1_nwProject.nwx | 1 + tests/reference/proj/2_nwProject.nwx | 1 + 6 files changed, 6 insertions(+) diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index c3337adb..e5368533 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -7,6 +7,7 @@ False + True None None 0 diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 193087e6..02119a65 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -7,6 +7,7 @@ True + True 31489056e0916 31489056e0916 86 diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index 02c51167..ca896ec2 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -9,6 +9,7 @@ False + True None None 0 diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 503c011c..f0bda720 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -7,6 +7,7 @@ False + True None None 0 diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index 4960024b..319d24b3 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -7,6 +7,7 @@ False + True None None 0 diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index 0a8e6b4d..a40a2e8d 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -7,6 +7,7 @@ False + True None None 0