diff --git a/nw/constants/constants.py b/nw/constants/constants.py
index e4e6af98..18443c1d 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
@@ -50,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 0ff7c919..a9a06e0d 100644
--- a/nw/core/options.py
+++ b/nw/core/options.py
@@ -45,14 +45,19 @@ class OptionState():
self.theState = {}
self.validMap = {
- "GuiSession": {
+ "GuiSessionLog": {
+ "winWidth",
+ "winHeight",
"widthCol0",
"widthCol1",
"widthCol2",
"sortCol",
"sortOrder",
+ "incNovel",
+ "incNotes",
"hideZeros",
"hideNegative",
+ "groupByDay",
},
"GuiDocSplit": {
"spLevel",
diff --git a/nw/core/project.py b/nw/core/project.py
index 3db93bf2..c981e4c8 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,24 @@ 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
+ 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" % (
+ 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/__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/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/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 5f3c61f0..cab4a92c 100644
--- a/nw/gui/sessionlog.py
+++ b/nw/gui/sessionlog.py
@@ -26,78 +26,106 @@
"""
import logging
+import json
import nw
from os import path
from datetime import datetime
from PyQt5.QtCore import Qt
-from PyQt5.QtGui import QFont
+from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import (
- QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox,
- QGridLayout, QLabel, QGroupBox, QCheckBox
+ qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout,
+ QLabel, QGroupBox, QMenu, QAction, QFileDialog
)
from nw.constants import nwConst, nwFiles, nwAlert
+from nw.gui.custom import QSwitch
logger = logging.getLogger(__name__)
-class GuiSessionLogView(QDialog):
+class GuiSessionLog(QDialog):
+
+ C_TIME = 0
+ C_LENGTH = 1
+ C_COUNT = 2
+ C_BAR = 3
+
+ FMT_JSON = 0
+ FMT_CSV = 1
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
- logger.debug("Initialising SessionLogView ...")
+ logger.debug("Initialising GuiSessionLog ...")
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.logData = []
+ self.filterData = []
self.timeFilter = 0.0
- self.timeTotal = 0.0
-
- self.outerBox = QGridLayout()
- self.bottomBox = QHBoxLayout()
+ self.wordOffset = 0
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))
+ )
- 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))
+ # List Box
+ wCol0 = self.mainConf.pxInt(
+ self.optState.getInt("GuiSessionLog", "widthCol0", 180)
+ )
+ 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(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.setColumnWidth(self.C_COUNT, wCol2)
+ 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.setPointSizeF(0.9*self.theTheme.fontPointSize)
+ self.monoFont.setFamily(self.theTheme.guiFontFixed.family())
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
)
-
self.listBox.sortByColumn(sortCol, sortOrder)
self.listBox.setSortingEnabled(True)
+ # 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())
+
# Session Info
- self.infoBox = QGroupBox("Sum Total Time", self)
- self.infoBoxForm = QGridLayout(self)
- self.infoBox.setLayout(self.infoBoxForm)
+ self.infoBox = QGroupBox("Sum Totals", self)
+ self.infoForm = QGridLayout(self)
+ self.infoBox.setLayout(self.infoForm)
self.labelTotal = QLabel(self._formatTime(0))
self.labelTotal.setFont(self.monoFont)
@@ -107,105 +135,300 @@ 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.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
- self.filterBox = QGroupBox("Filters", self)
- self.filterBoxForm = QGridLayout(self)
- self.filterBox.setLayout(self.filterBoxForm)
+ sPx = self.theTheme.baseIconSize
- self.hideZeros = QCheckBox("Hide zero word count", self)
+ self.filterBox = QGroupBox("Filters", self)
+ 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(
- self.optState.getBool("GuiSession", "hideZeros", True)
+ self.optState.getBool("GuiSessionLog", "hideZeros", True)
)
- self.hideZeros.stateChanged.connect(self._doHideZeros)
+ self.hideZeros.clicked.connect(self._updateListBox)
- 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("GuiSession", "hideNegative", False)
+ self.optState.getBool("GuiSessionLog", "hideNegative", False)
)
- self.hideNegative.stateChanged.connect(self._doHideNegative)
+ self.hideNegative.clicked.connect(self._updateListBox)
- self.filterBoxForm.addWidget(self.hideZeros, 0, 0)
- self.filterBoxForm.addWidget(self.hideNegative, 1, 0)
+ 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.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)
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)
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)
- self.show()
+ logger.debug("GuiSessionLog initialisation complete")
- logger.debug("SessionLogView initialisation complete")
-
- self._loadSessionLog()
+ qApp.processEvents()
+ self._loadLogFile()
+ self._updateListBox()
return
- def _loadSessionLog(self):
+ ##
+ # Slots
+ ##
- 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
+ def _doClose(self):
+ """Save the state of the window, clear cache, end close.
+ """
+ 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()
+ incNovel = self.incNovel.isChecked()
+ incNotes = self.incNotes.isChecked()
hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked()
+ groupByDay = self.groupByDay.isChecked()
+
+ 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", "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)
+
+ self.optState.saveSettings()
+ self.close()
+
+ 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 = ""
- logger.debug("Loading session log file")
try:
- with open(logFile,mode="r",encoding="utf8") as inFile:
+ 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
+ ##
+
+ def _loadLogFile(self):
+ """Load the content of the log file into a buffer.
+ """
+ logger.debug("Loading session log file")
+
+ self.logData = []
+
+ ttNovel = 0
+ ttNotes = 0
+ ttTime = 0
+
+ try:
+ logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
+ with open(logFile, mode="r", encoding="utf8") as inFile:
for inLine in inFile:
- inData = inLine.split()
- if len(inData) != 8:
+ if inLine.startswith("#"):
+ if inLine.startswith("# Offset"):
+ self.wordOffset = int(inLine[9:].strip())
+ logger.verbose(
+ "Initial word count when log was started is %d" % self.wordOffset
+ )
continue
+
+ inData = inLine.split()
+ 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()
+ ttTime += sDiff
- self.timeTotal += sDiff
- if abs(nWords) > 0:
- self.timeFilter += sDiff
+ wcNovel = int(inData[4])
+ wcNotes = int(inData[5])
+ ttNovel = wcNovel
+ ttNotes = wcNotes
- if hideZeros and nWords == 0:
- continue
-
- if hideNegative and nWords < 0:
- continue
-
- newItem = QTreeWidgetItem(
- [str(dStart), self._formatTime(sDiff), str(nWords), ""]
- )
-
- newItem.setTextAlignment(1,Qt.AlignRight)
- newItem.setTextAlignment(2,Qt.AlignRight)
-
- newItem.setFont(0,self.monoFont)
- newItem.setFont(1,self.monoFont)
- newItem.setFont(2,self.monoFont)
-
- self.listBox.addTopLevelItem(newItem)
+ self.logData.append((dStart, sDiff, wcNovel, wcNotes))
except Exception as e:
self.theParent.makeAlert(
@@ -213,47 +436,125 @@ class GuiSessionLogView(QDialog):
)
return False
- self.labelFilter.setText(self._formatTime(self.timeFilter))
- 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))
return True
- def _doClose(self):
+ def _updateListBox(self):
+ """Load/reload the content of the list box.
+ """
+ self.listBox.clear()
+ self.timeFilter = 0.0
- 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()
+ incNovel = self.incNovel.isChecked()
+ incNotes = self.incNotes.isChecked()
hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked()
+ groupByDay = self.groupByDay.isChecked()
- 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)
- self.optState.setValue("GuiSession", "hideNegative", hideNegative)
+ # Group the data
+ if groupByDay:
+ tempData = []
+ sessDate = None
+ sessTime = 0
+ lstNovel = 0
+ lstNotes = 0
- self.optState.saveSettings()
- self.close()
+ for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData):
+ if n == 0:
+ sessDate = dStart.date()
+ if sessDate != dStart.date():
+ tempData.append((sessDate, sessTime, lstNovel, lstNotes))
+ sessDate = dStart.date()
+ sessTime = sDiff
+ lstNovel = wcNovel
+ lstNotes = wcNotes
+ else:
+ sessTime += sDiff
+ lstNovel = wcNovel
+ lstNotes = wcNotes
- return
+ if sessDate is not None:
+ tempData.append((sessDate, sessTime, lstNovel, lstNotes))
- def _doHideZeros(self, newState):
- self._loadSessionLog()
- return
+ else:
+ tempData = self.logData
- def _doHideNegative(self, newState):
- self._loadSessionLog()
- return
+ # Calculate Word Diff
+ self.filterData = []
+ pcTotal = 0
+ listMax = 0
+ isFirst = True
+ 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
+
+ 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, "{:n}".format(nWords))
+
+ if nWords > 0 and listMax > 0:
+ theBar = self.barImage.scaled(
+ int(200*nWords/listMax),
+ 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)
+ newItem.setFont(self.C_COUNT, self.monoFont)
+
+ self.listBox.addTopLevelItem(newItem)
+ self.timeFilter += sDiff
+
+ self.labelFilter.setText(self._formatTime(self.timeFilter))
+
+ 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
tS = tS - tM*60 - tH*3600
return "%02d:%02d:%02d" % (tH,tM,tS)
-# END Class GuiSessionLogView
+# END Class GuiSessionLog
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()))
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
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..ecdc1d0c 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,21 +1,23 @@
-
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 407
- 71
- 15118
+ 587
+ 102
+ 24235
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
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%
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])