618 lines
23 KiB
Python
618 lines
23 KiB
Python
"""
|
||
novelWriter – GUI Writing Statistics
|
||
====================================
|
||
|
||
File History:
|
||
Created: 2019-10-20 [0.3.0] GuiWritingStats
|
||
|
||
This file is a part of novelWriter
|
||
Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful, but
|
||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
""" # noqa
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
|
||
from datetime import datetime
|
||
from typing import TYPE_CHECKING
|
||
|
||
from PyQt6.QtCore import Qt, pyqtSlot
|
||
from PyQt6.QtGui import QAction, QCloseEvent, QCursor, QPixmap
|
||
from PyQt6.QtWidgets import (
|
||
QApplication, QDialogButtonBox, QFileDialog, QGridLayout, QGroupBox,
|
||
QHBoxLayout, QLabel, QMenu, QSpinBox, QTreeWidget, QTreeWidgetItem
|
||
)
|
||
|
||
from novelwriter import CONFIG, SHARED
|
||
from novelwriter.common import checkInt, checkIntTuple, formatTime, minmax, qtLambda
|
||
from novelwriter.constants import nwConst
|
||
from novelwriter.enum import nwStandardButton
|
||
from novelwriter.error import formatException
|
||
from novelwriter.extensions.modified import NPushButton, NToolDialog
|
||
from novelwriter.extensions.switch import NSwitch
|
||
from novelwriter.types import (
|
||
QtAlignLeftMiddle, QtAlignRight, QtAlignRightMiddle, QtDecoration,
|
||
QtRoleAction, QtRoleDestruct
|
||
)
|
||
|
||
if TYPE_CHECKING:
|
||
from novelwriter.guimain import GuiMain
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class GuiWritingStats(NToolDialog):
|
||
"""GUI Tools: Writing Statistics.
|
||
|
||
Displays data from the NWSessionLog object.
|
||
"""
|
||
|
||
C_TIME = 0
|
||
C_LENGTH = 1
|
||
C_IDLE = 2
|
||
C_COUNT = 3
|
||
C_BAR = 4
|
||
|
||
FMT_JSON = 0
|
||
FMT_CSV = 1
|
||
|
||
def __init__(self, parent: GuiMain) -> None:
|
||
super().__init__(parent=parent)
|
||
|
||
logger.debug("Create: GuiWritingStats")
|
||
self.setObjectName("GuiWritingStats")
|
||
|
||
self.logData = []
|
||
self.filterData = []
|
||
self.timeFilter = 0.0
|
||
self.wordOffset = 0
|
||
|
||
pOptions = SHARED.project.options
|
||
|
||
self.setWindowTitle(self.tr("Writing Statistics"))
|
||
self.setMinimumWidth(420)
|
||
self.setMinimumHeight(400)
|
||
self.resize(
|
||
pOptions.getInt("GuiWritingStats", "winWidth", 550),
|
||
pOptions.getInt("GuiWritingStats", "winHeight", 500),
|
||
)
|
||
|
||
# List Box
|
||
wCol0 = pOptions.getInt("GuiWritingStats", "widthCol0", 180)
|
||
wCol1 = pOptions.getInt("GuiWritingStats", "widthCol1", 80)
|
||
wCol2 = pOptions.getInt("GuiWritingStats", "widthCol2", 80)
|
||
wCol3 = pOptions.getInt("GuiWritingStats", "widthCol3", 80)
|
||
|
||
self.listBox = QTreeWidget(self)
|
||
self.listBox.setHeaderLabels([
|
||
self.tr("Session Start"),
|
||
self.tr("Length"),
|
||
self.tr("Idle"),
|
||
self.tr("Words"),
|
||
self.tr("Histogram"),
|
||
])
|
||
self.listBox.setIndentation(0)
|
||
self.listBox.setColumnWidth(self.C_TIME, wCol0)
|
||
self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
|
||
self.listBox.setColumnWidth(self.C_IDLE, wCol2)
|
||
self.listBox.setColumnWidth(self.C_COUNT, wCol3)
|
||
|
||
hHeader = self.listBox.headerItem()
|
||
if hHeader is not None:
|
||
hHeader.setTextAlignment(self.C_LENGTH, QtAlignRight)
|
||
hHeader.setTextAlignment(self.C_IDLE, QtAlignRight)
|
||
hHeader.setTextAlignment(self.C_COUNT, QtAlignRight)
|
||
|
||
sortCol = minmax(pOptions.getInt("GuiWritingStats", "sortCol", 0), 0, 2)
|
||
sortOrder = checkIntTuple(
|
||
pOptions.getInt("GuiWritingStats", "sortOrder", 1), (0, 1), 1
|
||
)
|
||
sortOrders = (Qt.SortOrder.AscendingOrder, Qt.SortOrder.DescendingOrder)
|
||
self.listBox.sortByColumn(sortCol, sortOrders[sortOrder])
|
||
self.listBox.setSortingEnabled(True)
|
||
|
||
# Word Bar
|
||
self.barHeight = round(0.5*SHARED.theme.fontPixelSize)
|
||
self.barImage = QPixmap(self.barHeight, self.barHeight)
|
||
self.barImage.fill(self.palette().highlight().color())
|
||
|
||
# Session Info
|
||
self.infoBox = QGroupBox(self.tr("Sum Totals"), self)
|
||
self.infoForm = QGridLayout(self)
|
||
self.infoBox.setLayout(self.infoForm)
|
||
|
||
self.labelTotal = QLabel(formatTime(0), self)
|
||
self.labelTotal.setFont(SHARED.theme.guiFontFixed)
|
||
self.labelTotal.setAlignment(QtAlignRightMiddle)
|
||
|
||
self.labelIdleT = QLabel(formatTime(0), self)
|
||
self.labelIdleT.setFont(SHARED.theme.guiFontFixed)
|
||
self.labelIdleT.setAlignment(QtAlignRightMiddle)
|
||
|
||
self.labelFilter = QLabel(formatTime(0), self)
|
||
self.labelFilter.setFont(SHARED.theme.guiFontFixed)
|
||
self.labelFilter.setAlignment(QtAlignRightMiddle)
|
||
|
||
self.novelWords = QLabel("0", self)
|
||
self.novelWords.setFont(SHARED.theme.guiFontFixed)
|
||
self.novelWords.setAlignment(QtAlignRightMiddle)
|
||
|
||
self.notesWords = QLabel("0", self)
|
||
self.notesWords.setFont(SHARED.theme.guiFontFixed)
|
||
self.notesWords.setAlignment(QtAlignRightMiddle)
|
||
|
||
self.totalWords = QLabel("0", self)
|
||
self.totalWords.setFont(SHARED.theme.guiFontFixed)
|
||
self.totalWords.setAlignment(QtAlignRightMiddle)
|
||
|
||
lblTTime = QLabel(self.tr("Total Time:"), self)
|
||
lblITime = QLabel(self.tr("Idle Time:"), self)
|
||
lblFTime = QLabel(self.tr("Filtered Time:"), self)
|
||
lblNvCount = QLabel(self.tr("Novel Word Count:"), self)
|
||
lblNtCount = QLabel(self.tr("Notes Word Count:"), self)
|
||
lblTtCount = QLabel(self.tr("Total Word Count:"), self)
|
||
|
||
self.infoForm.addWidget(lblTTime, 0, 0)
|
||
self.infoForm.addWidget(lblITime, 1, 0)
|
||
self.infoForm.addWidget(lblFTime, 2, 0)
|
||
self.infoForm.addWidget(lblNvCount, 3, 0)
|
||
self.infoForm.addWidget(lblNtCount, 4, 0)
|
||
self.infoForm.addWidget(lblTtCount, 5, 0)
|
||
|
||
self.infoForm.addWidget(self.labelTotal, 0, 1)
|
||
self.infoForm.addWidget(self.labelIdleT, 1, 1)
|
||
self.infoForm.addWidget(self.labelFilter, 2, 1)
|
||
self.infoForm.addWidget(self.novelWords, 3, 1)
|
||
self.infoForm.addWidget(self.notesWords, 4, 1)
|
||
self.infoForm.addWidget(self.totalWords, 5, 1)
|
||
|
||
self.infoForm.setRowStretch(6, 1)
|
||
|
||
# Filter Options
|
||
iPx = SHARED.theme.baseIconHeight
|
||
bSz = SHARED.theme.buttonIconSize
|
||
|
||
self.filterForm = QGridLayout(self)
|
||
self.filterForm.setRowStretch(6, 1)
|
||
self.filterBox = QGroupBox(self.tr("Filters"), self)
|
||
self.filterBox.setLayout(self.filterForm)
|
||
|
||
# Include Novel Files
|
||
self.swtIncNovel = NSwitch(self, height=iPx)
|
||
self.swtIncNovel.setChecked(
|
||
pOptions.getBool("GuiWritingStats", "incNovel", True)
|
||
)
|
||
self.swtIncNovel.clicked.connect(self._updateListBox)
|
||
self.lblIncNovel = QLabel(self.tr("Count novel files"), self)
|
||
self.lblIncNovel.setBuddy(self.swtIncNovel)
|
||
|
||
self.filterForm.addWidget(self.lblIncNovel, 0, 0)
|
||
self.filterForm.addWidget(self.swtIncNovel, 0, 1)
|
||
|
||
# Include Note Files
|
||
self.swtIncNotes = NSwitch(self, height=iPx)
|
||
self.swtIncNotes.setChecked(
|
||
pOptions.getBool("GuiWritingStats", "incNotes", True)
|
||
)
|
||
self.swtIncNotes.clicked.connect(self._updateListBox)
|
||
self.lblIncNotes = QLabel(self.tr("Count note files"), self)
|
||
self.lblIncNotes.setBuddy(self.swtIncNotes)
|
||
|
||
self.filterForm.addWidget(self.lblIncNotes, 1, 0)
|
||
self.filterForm.addWidget(self.swtIncNotes, 1, 1)
|
||
|
||
# Hide Zero Counts
|
||
self.swtHideZeros = NSwitch(self, height=iPx)
|
||
self.swtHideZeros.setChecked(
|
||
pOptions.getBool("GuiWritingStats", "hideZeros", True)
|
||
)
|
||
self.swtHideZeros.clicked.connect(self._updateListBox)
|
||
self.lblHideZeros = QLabel(self.tr("Hide zero word count"), self)
|
||
self.lblHideZeros.setBuddy(self.swtHideZeros)
|
||
|
||
self.filterForm.addWidget(self.lblHideZeros, 2, 0)
|
||
self.filterForm.addWidget(self.swtHideZeros, 2, 1)
|
||
|
||
# Hide Negative Counts
|
||
self.swtHideNegative = NSwitch(self, height=iPx)
|
||
self.swtHideNegative.setChecked(
|
||
pOptions.getBool("GuiWritingStats", "hideNegative", False)
|
||
)
|
||
self.swtHideNegative.clicked.connect(self._updateListBox)
|
||
self.lblHideNegative = QLabel(self.tr("Hide negative word count"), self)
|
||
self.lblHideNegative.setBuddy(self.swtHideNegative)
|
||
|
||
self.filterForm.addWidget(self.lblHideNegative, 3, 0)
|
||
self.filterForm.addWidget(self.swtHideNegative, 3, 1)
|
||
|
||
# Group Entries
|
||
self.swtGroupByDay = NSwitch(self, height=iPx)
|
||
self.swtGroupByDay.setChecked(
|
||
pOptions.getBool("GuiWritingStats", "groupByDay", False)
|
||
)
|
||
self.swtGroupByDay.clicked.connect(self._updateListBox)
|
||
self.lblGroupByDay = QLabel(self.tr("Group entries by day"), self)
|
||
self.lblGroupByDay.setBuddy(self.swtGroupByDay)
|
||
|
||
self.filterForm.addWidget(self.lblGroupByDay, 4, 0)
|
||
self.filterForm.addWidget(self.swtGroupByDay, 4, 1)
|
||
|
||
# Show Idle
|
||
self.swtShowIdleTime = NSwitch(self, height=iPx)
|
||
self.swtShowIdleTime.setChecked(
|
||
pOptions.getBool("GuiWritingStats", "showIdleTime", False)
|
||
)
|
||
self.swtShowIdleTime.clicked.connect(self._updateListBox)
|
||
self.lblShowIdleTime = QLabel(self.tr("Show idle time"), self)
|
||
self.lblShowIdleTime.setBuddy(self.swtShowIdleTime)
|
||
|
||
self.filterForm.addWidget(self.lblShowIdleTime, 5, 0)
|
||
self.filterForm.addWidget(self.swtShowIdleTime, 5, 1)
|
||
|
||
# Settings
|
||
self.histMax = QSpinBox(self)
|
||
self.histMax.setMinimum(100)
|
||
self.histMax.setMaximum(100000)
|
||
self.histMax.setSingleStep(100)
|
||
self.histMax.setValue(
|
||
pOptions.getInt("GuiWritingStats", "histMax", 2000)
|
||
)
|
||
self.histMax.valueChanged.connect(self._updateListBox)
|
||
|
||
self.optsBox = QHBoxLayout()
|
||
self.optsBox.addStretch(1)
|
||
self.optsBox.addWidget(QLabel(self.tr("Word count cap for the histogram"), self), 0)
|
||
self.optsBox.addWidget(self.histMax, 0)
|
||
|
||
# Buttons
|
||
self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
|
||
self.btnClose.clicked.connect(self._doClose)
|
||
self.btnClose.setAutoDefault(False)
|
||
|
||
self.saveJSON = QAction(self.tr("JSON Data File (.json)"), self)
|
||
self.saveJSON.triggered.connect(qtLambda(self._saveData, self.FMT_JSON))
|
||
|
||
self.saveCSV = QAction(self.tr("CSV Data File (.csv)"), self)
|
||
self.saveCSV.triggered.connect(qtLambda(self._saveData, self.FMT_CSV))
|
||
|
||
self.saveMenu = QMenu(self)
|
||
self.saveMenu.addAction(self.saveJSON)
|
||
self.saveMenu.addAction(self.saveCSV)
|
||
|
||
self.btnSave = NPushButton(self, self.tr("Save As"), bSz, "btn_save", "action")
|
||
self.btnSave.setAutoDefault(False)
|
||
self.btnSave.setMenu(self.saveMenu)
|
||
|
||
self.btnBox = QDialogButtonBox(self)
|
||
self.btnBox.addButton(self.btnSave, QtRoleAction)
|
||
self.btnBox.addButton(self.btnClose, QtRoleDestruct)
|
||
|
||
# Assemble
|
||
self.outerBox = QGridLayout()
|
||
self.outerBox.addWidget(self.listBox, 0, 0, 1, 2)
|
||
self.outerBox.addLayout(self.optsBox, 1, 0, 1, 2)
|
||
self.outerBox.addWidget(self.infoBox, 2, 0)
|
||
self.outerBox.addWidget(self.filterBox, 2, 1)
|
||
self.outerBox.addWidget(self.btnBox, 3, 0, 1, 2)
|
||
self.outerBox.setRowStretch(0, 1)
|
||
|
||
self.setLayout(self.outerBox)
|
||
|
||
logger.debug("Ready: GuiWritingStats")
|
||
|
||
def __del__(self) -> None: # pragma: no cover
|
||
logger.debug("Delete: GuiWritingStats")
|
||
|
||
def populateGUI(self) -> None:
|
||
"""Populate list box with data from the log file."""
|
||
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
||
self._loadLogFile()
|
||
self._updateListBox()
|
||
QApplication.restoreOverrideCursor()
|
||
|
||
##
|
||
# Events
|
||
##
|
||
|
||
def closeEvent(self, event: QCloseEvent) -> None:
|
||
"""Capture the user closing the window."""
|
||
event.accept()
|
||
self.softDelete()
|
||
|
||
##
|
||
# Private Slots
|
||
##
|
||
|
||
@pyqtSlot()
|
||
def _doClose(self) -> None:
|
||
"""Save the state of the window, clear cache, end close."""
|
||
self.logData = []
|
||
|
||
header = self.listBox.header()
|
||
|
||
sortCol = self.listBox.sortColumn()
|
||
sortOrder = header.sortIndicatorOrder() if header else 0
|
||
incNovel = self.swtIncNovel.isChecked()
|
||
incNotes = self.swtIncNotes.isChecked()
|
||
hideZeros = self.swtHideZeros.isChecked()
|
||
hideNegative = self.swtHideNegative.isChecked()
|
||
groupByDay = self.swtGroupByDay.isChecked()
|
||
showIdleTime = self.swtShowIdleTime.isChecked()
|
||
histMax = self.histMax.value()
|
||
|
||
logger.debug("Saving State: GuiWritingStats")
|
||
pOptions = SHARED.project.options
|
||
pOptions.setValue("GuiWritingStats", "winWidth", self.width())
|
||
pOptions.setValue("GuiWritingStats", "winHeight", self.height())
|
||
pOptions.setValue("GuiWritingStats", "widthCol0", self.listBox.columnWidth(0))
|
||
pOptions.setValue("GuiWritingStats", "widthCol1", self.listBox.columnWidth(1))
|
||
pOptions.setValue("GuiWritingStats", "widthCol2", self.listBox.columnWidth(2))
|
||
pOptions.setValue("GuiWritingStats", "widthCol3", self.listBox.columnWidth(3))
|
||
pOptions.setValue("GuiWritingStats", "sortCol", sortCol)
|
||
pOptions.setValue("GuiWritingStats", "sortOrder", sortOrder)
|
||
pOptions.setValue("GuiWritingStats", "incNovel", incNovel)
|
||
pOptions.setValue("GuiWritingStats", "incNotes", incNotes)
|
||
pOptions.setValue("GuiWritingStats", "hideZeros", hideZeros)
|
||
pOptions.setValue("GuiWritingStats", "hideNegative", hideNegative)
|
||
pOptions.setValue("GuiWritingStats", "groupByDay", groupByDay)
|
||
pOptions.setValue("GuiWritingStats", "showIdleTime", showIdleTime)
|
||
pOptions.setValue("GuiWritingStats", "histMax", histMax)
|
||
pOptions.saveSettings()
|
||
|
||
self.close()
|
||
|
||
def _saveData(self, dataFmt: int) -> bool:
|
||
"""Save the content of the list box to a file."""
|
||
fileExt = ""
|
||
textFmt = ""
|
||
|
||
if dataFmt == self.FMT_JSON:
|
||
fileExt = "json"
|
||
textFmt = self.tr("JSON Data File")
|
||
elif dataFmt == self.FMT_CSV:
|
||
fileExt = "csv"
|
||
textFmt = self.tr("CSV Data File")
|
||
else:
|
||
return False
|
||
|
||
# Generate the file name
|
||
savePath = CONFIG.lastPath("stats") / f"sessionStats.{fileExt}"
|
||
savePath, _ = QFileDialog.getSaveFileName(
|
||
self, self.tr("Save Data As"), str(savePath), f"{textFmt} (*.{fileExt})"
|
||
)
|
||
if not savePath:
|
||
return False
|
||
|
||
CONFIG.setLastPath("stats", savePath)
|
||
|
||
# Do the actual writing
|
||
wSuccess = False
|
||
errMsg = ""
|
||
|
||
try:
|
||
with open(savePath, mode="w", encoding="utf-8") as outFile:
|
||
if dataFmt == self.FMT_JSON:
|
||
jsonData = []
|
||
for _, sD, tT, wD, wA, wB, tI in self.filterData:
|
||
jsonData.append({
|
||
"date": sD,
|
||
"length": tT,
|
||
"newWords": wD,
|
||
"novelWords": wA,
|
||
"noteWords": wB,
|
||
"idleTime": tI,
|
||
})
|
||
json.dump(jsonData, outFile, indent=2)
|
||
wSuccess = True
|
||
|
||
if dataFmt == self.FMT_CSV:
|
||
outFile.write(
|
||
'"Date","Length (sec)","Words Changed",'
|
||
'"Novel Words","Note Words","Idle Time (sec)"\n'
|
||
)
|
||
for _, sD, tT, wD, wA, wB, tI in self.filterData:
|
||
outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB},{tI}\n')
|
||
wSuccess = True
|
||
|
||
except Exception as exc:
|
||
errMsg = formatException(exc)
|
||
wSuccess = False
|
||
|
||
# Report to user
|
||
if wSuccess:
|
||
SHARED.info(
|
||
self.tr("{0} file successfully written to:").format(textFmt),
|
||
info=savePath
|
||
)
|
||
else:
|
||
SHARED.error(
|
||
self.tr("Failed to write {0} file.").format(textFmt),
|
||
info=errMsg
|
||
)
|
||
|
||
return wSuccess
|
||
|
||
##
|
||
# Internal Functions
|
||
##
|
||
|
||
def _loadLogFile(self) -> None:
|
||
"""Load the content of the log file into a buffer."""
|
||
logger.debug("Loading session log file")
|
||
|
||
self.logData = []
|
||
self.wordOffset = 0
|
||
|
||
ttNovel = 0
|
||
ttNotes = 0
|
||
ttTime = 0
|
||
ttIdle = 0
|
||
|
||
for record in SHARED.project.session.iterRecords():
|
||
rType = record.get("type")
|
||
if rType == "initial":
|
||
self.wordOffset = checkInt(record.get("offset"), 0)
|
||
logger.debug("Initial word count when log was started is %d", self.wordOffset)
|
||
elif rType == "record":
|
||
try:
|
||
dStart = datetime.fromisoformat(str(record.get("start")))
|
||
dEnd = datetime.fromisoformat(str(record.get("end")))
|
||
except Exception:
|
||
logger.error("Invalid session log record")
|
||
continue
|
||
wcNovel = max(checkInt(record.get("novel"), 0), 0)
|
||
wcNotes = max(checkInt(record.get("notes"), 0), 0)
|
||
sIdle = max(checkInt(record.get("idle"), 0), 0)
|
||
|
||
tDiff = dEnd - dStart
|
||
sDiff = tDiff.total_seconds()
|
||
ttTime += sDiff
|
||
ttIdle += sIdle
|
||
ttNovel = wcNovel
|
||
ttNotes = wcNotes
|
||
|
||
self.logData.append((dStart, sDiff, wcNovel, wcNotes, sIdle))
|
||
|
||
ttWords = ttNovel + ttNotes
|
||
self.labelTotal.setText(formatTime(round(ttTime)))
|
||
self.labelIdleT.setText(formatTime(round(ttIdle)))
|
||
self.novelWords.setText(f"{ttNovel:n}")
|
||
self.notesWords.setText(f"{ttNotes:n}")
|
||
self.totalWords.setText(f"{ttWords:n}")
|
||
|
||
##
|
||
# Private Slots
|
||
##
|
||
|
||
@pyqtSlot()
|
||
def _updateListBox(self) -> None:
|
||
"""Load/reload the content of the list box."""
|
||
self.listBox.clear()
|
||
self.timeFilter = 0.0
|
||
|
||
incNovel = self.swtIncNovel.isChecked()
|
||
incNotes = self.swtIncNotes.isChecked()
|
||
hideZeros = self.swtHideZeros.isChecked()
|
||
hideNegative = self.swtHideNegative.isChecked()
|
||
groupByDay = self.swtGroupByDay.isChecked()
|
||
histMax = self.histMax.value()
|
||
|
||
# Group the data
|
||
if groupByDay:
|
||
tempData = []
|
||
sessDate = None
|
||
sessTime = 0
|
||
sessIdle = 0
|
||
lstNovel = 0
|
||
lstNotes = 0
|
||
|
||
for n, (dStart, sDiff, wcNovel, wcNotes, sIdle) in enumerate(self.logData):
|
||
if n == 0:
|
||
sessDate = dStart.date()
|
||
if sessDate != dStart.date():
|
||
tempData.append((sessDate, sessTime, lstNovel, lstNotes, sessIdle))
|
||
sessDate = dStart.date()
|
||
sessTime = sDiff
|
||
sessIdle = sIdle
|
||
lstNovel = wcNovel
|
||
lstNotes = wcNotes
|
||
else:
|
||
sessTime += sDiff
|
||
sessIdle += sIdle
|
||
lstNovel = wcNovel
|
||
lstNotes = wcNotes
|
||
|
||
if sessDate is not None:
|
||
tempData.append((sessDate, sessTime, lstNovel, lstNotes, sessIdle))
|
||
|
||
else:
|
||
tempData = self.logData
|
||
|
||
# Calculate Word Diff
|
||
self.filterData = []
|
||
pcTotal = 0
|
||
listMax = 0
|
||
isFirst = True
|
||
for dStart, sDiff, wcNovel, wcNotes, sIdle 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:
|
||
pcTotal = wcTotal
|
||
continue
|
||
|
||
if isFirst:
|
||
# Subtract the offset from the first list entry
|
||
dwTotal -= self.wordOffset
|
||
dwTotal = max(dwTotal, 1) # Don't go zero or negative
|
||
isFirst = False
|
||
|
||
if groupByDay:
|
||
sStart = dStart.strftime(nwConst.FMT_DSTAMP)
|
||
else:
|
||
sStart = dStart.strftime(nwConst.FMT_TSTAMP)
|
||
|
||
self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes, sIdle))
|
||
listMax = min(max(listMax, dwTotal), histMax)
|
||
pcTotal = wcTotal
|
||
|
||
# Populate the list
|
||
mTrans = Qt.TransformationMode.FastTransformation
|
||
mAspect = Qt.AspectRatioMode.IgnoreAspectRatio
|
||
showIdleTime = self.swtShowIdleTime.isChecked()
|
||
for _, sStart, sDiff, nWords, _, _, sIdle in self.filterData:
|
||
|
||
if showIdleTime:
|
||
idleEntry = formatTime(sIdle)
|
||
else:
|
||
sRatio = sIdle/sDiff if sDiff > 0.0 else 0.0
|
||
idleEntry = f"{round(100.0 * sRatio)} %"
|
||
|
||
newItem = QTreeWidgetItem()
|
||
newItem.setText(self.C_TIME, sStart)
|
||
newItem.setText(self.C_LENGTH, formatTime(round(sDiff)))
|
||
newItem.setText(self.C_IDLE, idleEntry)
|
||
newItem.setText(self.C_COUNT, f"{nWords:n}")
|
||
|
||
if nWords > 0 and listMax > 0:
|
||
wBar = self.barImage.scaled(
|
||
int(200*min(nWords, histMax)/listMax),
|
||
self.barHeight, mAspect, mTrans
|
||
)
|
||
newItem.setData(self.C_BAR, QtDecoration, wBar)
|
||
|
||
newItem.setTextAlignment(self.C_LENGTH, QtAlignRight)
|
||
newItem.setTextAlignment(self.C_IDLE, QtAlignRight)
|
||
newItem.setTextAlignment(self.C_COUNT, QtAlignRight)
|
||
newItem.setTextAlignment(self.C_BAR, QtAlignLeftMiddle)
|
||
|
||
newItem.setFont(self.C_TIME, SHARED.theme.guiFontFixed)
|
||
newItem.setFont(self.C_LENGTH, SHARED.theme.guiFontFixed)
|
||
newItem.setFont(self.C_COUNT, SHARED.theme.guiFontFixed)
|
||
newItem.setFont(self.C_IDLE, SHARED.theme.guiFontFixed)
|
||
|
||
self.listBox.addTopLevelItem(newItem)
|
||
self.timeFilter += sDiff
|
||
|
||
self.labelFilter.setText(formatTime(round(self.timeFilter)))
|