diff --git a/nw/config.py b/nw/config.py index a12aaa9a..1627270f 100644 --- a/nw/config.py +++ b/nw/config.py @@ -25,6 +25,11 @@ class Config: WIN_WIDTH = 0 WIN_HEIGHT = 1 + CNF_STR = 0 + CNF_INT = 1 + CNF_BOOL = 2 + CNF_LIST = 3 + def __init__(self): # Set Application Variables @@ -38,6 +43,7 @@ class Config: self.confFile = None self.homePath = None self.appPath = None + self.appRoot = None self.guiPath = None self.themePath = None @@ -60,6 +66,7 @@ class Config: self.textWidth = 600 self.textMargin = [40, 40] self.textSize = 13 + self.tabWidth = 40 self.doJustify = True self.autoSelect = True self.doReplace = True @@ -68,7 +75,7 @@ class Config: self.doReplaceDash = True self.doReplaceDots = True self.wordCountTimer = 5.0 - + self.fmtDoubleQuotes = ["“","”"] self.fmtSingleQuotes = ["‘","’"] self.fmtApostrophe = "’" @@ -95,6 +102,7 @@ class Config: self.confFile = self.appHandle+".conf" self.homePath = path.expanduser("~") self.appPath = path.dirname(__file__) + self.appRoot = path.join(self.appPath,path.pardir) self.guiPath = path.join(self.appPath,"gui") self.themePath = path.join(self.appPath,"themes") @@ -117,140 +125,105 @@ class Config: def loadConfig(self): logger.debug("Loading config file") - confParser = configparser.ConfigParser() + cnfParse = configparser.ConfigParser() try: - confParser.read_file(open(path.join(self.confPath,self.confFile))) + cnfParse.read_file(open(path.join(self.confPath,self.confFile))) except Exception as e: logger.error("Could not load config file") return False - # Get options - ## Main cnfSec = "Main" - if confParser.has_section(cnfSec): - if confParser.has_option(cnfSec,"theme"): - self.guiTheme = confParser.get(cnfSec,"theme") + self.guiTheme = self._parseLine(cnfParse, cnfSec, "theme", self.CNF_STR, self.guiTheme) ## Sizes cnfSec = "Sizes" - if confParser.has_section(cnfSec): - if confParser.has_option(cnfSec,"geometry"): - self.winGeometry = self.unpackList( - confParser.get(cnfSec,"geometry"), 2, self.winGeometry - ) - if confParser.has_option(cnfSec,"treecols"): - self.treeColWidth = self.unpackList( - confParser.get(cnfSec,"treecols"), 3, self.treeColWidth - ) - if confParser.has_option(cnfSec,"mainpane"): - self.mainPanePos = self.unpackList( - confParser.get(cnfSec,"mainpane"), 2, self.mainPanePos - ) - if confParser.has_option(cnfSec,"docpane"): - self.docPanePos = self.unpackList( - confParser.get(cnfSec,"docpane"), 2, self.docPanePos - ) + self.winGeometry = self._parseLine(cnfParse, cnfSec, "geometry", self.CNF_LIST, self.winGeometry) + 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) ## Project cnfSec = "Project" - if confParser.has_section(cnfSec): - if confParser.has_option(cnfSec,"autosaveproject"): - self.autoSaveProj = confParser.getint(cnfSec,"autosaveproject") - if confParser.has_option(cnfSec,"autosavedoc"): - self.autoSaveDoc = confParser.getint(cnfSec,"autosavedoc") + self.autoSaveProj = self._parseLine(cnfParse, cnfSec, "autosaveproject", self.CNF_INT, self.autoSaveProj) + self.autoSaveDoc = self._parseLine(cnfParse, cnfSec, "autosavedoc", self.CNF_INT, self.autoSaveDoc) ## Editor cnfSec = "Editor" - if confParser.has_section(cnfSec): - if confParser.has_option(cnfSec,"fixedwidth"): - self.textFixedW = confParser.getboolean(cnfSec,"fixedwidth") - if confParser.has_option(cnfSec,"width"): - self.textWidth = confParser.getint(cnfSec,"width") - if confParser.has_option(cnfSec,"margins"): - self.textMargin = self.unpackList( - confParser.get(cnfSec,"margins"), 2, self.textMargin - ) - if confParser.has_option(cnfSec,"textsize"): - self.textSize = confParser.getint(cnfSec,"textsize") - if confParser.has_option(cnfSec,"justify"): - self.doJustify = confParser.getboolean(cnfSec,"justify") - if confParser.has_option(cnfSec,"autoselect"): - self.autoSelect = confParser.getboolean(cnfSec,"autoselect") - if confParser.has_option(cnfSec,"autoreplace"): - self.doReplace = confParser.getboolean(cnfSec,"autoreplace") - if confParser.has_option(cnfSec,"repsquotes"): - self.doReplaceSQuote = confParser.getboolean(cnfSec,"repsquotes") - if confParser.has_option(cnfSec,"repdquotes"): - self.doReplaceDQuote = confParser.getboolean(cnfSec,"repdquotes") - if confParser.has_option(cnfSec,"repdash"): - self.doReplaceDash = confParser.getboolean(cnfSec,"repdash") - if confParser.has_option(cnfSec,"repdots"): - self.doReplaceDots = confParser.getboolean(cnfSec,"repdots") - if confParser.has_option(cnfSec,"spellcheck"): - self.spellLanguage = confParser.get(cnfSec,"spellcheck") + self.textFixedW = self._parseLine(cnfParse, cnfSec, "fixedwidth", self.CNF_BOOL, self.textFixedW) + self.textWidth = self._parseLine(cnfParse, cnfSec, "width", self.CNF_INT, self.textWidth) + self.textMargin = self._parseLine(cnfParse, cnfSec, "margins", self.CNF_LIST, self.textMargin) + self.textSize = self._parseLine(cnfParse, cnfSec, "textsize", self.CNF_INT, self.textSize) + self.tabWidth = self._parseLine(cnfParse, cnfSec, "tabwidth", self.CNF_INT, self.tabWidth) + self.doJustify = self._parseLine(cnfParse, cnfSec, "justify", self.CNF_BOOL, self.doJustify) + self.autoSelect = self._parseLine(cnfParse, cnfSec, "autoselect", self.CNF_BOOL, self.autoSelect) + self.doReplace = self._parseLine(cnfParse, cnfSec, "autoreplace", self.CNF_BOOL, self.doReplace) + self.doReplaceSQuote = self._parseLine(cnfParse, cnfSec, "repsquotes", self.CNF_BOOL, self.doReplaceSQuote) + self.doReplaceDQuote = self._parseLine(cnfParse, cnfSec, "repdquotes", self.CNF_BOOL, self.doReplaceDQuote) + self.doReplaceDash = self._parseLine(cnfParse, cnfSec, "repdash", self.CNF_BOOL, self.doReplaceDash) + self.doReplaceDots = self._parseLine(cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots) + self.spellLanguage = self._parseLine(cnfParse, cnfSec, "spellcheck", self.CNF_STR, self.spellLanguage) ## Path cnfSec = "Path" - if confParser.has_section(cnfSec): - for i in range(10): - if confParser.has_option(cnfSec,"recent%d" % i): - self.recentList[i] = confParser.get(cnfSec,"recent%d" % i) + for i in range(10): + self.recentList[i] = self._parseLine(cnfParse, cnfSec, "recent%d" % i,self.CNF_STR, self.recentList[i]) return True def saveConfig(self): logger.debug("Saving config file") - confParser = configparser.ConfigParser() + cnfParse = configparser.ConfigParser() # Set options ## Main cnfSec = "Main" - confParser.add_section(cnfSec) - confParser.set(cnfSec,"timestamp", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) - confParser.set(cnfSec,"theme", str(self.guiTheme)) + cnfParse.add_section(cnfSec) + cnfParse.set(cnfSec,"timestamp", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + cnfParse.set(cnfSec,"theme", str(self.guiTheme)) ## Sizes cnfSec = "Sizes" - confParser.add_section(cnfSec) - confParser.set(cnfSec,"geometry", self.packList(self.winGeometry)) - confParser.set(cnfSec,"treecols", self.packList(self.treeColWidth)) - confParser.set(cnfSec,"mainpane", self.packList(self.mainPanePos)) - confParser.set(cnfSec,"docpane", self.packList(self.docPanePos)) + cnfParse.add_section(cnfSec) + cnfParse.set(cnfSec,"geometry", self._packList(self.winGeometry)) + cnfParse.set(cnfSec,"treecols", self._packList(self.treeColWidth)) + cnfParse.set(cnfSec,"mainpane", self._packList(self.mainPanePos)) + cnfParse.set(cnfSec,"docpane", self._packList(self.docPanePos)) ## Project cnfSec = "Project" - confParser.add_section(cnfSec) - confParser.set(cnfSec,"autosaveproject", str(self.autoSaveProj)) - confParser.set(cnfSec,"autosavedoc", str(self.autoSaveDoc)) + cnfParse.add_section(cnfSec) + cnfParse.set(cnfSec,"autosaveproject", str(self.autoSaveProj)) + cnfParse.set(cnfSec,"autosavedoc", str(self.autoSaveDoc)) ## Editor cnfSec = "Editor" - confParser.add_section(cnfSec) - confParser.set(cnfSec,"fixedwidth", str(self.textFixedW)) - confParser.set(cnfSec,"width", str(self.textWidth)) - confParser.set(cnfSec,"margins", self.packList(self.textMargin)) - confParser.set(cnfSec,"textsize", str(self.textSize)) - confParser.set(cnfSec,"justify", str(self.doJustify)) - confParser.set(cnfSec,"autoselect", str(self.autoSelect)) - confParser.set(cnfSec,"autoreplace",str(self.doReplace)) - confParser.set(cnfSec,"repsquotes", str(self.doReplaceSQuote)) - confParser.set(cnfSec,"repdquotes", str(self.doReplaceDQuote)) - confParser.set(cnfSec,"repdash", str(self.doReplaceDash)) - confParser.set(cnfSec,"repdots", str(self.doReplaceDots)) - confParser.set(cnfSec,"spellcheck", str(self.spellLanguage)) + cnfParse.add_section(cnfSec) + cnfParse.set(cnfSec,"fixedwidth", str(self.textFixedW)) + cnfParse.set(cnfSec,"width", str(self.textWidth)) + cnfParse.set(cnfSec,"margins", self._packList(self.textMargin)) + cnfParse.set(cnfSec,"textsize", str(self.textSize)) + cnfParse.set(cnfSec,"tabwidth", str(self.tabWidth)) + cnfParse.set(cnfSec,"justify", str(self.doJustify)) + cnfParse.set(cnfSec,"autoselect", str(self.autoSelect)) + cnfParse.set(cnfSec,"autoreplace",str(self.doReplace)) + cnfParse.set(cnfSec,"repsquotes", str(self.doReplaceSQuote)) + cnfParse.set(cnfSec,"repdquotes", str(self.doReplaceDQuote)) + cnfParse.set(cnfSec,"repdash", str(self.doReplaceDash)) + cnfParse.set(cnfSec,"repdots", str(self.doReplaceDots)) + cnfParse.set(cnfSec,"spellcheck", str(self.spellLanguage)) ## Path cnfSec = "Path" - confParser.add_section(cnfSec) + cnfParse.add_section(cnfSec) for i in range(10): - confParser.set(cnfSec,"recent%d" % i, str(self.recentList[i])) + cnfParse.set(cnfSec,"recent%d" % i, str(self.recentList[i])) # Write config file try: - confParser.write(open(path.join(self.confPath,self.confFile),"w")) + cnfParse.write(open(path.join(self.confPath,self.confFile),"w")) self.confChanged = False except Exception as e: logger.error("Could not save config file") @@ -258,19 +231,6 @@ class Config: return True - def unpackList(self, inStr, listLen, listDefault, castTo=int): - inData = inStr.split(",") - outData = [] - for i in range(listLen): - try: - outData.append(castTo(inData[i])) - except: - outData.append(listDefault[i]) - return outData - - def packList(self, inData): - return ", ".join(str(inVal) for inVal in inData) - ## # Setters ## @@ -316,4 +276,36 @@ class Config: self.confChanged = True return True + ## + # Internal Functions + ## + + def _unpackList(self, inStr, listLen, listDefault, castTo=int): + inData = inStr.split(",") + outData = [] + for i in range(listLen): + try: + outData.append(castTo(inData[i])) + except: + outData.append(listDefault[i]) + return outData + + def _packList(self, inData): + return ", ".join(str(inVal) for inVal in inData) + + def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault): + if cnfParse.has_section(cnfSec): + if cnfParse.has_option(cnfSec, cnfName): + if cnfType == self.CNF_STR: + return cnfParse.get(cnfSec, cnfName) + elif cnfType == self.CNF_INT: + return cnfParse.getint(cnfSec, cnfName) + elif cnfType == self.CNF_BOOL: + return cnfParse.getboolean(cnfSec, cnfName) + elif cnfType == self.CNF_LIST: + return self._unpackList( + cnfParse.get(cnfSec, cnfName), len(cnfDefault), cnfDefault + ) + return cnfDefault + # End Class Config diff --git a/nw/constants.py b/nw/constants.py index 7dc39f05..478a7f5f 100644 --- a/nw/constants.py +++ b/nw/constants.py @@ -12,6 +12,15 @@ from nw.enum import nwItemClass, nwItemLayout +class nwFiles(): + + APP_ICON = "novelWriter.svg" + PROJ_FILE = "nwProject.nwx" + PROJ_DICT = "wordlist.txt" + SESS_INFO = "sessionInfo.log" + +# END Class nwFiles + class nwLabels(): CLASS_NAME = { @@ -59,5 +68,4 @@ class nwLabels(): nwItemLayout.NOTE : "Nt", } - -# END nwLabels +# END Class nwLabels diff --git a/nw/gui/aboutview.py b/nw/gui/aboutview.py deleted file mode 100644 index fa3e747c..00000000 --- a/nw/gui/aboutview.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter GUI About View - - novelWriter – GUI About View -============================== - Class holding the tab viewing the about information - - File History: - Created: 2018-10-02 [0.0.1] - -""" - -import logging -import nw - -from os import path -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel -from PyQt5.QtSvg import QSvgWidget -from PyQt5.QtCore import Qt, QSize -from PyQt5.QtGui import QFont - -logger = logging.getLogger(__name__) - -class GuiAboutView(QWidget): - - def __init__(self): - QWidget.__init__(self) - - logger.debug("Initialising AboutView ...") - self.mainConf = nw.CONFIG - self.innerBox = QHBoxLayout() - self.outerBox = QVBoxLayout() - - logoPath = path.abspath(path.join(self.mainConf.appPath,"..","novelWriter.svg")) - logger.verbose("Loading image: %s" % logoPath) - nwLogo = QSvgWidget(logoPath) - nwLogo.setFixedSize(QSize(300,300)) - - nwName = QLabel() - nwName.setText(nw.__package__) - nwName.setAlignment(Qt.AlignCenter) - fnName = QFont() - fnName.setPointSize(22) - fnName.setBold(True) - nwName.setFont(fnName) - - nwVersion = QLabel() - nwVersion.setText("Version %s" % nw.__version__) - nwVersion.setAlignment(Qt.AlignCenter) - - nwCredits = QLabel() - nwCredits.setText("Created By: %s" % ",".join(nw.__credits__)) - nwCredits.setAlignment(Qt.AlignCenter) - - nwWebsite = QLabel() - nwWebsite.setText("%s" % (nw.__website__,nw.__website__)) - nwWebsite.setOpenExternalLinks(True) - nwWebsite.setAlignment(Qt.AlignCenter) - - self.outerBox.addStretch(1) - self.innerBox.addStretch(1) - self.innerBox.addWidget(nwLogo) - self.innerBox.addStretch(1) - self.outerBox.addLayout(self.innerBox) - self.outerBox.addWidget(nwName) - self.outerBox.addWidget(nwVersion) - self.outerBox.addWidget(nwCredits) - self.outerBox.addWidget(nwWebsite) - self.outerBox.addStretch(1) - - self.setLayout(self.outerBox) - - logger.debug("AboutView initialisation complete") - - return - -# END Class GuiAboutView diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 7ba063d7..c708536e 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -104,8 +104,12 @@ class GuiDocEditor(QTextEdit): mTB = self.mainConf.textMargin[0] mLR = self.mainConf.textMargin[1] self.setViewportMargins(mLR,mTB,mLR,mTB) + theOpt = QTextOption() + if self.mainConf.tabWidth is not None: + theOpt.setTabStopDistance(self.mainConf.tabWidth) if self.mainConf.doJustify: - self.theDoc.setDefaultTextOption(QTextOption(Qt.AlignJustify)) + theOpt.setAlignment(Qt.AlignJustify) + self.theDoc.setDefaultTextOption(theOpt) return True ## @@ -376,6 +380,7 @@ class GuiDocEditor(QTextEdit): self.paraCount = self.wCounter.paraCount self.theParent.statusBar.setCounts(self.charCount,self.wordCount,self.paraCount) self.theParent.treeView.propagateCount(tHandle, self.wordCount) + self.theParent.treeView.projectWordCount() return diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 4db6267a..697b2dea 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -194,13 +194,14 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.setCurrentBlockState(0) - if self.theDict is None or not self.spellCheck: + if self.theDict is None or not self.spellCheck or theText.startswith("@"): return rxSpell = self.spellRx.globalMatch(theText.replace("_"," "), 0) while rxSpell.hasNext(): rxMatch = rxSpell.next() if not self.theDict.check(rxMatch.captured(0)): + if rxMatch.captured(0) == rxMatch.captured(0).upper(): continue xPos = rxMatch.capturedStart(0) xLen = rxMatch.capturedLength(0) spFmt = self.format(xPos) diff --git a/nw/gui/doctree.py b/nw/gui/doctree.py index da074375..280fa09a 100644 --- a/nw/gui/doctree.py +++ b/nw/gui/doctree.py @@ -50,7 +50,7 @@ class GuiDocTree(QTreeWidget): self.setExpandsOnDoubleClick(True) self.setIndentation(13) self.setColumnCount(4) - self.setHeaderLabels(["Name","Count","Flags","Handle"]) + self.setHeaderLabels(["Name","Words","Flags","Handle"]) if not self.debugGUI: self.hideColumn(self.C_HANDLE) @@ -286,6 +286,18 @@ class GuiDocTree(QTreeWidget): self.propagateCount(pHandle, pCount, nDepth+1) return + def projectWordCount(self): + nWords = 0 + for n in range(self.topLevelItemCount()): + tItem = self.topLevelItem(n) + if tItem == self.orphRoot: + continue + nWords += int(tItem.text(self.C_COUNT)) + self.theProject.setProjectWordCount(nWords) + sWords = self.theProject.getSessionWordCount() + self.theParent.statusBar.setStats(nWords,sWords) + return + def buildTree(self): self.clear() for nwItem in self.theProject.getProjectItems(): diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 7cb5274d..981933ee 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -253,6 +253,13 @@ class GuiMainMenu(QMenuBar): menuItem.triggered.connect(self.theParent.saveDocument) self.docuMenu.addAction(menuItem) + # Document > Close + menuItem = QAction(QIcon.fromTheme("document-revert"), "Close Document", self) + menuItem.setStatusTip("Close Current Document") + menuItem.setShortcut("Ctrl+W") + menuItem.triggered.connect(self.theParent.closeDocEditor) + self.docuMenu.addAction(menuItem) + # Document > Separator self.docuMenu.addSeparator() diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index 3e68690f..484e9519 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -13,6 +13,9 @@ import logging import nw +from time import time +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtGui import QIcon, QColor, QPixmap from PyQt5.QtWidgets import QStatusBar, QLabel, QFrame logger = logging.getLogger(__name__) @@ -25,36 +28,74 @@ class GuiMainStatus(QStatusBar): logger.debug("Initialising GuiMainStatus ...") self.mainConf = nw.CONFIG + self.refTime = None + + self.iconGrey = QPixmap(16,16) + self.iconGrey.fill(QColor(120,120,120)) + self.iconYellow = QPixmap(16,16) + self.iconYellow.fill(QColor(120,120, 40)) + self.iconGreen = QPixmap(16,16) + self.iconGreen.fill(QColor( 40,120, 0)) + + self.boxStats = QLabel() + self.boxStats.setToolTip("Project Word Count | Session Word Count") + + self.boxTime = QLabel("") + self.boxTime.setToolTip("Session Time") + self.boxTime.setAlignment(Qt.AlignRight) + self.boxTime.setMinimumWidth(80) self.boxCounts = QLabel() - self.boxCounts.setToolTip("Character, Word, Paragraph Count") - self.boxCounts.setFrameStyle(QFrame.Panel | QFrame.Sunken); - self.addPermanentWidget(self.boxCounts) + self.boxCounts.setToolTip("Document Character | Word | Paragraph Count") - self.projChanged = QLabel("P") + self.projChanged = QLabel("") + self.projChanged.setFixedHeight(16) + self.projChanged.setFixedWidth(16) self.projChanged.setToolTip("Project Changes Saved") - self.projChanged.setFrameStyle(QFrame.Panel | QFrame.Sunken); - self.addPermanentWidget(self.projChanged) - self.docChanged = QLabel("D") + self.docChanged = QLabel("") + self.docChanged.setFixedHeight(16) + self.docChanged.setFixedWidth(16) self.docChanged.setToolTip("Document Changes Saved") - self.docChanged.setFrameStyle(QFrame.Panel | QFrame.Sunken); - self.addPermanentWidget(self.docChanged) self.boxDocHandle = QLabel() self.boxDocHandle.setFrameStyle(QFrame.Panel | QFrame.Sunken); + + # Add Them + self.addPermanentWidget(self.docChanged) + self.addPermanentWidget(self.boxCounts) + self.addPermanentWidget(QLabel(" ")) + self.addPermanentWidget(self.projChanged) + self.addPermanentWidget(self.boxStats) + self.addPermanentWidget(self.boxTime) if self.mainConf.debugGUI: self.addPermanentWidget(self.boxDocHandle) + self.setSizeGripEnabled(True) + + self.sessionTimer = QTimer() + self.sessionTimer.setInterval(1000) + self.sessionTimer.timeout.connect(self._updateTime) + self.sessionTimer.start() + logger.debug("GuiMainStatus initialisation complete") + self.clearStatus() + + return + + def clearStatus(self): + self.setRefTime(None) + self.setStats(0,0) self.setCounts(0,0,0) self.setDocHandleCount(None) self.setProjectStatus(None) self.setDocumentStatus(None) + self._updateTime() + return True - self.setSizeGripEnabled(True) - + def setRefTime(self, theTime): + self.refTime = theTime return def setStatus(self, theMessage, timeOut=10.0): @@ -63,28 +104,32 @@ class GuiMainStatus(QStatusBar): def setProjectStatus(self, isChanged): if isChanged is None: - self.projChanged.setStyleSheet("QLabel {background-color: rgba(120,120,120,1.0);}") + self.projChanged.setPixmap(self.iconGrey) elif isChanged == True: - self.projChanged.setStyleSheet("QLabel {background-color: rgba(120,120,40,1.0);}") + self.projChanged.setPixmap(self.iconYellow) elif isChanged == False: - self.projChanged.setStyleSheet("QLabel {background-color: rgba(40,120,0,1.0);}") + self.projChanged.setPixmap(self.iconGreen) else: - self.projChanged.setStyleSheet("QLabel {background-color: rgba(120,120,120,1.0);}") + self.projChanged.setPixmap(self.iconGrey) return def setDocumentStatus(self, isChanged): if isChanged is None: - self.docChanged.setStyleSheet("QLabel {background-color: rgba(120,120,120,1.0);}") + self.docChanged.setPixmap(self.iconGrey) elif isChanged == True: - self.docChanged.setStyleSheet("QLabel {background-color: rgba(120,120,40,1.0);}") + self.docChanged.setPixmap(self.iconYellow) elif isChanged == False: - self.docChanged.setStyleSheet("QLabel {background-color: rgba(40,120,0,1.0);}") + self.docChanged.setPixmap(self.iconGreen) else: - self.docChanged.setStyleSheet("QLabel {background-color: rgba(120,120,120,1.0);}") + self.docChanged.setPixmap(self.iconGrey) + return + + def setStats(self, pWC, sWC): + self.boxStats.setText("Project: {:d} : {:d}".format(pWC,sWC)) return def setCounts(self, cC, wC, pC): - self.boxCounts.setText("C: {:n}  W: {:n}  P: {:n}".format(cC,wC,pC)) + self.boxCounts.setText("Document: {:d} : {:d} : {:d}".format(cC,wC,pC)) return def setDocHandleCount(self, theHandle): @@ -94,4 +139,22 @@ class GuiMainStatus(QStatusBar): self.boxDocHandle.setText("%13s" % theHandle) return + ## + # Internal Functions + ## + + def _updateTime(self): + if self.refTime is None: + theTime = "00:00:00" + else: + # This is much faster than using datetime format + tS = int(time() - self.refTime) + tM = int(tS/60) + tH = int(tM/60) + tM = tM - tH*60 + tS = tS - tM*60 - tH*3600 + theTime = "%02d:%02d:%02d" % (tH,tM,tS) + self.boxTime.setText(theTime) + return + # END Class GuiMainStatus diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index 6eabeeb9..0de1c27f 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -36,6 +36,7 @@ from nw.project.item import NWItem from nw.convert.tokenizer import Tokenizer from nw.convert.tohtml import ToHtml from nw.enum import nwItemType, nwAlert +from nw.constants import nwFiles logger = logging.getLogger(__name__) @@ -53,7 +54,7 @@ class GuiMain(QMainWindow): self.resize(*self.mainConf.winGeometry) self._setWindowTitle() - self.setWindowIcon(QIcon(path.join(self.mainConf.appPath,"..","novelWriter.svg"))) + self.setWindowIcon(QIcon(path.join(self.mainConf.appRoot, nwFiles.APP_ICON))) self.theTheme.loadTheme() # Main GUI Elements @@ -139,43 +140,11 @@ class GuiMain(QMainWindow): return - def makeAlert(self, theMessage, theLevel=nwAlert.INFO): - """Alert both the user and the logger at the same time. Message can be either a string or an - array of strings. Severity level is 0 = info, 1 = warning, and 2 = error. - """ - - if isinstance(theMessage, list): - popMsg = "
".join(theMessage) - logMsg = theMessage - else: - popMsg = theMessage - logMsg = [theMessage] - - msgBox = QMessageBox() - if theLevel == nwAlert.INFO: - for msgLine in logMsg: - logger.info(msgLine) - msgBox.information(self, "Information", popMsg) - elif theLevel == nwAlert.WARN: - for msgLine in logMsg: - logger.warning(msgLine) - msgBox.warning(self, "Warning", popMsg) - elif theLevel == nwAlert.ERROR: - for msgLine in logMsg: - logger.error(msgLine) - msgBox.critical(self, "Error", popMsg) - elif theLevel == nwAlert.BUG: - for msgLine in logMsg: - logger.error(msgLine) - popMsg += "
This is a bug!" - msgBox.critical(self, "Internal Error", popMsg) - - return - def clearGUI(self): self.treeView.clearTree() self.docEditor.clearEditor() self.closeDocViewer() + self.statusBar.clearStatus() return True ## @@ -211,19 +180,22 @@ class GuiMain(QMainWindow): self.rebuildTree() self.saveProject() self.hasProject = True + self.statusBar.setRefTime(self.theProject.projOpened) return True def closeProject(self, isYes=False): - + """Closes the project if one is open. + isYes is passed on from the close application event so the user doesn't get prompted twice. + """ if not self.hasProject: + # There is no project loaded, everything OK return True - if not isYes: + if self.mainConf.showGUI and not isYes: msgBox = QMessageBox() msgRes = msgBox.question( - self, "Close Project", - "Close current project?
Unsaved changes will be saved." + self, "Close Project", "Save changes and close current project?" ) if msgRes != QMessageBox.Yes: return False @@ -235,29 +207,38 @@ class GuiMain(QMainWindow): saveOK = True if saveOK: - self.theProject.clearProject() + self.theProject.closeProject() self.clearGUI() self.hasProject = False - + return saveOK def openProject(self, projFile=None): - + """Open a project. + projFile is passed from the open recent projects menu, so can be set. If not, we pop the dialog. + """ if projFile is None: projFile = self.openProjectDialog() if projFile is None: return False + # Make sure any open project is cleared out first before we load another one if not self.closeProject(): return False - self.theProject.openProject(projFile) + # Try to open the project + if not self.theProject.openProject(projFile): + return False + + # Update GUI self._setWindowTitle(self.theProject.projName) self.rebuildTree() - self.docEditor.setPwl(path.join(self.theProject.projMeta,"wordlist.txt")) + self.docEditor.setPwl(path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)) self.docEditor.setSpellCheck(self.theProject.spellCheck) + self.statusBar.setRefTime(self.theProject.projOpened) self.mainMenu.updateMenu() + # Restore previously open documents, if any if self.theProject.lastEdited is not None: self.openDocument(self.theProject.lastEdited) if self.theProject.lastViewed is not None: @@ -268,7 +249,9 @@ class GuiMain(QMainWindow): return True def saveProject(self): - + """Save the current project. + """ + # If the project is new, it may not have a path, so we need one if self.theProject.projPath is None: projPath = self.saveProjectDialog() self.theProject.setProjectPath(projPath) @@ -286,13 +269,14 @@ class GuiMain(QMainWindow): ## def closeDocument(self): - self.saveDocument() + if self.docEditor.docChanged: + self.saveDocument() self.theDocument.clearDocument() + self.docEditor.clearEditor() return True def openDocument(self, tHandle): - if self.docEditor.docChanged: - self.saveDocument() + self.closeDocument() self.docEditor.setText(self.theDocument.openDocument(tHandle)) self.docEditor.setReadOnly(False) self.docEditor.setCursorPosition(self.theDocument.theItem.cursorPos) @@ -303,13 +287,13 @@ class GuiMain(QMainWindow): def saveDocument(self): if self.theDocument.theItem is not None: - docHtml = self.docEditor.getText() + docText = self.docEditor.getText() cursPos = self.docEditor.getCursorPosition() self.theDocument.theItem.setCharCount(self.docEditor.charCount) self.theDocument.theItem.setWordCount(self.docEditor.wordCount) self.theDocument.theItem.setParaCount(self.docEditor.paraCount) self.theDocument.theItem.setCursorPos(cursPos) - self.theDocument.saveDocument(docHtml) + self.theDocument.saveDocument(docText) self.docEditor.setDocumentChanged(False) return True @@ -393,7 +377,9 @@ class GuiMain(QMainWindow): dlgOpt = QFileDialog.Options() dlgOpt |= QFileDialog.DontUseNativeDialog projFile, _ = QFileDialog.getOpenFileName( - self,"Open novelWriter Project","","novelWriter Project File (nwProject.nwx);;All Files (*)",options=dlgOpt + self, "Open novelWriter Project", "", + "novelWriter Project File (%s);;All Files (*)" % nwFiles.PROJ_FILE, + options=dlgOpt ) if projFile: return projFile @@ -427,26 +413,56 @@ class GuiMain(QMainWindow): self._setWindowTitle(self.theProject.projName) return True + def makeAlert(self, theMessage, theLevel=nwAlert.INFO): + """Alert both the user and the logger at the same time. Message can be either a string or an + array of strings. Severity level is 0 = info, 1 = warning, and 2 = error. + """ + + if isinstance(theMessage, list): + popMsg = "
".join(theMessage) + logMsg = theMessage + else: + popMsg = theMessage + logMsg = [theMessage] + + msgBox = QMessageBox() + if theLevel == nwAlert.INFO: + for msgLine in logMsg: + logger.info(msgLine) + msgBox.information(self, "Information", popMsg) + elif theLevel == nwAlert.WARN: + for msgLine in logMsg: + logger.warning(msgLine) + msgBox.warning(self, "Warning", popMsg) + elif theLevel == nwAlert.ERROR: + for msgLine in logMsg: + logger.error(msgLine) + msgBox.critical(self, "Error", popMsg) + elif theLevel == nwAlert.BUG: + for msgLine in logMsg: + logger.error(msgLine) + popMsg += "
This is a bug!" + msgBox.critical(self, "Internal Error", popMsg) + + return + ## # Main Window Actions ## - def closeMain(self, isYes=False): + def closeMain(self): - if not isYes: + if self.mainConf.showGUI: msgBox = QMessageBox() msgRes = msgBox.question( - self, "Exit", - "Do you want to exit %s?" % nw.__package__ + self, "Exit", "Do you want to save changes and exit?" ) if msgRes != QMessageBox.Yes: return False logger.info("Exiting %s" % nw.__package__) - if self._takeDocumentAction(): - self.saveDocument() - if self._takeProjectAction(): - self.saveProject() + self.closeProject(True) + self.mainConf.setWinSize(self.width(), self.height()) self.mainConf.setTreeColWidths(self.treeView.getColumnSizes()) self.mainConf.setMainPanePos(self.splitMain.sizes()) @@ -466,6 +482,11 @@ class GuiMain(QMainWindow): self.docViewer.setFocus() return + def closeDocEditor(self): + self.closeDocument() + self.theProject.setLastEdited(None) + return + def closeDocViewer(self): self.docViewer.clearViewer() self.theProject.setLastViewed(None) @@ -488,31 +509,17 @@ class GuiMain(QMainWindow): return True def _autoSaveProject(self): - if self._takeProjectAction(): + if self.hasProject and self.theProject.projChanged and self.theProject.projPath is not None: logger.debug("Autosaving project") self.saveProject() return def _autoSaveDocument(self): - if self._takeDocumentAction(): + if self.hasProject and self.docEditor.docChanged and self.theDocument.theItem is not None: logger.debug("Autosaving document") self.saveDocument() return - def _takeProjectAction(self): - if self.theProject.projPath is None: - return False - if not self.theProject.projChanged: - return False - return True - - def _takeDocumentAction(self): - if self.theDocument.theItem is None: - return False - if not self.docEditor.docChanged: - return False - return True - def _makeStatusIcons(self): self.statusIcons = {} for sLabel, sCol, _ in self.theProject.statusItems: @@ -541,7 +548,10 @@ class GuiMain(QMainWindow): return def closeEvent(self, theEvent): - self.closeMain() + if self.closeMain(): + theEvent.accept() + else: + theEvent.ignore() return ## diff --git a/nw/project/project.py b/nw/project/project.py index 3c2f68fa..dce45ef3 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -19,10 +19,11 @@ from hashlib import sha256 from datetime import datetime from time import time -from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from nw.common import checkString, checkBool from nw.project.item import NWItem from nw.project.status import NWStatus +from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert +from nw.common import checkString, checkBool, checkInt +from nw.constants import nwFiles logger = logging.getLogger(__name__) @@ -34,6 +35,7 @@ class NWProject(): self.theParent = theParent self.mainConf = self.theParent.mainConf self.projChanged = None + self.projOpened = None # Debug self.handleSeed = None @@ -59,6 +61,8 @@ class NWProject(): self.importItems = None self.lastEdited = None self.lastViewed = None + self.lastWCount = 0 + self.currWCount = 0 # Set Defaults self.clearProject() @@ -119,19 +123,20 @@ class NWProject(): ## def newProject(self): - hNovel = self.newRoot("Novel", nwItemClass.NOVEL) hChars = self.newRoot("Characters", nwItemClass.CHARACTER) hWorld = self.newRoot("Plot", nwItemClass.PLOT) hWorld = self.newRoot("World", nwItemClass.WORLD) hChapt = self.newFolder("New Chapter", nwItemClass.NOVEL, hNovel) hScene = self.newFile("New Scene", nwItemClass.NOVEL, hChapt) - + self.projOpened = time() + self.setProjectChanged(True) return True def clearProject(self): self.projChanged = None + self.projOpened = None # Project Settings self.projTree = {} @@ -141,7 +146,7 @@ class NWProject(): self.projPath = None self.projMeta = None self.projCache = None - self.projFile = "nwProject.nwx" + self.projFile = nwFiles.PROJ_FILE self.projName = "" self.bookTitle = "" self.bookAuthors = [] @@ -156,13 +161,17 @@ class NWProject(): self.importItems.addEntry("Minor", (200, 50, 0)) self.importItems.addEntry("Major", (200,150, 0)) self.importItems.addEntry("Main", ( 50,200, 0)) + self.lastEdited = None + self.lastViewed = None + self.lastWCount = 0 + self.currWCount = 0 return def openProject(self, fileName): if not path.isfile(fileName): - fileName = path.join(fileName, "nwProject.nwx") + fileName = path.join(fileName, nwFiles.PROJ_FILE) if not path.isfile(fileName): self.makeAlert("File not found: %s" % fileName, nwAlert.ERROR) return False @@ -215,6 +224,8 @@ class NWProject(): self.lastEdited = checkString(xItem.text,None,True) if xItem.tag == "lastViewed": self.lastViewed = checkString(xItem.text,None,True) + if xItem.tag == "lastWordCount": + self.lastWCount = checkInt(xItem.text,0,False) if xItem.tag == "status": self.statusItems.unpackEntries(xItem) if xItem.tag == "importance": @@ -242,6 +253,7 @@ class NWProject(): self._scanProjectFolder() self.setProjectChanged(False) + self.projOpened = time() return True @@ -276,9 +288,10 @@ class NWProject(): # Save Project Settings xSettings = etree.SubElement(nwXML,"settings") - self._saveProjectValue(xSettings,"spellCheck",self.spellCheck) - self._saveProjectValue(xSettings,"lastEdited",self.lastEdited) - self._saveProjectValue(xSettings,"lastViewed",self.lastViewed) + self._saveProjectValue(xSettings,"spellCheck", self.spellCheck) + self._saveProjectValue(xSettings,"lastEdited", self.lastEdited) + self._saveProjectValue(xSettings,"lastViewed", self.lastViewed) + self._saveProjectValue(xSettings,"lastWordCount",self.currWCount) xStatus = etree.SubElement(xSettings,"status") self.statusItems.packEntries(xStatus) @@ -311,6 +324,11 @@ class NWProject(): return True + def closeProject(self): + self._appendSessionStats() + self.clearProject() + return True + ## # Set Functions ## @@ -365,6 +383,14 @@ class NWProject(): self.setProjectChanged(True) return True + def setProjectWordCount(self, theCount): + self.currWCount = theCount + self.setProjectChanged(True) + return True + + def getSessionWordCount(self): + return self.currWCount - self.lastWCount + def setStatusColours(self, newCols): replaceMap = self.statusItems.setNewEntries(newCols) if self.projTree is not None: @@ -576,6 +602,24 @@ class NWProject(): return + def _appendSessionStats(self): + + if self.projMeta is None: + return False + + with open(path.join(self.projMeta, nwFiles.SESS_INFO), mode="a+") as outFile: + print(( + "Start: {opened:s} " + "End: {closed:s} " + "Words: {words:8d}" + ).format( + opened = datetime.fromtimestamp(self.projOpened).strftime("%Y-%m-%d %H:%M:%S"), + closed = datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + words = self.getSessionWordCount(), + ), file=outFile) + + return True + def _makeHandle(self, addSeed=""): if self.handleSeed is None: newSeed = str(time()) + addSeed diff --git a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd index 72d112c6..eb71a80b 100644 --- a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd +++ b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd @@ -13,8 +13,12 @@ Some text here would look good as well, and maybe some "dialogue"? So, this is some __text__ that we’ve been adding to this document. It is utterly meaningless text, _but_ since this is just dummy text, that doesn’t really matter. The text is perfectly happy to live in this document regardless. -This paragraph is also meaningless. At least a bit. It’s also very short. +This paragraph is also meaningless. At least a bit. It’s also very short. But we could make it less meaningless if we wanted to … but we won’t, will we. This one is a bit longer. “It also has some dialogue in it” she said, before she moved on to check if the spellchecker worked. It did. “Cool,” she concluded. +@ToDo: Stuff that will be done at some point + + + diff --git a/sample/sampleNovel/meta/sessionInfo.log b/sample/sampleNovel/meta/sessionInfo.log new file mode 100644 index 00000000..79d3266e --- /dev/null +++ b/sample/sampleNovel/meta/sessionInfo.log @@ -0,0 +1,15 @@ +Start: 2019-05-26 14:28:28 End: 2019-05-26 14:28:38 Words: -23 +Start: 2019-05-26 14:31:12 End: 2019-05-26 14:31:34 Words: 12 +Start: 2019-05-26 14:33:27 End: 2019-05-26 14:33:31 Words: 0 +Start: 2019-05-26 14:40:29 End: 2019-05-26 14:40:32 Words: 0 +Start: 2019-05-26 14:40:43 End: 2019-05-26 14:40:46 Words: 0 +Start: 2019-05-26 14:48:40 End: 2019-05-26 14:48:50 Words: 0 +Start: 2019-05-26 14:50:18 End: 2019-05-26 14:50:21 Words: 0 +Start: 2019-05-26 14:50:29 End: 2019-05-26 14:50:47 Words: 3 +Start: 2019-05-26 15:10:52 End: 2019-05-26 15:11:09 Words: 0 +Start: 2019-05-26 15:28:56 End: 2019-05-26 15:29:08 Words: 0 +Start: 2019-05-26 15:37:34 End: 2019-05-26 15:37:43 Words: -538 +Start: 2019-05-26 15:37:47 End: 2019-05-26 15:38:07 Words: 538 +Start: 2019-05-26 15:38:11 End: 2019-05-26 15:43:07 Words: 2 +Start: 2019-05-26 15:49:18 End: 2019-05-26 15:49:47 Words: 0 +Start: 2019-05-26 15:52:39 End: 2019-05-26 15:52:56 Words: 0 diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 0a81731f..448da65a 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -9,7 +9,8 @@ True 636b6aa9b697b - 636b6aa9b697b + None + 540 New Notes @@ -72,10 +73,10 @@ Notes False SCENE - 577 - 104 + 656 + 121 5 - 603 + 573 New File diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index a42179e9..e1685c0f 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -8,6 +8,7 @@ False None None + 0 New Note diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 971954a6..ae897362 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -8,6 +8,7 @@ True 31489056e0916 31489056e0916 + 69 New Note diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index 02f3c384..8d2ab8f5 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -10,6 +10,7 @@ False None None + 0 New Note diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 510192ff..d499a736 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -8,6 +8,7 @@ False None None + 0 New Note diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index 112f9fd8..cb999b5a 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -1,5 +1,5 @@ [Main] -timestamp = 2019-05-18 15:06:44 +timestamp = 2019-05-25 23:33:23 theme = default [Sizes] @@ -17,6 +17,7 @@ fixedwidth = True width = 600 margins = 40, 40 textsize = 13 +tabwidth = 40 justify = True autoselect = True autoreplace = True diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index e9ff11f5..40ec363c 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -8,6 +8,7 @@ False None None + 0 New Note diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index 8c68bc42..314a4f75 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -1,5 +1,5 @@ - + @@ -8,6 +8,7 @@ False None None + 0 New Note diff --git a/tests/test_gui.py b/tests/test_gui.py index 94ce7eb4..7f141b47 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -27,7 +27,7 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): nwGUI.theProject.handleSeed = 42 assert nwGUI.newProject(nwTempGUI, True) assert nwGUI.saveProject() - assert nwGUI.closeProject(True) + assert nwGUI.closeProject() assert len(nwGUI.theProject.projTree) == 0 assert len(nwGUI.theProject.treeOrder) == 0 @@ -155,7 +155,7 @@ def testMainWindows(qtbot, nwTempGUI, nwRef): sceneFile = path.join(nwTempGUI,"data_3","1489056e0916_main.nwd") assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd")) - nwGUI.closeMain(True) + nwGUI.closeMain() # qtbot.stopForInteraction() @pytest.mark.gui @@ -217,7 +217,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef): projFile = path.join(nwTempGUI,"nwProject.nwx") assert cmpFiles(projFile, path.join(nwRef,"gui","2_nwProject.nwx"), [2]) - nwGUI.closeMain(True) + nwGUI.closeMain() # qtbot.stopForInteraction() @pytest.mark.gui @@ -263,5 +263,5 @@ def testItemEditor(qtbot, nwTempGUI, nwRef): projFile = path.join(nwTempGUI,"nwProject.nwx") assert cmpFiles(projFile, path.join(nwRef,"gui","3_nwProject.nwx"), [2]) - nwGUI.closeMain(True) + nwGUI.closeMain() # qtbot.stopForInteraction()