diff --git a/nw/constants.py b/nw/constants.py index a5244d46..36484f00 100644 --- a/nw/constants.py +++ b/nw/constants.py @@ -12,6 +12,12 @@ from nw.enum import nwItemClass, nwItemLayout +class nwConst(): + + tStampFmt = "%Y-%m-%d %H:%M:%S" + +# END Class nwConst + class nwFiles(): APP_ICON = "novelWriter.svg" diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index a5d77820..366ff5fa 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -199,6 +199,12 @@ class GuiMainMenu(QMenuBar): menuItem.triggered.connect(self.theParent.exportProjectDialog) self.projMenu.addAction(menuItem) + # Project > Session Log + menuItem = QAction("Session Log", self) + menuItem.setStatusTip("Show the session log") + menuItem.triggered.connect(self.theParent.showTimeLineDialog) + self.projMenu.addAction(menuItem) + # Project > Separator self.projMenu.addSeparator() diff --git a/nw/gui/sessionlog.py b/nw/gui/sessionlog.py new file mode 100644 index 00000000..635428ca --- /dev/null +++ b/nw/gui/sessionlog.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +"""novelWriter GUI Session Log Viewer + + novelWriter – GUI Session Log Viewer +====================================== + Class holding the session log view window + + File History: + Created: 2019-10-20 [0.3] + +""" + +import logging +import nw + +from os import path +from datetime import datetime +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon, QColor, QPixmap, QFont +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QHeaderView, + QGridLayout, QLabel, QGroupBox, QCheckBox +) + +from nw.enum import nwAlert +from nw.constants import nwConst, nwFiles + +logger = logging.getLogger(__name__) + +class GuiSessionLogView(QDialog): + + def __init__(self, theParent, theProject): + QDialog.__init__(self, theParent) + + logger.debug("Initialising SessionLogView ...") + + self.mainConf = nw.CONFIG + self.theProject = theProject + self.theParent = theParent + + self.timeFilter = 0.0 + self.timeTotal = 0.0 + + self.outerBox = QGridLayout() + self.bottomBox = QHBoxLayout() + + self.setWindowTitle("Session Log") + self.setMinimumWidth(420) + self.setMinimumHeight(400) + + self.listBox = QTreeWidget() + self.listBox.setHeaderLabels(["Session Start","Length","Words",""]) + self.listBox.setIndentation(0) + self.listBox.setColumnWidth(0,180) + self.listBox.setColumnWidth(1,80) + self.listBox.setColumnWidth(2,80) + self.listBox.setColumnWidth(3,0) + + hHeader = self.listBox.headerItem() + hHeader.setTextAlignment(1,Qt.AlignRight) + hHeader.setTextAlignment(2,Qt.AlignRight) + + self.monoFont = QFont("Monospace",10) + + self.listBox.sortByColumn(0, Qt.DescendingOrder) + self.listBox.setSortingEnabled(True) + + # Session Info + self.infoBox = QGroupBox("Sum Total Time", self) + self.infoBoxForm = QGridLayout(self) + self.infoBox.setLayout(self.infoBoxForm) + + self.labelTotal = QLabel(self._formatTime(0)) + self.labelTotal.setFont(self.monoFont) + self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight) + + self.labelFilter = QLabel(self._formatTime(0)) + 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) + + # Filter Options + self.filterBox = QGroupBox("Filters", self) + self.filterBoxForm = QGridLayout(self) + self.filterBox.setLayout(self.filterBoxForm) + + self.hideZeros = QCheckBox("Hide zero word count", self) + self.hideZeros.setChecked(True) + self.hideZeros.stateChanged.connect(self._doHideZeros) + + self.hideNegative = QCheckBox("Hide negative word count", self) + self.hideNegative.setChecked(False) + self.hideNegative.stateChanged.connect(self._doHideNegative) + + self.filterBoxForm.addWidget(self.hideZeros, 0, 0) + self.filterBoxForm.addWidget(self.hideNegative, 1, 0) + + # Buttons + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) + self.buttonBox.rejected.connect(self._doClose) + + # Assemble + 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.setLayout(self.outerBox) + + self.show() + + logger.debug("SessionLogView initialisation complete") + + self._loadSessionLog() + + return + + def _loadSessionLog(self): + + logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO) + if not path.isfile(logFile): + logger.warning("No session log file found for this project.") + return False + + self.listBox.clear() + + self.timeFilter = 0.0 + self.timeTotal = 0.0 + + hideZeros = self.hideZeros.isChecked() + hideNegative = self.hideNegative.isChecked() + + logger.debug("Loading session log file") + try: + with open(logFile,mode="r") as inFile: + for inLine in inFile: + inData = inLine.split() + if len(inData) != 8: + continue + dStart = datetime.strptime("%s %s" % (inData[1],inData[2]),nwConst.tStampFmt) + dEnd = datetime.strptime("%s %s" % (inData[4],inData[5]),nwConst.tStampFmt) + nWords = int(inData[7]) + tDiff = dEnd - dStart + sDiff = tDiff.total_seconds() + + self.timeTotal += sDiff + if abs(nWords) > 0: + self.timeFilter += sDiff + + 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) + + except Exception as e: + self.theParent.makeAlert(["Failed to read session log file.",str(e)], nwAlert.ERROR) + return False + + self.labelFilter.setText(self._formatTime(self.timeFilter)) + self.labelTotal.setText(self._formatTime(self.timeTotal)) + + return True + + def _doClose(self): + self.close() + return + + def _doHideZeros(self, newState): + self._loadSessionLog() + return + + def _doHideNegative(self, newState): + self._loadSessionLog() + return + + def _formatTime(self, tS): + 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 diff --git a/nw/gui/timelineview.py b/nw/gui/timelineview.py index 05a041a3..9f13c2a0 100644 --- a/nw/gui/timelineview.py +++ b/nw/gui/timelineview.py @@ -127,4 +127,4 @@ class GuiTimeLineView(QDialog): self.close() return -# END Class GuiItemEditor +# END Class GuiTimeLineView diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index 52ea0914..aed55a4c 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -34,6 +34,7 @@ from nw.gui.export import GuiExport from nw.gui.itemeditor import GuiItemEditor from nw.gui.statusbar import GuiMainStatus from nw.gui.timelineview import GuiTimeLineView +from nw.gui.sessionlog import GuiSessionLogView from nw.project.project import NWProject from nw.project.document import NWDoc from nw.project.item import NWItem @@ -522,10 +523,13 @@ class GuiMain(QMainWindow): def showTimeLineDialog(self): if self.hasProject: - dlgTLine = GuiTimeLineView(self, self.theProject, self.theIndex) + dlgTLine = GuiSessionLogView(self, self.theProject) dlgTLine.exec_() return True + def showSessionLogDialog(self): + return True + def makeAlert(self, theMessage, theLevel=nwAlert.INFO): """Alert both the user and the logger at the same time. Message can be either a string or an array of strings. Severity level is 0 = info, 1 = warning, and 2 = error. diff --git a/nw/project/project.py b/nw/project/project.py index 7aea4790..603113dd 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -23,7 +23,7 @@ from nw.project.item import NWItem from nw.project.status import NWStatus from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from nw.common import checkString, checkBool, checkInt -from nw.constants import nwFiles +from nw.constants import nwFiles, nwConst logger = logging.getLogger(__name__) @@ -663,8 +663,8 @@ class NWProject(): "End: {closed:s} " "Words: {words:8d}" ).format( - opened = datetime.fromtimestamp(self.projOpened).strftime("%Y-%m-%d %H:%M:%S"), - closed = datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + opened = datetime.fromtimestamp(self.projOpened).strftime(nwConst.tStampFmt), + closed = datetime.now().strftime(nwConst.tStampFmt), words = self.getSessionWordCount(), ), file=outFile)