diff --git a/nw/config.py b/nw/config.py index 6dcaf7dd..4e7ecde5 100644 --- a/nw/config.py +++ b/nw/config.py @@ -65,9 +65,6 @@ class Config: self.mainPanePos = [300, 800] self.docPanePos = [400, 400] - ## Dialogs - self.dlgTimeLine = [600, 400] - ## Project self.autoSaveProj = 60 self.autoSaveDoc = 30 @@ -182,7 +179,6 @@ class Config: self.treeColWidth = self._parseLine(cnfParse, cnfSec, "treecols", self.CNF_LIST, self.treeColWidth) self.mainPanePos = self._parseLine(cnfParse, cnfSec, "mainpane", self.CNF_LIST, self.mainPanePos) self.docPanePos = self._parseLine(cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos) - self.dlgTimeLine = self._parseLine(cnfParse, cnfSec, "timeline", self.CNF_LIST, self.dlgTimeLine) ## Project cnfSec = "Project" @@ -245,7 +241,6 @@ class Config: cnfParse.set(cnfSec,"treecols", self._packList(self.treeColWidth)) cnfParse.set(cnfSec,"mainpane", self._packList(self.mainPanePos)) cnfParse.set(cnfSec,"docpane", self._packList(self.docPanePos)) - cnfParse.set(cnfSec,"timeline", self._packList(self.dlgTimeLine)) ## Project cnfSec = "Project" @@ -326,15 +321,6 @@ class Config: self.confChanged = True return True - def setTLineSize(self, newWidth, newHeight): - if abs(self.dlgTimeLine[0] - newWidth) > 5: - self.dlgTimeLine[0] = newWidth - self.confChanged = True - if abs(self.dlgTimeLine[1] - newHeight) > 5: - self.dlgTimeLine[1] = newHeight - self.confChanged = True - return True - def setTreeColWidths(self, colWidths): self.treeColWidth = colWidths self.confChanged = True diff --git a/nw/constants.py b/nw/constants.py index 36484f00..a2349570 100644 --- a/nw/constants.py +++ b/nw/constants.py @@ -26,6 +26,7 @@ class nwFiles(): SESS_INFO = "sessionInfo.log" INDEX_FILE = "tagsIndex.json" EXPORT_OPT = "exportOptions.json" + TLINE_OPT = "timelineOptions.json" # END Class nwFiles diff --git a/nw/gui/export.py b/nw/gui/export.py index 0f493993..da09f3f2 100644 --- a/nw/gui/export.py +++ b/nw/gui/export.py @@ -12,7 +12,6 @@ import logging import time -import json import nw from os import path @@ -26,10 +25,10 @@ from PyQt5.QtWidgets import ( from nw.project.document import NWDoc from nw.tools.translate import numberToWord +from nw.tools.optlaststate import OptLastState from nw.convert.textfile import TextFile from nw.convert.htmlfile import HtmlFile from nw.convert.markdownfile import MarkdownFile -from nw.common import checkString, checkBool, checkInt from nw.constants import nwFiles from nw.enum import nwItemType @@ -45,7 +44,8 @@ class GuiExport(QDialog): self.mainConf = nw.CONFIG self.theParent = theParent self.theProject = theProject - self.optState = ExportLastState(self.theProject) + self.optState = ExportLastState(self.theProject,nwFiles.EXPORT_OPT) + self.optState.loadSettings() self.outerBox = QHBoxLayout() self.innerBox = QVBoxLayout() @@ -443,10 +443,10 @@ class GuiExportMain(QWidget): # END Class GuiExportMain -class ExportLastState(): +class ExportLastState(OptLastState): - def __init__(self, theProject): - self.theProject = theProject + def __init__(self, theProject, theFile): + OptLastState.__init__(self, theProject, theFile) self.theState = { "wNovel" : True, "wNotes" : False, @@ -462,52 +462,6 @@ class ExportLastState(): self.stringOpt = ("chFormat","unFormat","scFormat","seFormat","saveTo") self.boolOpt = ("wNovel","wNotes","wComments") self.intOpt = ("eFormat","fixWidth") - self.loadSettings() return - def loadSettings(self): - stateFile = path.join(self.theProject.projMeta, nwFiles.EXPORT_OPT) - theState = {} - if path.isfile(stateFile): - logger.debug("Loading export options file") - try: - with open(stateFile,mode="r") as inFile: - theJson = inFile.read() - theState = json.loads(theJson) - except Exception as e: - logger.error("Failed to load export options file") - logger.error(str(e)) - return False - for anOpt in theState: - self.theState[anOpt] = theState[anOpt] - return True - - def saveSettings(self): - stateFile = path.join(self.theProject.projMeta, nwFiles.EXPORT_OPT) - logger.debug("Saving export options file") - try: - with open(stateFile,mode="w+") as outFile: - outFile.write(json.dumps(self.theState, indent=2)) - except Exception as e: - logger.error("Failed to save export options file") - logger.error(str(e)) - return False - return True - - def setSetting(self, setName, setValue): - if setName in self.theState: - self.theState[setName] = setValue - else: - return False - return True - - def getSetting(self, setName): - if setName in self.stringOpt: - 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.intOpt: - return checkInt(self.theState[setName],self.theState[setName],False) - return None - # END Class ExportLastState diff --git a/nw/gui/timelineview.py b/nw/gui/timelineview.py index 9f13c2a0..160a3f8a 100644 --- a/nw/gui/timelineview.py +++ b/nw/gui/timelineview.py @@ -13,13 +13,18 @@ import logging import nw +from os import path from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QColor, QPixmap from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, - QDialogButtonBox, QLabel, QPushButton, QHeaderView + QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QDialogButtonBox, QLabel, + QPushButton, QHeaderView, QGridLayout, QGroupBox, QCheckBox ) +from nw.tools.optlaststate import OptLastState +from nw.constants import nwFiles +from nw.enum import nwItemClass + logger = logging.getLogger(__name__) class GuiTimeLineView(QDialog): @@ -33,16 +38,27 @@ class GuiTimeLineView(QDialog): 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.setMinimumSize(*self.mainConf.dlgTimeLine) + self.setMinimumWidth(700) + self.setMinimumHeight(400) + self.resize( + self.optState.getSetting("winWidth"), + self.optState.getSetting("winHeight") + ) + # TimeLine Table self.mainTable = QTableWidget() self.mainTable.setGridStyle(Qt.NoPen) @@ -54,6 +70,41 @@ class GuiTimeLineView(QDialog): 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.filterChar = QCheckBox("Character tags", self) + self.filterChar.setChecked(self.optState.getSetting("fChar")) + self.filterWorld = QCheckBox("Location tags", self) + self.filterWorld.setChecked(self.optState.getSetting("fWorld")) + self.filterTime = QCheckBox("Timeline tags", self) + self.filterTime.setChecked(self.optState.getSetting("fTime")) + self.filterObject = QCheckBox("Object tags", self) + self.filterObject.setChecked(self.optState.getSetting("fObject")) + self.filterCustom = QCheckBox("Custom tags", self) + self.filterCustom.setChecked(self.optState.getSetting("fCustom")) + + 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.optHideGrid.addWidget(self.hideUnused, 0, 1) + + # Button Box self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) @@ -63,14 +114,21 @@ class GuiTimeLineView(QDialog): self.btnRefresh = QPushButton("Refresh Table") self.btnRefresh.clicked.connect(self._buildNovelList) - self.setLayout(self.outerBox) - self.outerBox.addWidget(self.mainTable) - self.outerBox.addLayout(self.bottomBox) 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() @@ -82,13 +140,28 @@ class GuiTimeLineView(QDialog): def _buildNovelList(self): - self.theIndex.buildNovelList() - self.numRows = len(self.theIndex.novelList) - self.numCols = len(self.theIndex.tagIndex.keys()) - self.mainTable.clear() + self.theIndex.buildNovelList() + + self.numRows = len(self.theIndex.novelList) self.mainTable.setRowCount(self.numRows) - self.mainTable.setColumnCount(self.numCols) + + 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] @@ -96,14 +169,17 @@ class GuiTimeLineView(QDialog): newItem = QTableWidgetItem("%s%s " % (" "*iDepth,iTitle)) self.mainTable.setVerticalHeaderItem(n, newItem) - theMap = self.theIndex.buildTagNovelMap(self.theIndex.tagIndex.keys()) - nCol = 0 + 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 = QPixmap(10,10) pxNew.fill(QColor(0,120,0)) lblNew = QLabel() lblNew.setPixmap(pxNew) @@ -111,7 +187,7 @@ class GuiTimeLineView(QDialog): lblNew.setAttribute(Qt.WA_TranslucentBackground) self.mainTable.setCellWidget(n, nCol, lblNew) elif theCols[n] == 2: - pxNew = QPixmap(10,10) + pxNew = QPixmap(10,10) pxNew.fill(QColor(0,0,120)) lblNew = QLabel() lblNew.setPixmap(pxNew) @@ -123,8 +199,54 @@ class GuiTimeLineView(QDialog): return def _doClose(self): - self.mainConf.setTLineSize(self.width(), self.height()) + + 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 # 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/winmain.py b/nw/gui/winmain.py index ea351d15..5e2f4b64 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -54,11 +54,11 @@ class GuiMain(QMainWindow): logger.info("Starting %s" % nw.__package__) logger.debug("Initialising GUI ...") - self.mainConf = nw.CONFIG - self.theTheme = Theme(self) - self.theProject = NWProject(self) - self.theIndex = NWIndex(self.theProject, self) - self.hasProject = False + self.mainConf = nw.CONFIG + self.theTheme = Theme(self) + self.theProject = NWProject(self) + self.theIndex = NWIndex(self.theProject, self) + self.hasProject = False logger.info("Qt5 Version: %s (%d)" % (self.mainConf.verQtString, self.mainConf.verQtValue)) logger.info("PyQt5 Version: %s (%d)" % (self.mainConf.verPyQtString, self.mainConf.verPyQtValue)) diff --git a/nw/project/index.py b/nw/project/index.py index 8a4c6bf1..622b26c3 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -319,18 +319,24 @@ class NWIndex(): return True - def buildTagNovelMap(self, theTags): + def buildTagNovelMap(self, theTags, theFilters=None): tagMap = {} tagClass = {} + exClass = [] + + if theFilters is not None: + if "exClass" in theFilters.keys(): + exClass = theFilters["exClass"] for theTag in theTags: - tagMap[theTag] = [0]*len(self.novelOrder) try: tagClass[theTag] = nwItemClass[self.tagIndex[theTag][2]] except: logger.error("Could not map '%s' to nwItemClass" % self.tagIndex[theTag][2]) tagClass[theTag] = None + 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]: @@ -342,6 +348,13 @@ class NWIndex(): 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/nw/tools/optlaststate.py b/nw/tools/optlaststate.py new file mode 100644 index 00000000..b4fb9a12 --- /dev/null +++ b/nw/tools/optlaststate.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +"""novelWriter Options Last State + + novelWriter – Options Last State +================================== + Class holding the last state of GUI options + + File History: + Created: 2019-10-21 [0.3.1] + +""" + +import logging +import json +import nw + +from os import path + +from nw.common import checkString, checkBool, checkInt + +logger = logging.getLogger(__name__) + +class OptLastState(): + + def __init__(self, theProject, theFile): + self.theProject = theProject + self.theFile = theFile + self.theState = {} + self.stringOpt = () + self.boolOpt = () + self.intOpt = () + return + + def loadSettings(self): + stateFile = path.join(self.theProject.projMeta,self.theFile) + theState = {} + if path.isfile(stateFile): + logger.debug("Loading options file") + try: + with open(stateFile,mode="r") as inFile: + theJson = inFile.read() + theState = json.loads(theJson) + except Exception as e: + 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): + stateFile = path.join(self.theProject.projMeta,self.theFile) + logger.debug("Saving options file") + try: + with open(stateFile,mode="w+") as outFile: + outFile.write(json.dumps(self.theState, indent=2)) + except Exception as e: + logger.error("Failed to save options file") + logger.error(str(e)) + return False + return True + + def setSetting(self, setName, setValue): + if setName in self.theState: + self.theState[setName] = setValue + else: + return False + return True + + def getSetting(self, setName): + if setName in self.stringOpt: + 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.intOpt: + return checkInt(self.theState[setName],self.theState[setName],False) + return None + +# END Class OptLastState diff --git a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd index 0a53e976..5b15e42b 100644 --- a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd +++ b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd @@ -3,7 +3,7 @@ % Begin Meta @pov: Jane @char: John -@location: Earth +@location: Space % End Meta Some text here would look good as well, and maybe some "dialogue"? diff --git a/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd b/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd index 525f47c8..52291896 100644 --- a/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd +++ b/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd @@ -1,7 +1,7 @@ ### Another Scene @pov: John -@location: Space +@location: Earth This is the second scene in out story. We have no idea what’s going on, so we’re just going to ramble on until we have a few lines of text so that the editor has something to work with. diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index dba1732b..e6689e70 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -82,7 +82,7 @@ 713 132 6 - 85 + 72 Another Scene diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index 8b6bff87..7ea29925 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -8,7 +8,6 @@ geometry = 1100, 650 treecols = 120, 30, 50 mainpane = 300, 800 docpane = 400, 400 -timeline = 600, 400 [Project] autosaveproject = 60