From a5c83a45f50468bde2042b7fee287f853c2452d8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:20:22 +0200 Subject: [PATCH 01/12] First some code cleanup and fix of the filtered total --- nw/core/options.py | 1 - nw/gui/sessionlog.py | 86 ++++++++++++++++++++++---------------------- nw/gui/theme.py | 2 ++ 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index 0ff7c919..b1986505 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -48,7 +48,6 @@ class OptionState(): "GuiSession": { "widthCol0", "widthCol1", - "widthCol2", "sortCol", "sortOrder", "hideZeros", diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 5f3c61f0..98172f0d 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -44,43 +44,45 @@ logger = logging.getLogger(__name__) class GuiSessionLogView(QDialog): + C_TIME = 0 + C_LENGTH = 1 + C_COUNT = 2 + def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) logger.debug("Initialising SessionLogView ...") self.mainConf = nw.CONFIG - self.theProject = theProject self.theParent = theParent - self.optState = self.theProject.optState + self.theProject = theProject + self.theTheme = theParent.theTheme + self.optState = theProject.optState self.timeFilter = 0.0 self.timeTotal = 0.0 - self.outerBox = QGridLayout() - self.bottomBox = QHBoxLayout() - self.setWindowTitle("Session Log") self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumHeight(self.mainConf.pxInt(400)) + # List Box wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol0", 180)) wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol1", 80)) - wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol2", 80)) self.listBox = QTreeWidget() - self.listBox.setHeaderLabels(["Session Start","Length","Words",""]) + self.listBox.setHeaderLabels(["Session Start","Length","Words"]) self.listBox.setIndentation(0) - self.listBox.setColumnWidth(0, wCol0) - self.listBox.setColumnWidth(1, wCol1) - self.listBox.setColumnWidth(2, wCol2) - self.listBox.setColumnWidth(3, 0) + self.listBox.setColumnWidth(self.C_TIME, wCol0) + self.listBox.setColumnWidth(self.C_LENGTH, wCol1) + self.listBox.resizeColumnToContents(self.C_COUNT) hHeader = self.listBox.headerItem() - hHeader.setTextAlignment(1,Qt.AlignRight) - hHeader.setTextAlignment(2,Qt.AlignRight) + hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight) + hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight) - self.monoFont = QFont("Monospace", 10) + self.monoFont = QFont() + self.monoFont.setFamily(self.theTheme.guiFontFixed.family()) sortValid = (Qt.AscendingOrder, Qt.DescendingOrder) sortCol = self.optState.validIntRange( @@ -95,9 +97,9 @@ class GuiSessionLogView(QDialog): self.listBox.setSortingEnabled(True) # Session Info - self.infoBox = QGroupBox("Sum Total Time", self) - self.infoBoxForm = QGridLayout(self) - self.infoBox.setLayout(self.infoBoxForm) + self.infoBox = QGroupBox("Sum Total Time", self) + self.infoForm = QGridLayout(self) + self.infoBox.setLayout(self.infoForm) self.labelTotal = QLabel(self._formatTime(0)) self.labelTotal.setFont(self.monoFont) @@ -107,15 +109,15 @@ class GuiSessionLogView(QDialog): self.labelFilter.setFont(self.monoFont) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) - self.infoBoxForm.addWidget(QLabel("All:"), 0, 0) - self.infoBoxForm.addWidget(self.labelTotal, 0, 1) - self.infoBoxForm.addWidget(QLabel("Filtered:"), 1, 0) - self.infoBoxForm.addWidget(self.labelFilter, 1, 1) + self.infoForm.addWidget(QLabel("All:"), 0, 0) + self.infoForm.addWidget(self.labelTotal, 0, 1) + self.infoForm.addWidget(QLabel("Filtered:"), 1, 0) + self.infoForm.addWidget(self.labelFilter, 1, 1) # Filter Options - self.filterBox = QGroupBox("Filters", self) - self.filterBoxForm = QGridLayout(self) - self.filterBox.setLayout(self.filterBoxForm) + self.filterBox = QGroupBox("Filters", self) + self.filterForm = QGridLayout(self) + self.filterBox.setLayout(self.filterForm) self.hideZeros = QCheckBox("Hide zero word count", self) self.hideZeros.setChecked( @@ -129,14 +131,15 @@ class GuiSessionLogView(QDialog): ) self.hideNegative.stateChanged.connect(self._doHideNegative) - self.filterBoxForm.addWidget(self.hideZeros, 0, 0) - self.filterBoxForm.addWidget(self.hideNegative, 1, 0) + self.filterForm.addWidget(self.hideZeros, 0, 0) + self.filterForm.addWidget(self.hideNegative, 1, 0) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) # Assemble + self.outerBox = QGridLayout() self.outerBox.addWidget(self.listBox, 0, 0, 1, 2) self.outerBox.addWidget(self.infoBox, 1, 0) self.outerBox.addWidget(self.filterBox, 1, 1) @@ -144,8 +147,6 @@ class GuiSessionLogView(QDialog): self.setLayout(self.outerBox) - self.show() - logger.debug("SessionLogView initialisation complete") self._loadSessionLog() @@ -175,18 +176,16 @@ class GuiSessionLogView(QDialog): if len(inData) != 8: continue dStart = datetime.strptime( - "%s %s" % (inData[1],inData[2]), nwConst.tStampFmt + "%s %s" % (inData[1], inData[2]), nwConst.tStampFmt ) dEnd = datetime.strptime( - "%s %s" % (inData[4],inData[5]), nwConst.tStampFmt + "%s %s" % (inData[4], inData[5]), nwConst.tStampFmt ) nWords = int(inData[7]) tDiff = dEnd - dStart sDiff = tDiff.total_seconds() - self.timeTotal += sDiff - if abs(nWords) > 0: - self.timeFilter += sDiff + self.timeTotal += sDiff if hideZeros and nWords == 0: continue @@ -194,16 +193,19 @@ class GuiSessionLogView(QDialog): if hideNegative and nWords < 0: continue - newItem = QTreeWidgetItem( - [str(dStart), self._formatTime(sDiff), str(nWords), ""] - ) + self.timeFilter += sDiff - newItem.setTextAlignment(1,Qt.AlignRight) - newItem.setTextAlignment(2,Qt.AlignRight) + newItem = QTreeWidgetItem() + newItem.setText(self.C_TIME, str(dStart)) + newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) + newItem.setText(self.C_COUNT, str(nWords)) - newItem.setFont(0,self.monoFont) - newItem.setFont(1,self.monoFont) - newItem.setFont(2,self.monoFont) + newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight) + newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) + + newItem.setFont(self.C_TIME, self.monoFont) + newItem.setFont(self.C_LENGTH, self.monoFont) + newItem.setFont(self.C_COUNT, self.monoFont) self.listBox.addTopLevelItem(newItem) @@ -222,7 +224,6 @@ class GuiSessionLogView(QDialog): widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0)) widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1)) - widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2)) sortCol = self.listBox.sortColumn() sortOrder = self.listBox.header().sortIndicatorOrder() hideZeros = self.hideZeros.isChecked() @@ -230,7 +231,6 @@ class GuiSessionLogView(QDialog): self.optState.setValue("GuiSession", "widthCol0", widthCol0) self.optState.setValue("GuiSession", "widthCol1", widthCol1) - self.optState.setValue("GuiSession", "widthCol2", widthCol2) self.optState.setValue("GuiSession", "sortCol", sortCol) self.optState.setValue("GuiSession", "sortOrder", sortOrder) self.optState.setValue("GuiSession", "hideZeros", hideZeros) diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 7df0c560..0265df62 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -139,6 +139,8 @@ class GuiTheme: logger.verbose("GUI Scale: %.2f" % self.guiScale) self.guiFont = qApp.font() + self.guiFontFixed = QFontDatabase.systemFont(QFontDatabase.FixedFont) + qMetric = QFontMetrics(self.guiFont) self.fontPointSize = self.guiFont.pointSizeF() self.fontPixelSize = int(round(qMetric.height())) From 1110f40231b5e6736084cc58ab9c7dc2b11c09ea Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:36:08 +0200 Subject: [PATCH 02/12] Restructured the session log class and made it consistent with other source files --- nw/gui/sessionlog.py | 171 +++++++++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 70 deletions(-) diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 98172f0d..d58844d9 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -34,7 +34,7 @@ from datetime import datetime from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, + qApp, QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, QLabel, QGroupBox, QCheckBox ) @@ -59,6 +59,7 @@ class GuiSessionLogView(QDialog): self.theTheme = theParent.theTheme self.optState = theProject.optState + self.logData = [] self.timeFilter = 0.0 self.timeTotal = 0.0 @@ -82,6 +83,7 @@ class GuiSessionLogView(QDialog): hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight) self.monoFont = QFont() + self.monoFont.setPointSizeF(0.9*self.theTheme.fontPointSize) self.monoFont.setFamily(self.theTheme.guiFontFixed.family()) sortValid = (Qt.AscendingOrder, Qt.DescendingOrder) @@ -149,78 +151,20 @@ class GuiSessionLogView(QDialog): logger.debug("SessionLogView initialisation complete") - self._loadSessionLog() + qApp.processEvents() + self._loadLogFile() + self._updateListBox() return - def _loadSessionLog(self): - - logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO) - if not path.isfile(logFile): - logger.warning("No session log file found for this project.") - return False - - self.listBox.clear() - - self.timeFilter = 0.0 - self.timeTotal = 0.0 - - hideZeros = self.hideZeros.isChecked() - hideNegative = self.hideNegative.isChecked() - - logger.debug("Loading session log file") - try: - with open(logFile,mode="r",encoding="utf8") as inFile: - for inLine in inFile: - inData = inLine.split() - if len(inData) != 8: - continue - dStart = datetime.strptime( - "%s %s" % (inData[1], inData[2]), nwConst.tStampFmt - ) - dEnd = datetime.strptime( - "%s %s" % (inData[4], inData[5]), nwConst.tStampFmt - ) - nWords = int(inData[7]) - tDiff = dEnd - dStart - sDiff = tDiff.total_seconds() - - self.timeTotal += sDiff - - if hideZeros and nWords == 0: - continue - - if hideNegative and nWords < 0: - continue - - self.timeFilter += sDiff - - newItem = QTreeWidgetItem() - newItem.setText(self.C_TIME, str(dStart)) - newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) - newItem.setText(self.C_COUNT, str(nWords)) - - newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight) - newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) - - newItem.setFont(self.C_TIME, self.monoFont) - newItem.setFont(self.C_LENGTH, self.monoFont) - newItem.setFont(self.C_COUNT, self.monoFont) - - self.listBox.addTopLevelItem(newItem) - - except Exception as e: - self.theParent.makeAlert( - ["Failed to read session log file.",str(e)], nwAlert.ERROR - ) - return False - - self.labelFilter.setText(self._formatTime(self.timeFilter)) - self.labelTotal.setText(self._formatTime(self.timeTotal)) - - return True + ## + # Slots + ## def _doClose(self): + """Save the state of the window, clear cache, end close. + """ + self.logData = [] widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0)) widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1)) @@ -242,14 +186,101 @@ class GuiSessionLogView(QDialog): return def _doHideZeros(self, newState): - self._loadSessionLog() + """Reload the list box with the new filter settings in place. + """ + self._updateListBox() return def _doHideNegative(self, newState): - self._loadSessionLog() + """Reload the list box with the new filter settings in place. + """ + self._updateListBox() return + ## + # Internal Functions + ## + + def _loadLogFile(self): + """Load the content of the log file into a buffer. + """ + self.logData = [] + logger.debug("Loading session log file") + + try: + logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO) + with open(logFile, mode="r", encoding="utf8") as inFile: + for inLine in inFile: + + inData = inLine.split() + if len(inData) != 8: + continue + + dStart = datetime.strptime( + "%s %s" % (inData[1], inData[2]), nwConst.tStampFmt + ) + dEnd = datetime.strptime( + "%s %s" % (inData[4], inData[5]), nwConst.tStampFmt + ) + + nWords = int(inData[7]) + tDiff = dEnd - dStart + sDiff = tDiff.total_seconds() + + self.logData.append((dStart, sDiff, nWords)) + + except Exception as e: + self.theParent.makeAlert( + ["Failed to read session log file.",str(e)], nwAlert.ERROR + ) + return False + + return True + + def _updateListBox(self): + """Load/reload the content of the list box. + """ + self.listBox.clear() + self.timeFilter = 0.0 + self.timeTotal = 0.0 + + hideZeros = self.hideZeros.isChecked() + hideNegative = self.hideNegative.isChecked() + + for dStart, sDiff, nWords in self.logData: + + self.timeTotal += sDiff + + if hideZeros and nWords == 0: + continue + + if hideNegative and nWords < 0: + continue + + self.timeFilter += sDiff + + newItem = QTreeWidgetItem() + newItem.setText(self.C_TIME, str(dStart)) + newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) + newItem.setText(self.C_COUNT, str(nWords)) + + newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight) + newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) + + newItem.setFont(self.C_TIME, self.monoFont) + newItem.setFont(self.C_LENGTH, self.monoFont) + newItem.setFont(self.C_COUNT, self.monoFont) + + self.listBox.addTopLevelItem(newItem) + + self.labelFilter.setText(self._formatTime(self.timeFilter)) + self.labelTotal.setText(self._formatTime(self.timeTotal)) + + return True + def _formatTime(self, tS): + """Format the time spent in 00:00:00 format. + """ tM = int(tS/60) tH = int(tM/60) tM = tM - tH*60 From 935971fa0ca59e6250c65a63783c0c6db3314d64 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:45:41 +0200 Subject: [PATCH 03/12] Give the session log class a shorter name --- nw/core/options.py | 2 +- nw/gui/__init__.py | 4 ++-- nw/gui/sessionlog.py | 40 +++++++++++++++++++--------------------- nw/guimain.py | 4 ++-- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index b1986505..b3a144d3 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -45,7 +45,7 @@ class OptionState(): self.theState = {} self.validMap = { - "GuiSession": { + "GuiSessionLog": { "widthCol0", "widthCol1", "sortCol", diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 4e96b4ee..3a57b557 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -15,7 +15,7 @@ from nw.gui.preferences import GuiPreferences from nw.gui.projload import GuiProjectLoad from nw.gui.projsettings import GuiProjectSettings from nw.gui.projtree import GuiProjectTree -from nw.gui.sessionlog import GuiSessionLogView +from nw.gui.sessionlog import GuiSessionLog from nw.gui.statusbar import GuiMainStatus from nw.gui.theme import GuiIcons, GuiTheme @@ -36,7 +36,7 @@ __all__ = [ "GuiProjectLoad", "GuiProjectSettings", "GuiProjectTree", - "GuiSessionLogView", + "GuiSessionLog", "GuiMainStatus", "GuiIcons", "GuiTheme", diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index d58844d9..e7c62d5a 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -42,7 +42,7 @@ from nw.constants import nwConst, nwFiles, nwAlert logger = logging.getLogger(__name__) -class GuiSessionLogView(QDialog): +class GuiSessionLog(QDialog): C_TIME = 0 C_LENGTH = 1 @@ -51,7 +51,7 @@ class GuiSessionLogView(QDialog): def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) - logger.debug("Initialising SessionLogView ...") + logger.debug("Initialising GuiSessionLog ...") self.mainConf = nw.CONFIG self.theParent = theParent @@ -68,8 +68,8 @@ class GuiSessionLogView(QDialog): self.setMinimumHeight(self.mainConf.pxInt(400)) # List Box - wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol0", 180)) - wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol1", 80)) + wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "widthCol0", 180)) + wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "widthCol1", 80)) self.listBox = QTreeWidget() self.listBox.setHeaderLabels(["Session Start","Length","Words"]) @@ -88,10 +88,10 @@ class GuiSessionLogView(QDialog): sortValid = (Qt.AscendingOrder, Qt.DescendingOrder) sortCol = self.optState.validIntRange( - self.optState.getInt("GuiSession", "sortCol", 0), 0, 2, 0 + self.optState.getInt("GuiSessionLog", "sortCol", 0), 0, 2, 0 ) sortOrder = self.optState.validIntTuple( - self.optState.getInt("GuiSession", "sortOrder", Qt.DescendingOrder), + self.optState.getInt("GuiSessionLog", "sortOrder", Qt.DescendingOrder), sortValid, Qt.DescendingOrder ) @@ -123,13 +123,13 @@ class GuiSessionLogView(QDialog): self.hideZeros = QCheckBox("Hide zero word count", self) self.hideZeros.setChecked( - self.optState.getBool("GuiSession", "hideZeros", True) + self.optState.getBool("GuiSessionLog", "hideZeros", True) ) self.hideZeros.stateChanged.connect(self._doHideZeros) self.hideNegative = QCheckBox("Hide negative word count", self) self.hideNegative.setChecked( - self.optState.getBool("GuiSession", "hideNegative", False) + self.optState.getBool("GuiSessionLog", "hideNegative", False) ) self.hideNegative.stateChanged.connect(self._doHideNegative) @@ -149,7 +149,7 @@ class GuiSessionLogView(QDialog): self.setLayout(self.outerBox) - logger.debug("SessionLogView initialisation complete") + logger.debug("GuiSessionLog initialisation complete") qApp.processEvents() self._loadLogFile() @@ -173,12 +173,12 @@ class GuiSessionLogView(QDialog): hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() - self.optState.setValue("GuiSession", "widthCol0", widthCol0) - self.optState.setValue("GuiSession", "widthCol1", widthCol1) - self.optState.setValue("GuiSession", "sortCol", sortCol) - self.optState.setValue("GuiSession", "sortOrder", sortOrder) - self.optState.setValue("GuiSession", "hideZeros", hideZeros) - self.optState.setValue("GuiSession", "hideNegative", hideNegative) + self.optState.setValue("GuiSessionLog", "widthCol0", widthCol0) + self.optState.setValue("GuiSessionLog", "widthCol1", widthCol1) + self.optState.setValue("GuiSessionLog", "sortCol", sortCol) + self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) + self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) + self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative) self.optState.saveSettings() self.close() @@ -226,6 +226,7 @@ class GuiSessionLogView(QDialog): nWords = int(inData[7]) tDiff = dEnd - dStart sDiff = tDiff.total_seconds() + self.timeTotal += sDiff self.logData.append((dStart, sDiff, nWords)) @@ -235,6 +236,8 @@ class GuiSessionLogView(QDialog): ) return False + self.labelTotal.setText(self._formatTime(self.timeTotal)) + return True def _updateListBox(self): @@ -242,18 +245,14 @@ class GuiSessionLogView(QDialog): """ self.listBox.clear() self.timeFilter = 0.0 - self.timeTotal = 0.0 hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() for dStart, sDiff, nWords in self.logData: - self.timeTotal += sDiff - if hideZeros and nWords == 0: continue - if hideNegative and nWords < 0: continue @@ -274,7 +273,6 @@ class GuiSessionLogView(QDialog): self.listBox.addTopLevelItem(newItem) self.labelFilter.setText(self._formatTime(self.timeFilter)) - self.labelTotal.setText(self._formatTime(self.timeTotal)) return True @@ -287,4 +285,4 @@ class GuiSessionLogView(QDialog): tS = tS - tM*60 - tH*3600 return "%02d:%02d:%02d" % (tH,tM,tS) -# END Class GuiSessionLogView +# END Class GuiSessionLog diff --git a/nw/guimain.py b/nw/guimain.py index 13c1eeec..091c659d 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -43,7 +43,7 @@ from nw.gui import ( GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme, - GuiProjectSettings, GuiProjectTree, GuiSessionLogView + GuiProjectSettings, GuiProjectTree, GuiSessionLog ) from nw.core import NWProject, NWDoc, NWIndex from nw.constants import nwFiles, nwItemType, nwAlert @@ -810,7 +810,7 @@ class GuiMain(QMainWindow): """Open the session log dialog. """ if self.hasProject: - dlgTLine = GuiSessionLogView(self, self.theProject) + dlgTLine = GuiSessionLog(self, self.theProject) dlgTLine.exec_() return True From 9f1826cb5956d7573ae26fdb0ba11c40d7aa67b0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:53:31 +0200 Subject: [PATCH 04/12] Replaced checkboxes with switches --- nw/gui/projtree.py | 1 + nw/gui/sessionlog.py | 33 +++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index b42c19dc..d6eb8cdf 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -54,6 +54,7 @@ class GuiProjectTree(QTreeWidget): QTreeWidget.__init__(self, theParent) logger.debug("Initialising GuiProjectTree ...") + self.mainConf = nw.CONFIG self.theParent = theParent self.theTheme = theParent.theTheme diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index e7c62d5a..a880f45a 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -34,11 +34,12 @@ from datetime import datetime from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont from PyQt5.QtWidgets import ( - qApp, QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, - QGridLayout, QLabel, QGroupBox, QCheckBox + qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, + QLabel, QGroupBox ) from nw.constants import nwConst, nwFiles, nwAlert +from nw.gui.custom import QSwitch logger = logging.getLogger(__name__) @@ -62,14 +63,19 @@ class GuiSessionLog(QDialog): self.logData = [] self.timeFilter = 0.0 self.timeTotal = 0.0 + self.maxWords = 0 self.setWindowTitle("Session Log") self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumHeight(self.mainConf.pxInt(400)) # List Box - wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "widthCol0", 180)) - wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "widthCol1", 80)) + wCol0 = self.mainConf.pxInt( + self.optState.getInt("GuiSessionLog", "widthCol0", 180) + ) + wCol1 = self.mainConf.pxInt( + self.optState.getInt("GuiSessionLog", "widthCol1", 80) + ) self.listBox = QTreeWidget() self.listBox.setHeaderLabels(["Session Start","Length","Words"]) @@ -117,24 +123,30 @@ class GuiSessionLog(QDialog): self.infoForm.addWidget(self.labelFilter, 1, 1) # Filter Options + sPx = self.theTheme.baseIconSize + self.filterBox = QGroupBox("Filters", self) self.filterForm = QGridLayout(self) self.filterBox.setLayout(self.filterForm) - self.hideZeros = QCheckBox("Hide zero word count", self) + self.labelZeros = QLabel("Hide zero word count") + self.hideZeros = QSwitch(width=2*sPx, height=sPx) self.hideZeros.setChecked( self.optState.getBool("GuiSessionLog", "hideZeros", True) ) - self.hideZeros.stateChanged.connect(self._doHideZeros) + self.hideZeros.toggled.connect(self._doHideZeros) - self.hideNegative = QCheckBox("Hide negative word count", self) + self.labelNegative = QLabel("Hide negative word count") + self.hideNegative = QSwitch(width=2*sPx, height=sPx) self.hideNegative.setChecked( self.optState.getBool("GuiSessionLog", "hideNegative", False) ) - self.hideNegative.stateChanged.connect(self._doHideNegative) + self.hideNegative.toggled.connect(self._doHideNegative) - self.filterForm.addWidget(self.hideZeros, 0, 0) - self.filterForm.addWidget(self.hideNegative, 1, 0) + 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) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) @@ -229,6 +241,7 @@ class GuiSessionLog(QDialog): self.timeTotal += sDiff self.logData.append((dStart, sDiff, nWords)) + self.maxWords = max(self.maxWords, nWords) except Exception as e: self.theParent.makeAlert( From 274956948283a7750d0837038a254f86a86fa49f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 22:49:08 +0200 Subject: [PATCH 05/12] Added histogram bars for relative word count in session log --- nw/core/options.py | 3 ++ nw/gui/sessionlog.py | 77 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index b3a144d3..5f6f75de 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -46,8 +46,11 @@ class OptionState(): self.theState = {} self.validMap = { "GuiSessionLog": { + "winWidth", + "winHeight", "widthCol0", "widthCol1", + "widthCol2", "sortCol", "sortOrder", "hideZeros", diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index a880f45a..9777605f 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -31,11 +31,11 @@ import nw from os import path from datetime import datetime -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont +from PyQt5.QtCore import Qt, QRect +from PyQt5.QtGui import QFont, QPixmap, QIcon, QColor, QPainter from PyQt5.QtWidgets import ( qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, - QLabel, QGroupBox + QLabel, QGroupBox, QAbstractButton ) from nw.constants import nwConst, nwFiles, nwAlert @@ -48,6 +48,7 @@ class GuiSessionLog(QDialog): C_TIME = 0 C_LENGTH = 1 C_COUNT = 2 + C_BAR = 3 def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -68,6 +69,10 @@ class GuiSessionLog(QDialog): self.setWindowTitle("Session Log") self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumHeight(self.mainConf.pxInt(400)) + self.resize( + self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "winWidth", 550)), + self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "winHeight", 500)) + ) # List Box wCol0 = self.mainConf.pxInt( @@ -76,12 +81,16 @@ class GuiSessionLog(QDialog): wCol1 = self.mainConf.pxInt( self.optState.getInt("GuiSessionLog", "widthCol1", 80) ) + wCol2 = self.mainConf.pxInt( + self.optState.getInt("GuiSessionLog", "widthCol2", 80) + ) self.listBox = QTreeWidget() - self.listBox.setHeaderLabels(["Session Start","Length","Words"]) + self.listBox.setHeaderLabels(["Session Start","Length","Words","Histogram"]) self.listBox.setIndentation(0) self.listBox.setColumnWidth(self.C_TIME, wCol0) self.listBox.setColumnWidth(self.C_LENGTH, wCol1) + self.listBox.setColumnWidth(self.C_COUNT, wCol2) self.listBox.resizeColumnToContents(self.C_COUNT) hHeader = self.listBox.headerItem() @@ -104,6 +113,11 @@ class GuiSessionLog(QDialog): self.listBox.sortByColumn(sortCol, sortOrder) self.listBox.setSortingEnabled(True) + # Word Bar + self.barHeight = int(round(0.5*self.theTheme.fontPixelSize)) + self.barImage = QPixmap(self.barHeight, self.barHeight) + self.barImage.fill(self.palette().highlight().color()) + # Session Info self.infoBox = QGroupBox("Sum Total Time", self) self.infoForm = QGridLayout(self) @@ -178,18 +192,24 @@ class GuiSessionLog(QDialog): """ self.logData = [] + winWidth = self.mainConf.rpxInt(self.width()) + winHeight = self.mainConf.rpxInt(self.height()) widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0)) widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1)) + widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2)) sortCol = self.listBox.sortColumn() sortOrder = self.listBox.header().sortIndicatorOrder() hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() - self.optState.setValue("GuiSessionLog", "widthCol0", widthCol0) - self.optState.setValue("GuiSessionLog", "widthCol1", widthCol1) - self.optState.setValue("GuiSessionLog", "sortCol", sortCol) - self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) - self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) + self.optState.setValue("GuiSessionLog", "winWidth", winWidth) + self.optState.setValue("GuiSessionLog", "winHeight", winHeight) + self.optState.setValue("GuiSessionLog", "widthCol0", widthCol0) + self.optState.setValue("GuiSessionLog", "widthCol1", widthCol1) + self.optState.setValue("GuiSessionLog", "widthCol2", widthCol2) + self.optState.setValue("GuiSessionLog", "sortCol", sortCol) + self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) + self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative) self.optState.saveSettings() @@ -276,8 +296,18 @@ class GuiSessionLog(QDialog): newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) newItem.setText(self.C_COUNT, str(nWords)) + if nWords > 0: + theBar = self.barImage.scaled( + int(200*nWords/self.maxWords), + self.barHeight, + Qt.IgnoreAspectRatio, + Qt.FastTransformation + ) + newItem.setData(self.C_BAR, Qt.DecorationRole, theBar) + newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) + newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter) newItem.setFont(self.C_TIME, self.monoFont) newItem.setFont(self.C_LENGTH, self.monoFont) @@ -299,3 +329,32 @@ class GuiSessionLog(QDialog): return "%02d:%02d:%02d" % (tH,tM,tS) # END Class GuiSessionLog + +# class WordBar(QAbstractButton): + +# def __init__(self, sW, sH, parent=None): +# super().__init__(parent=parent) + +# self._theCol = self.palette().highlight().color() + +# self.setFixedWidth(sW) +# self.setFixedHeight(sH) + +# return + +# ## +# # Events +# ## + +# def paintEvent(self, event): +# """Drawing the bar. +# """ +# qPaint = QPainter(self) +# qPaint.setRenderHint(QPainter.Antialiasing, True) +# qPaint.setPen(self._theCol) +# qPaint.setBrush(self._theCol) +# qPaint.setOpacity(1.0) +# qPaint.drawRect(QRect(0, 0, self.width(), self.height())) +# return + +# # END Class StatusLED From 9e6feb6b05a7a8fd3c8360f637dd4d96142ff621 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jun 2020 23:39:20 +0200 Subject: [PATCH 06/12] Session log entries can be grouped by day --- nw/constants/constants.py | 1 + nw/core/options.py | 1 + nw/gui/sessionlog.py | 117 ++++++++++++++++++++------------------ 3 files changed, 64 insertions(+), 55 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index e4e6af98..6c57d2d6 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -31,6 +31,7 @@ class nwConst(): tStampFmt = "%Y-%m-%d %H:%M:%S" # Default format fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format + dStampFmt = "%Y-%m-%d" # Date only format # END Class nwConst diff --git a/nw/core/options.py b/nw/core/options.py index 5f6f75de..42c2dbd0 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -55,6 +55,7 @@ class OptionState(): "sortOrder", "hideZeros", "hideNegative", + "groupByDay", }, "GuiDocSplit": { "spLevel", diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 9777605f..cd9be467 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -31,11 +31,11 @@ import nw from os import path from datetime import datetime -from PyQt5.QtCore import Qt, QRect -from PyQt5.QtGui import QFont, QPixmap, QIcon, QColor, QPainter +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtWidgets import ( qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, - QLabel, QGroupBox, QAbstractButton + QLabel, QGroupBox ) from nw.constants import nwConst, nwFiles, nwAlert @@ -109,7 +109,6 @@ class GuiSessionLog(QDialog): self.optState.getInt("GuiSessionLog", "sortOrder", Qt.DescendingOrder), sortValid, Qt.DescendingOrder ) - self.listBox.sortByColumn(sortCol, sortOrder) self.listBox.setSortingEnabled(True) @@ -119,7 +118,7 @@ class GuiSessionLog(QDialog): self.barImage.fill(self.palette().highlight().color()) # Session Info - self.infoBox = QGroupBox("Sum Total Time", self) + self.infoBox = QGroupBox("Sum Totals", self) self.infoForm = QGridLayout(self) self.infoBox.setLayout(self.infoForm) @@ -131,10 +130,11 @@ class GuiSessionLog(QDialog): self.labelFilter.setFont(self.monoFont) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) - self.infoForm.addWidget(QLabel("All:"), 0, 0) - self.infoForm.addWidget(self.labelTotal, 0, 1) - self.infoForm.addWidget(QLabel("Filtered:"), 1, 0) - self.infoForm.addWidget(self.labelFilter, 1, 1) + 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) # Filter Options sPx = self.theTheme.baseIconSize @@ -148,19 +148,28 @@ class GuiSessionLog(QDialog): self.hideZeros.setChecked( self.optState.getBool("GuiSessionLog", "hideZeros", True) ) - self.hideZeros.toggled.connect(self._doHideZeros) + self.hideZeros.clicked.connect(self._updateListBox) self.labelNegative = QLabel("Hide negative word count") self.hideNegative = QSwitch(width=2*sPx, height=sPx) self.hideNegative.setChecked( self.optState.getBool("GuiSessionLog", "hideNegative", False) ) - self.hideNegative.toggled.connect(self._doHideNegative) + self.hideNegative.clicked.connect(self._updateListBox) + + self.labelByDay = QLabel("Group entries by day") + self.groupByDay = QSwitch(width=2*sPx, height=sPx) + self.groupByDay.setChecked( + self.optState.getBool("GuiSessionLog", "groupByDay", False) + ) + 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) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) @@ -172,6 +181,7 @@ class GuiSessionLog(QDialog): self.outerBox.addWidget(self.infoBox, 1, 0) self.outerBox.addWidget(self.filterBox, 1, 1) self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2) + self.outerBox.setRowStretch(0, 1) self.setLayout(self.outerBox) @@ -201,6 +211,7 @@ class GuiSessionLog(QDialog): sortOrder = self.listBox.header().sortIndicatorOrder() hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() + groupByDay = self.groupByDay.isChecked() self.optState.setValue("GuiSessionLog", "winWidth", winWidth) self.optState.setValue("GuiSessionLog", "winHeight", winHeight) @@ -211,24 +222,13 @@ class GuiSessionLog(QDialog): self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder) self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros) self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative) + self.optState.setValue("GuiSessionLog", "groupByDay", groupByDay) self.optState.saveSettings() self.close() return - def _doHideZeros(self, newState): - """Reload the list box with the new filter settings in place. - """ - self._updateListBox() - return - - def _doHideNegative(self, newState): - """Reload the list box with the new filter settings in place. - """ - self._updateListBox() - return - ## # Internal Functions ## @@ -281,8 +281,39 @@ class GuiSessionLog(QDialog): hideZeros = self.hideZeros.isChecked() hideNegative = self.hideNegative.isChecked() + groupByDay = self.groupByDay.isChecked() - for dStart, sDiff, nWords in self.logData: + # Group the data + if groupByDay: + listData = [] + listMax = 0 + + dayDate = None + daySDiff = 0 + dayWords = 0 + + for n, (dStart, sDiff, nWords) 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 + else: + daySDiff += sDiff + dayWords += nWords + + if dayDate is not None: + listData.append((dayDate, daySDiff, dayWords)) + + else: + listData = self.logData + listMax = self.maxWords + + # Populate the list + for dStart, sDiff, nWords in listData: if hideZeros and nWords == 0: continue @@ -291,14 +322,19 @@ class GuiSessionLog(QDialog): self.timeFilter += sDiff + if groupByDay: + sStart = dStart.strftime(nwConst.dStampFmt) + else: + sStart = dStart.strftime(nwConst.tStampFmt) + newItem = QTreeWidgetItem() - newItem.setText(self.C_TIME, str(dStart)) + newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) newItem.setText(self.C_COUNT, str(nWords)) if nWords > 0: theBar = self.barImage.scaled( - int(200*nWords/self.maxWords), + int(200*nWords/listMax), self.barHeight, Qt.IgnoreAspectRatio, Qt.FastTransformation @@ -329,32 +365,3 @@ class GuiSessionLog(QDialog): return "%02d:%02d:%02d" % (tH,tM,tS) # END Class GuiSessionLog - -# class WordBar(QAbstractButton): - -# def __init__(self, sW, sH, parent=None): -# super().__init__(parent=parent) - -# self._theCol = self.palette().highlight().color() - -# self.setFixedWidth(sW) -# self.setFixedHeight(sH) - -# return - -# ## -# # Events -# ## - -# def paintEvent(self, event): -# """Drawing the bar. -# """ -# qPaint = QPainter(self) -# qPaint.setRenderHint(QPainter.Antialiasing, True) -# qPaint.setPen(self._theCol) -# qPaint.setBrush(self._theCol) -# qPaint.setOpacity(1.0) -# qPaint.drawRect(QRect(0, 0, self.width(), self.height())) -# return - -# # END Class StatusLED 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 07/12] 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 From 98b822fa47da2f38e6c4288d75e25eea75056b3b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 11:19:40 +0200 Subject: [PATCH 08/12] Fix tests --- tests/reference/gui/0_nwProject.nwx | 2 ++ tests/reference/gui/1_nwProject.nwx | 2 ++ tests/reference/gui/2_nwProject.nwx | 2 ++ tests/reference/gui/3_nwProject.nwx | 2 ++ tests/reference/proj/1_nwProject.nwx | 2 ++ tests/reference/proj/2_nwProject.nwx | 2 ++ tests/reference/proj/3_nwProject.nwx | 2 ++ 7 files changed, 14 insertions(+) diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index 63535bfa..d1a794e7 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -14,6 +14,8 @@ None None 0 + 0 + 0 %title% diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 17180919..e8a705b1 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -14,6 +14,8 @@ 31489056e0916 None 86 + 59 + 27 %title% diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index 316e30c1..2f4ff89c 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -16,6 +16,8 @@ None None 0 + 0 + 0 With This Stuff diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 4d6ef96a..8eeb4dca 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -14,6 +14,8 @@ 31489056e0916 None 59 + 59 + 0 %title% diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index 8516baae..12697dbe 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -14,6 +14,8 @@ None None 0 + 0 + 0 %title% diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index da82f956..ed6bbbe4 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -14,6 +14,8 @@ None None 0 + 0 + 0 %title% diff --git a/tests/reference/proj/3_nwProject.nwx b/tests/reference/proj/3_nwProject.nwx index 8adfadb8..58bef95d 100644 --- a/tests/reference/proj/3_nwProject.nwx +++ b/tests/reference/proj/3_nwProject.nwx @@ -14,6 +14,8 @@ None None 0 + 0 + 0 %title% From bdedc7a11c4b2e07158cf4e36a8a6894ccabaacc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 18:23:44 +0200 Subject: [PATCH 09/12] The session data can now be exported to JSON and CSV files with the filters applied --- nw/gui/build.py | 3 - nw/gui/sessionlog.py | 156 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 132 insertions(+), 27 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index ab5b3611..dab7360e 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -568,9 +568,6 @@ class GuiBuildNovel(QDialog): def _saveDocument(self, theFormat): """Save the document to various formats. """ - - # FMT_PDF - byteFmt = QByteArray() fileExt = "" textFmt = "" diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 03ace4db..709a0961 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -26,6 +26,7 @@ """ import logging +import json import nw from os import path @@ -35,7 +36,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtWidgets import ( qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout, - QLabel, QGroupBox + QLabel, QGroupBox, QMenu, QAction, QFileDialog ) from nw.constants import nwConst, nwFiles, nwAlert @@ -50,6 +51,9 @@ class GuiSessionLog(QDialog): C_COUNT = 2 C_BAR = 3 + FMT_JSON = 0 + FMT_CSV = 1 + def __init__(self, theParent, theProject): QDialog.__init__(self, theParent) @@ -62,6 +66,7 @@ class GuiSessionLog(QDialog): self.optState = theProject.optState self.logData = [] + self.filterData = [] self.timeFilter = 0.0 self.timeTotal = 0.0 self.wordOffset = 0 @@ -212,6 +217,18 @@ class GuiSessionLog(QDialog): self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox.rejected.connect(self._doClose) + self.btnSave = self.buttonBox.addButton("Save As", QDialogButtonBox.ActionRole) + self.saveMenu = QMenu(self) + self.btnSave.setMenu(self.saveMenu) + + self.saveJSON = QAction("JSON Data File (.json)", self) + self.saveJSON.triggered.connect(lambda: self._saveData(self.FMT_JSON)) + self.saveMenu.addAction(self.saveJSON) + + self.saveCSV = QAction("CSV Data File (.csv)", self) + self.saveCSV.triggered.connect(lambda: self._saveData(self.FMT_CSV)) + self.saveMenu.addAction(self.saveCSV) + # Assemble self.outerBox = QGridLayout() self.outerBox.addWidget(self.listBox, 0, 0, 1, 2) @@ -270,6 +287,101 @@ class GuiSessionLog(QDialog): return + def _saveData(self, dataFmt): + """Save the content of the list box to a file. + """ + fileExt = "" + textFmt = "" + + if dataFmt == self.FMT_JSON: + fileExt = "json" + textFmt = "JSON Data File" + + elif dataFmt == self.FMT_CSV: + fileExt = "csv" + textFmt = "CSV Data File" + + else: + return False + + # Generate the file name + if fileExt: + fileName = "sessionStats.%s" % fileExt + saveDir = self.mainConf.lastPath + savePath = path.join(saveDir, fileName) + if not path.isdir(saveDir): + saveDir = self.mainConf.homePath + + if self.mainConf.showGUI: + dlgOpt = QFileDialog.Options() + dlgOpt |= QFileDialog.DontUseNativeDialog + saveTo = QFileDialog.getSaveFileName( + self, "Save Document As", savePath, options=dlgOpt + ) + if saveTo[0]: + savePath = saveTo[0] + else: + return False + + self.mainConf.setLastPath(savePath) + + else: + return False + + # Do the actual writing + wSuccess = False + errMsg = "" + + try: + with open(savePath, mode="w", encoding="utf8") as outFile: + if dataFmt == self.FMT_JSON: + jsonData = [] + for _, sD, tT, wD, wA, wB in self.filterData: + jsonData.append({ + "date": sD, + "length": tT, + "newWords": wD, + "novelWords": wA, + "noteWords": wB, + }) + outFile.write(json.dumps(jsonData, indent=2)) + wSuccess = True + + elif dataFmt == self.FMT_CSV: + outFile.write( + "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n" % ( + "Date", "Length (sec)", "Words Changed", "Novel Words", "Note Words" + ) + ) + for _, sD, tT, wD, wA, wB in self.filterData: + outFile.write( + "\"%s\",%d,%d,%d,%d\n" % (sD, tT, wD, wA, wB) + ) + wSuccess = True + + else: + errMsg = "Unknown format" + + except Exception as e: + errMsg = str(e) + + # Report to user + if self.mainConf.showGUI: + if wSuccess: + self.theParent.makeAlert( + "%s file successfully written to:
%s" % ( + textFmt, savePath + ), nwAlert.INFO + ) + else: + self.theParent.makeAlert( + "Failed to write %s file. %s" % ( + textFmt, errMsg + ), nwAlert.ERROR + ) + + return True + ## # Internal Functions ## @@ -277,16 +389,13 @@ class GuiSessionLog(QDialog): def _loadLogFile(self): """Load the content of the log file into a buffer. """ - self.logData = [] logger.debug("Loading session log file") + self.logData = [] + ttNovel = 0 ttNotes = 0 - isFirst = True - osNovel = 0 - osNotes = 0 - try: logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS) with open(logFile, mode="r", encoding="utf8") as inFile: @@ -319,16 +428,7 @@ class GuiSessionLog(QDialog): 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)) + self.logData.append((dStart, sDiff, wcNovel, wcNotes)) except Exception as e: self.theParent.makeAlert( @@ -384,9 +484,10 @@ class GuiSessionLog(QDialog): tempData = self.logData # Calculate Word Diff - listData = [] + self.filterData = [] pcTotal = 0 listMax = 0 + isFirst = True for dStart, sDiff, wcNovel, wcNotes in tempData: wcTotal = 0 @@ -401,22 +502,29 @@ class GuiSessionLog(QDialog): 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 isFirst: + # Subtract the offset from the first list entry + dwTotal -= self.wordOffset + isFirst = False if groupByDay: sStart = dStart.strftime(nwConst.dStampFmt) else: sStart = dStart.strftime(nwConst.tStampFmt) + self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes)) + listMax = max(listMax, dwTotal) + pcTotal = wcTotal + + # Populate the list + for _, sStart, sDiff, nWords, _, _ in self.filterData: + newItem = QTreeWidgetItem() newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) - newItem.setText(self.C_COUNT, str(nWords)) + newItem.setData(self.C_LENGTH, Qt.UserRole, sDiff) + newItem.setText(self.C_COUNT, "{:n}".format(nWords)) + newItem.setData(self.C_COUNT, Qt.UserRole, nWords) if nWords > 0 and listMax > 0: theBar = self.barImage.scaled( From b05d6f2dc12e46cf7c23f134a25a88435143b7fd Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 18:28:36 +0200 Subject: [PATCH 10/12] Remove some unused data and variables from the code --- nw/gui/sessionlog.py | 8 +++----- sample/nwProject.nwx | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 709a0961..9c66e923 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -68,7 +68,6 @@ class GuiSessionLog(QDialog): self.logData = [] self.filterData = [] self.timeFilter = 0.0 - self.timeTotal = 0.0 self.wordOffset = 0 self.setWindowTitle("Session Log") @@ -395,6 +394,7 @@ class GuiSessionLog(QDialog): ttNovel = 0 ttNotes = 0 + ttTime = 0 try: logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS) @@ -421,7 +421,7 @@ class GuiSessionLog(QDialog): tDiff = dEnd - dStart sDiff = tDiff.total_seconds() - self.timeTotal += sDiff + ttTime += sDiff wcNovel = int(inData[4]) wcNotes = int(inData[5]) @@ -436,7 +436,7 @@ class GuiSessionLog(QDialog): ) return False - self.labelTotal.setText(self._formatTime(self.timeTotal)) + self.labelTotal.setText(self._formatTime(ttTime)) self.novelWords.setText("{:n}".format(ttNovel)) self.notesWords.setText("{:n}".format(ttNotes)) self.totalWords.setText("{:n}".format(ttNovel + ttNotes)) @@ -522,9 +522,7 @@ class GuiSessionLog(QDialog): newItem = QTreeWidgetItem() newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) - newItem.setData(self.C_LENGTH, Qt.UserRole, sDiff) newItem.setText(self.C_COUNT, "{:n}".format(nWords)) - newItem.setData(self.C_COUNT, Qt.UserRole, nWords) if nWords > 0 and listMax > 0: theBar = self.barImage.scaled( diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index c29a7788..8f8ee49c 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 565 - 99 - 23298 + 583 + 102 + 24189 False From 198836c261bf7caf91cce19e03ea990510378d77 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 18:37:17 +0200 Subject: [PATCH 11/12] No need to set the session log offset value if it's 0 --- nw/core/project.py | 6 ++++-- nw/gui/sessionlog.py | 4 ++-- sample/nwProject.nwx | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index 0d43229c..c981e4c8 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1121,8 +1121,10 @@ class NWProject(): with open(sessionFile, mode="a+", encoding="utf8") as 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" + if self.lastWCount > 0: + outFile.write("# Offset %d\n" % self.lastWCount) + outFile.write("# %-17s %-19s %8s %8s\n" % ( + "Start Time", "End Time", "Novel", "Notes" )) outFile.write("%-19s %-19s %8d %8d\n" % ( diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py index 9c66e923..cab4a92c 100644 --- a/nw/gui/sessionlog.py +++ b/nw/gui/sessionlog.py @@ -401,8 +401,8 @@ class GuiSessionLog(QDialog): 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()) + if inLine.startswith("# Offset"): + self.wordOffset = int(inLine[9:].strip()) logger.verbose( "Initial word count when log was started is %d" % self.wordOffset ) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 8f8ee49c..ecdc1d0c 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 583 + 587 102 - 24189 + 24235 False From f78fbcbc8f644eda8beb7cd9b62d5441307c423e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jun 2020 19:07:00 +0200 Subject: [PATCH 12/12] Added test for the session log tool --- tests/test_gui.py | 113 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/tests/test_gui.py b/tests/test_gui.py index 3ad703a5..d52c4250 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -2,7 +2,7 @@ """novelWriter Main GUI Class Tester """ -import nw, pytest, sys +import nw, pytest, sys, json from nwtools import * from os import path, unlink @@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt from nw.gui import ( GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel, - GuiDocMerge, GuiDocSplit + GuiDocMerge, GuiDocSplit, GuiSessionLog ) from nw.constants import * @@ -409,6 +409,115 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): nwGUI.closeMain() # qtbot.stopForInteraction() +@pytest.mark.gui +def testSessionLogExport(qtbot, nwTempGUI, nwRef, nwTemp): + nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + assert nwGUI.openProject(nwTempGUI) + qtbot.wait(stepDelay) + + nwGUI.mainConf.lastPath = nwTempGUI + sessLog = GuiSessionLog(nwGUI, nwGUI.theProject) + sessLog.show() + qtbot.wait(stepDelay) + + assert sessLog._saveData(sessLog.FMT_CSV) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 3 + assert jsonData[0]["length"] > 0 + assert jsonData[0]["newWords"] == 86 + assert jsonData[0]["novelWords"] == 59 + assert jsonData[0]["noteWords"] == 27 + + # No Novel Files + qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 2 + assert jsonData[0]["length"] > 0 + assert jsonData[0]["newWords"] == 27 + assert jsonData[0]["novelWords"] == 59 + assert jsonData[0]["noteWords"] == 27 + + # No Note Files + qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton) + qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 3 + assert jsonData[0]["length"] > 0 + assert jsonData[0]["newWords"] == 59 + assert jsonData[0]["novelWords"] == 59 + assert jsonData[0]["noteWords"] == 27 + + # No Negative Entries + qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton) + qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 1 + + # Un-hide Zero Entries + qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton) + qtbot.mouseClick(sessLog.hideZeros, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + assert len(jsonData) == 4 + + # Group by Day + qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton) + qtbot.wait(stepDelay) + assert sessLog._saveData(sessLog.FMT_JSON) + qtbot.wait(stepDelay) + + jsonStats = path.join(nwTempGUI, "sessionStats.json") + with open(jsonStats, mode="r", encoding="utf-8") as inFile: + jsonData = json.loads(inFile.read()) + + # Check against both 1 and 2 as this can be 2 if test was started just before midnight. + # A failed test should in any case produce a 4 + assert len(jsonData) in (1, 2) + + # qtbot.stopForInteraction() + + sessLog._doClose() + nwGUI.closeMain() + @pytest.mark.gui def testAboutBox(qtbot, nwTempGUI, nwRef, nwTemp): nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])