From 09be23a11ef01b4ad1d90bd568d57e6c34ecdd23 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 25 Jun 2020 21:11:37 +0200 Subject: [PATCH] Brand new session stats log with saved counts for novel and notes seperately, and new parsing code --- nw/constants/constants.py | 2 +- nw/core/options.py | 2 + nw/core/project.py | 38 +++++-- nw/core/tree.py | 19 +++- nw/gui/sessionlog.py | 181 +++++++++++++++++++++++-------- sample/content/636b6aa9b697b.nwd | 2 +- sample/nwProject.nwx | 20 ++-- 7 files changed, 195 insertions(+), 69 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 6c57d2d6..18443c1d 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -51,7 +51,7 @@ class nwFiles(): PROJ_LOCK = "nwProject.lock" TOC_TXT = "ToC.txt" TOC_JSON = "ToC.json" - SESS_INFO = "sessionInfo.log" + SESS_STATS = "sessionStats.log" INDEX_FILE = "tagsIndex.json" OPTS_FILE = "guiOptions.json" RECENT_FILE = "recentProjects.json" diff --git a/nw/core/options.py b/nw/core/options.py index 42c2dbd0..a9a06e0d 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -53,6 +53,8 @@ class OptionState(): "widthCol2", "sortCol", "sortOrder", + "incNovel", + "incNotes", "hideZeros", "hideNegative", "groupByDay", diff --git a/nw/core/project.py b/nw/core/project.py index 3db93bf2..0d43229c 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -93,6 +93,8 @@ class NWProject(): self.lastViewed = None # The handle of the last file to be viewed self.lastWCount = 0 # The project word count from last session self.currWCount = 0 # The project word count in current session + self.novelWCount = 0 # Total number of words in novel files + self.notesWCount = 0 # Total number of words in note files self.doBackup = True # Run project backup on exit # Set Defaults @@ -231,6 +233,8 @@ class NWProject(): self.lastViewed = None self.lastWCount = 0 self.currWCount = 0 + self.novelWCount = 0 + self.notesWCount = 0 return @@ -435,6 +439,10 @@ class NWProject(): self.lastViewed = checkString(xItem.text, None, True) elif xItem.tag == "lastWordCount": self.lastWCount = checkInt(xItem.text, 0, False) + elif xItem.tag == "novelWordCount": + self.novelWCount = checkInt(xItem.text, 0, False) + elif xItem.tag == "notesWordCount": + self.notesWCount = checkInt(xItem.text, 0, False) elif xItem.tag == "status": self.statusItems.unpackEntries(xItem) elif xItem.tag == "importance": @@ -501,6 +509,10 @@ class NWProject(): }) editTime = int(self.editTime + saveTime - self.projOpened) + wcNovel, wcNotes = self.projTree.sumWords() + self.novelWCount = wcNovel + self.notesWCount = wcNotes + self.setProjectWordCount(wcNovel + wcNotes) # Save Project Meta xProject = etree.SubElement(nwXML, "project") @@ -519,6 +531,8 @@ class NWProject(): self._packProjectValue(xSettings, "lastEdited", self.lastEdited) self._packProjectValue(xSettings, "lastViewed", self.lastViewed) self._packProjectValue(xSettings, "lastWordCount", self.currWCount) + self._packProjectValue(xSettings, "novelWordCount", wcNovel) + self._packProjectValue(xSettings, "notesWordCount", wcNotes) xAutoRep = etree.SubElement(xSettings, "autoReplace") for aKey, aValue in self.autoReplace.items(): @@ -1101,18 +1115,22 @@ class NWProject(): if not self.ensureFolderStructure(): return False - sessionFile = path.join(self.projMeta, nwFiles.SESS_INFO) + sessionFile = path.join(self.projMeta, nwFiles.SESS_STATS) + isFile = path.isfile(sessionFile) with open(sessionFile, mode="a+", encoding="utf8") as outFile: - print(( - "Start: {opened:s} " - "End: {closed:s} " - "Words: {words:8d}" - ).format( - opened = formatTimeStamp(self.projOpened), - closed = formatTimeStamp(time()), - words = self.getSessionWordCount(), - ), file=outFile) + if not isFile: + # It's a new file, so add a header + outFile.write("# Initial: %d\n# %-17s %-19s %8s %8s\n" % ( + self.lastWCount, "Start Time", "End Time", "Novel", "Notes" + )) + + outFile.write("%-19s %-19s %8d %8d\n" % ( + formatTimeStamp(self.projOpened), + formatTimeStamp(time()), + self.novelWCount, + self.notesWCount, + )) return True diff --git a/nw/core/tree.py b/nw/core/tree.py index 7f1482fe..beb5d2e3 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -36,7 +36,7 @@ from time import time from nw.core.item import NWItem from nw.common import checkString -from nw.constants import nwFiles, nwItemType, nwItemClass +from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout logger = logging.getLogger(__name__) @@ -182,6 +182,23 @@ class NWTree(): return + def sumWords(self): + """Loops over all entries and adds up the word counts. + """ + noteWords = 0 + novelWords = 0 + for tHandle in self._treeOrder: + tItem = self.__getitem__(tHandle) + if tItem is None: + continue + if tItem.itemLayout == nwItemLayout.NO_LAYOUT: + pass + elif tItem.itemLayout == nwItemLayout.NOTE: + noteWords += tItem.wordCount + else: + novelWords += tItem.wordCount + return novelWords, noteWords + ## # Tree Structure Methods ## diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index cd9be467..03ace4db 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -64,7 +64,7 @@ class GuiSessionLog(QDialog): self.logData = [] self.timeFilter = 0.0 self.timeTotal = 0.0 - self.maxWords = 0 + self.wordOffset = 0 self.setWindowTitle("Session Log") self.setMinimumWidth(self.mainConf.pxInt(420)) @@ -114,6 +114,7 @@ class GuiSessionLog(QDialog): # Word Bar self.barHeight = int(round(0.5*self.theTheme.fontPixelSize)) + self.barWidth = self.mainConf.pxInt(200) self.barImage = QPixmap(self.barHeight, self.barHeight) self.barImage.fill(self.palette().highlight().color()) @@ -130,11 +131,29 @@ class GuiSessionLog(QDialog): self.labelFilter.setFont(self.monoFont) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) - self.infoForm.addWidget(QLabel("Total Time:"), 0, 0) - self.infoForm.addWidget(self.labelTotal, 0, 1) - self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0) - self.infoForm.addWidget(self.labelFilter, 1, 1) - self.infoForm.setRowStretch(2, 1) + self.novelWords = QLabel("0") + self.novelWords.setFont(self.monoFont) + self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + + self.notesWords = QLabel("0") + self.notesWords.setFont(self.monoFont) + self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + + self.totalWords = QLabel("0") + self.totalWords.setFont(self.monoFont) + self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + + self.infoForm.addWidget(QLabel("Total Time:"), 0, 0) + self.infoForm.addWidget(self.labelTotal, 0, 1) + self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0) + self.infoForm.addWidget(self.labelFilter, 1, 1) + self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0) + self.infoForm.addWidget(self.novelWords, 2, 1) + self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0) + self.infoForm.addWidget(self.notesWords, 3, 1) + self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0) + self.infoForm.addWidget(self.totalWords, 4, 1) + self.infoForm.setRowStretch(5, 1) # Filter Options sPx = self.theTheme.baseIconSize @@ -143,6 +162,20 @@ class GuiSessionLog(QDialog): self.filterForm = QGridLayout(self) self.filterBox.setLayout(self.filterForm) + self.labelNovel = QLabel("Count novel files") + self.incNovel = QSwitch(width=2*sPx, height=sPx) + self.incNovel.setChecked( + self.optState.getBool("GuiSessionLog", "incNovel", True) + ) + self.incNovel.clicked.connect(self._updateListBox) + + self.labelNotes = QLabel("Count note files") + self.incNotes = QSwitch(width=2*sPx, height=sPx) + self.incNotes.setChecked( + self.optState.getBool("GuiSessionLog", "incNotes", True) + ) + self.incNotes.clicked.connect(self._updateListBox) + self.labelZeros = QLabel("Hide zero word count") self.hideZeros = QSwitch(width=2*sPx, height=sPx) self.hideZeros.setChecked( @@ -164,12 +197,16 @@ class GuiSessionLog(QDialog): ) self.groupByDay.clicked.connect(self._updateListBox) - self.filterForm.addWidget(self.labelZeros, 0, 0) - self.filterForm.addWidget(self.hideZeros, 0, 1) - self.filterForm.addWidget(self.labelNegative, 1, 0) - self.filterForm.addWidget(self.hideNegative, 1, 1) - self.filterForm.addWidget(self.labelByDay, 2, 0) - self.filterForm.addWidget(self.groupByDay, 2, 1) + self.filterForm.addWidget(self.labelNovel, 0, 0) + self.filterForm.addWidget(self.incNovel, 0, 1) + self.filterForm.addWidget(self.labelNotes, 1, 0) + self.filterForm.addWidget(self.incNotes, 1, 1) + self.filterForm.addWidget(self.labelZeros, 2, 0) + self.filterForm.addWidget(self.hideZeros, 2, 1) + self.filterForm.addWidget(self.labelNegative, 3, 0) + self.filterForm.addWidget(self.hideNegative, 3, 1) + self.filterForm.addWidget(self.labelByDay, 4, 0) + self.filterForm.addWidget(self.groupByDay, 4, 1) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) @@ -209,6 +246,8 @@ class GuiSessionLog(QDialog): widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2)) sortCol = self.listBox.sortColumn() sortOrder = self.listBox.header().sortIndicatorOrder() + incNovel = self.incNovel.isChecked() + incNotes = self.incNotes.isChecked() hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() groupByDay = self.groupByDay.isChecked() @@ -220,6 +259,8 @@ class GuiSessionLog(QDialog): self.optState.setValue("GuiSessionLog", "widthCol2", widthCol2) self.optState.setValue("GuiSessionLog", "sortCol", sortCol) self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) + self.optState.setValue("GuiSessionLog", "incNovel", incNovel) + self.optState.setValue("GuiSessionLog", "incNotes", incNotes) self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative) self.optState.setValue("GuiSessionLog", "groupByDay", groupByDay) @@ -239,29 +280,55 @@ class GuiSessionLog(QDialog): self.logData = [] logger.debug("Loading session log file") + ttNovel = 0 + ttNotes = 0 + + isFirst = True + osNovel = 0 + osNotes = 0 + try: - logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO) + logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS) with open(logFile, mode="r", encoding="utf8") as inFile: for inLine in inFile: + if inLine.startswith("#"): + if inLine.startswith("# Initial:"): + self.wordOffset = int(inLine[11:].strip()) + logger.verbose( + "Initial word count when log was started is %d" % self.wordOffset + ) + continue inData = inLine.split() - if len(inData) != 8: + if len(inData) != 6: continue dStart = datetime.strptime( - "%s %s" % (inData[1], inData[2]), nwConst.tStampFmt + "%s %s" % (inData[0], inData[1]), nwConst.tStampFmt ) dEnd = datetime.strptime( - "%s %s" % (inData[4], inData[5]), nwConst.tStampFmt + "%s %s" % (inData[2], inData[3]), nwConst.tStampFmt ) - nWords = int(inData[7]) tDiff = dEnd - dStart sDiff = tDiff.total_seconds() self.timeTotal += sDiff - self.logData.append((dStart, sDiff, nWords)) - self.maxWords = max(self.maxWords, nWords) + wcNovel = int(inData[4]) + wcNotes = int(inData[5]) + ttNovel = wcNovel + ttNotes = wcNotes + + if isFirst: + isFirst = False + if self.wordOffset > 0: + # First entry is used as the reference word + # count, and the data is then discarded. + osNovel = wcNovel + osNotes = wcNotes + continue + + self.logData.append((dStart, sDiff, wcNovel-osNovel, wcNotes-osNotes)) except Exception as e: self.theParent.makeAlert( @@ -270,6 +337,9 @@ class GuiSessionLog(QDialog): return False self.labelTotal.setText(self._formatTime(self.timeTotal)) + self.novelWords.setText("{:n}".format(ttNovel)) + self.notesWords.setText("{:n}".format(ttNotes)) + self.totalWords.setText("{:n}".format(ttNovel + ttNotes)) return True @@ -279,49 +349,65 @@ class GuiSessionLog(QDialog): self.listBox.clear() self.timeFilter = 0.0 + incNovel = self.incNovel.isChecked() + incNotes = self.incNotes.isChecked() hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() groupByDay = self.groupByDay.isChecked() # Group the data if groupByDay: - listData = [] - listMax = 0 + tempData = [] + sessDate = None + sessTime = 0 + lstNovel = 0 + lstNotes = 0 - dayDate = None - daySDiff = 0 - dayWords = 0 - - for n, (dStart, sDiff, nWords) in enumerate(self.logData): + for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData): if n == 0: - dayDate = dStart.date() - if dayDate != dStart.date(): - listData.append((dayDate, daySDiff, dayWords)) - listMax = max(listMax, dayWords) - dayDate = dStart.date() - daySDiff = sDiff - dayWords = nWords + sessDate = dStart.date() + if sessDate != dStart.date(): + tempData.append((sessDate, sessTime, lstNovel, lstNotes)) + sessDate = dStart.date() + sessTime = sDiff + lstNovel = wcNovel + lstNotes = wcNotes else: - daySDiff += sDiff - dayWords += nWords + sessTime += sDiff + lstNovel = wcNovel + lstNotes = wcNotes - if dayDate is not None: - listData.append((dayDate, daySDiff, dayWords)) + if sessDate is not None: + tempData.append((sessDate, sessTime, lstNovel, lstNotes)) else: - listData = self.logData - listMax = self.maxWords + tempData = self.logData + + # Calculate Word Diff + listData = [] + pcTotal = 0 + listMax = 0 + for dStart, sDiff, wcNovel, wcNotes in tempData: + + wcTotal = 0 + if incNovel: + wcTotal += wcNovel + if incNotes: + wcTotal += wcNotes + + dwTotal = wcTotal - pcTotal + if hideZeros and dwTotal == 0: + continue + if hideNegative and dwTotal < 0: + continue + + listData.append((dStart, sDiff, dwTotal)) + listMax = max(listMax, dwTotal) + pcTotal = wcTotal # Populate the list for dStart, sDiff, nWords in listData: - if hideZeros and nWords == 0: - continue - if hideNegative and nWords < 0: - continue - - self.timeFilter += sDiff - if groupByDay: sStart = dStart.strftime(nwConst.dStampFmt) else: @@ -332,7 +418,7 @@ class GuiSessionLog(QDialog): newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) newItem.setText(self.C_COUNT, str(nWords)) - if nWords > 0: + if nWords > 0 and listMax > 0: theBar = self.barImage.scaled( int(200*nWords/listMax), self.barHeight, @@ -350,6 +436,7 @@ class GuiSessionLog(QDialog): newItem.setFont(self.C_COUNT, self.monoFont) self.listBox.addTopLevelItem(newItem) + self.timeFilter += sDiff self.labelFilter.setText(self._formatTime(self.timeFilter)) diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 394a96ce..0ab145f1 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -21,4 +21,4 @@ Thin spaces and thin non-breaking spaces are also supported from the Insert menu If you need to split a scene file up into further pieces, you can do so with the level four heading, like above. This is referred to as a section. -Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the export dialog. +Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the Build Novel Project dialog. You can also have them replaced with scene separators like “* * *”. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 4ce89213..c29a7788 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,21 +1,23 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 407 - 71 - 15118 + 565 + 99 + 23298 False True True 636b6aa9b697b - bb2c23b3c42cc - 967 + b3e74dbc1f584 + 982 + 606 + 376 B E @@ -114,10 +116,10 @@ 1st Draft True SCENE - 1483 - 263 + 1564 + 278 8 - 1086 + 1633 Another Scene