Merge pull request #339 from vkbo/session_log

Session Log Updates
This commit is contained in:
Veronica K. Berglyd Olsen
2020-06-26 19:11:40 +02:00
committed by GitHub
20 changed files with 611 additions and 142 deletions
+2 -1
View File
@@ -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"
+6 -1
View File
@@ -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",
+30 -10
View File
@@ -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
+18 -1
View File
@@ -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
##
+2 -2
View File
@@ -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",
-3
View File
@@ -568,9 +568,6 @@ class GuiBuildNovel(QDialog):
def _saveDocument(self, theFormat):
"""Save the document to various formats.
"""
# FMT_PDF
byteFmt = QByteArray()
fileExt = ""
textFmt = ""
+1
View File
@@ -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
+411 -110
View File
@@ -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:<br> %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
+2
View File
@@ -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()))
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 “* * *”.
+11 -9
View File
@@ -1,21 +1,23 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.9.0rc1" hexVersion="0x000900c1" fileVersion="1.1" timeStamp="2020-06-19 19:50:54">
<novelWriterXML appVersion="0.10.0rc1" hexVersion="0x001000c1" fileVersion="1.1" timeStamp="2020-06-26 18:36:47">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>407</saveCount>
<autoCount>71</autoCount>
<editTime>15118</editTime>
<saveCount>587</saveCount>
<autoCount>102</autoCount>
<editTime>24235</editTime>
</project>
<settings>
<doBackup>False</doBackup>
<spellCheck>True</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>bb2c23b3c42cc</lastViewed>
<lastWordCount>967</lastWordCount>
<lastViewed>b3e74dbc1f584</lastViewed>
<lastWordCount>982</lastWordCount>
<novelWordCount>606</novelWordCount>
<notesWordCount>376</notesWordCount>
<autoReplace>
<A>B</A>
<B>E</B>
@@ -114,10 +116,10 @@
<status>1st Draft</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>1483</charCount>
<wordCount>263</wordCount>
<charCount>1564</charCount>
<wordCount>278</wordCount>
<paraCount>8</paraCount>
<cursorPos>1086</cursorPos>
<cursorPos>1633</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>86</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>27</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -16,6 +16,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace>
<This>With This Stuff </This>
</autoReplace>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>59</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+111 -2
View File
@@ -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])