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 tStampFmt = "%Y-%m-%d %H:%M:%S" # Default format
fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format
dStampFmt = "%Y-%m-%d" # Date only format
# END Class nwConst # END Class nwConst
@@ -50,7 +51,7 @@ class nwFiles():
PROJ_LOCK = "nwProject.lock" PROJ_LOCK = "nwProject.lock"
TOC_TXT = "ToC.txt" TOC_TXT = "ToC.txt"
TOC_JSON = "ToC.json" TOC_JSON = "ToC.json"
SESS_INFO = "sessionInfo.log" SESS_STATS = "sessionStats.log"
INDEX_FILE = "tagsIndex.json" INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json" OPTS_FILE = "guiOptions.json"
RECENT_FILE = "recentProjects.json" RECENT_FILE = "recentProjects.json"
+6 -1
View File
@@ -45,14 +45,19 @@ class OptionState():
self.theState = {} self.theState = {}
self.validMap = { self.validMap = {
"GuiSession": { "GuiSessionLog": {
"winWidth",
"winHeight",
"widthCol0", "widthCol0",
"widthCol1", "widthCol1",
"widthCol2", "widthCol2",
"sortCol", "sortCol",
"sortOrder", "sortOrder",
"incNovel",
"incNotes",
"hideZeros", "hideZeros",
"hideNegative", "hideNegative",
"groupByDay",
}, },
"GuiDocSplit": { "GuiDocSplit": {
"spLevel", "spLevel",
+30 -10
View File
@@ -93,6 +93,8 @@ class NWProject():
self.lastViewed = None # The handle of the last file to be viewed self.lastViewed = None # The handle of the last file to be viewed
self.lastWCount = 0 # The project word count from last session self.lastWCount = 0 # The project word count from last session
self.currWCount = 0 # The project word count in current 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 self.doBackup = True # Run project backup on exit
# Set Defaults # Set Defaults
@@ -231,6 +233,8 @@ class NWProject():
self.lastViewed = None self.lastViewed = None
self.lastWCount = 0 self.lastWCount = 0
self.currWCount = 0 self.currWCount = 0
self.novelWCount = 0
self.notesWCount = 0
return return
@@ -435,6 +439,10 @@ class NWProject():
self.lastViewed = checkString(xItem.text, None, True) self.lastViewed = checkString(xItem.text, None, True)
elif xItem.tag == "lastWordCount": elif xItem.tag == "lastWordCount":
self.lastWCount = checkInt(xItem.text, 0, False) 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": elif xItem.tag == "status":
self.statusItems.unpackEntries(xItem) self.statusItems.unpackEntries(xItem)
elif xItem.tag == "importance": elif xItem.tag == "importance":
@@ -501,6 +509,10 @@ class NWProject():
}) })
editTime = int(self.editTime + saveTime - self.projOpened) 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 # Save Project Meta
xProject = etree.SubElement(nwXML, "project") xProject = etree.SubElement(nwXML, "project")
@@ -519,6 +531,8 @@ class NWProject():
self._packProjectValue(xSettings, "lastEdited", self.lastEdited) self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
self._packProjectValue(xSettings, "lastViewed", self.lastViewed) self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
self._packProjectValue(xSettings, "lastWordCount", self.currWCount) self._packProjectValue(xSettings, "lastWordCount", self.currWCount)
self._packProjectValue(xSettings, "novelWordCount", wcNovel)
self._packProjectValue(xSettings, "notesWordCount", wcNotes)
xAutoRep = etree.SubElement(xSettings, "autoReplace") xAutoRep = etree.SubElement(xSettings, "autoReplace")
for aKey, aValue in self.autoReplace.items(): for aKey, aValue in self.autoReplace.items():
@@ -1101,18 +1115,24 @@ class NWProject():
if not self.ensureFolderStructure(): if not self.ensureFolderStructure():
return False 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: with open(sessionFile, mode="a+", encoding="utf8") as outFile:
print(( if not isFile:
"Start: {opened:s} " # It's a new file, so add a header
"End: {closed:s} " if self.lastWCount > 0:
"Words: {words:8d}" outFile.write("# Offset %d\n" % self.lastWCount)
).format( outFile.write("# %-17s %-19s %8s %8s\n" % (
opened = formatTimeStamp(self.projOpened), "Start Time", "End Time", "Novel", "Notes"
closed = formatTimeStamp(time()), ))
words = self.getSessionWordCount(),
), file=outFile) outFile.write("%-19s %-19s %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(time()),
self.novelWCount,
self.notesWCount,
))
return True return True
+18 -1
View File
@@ -36,7 +36,7 @@ from time import time
from nw.core.item import NWItem from nw.core.item import NWItem
from nw.common import checkString from nw.common import checkString
from nw.constants import nwFiles, nwItemType, nwItemClass from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -182,6 +182,23 @@ class NWTree():
return 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 # 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.projload import GuiProjectLoad
from nw.gui.projsettings import GuiProjectSettings from nw.gui.projsettings import GuiProjectSettings
from nw.gui.projtree import GuiProjectTree 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.statusbar import GuiMainStatus
from nw.gui.theme import GuiIcons, GuiTheme from nw.gui.theme import GuiIcons, GuiTheme
@@ -36,7 +36,7 @@ __all__ = [
"GuiProjectLoad", "GuiProjectLoad",
"GuiProjectSettings", "GuiProjectSettings",
"GuiProjectTree", "GuiProjectTree",
"GuiSessionLogView", "GuiSessionLog",
"GuiMainStatus", "GuiMainStatus",
"GuiIcons", "GuiIcons",
"GuiTheme", "GuiTheme",
-3
View File
@@ -568,9 +568,6 @@ class GuiBuildNovel(QDialog):
def _saveDocument(self, theFormat): def _saveDocument(self, theFormat):
"""Save the document to various formats. """Save the document to various formats.
""" """
# FMT_PDF
byteFmt = QByteArray() byteFmt = QByteArray()
fileExt = "" fileExt = ""
textFmt = "" textFmt = ""
+1
View File
@@ -54,6 +54,7 @@ class GuiProjectTree(QTreeWidget):
QTreeWidget.__init__(self, theParent) QTreeWidget.__init__(self, theParent)
logger.debug("Initialising GuiProjectTree ...") logger.debug("Initialising GuiProjectTree ...")
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
+411 -110
View File
@@ -26,78 +26,106 @@
""" """
import logging import logging
import json
import nw import nw
from os import path from os import path
from datetime import datetime from datetime import datetime
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout,
QGridLayout, QLabel, QGroupBox, QCheckBox QLabel, QGroupBox, QMenu, QAction, QFileDialog
) )
from nw.constants import nwConst, nwFiles, nwAlert from nw.constants import nwConst, nwFiles, nwAlert
from nw.gui.custom import QSwitch
logger = logging.getLogger(__name__) 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): def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent) QDialog.__init__(self, theParent)
logger.debug("Initialising SessionLogView ...") logger.debug("Initialising GuiSessionLog ...")
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent 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.timeFilter = 0.0
self.timeTotal = 0.0 self.wordOffset = 0
self.outerBox = QGridLayout()
self.bottomBox = QHBoxLayout()
self.setWindowTitle("Session Log") self.setWindowTitle("Session Log")
self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumWidth(self.mainConf.pxInt(420))
self.setMinimumHeight(self.mainConf.pxInt(400)) 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)) # List Box
wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol1", 80)) wCol0 = self.mainConf.pxInt(
wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol2", 80)) 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 = QTreeWidget()
self.listBox.setHeaderLabels(["Session Start","Length","Words",""]) self.listBox.setHeaderLabels(["Session Start","Length","Words","Histogram"])
self.listBox.setIndentation(0) self.listBox.setIndentation(0)
self.listBox.setColumnWidth(0, wCol0) self.listBox.setColumnWidth(self.C_TIME, wCol0)
self.listBox.setColumnWidth(1, wCol1) self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
self.listBox.setColumnWidth(2, wCol2) self.listBox.setColumnWidth(self.C_COUNT, wCol2)
self.listBox.setColumnWidth(3, 0) self.listBox.resizeColumnToContents(self.C_COUNT)
hHeader = self.listBox.headerItem() hHeader = self.listBox.headerItem()
hHeader.setTextAlignment(1,Qt.AlignRight) hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
hHeader.setTextAlignment(2,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) sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
sortCol = self.optState.validIntRange( 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( sortOrder = self.optState.validIntTuple(
self.optState.getInt("GuiSession", "sortOrder", Qt.DescendingOrder), self.optState.getInt("GuiSessionLog", "sortOrder", Qt.DescendingOrder),
sortValid, Qt.DescendingOrder sortValid, Qt.DescendingOrder
) )
self.listBox.sortByColumn(sortCol, sortOrder) self.listBox.sortByColumn(sortCol, sortOrder)
self.listBox.setSortingEnabled(True) 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 # Session Info
self.infoBox = QGroupBox("Sum Total Time", self) self.infoBox = QGroupBox("Sum Totals", self)
self.infoBoxForm = QGridLayout(self) self.infoForm = QGridLayout(self)
self.infoBox.setLayout(self.infoBoxForm) self.infoBox.setLayout(self.infoForm)
self.labelTotal = QLabel(self._formatTime(0)) self.labelTotal = QLabel(self._formatTime(0))
self.labelTotal.setFont(self.monoFont) self.labelTotal.setFont(self.monoFont)
@@ -107,105 +135,300 @@ class GuiSessionLogView(QDialog):
self.labelFilter.setFont(self.monoFont) self.labelFilter.setFont(self.monoFont)
self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.infoBoxForm.addWidget(QLabel("All:"), 0, 0) self.novelWords = QLabel("0")
self.infoBoxForm.addWidget(self.labelTotal, 0, 1) self.novelWords.setFont(self.monoFont)
self.infoBoxForm.addWidget(QLabel("Filtered:"), 1, 0) self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.infoBoxForm.addWidget(self.labelFilter, 1, 1)
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 # Filter Options
self.filterBox = QGroupBox("Filters", self) sPx = self.theTheme.baseIconSize
self.filterBoxForm = QGridLayout(self)
self.filterBox.setLayout(self.filterBoxForm)
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.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.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.labelByDay = QLabel("Group entries by day")
self.filterBoxForm.addWidget(self.hideNegative, 1, 0) 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 # Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
self.buttonBox.rejected.connect(self._doClose) 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 # Assemble
self.outerBox = QGridLayout()
self.outerBox.addWidget(self.listBox, 0, 0, 1, 2) self.outerBox.addWidget(self.listBox, 0, 0, 1, 2)
self.outerBox.addWidget(self.infoBox, 1, 0) self.outerBox.addWidget(self.infoBox, 1, 0)
self.outerBox.addWidget(self.filterBox, 1, 1) self.outerBox.addWidget(self.filterBox, 1, 1)
self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2) self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2)
self.outerBox.setRowStretch(0, 1)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.show() logger.debug("GuiSessionLog initialisation complete")
logger.debug("SessionLogView initialisation complete") qApp.processEvents()
self._loadLogFile()
self._loadSessionLog() self._updateListBox()
return return
def _loadSessionLog(self): ##
# Slots
##
logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO) def _doClose(self):
if not path.isfile(logFile): """Save the state of the window, clear cache, end close.
logger.warning("No session log file found for this project.") """
return False self.logData = []
self.listBox.clear()
self.timeFilter = 0.0
self.timeTotal = 0.0
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() hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.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: 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: for inLine in inFile:
inData = inLine.split() if inLine.startswith("#"):
if len(inData) != 8: if inLine.startswith("# Offset"):
self.wordOffset = int(inLine[9:].strip())
logger.verbose(
"Initial word count when log was started is %d" % self.wordOffset
)
continue continue
inData = inLine.split()
if len(inData) != 6:
continue
dStart = datetime.strptime( dStart = datetime.strptime(
"%s %s" % (inData[1],inData[2]), nwConst.tStampFmt "%s %s" % (inData[0], inData[1]), nwConst.tStampFmt
) )
dEnd = datetime.strptime( 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 tDiff = dEnd - dStart
sDiff = tDiff.total_seconds() sDiff = tDiff.total_seconds()
ttTime += sDiff
self.timeTotal += sDiff wcNovel = int(inData[4])
if abs(nWords) > 0: wcNotes = int(inData[5])
self.timeFilter += sDiff ttNovel = wcNovel
ttNotes = wcNotes
if hideZeros and nWords == 0: self.logData.append((dStart, sDiff, wcNovel, wcNotes))
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)
except Exception as e: except Exception as e:
self.theParent.makeAlert( self.theParent.makeAlert(
@@ -213,47 +436,125 @@ class GuiSessionLogView(QDialog):
) )
return False return False
self.labelFilter.setText(self._formatTime(self.timeFilter)) self.labelTotal.setText(self._formatTime(ttTime))
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 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)) incNovel = self.incNovel.isChecked()
widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1)) incNotes = self.incNotes.isChecked()
widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2))
sortCol = self.listBox.sortColumn()
sortOrder = self.listBox.header().sortIndicatorOrder()
hideZeros = self.hideZeros.isChecked() hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked() hideNegative = self.hideNegative.isChecked()
groupByDay = self.groupByDay.isChecked()
self.optState.setValue("GuiSession", "widthCol0", widthCol0) # Group the data
self.optState.setValue("GuiSession", "widthCol1", widthCol1) if groupByDay:
self.optState.setValue("GuiSession", "widthCol2", widthCol2) tempData = []
self.optState.setValue("GuiSession", "sortCol", sortCol) sessDate = None
self.optState.setValue("GuiSession", "sortOrder", sortOrder) sessTime = 0
self.optState.setValue("GuiSession", "hideZeros", hideZeros) lstNovel = 0
self.optState.setValue("GuiSession", "hideNegative", hideNegative) lstNotes = 0
self.optState.saveSettings() for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData):
self.close() 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): else:
self._loadSessionLog() tempData = self.logData
return
def _doHideNegative(self, newState): # Calculate Word Diff
self._loadSessionLog() self.filterData = []
return 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): def _formatTime(self, tS):
"""Format the time spent in 00:00:00 format.
"""
tM = int(tS/60) tM = int(tS/60)
tH = int(tM/60) tH = int(tM/60)
tM = tM - tH*60 tM = tM - tH*60
tS = tS - tM*60 - tH*3600 tS = tS - tM*60 - tH*3600
return "%02d:%02d:%02d" % (tH,tM,tS) 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) logger.verbose("GUI Scale: %.2f" % self.guiScale)
self.guiFont = qApp.font() self.guiFont = qApp.font()
self.guiFontFixed = QFontDatabase.systemFont(QFontDatabase.FixedFont)
qMetric = QFontMetrics(self.guiFont) qMetric = QFontMetrics(self.guiFont)
self.fontPointSize = self.guiFont.pointSizeF() self.fontPointSize = self.guiFont.pointSizeF()
self.fontPixelSize = int(round(qMetric.height())) self.fontPixelSize = int(round(qMetric.height()))
+2 -2
View File
@@ -43,7 +43,7 @@ from nw.gui import (
GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails,
GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus,
GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme, GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme,
GuiProjectSettings, GuiProjectTree, GuiSessionLogView GuiProjectSettings, GuiProjectTree, GuiSessionLog
) )
from nw.core import NWProject, NWDoc, NWIndex from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwFiles, nwItemType, nwAlert from nw.constants import nwFiles, nwItemType, nwAlert
@@ -810,7 +810,7 @@ class GuiMain(QMainWindow):
"""Open the session log dialog. """Open the session log dialog.
""" """
if self.hasProject: if self.hasProject:
dlgTLine = GuiSessionLogView(self, self.theProject) dlgTLine = GuiSessionLog(self, self.theProject)
dlgTLine.exec_() dlgTLine.exec_()
return True 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. 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'?> <?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> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
<author>Jay Doh</author> <author>Jay Doh</author>
<saveCount>407</saveCount> <saveCount>587</saveCount>
<autoCount>71</autoCount> <autoCount>102</autoCount>
<editTime>15118</editTime> <editTime>24235</editTime>
</project> </project>
<settings> <settings>
<doBackup>False</doBackup> <doBackup>False</doBackup>
<spellCheck>True</spellCheck> <spellCheck>True</spellCheck>
<autoOutline>True</autoOutline> <autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited> <lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>bb2c23b3c42cc</lastViewed> <lastViewed>b3e74dbc1f584</lastViewed>
<lastWordCount>967</lastWordCount> <lastWordCount>982</lastWordCount>
<novelWordCount>606</novelWordCount>
<notesWordCount>376</notesWordCount>
<autoReplace> <autoReplace>
<A>B</A> <A>B</A>
<B>E</B> <B>E</B>
@@ -114,10 +116,10 @@
<status>1st Draft</status> <status>1st Draft</status>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>1483</charCount> <charCount>1564</charCount>
<wordCount>263</wordCount> <wordCount>278</wordCount>
<paraCount>8</paraCount> <paraCount>8</paraCount>
<cursorPos>1086</cursorPos> <cursorPos>1633</cursorPos>
</item> </item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a"> <item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name> <name>Another Scene</name>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>31489056e0916</lastEdited> <lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>86</lastWordCount> <lastWordCount>86</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>27</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+2
View File
@@ -16,6 +16,8 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace> <autoReplace>
<This>With This Stuff </This> <This>With This Stuff </This>
</autoReplace> </autoReplace>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>31489056e0916</lastEdited> <lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>59</lastWordCount> <lastWordCount>59</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+111 -2
View File
@@ -2,7 +2,7 @@
"""novelWriter Main GUI Class Tester """novelWriter Main GUI Class Tester
""" """
import nw, pytest, sys import nw, pytest, sys, json
from nwtools import * from nwtools import *
from os import path, unlink from os import path, unlink
@@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt
from nw.gui import ( from nw.gui import (
GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel, GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel,
GuiDocMerge, GuiDocSplit GuiDocMerge, GuiDocSplit, GuiSessionLog
) )
from nw.constants import * from nw.constants import *
@@ -409,6 +409,115 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI.closeMain() nwGUI.closeMain()
# qtbot.stopForInteraction() # 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 @pytest.mark.gui
def testAboutBox(qtbot, nwTempGUI, nwRef, nwTemp): def testAboutBox(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp]) nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])