Merge pull request #656 from vkbo/session_idle

Session Idle Time
This commit is contained in:
Veronica K. Berglyd Olsen
2021-02-09 20:36:10 +00:00
committed by GitHub
15 changed files with 351 additions and 132 deletions
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg5582">
<metadata
id="metadata5588">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5586" />
<path
d="m 6.7499997,3 c -1.656,0 -3,1.3440001 -3,3.0000001 V 17.999999 c 0,1.656 1.344,3.000001 3,3.000001 1.6560002,0 2.9999998,-1.344001 2.9999998,-3.000001 V 6.0000001 C 9.7499995,4.3440001 8.4059999,3 6.7499997,3 Z M 17.249999,3 c -1.656,0 -3.000001,1.3440001 -3.000001,3.0000001 V 17.999999 c 0,1.656 1.344001,3.000001 3.000001,3.000001 1.656,0 3.000001,-1.344001 3.000001,-3.000001 V 6.0000001 C 20.25,4.3440001 18.905999,3 17.249999,3 Z"
id="path5580"
style="fill:#aeaeae;fill-opacity:1;stroke-width:1.49999" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg5582">
<metadata
id="metadata5588">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5586" />
<path
d="m 6.7499997,3 c -1.656,0 -3,1.3440001 -3,3.0000001 V 17.999999 c 0,1.656 1.344,3.000001 3,3.000001 1.6560002,0 2.9999998,-1.344001 2.9999998,-3.000001 V 6.0000001 C 9.7499995,4.3440001 8.4059999,3 6.7499997,3 Z M 17.249999,3 c -1.656,0 -3.000001,1.3440001 -3.000001,3.0000001 V 17.999999 c 0,1.656 1.344001,3.000001 3.000001,3.000001 1.656,0 3.000001,-1.344001 3.000001,-3.000001 V 6.0000001 C 20.25,4.3440001 18.905999,3 17.249999,3 Z"
id="path5580"
style="fill:#000000;fill-opacity:0.780392;stroke-width:1.49999" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+11
View File
@@ -144,6 +144,9 @@ class Config:
self.allowOpenDQuote = True # Allow open-ended double quotes
self.highlightEmph = True # Add colour to text emphasis
self.stopWhenIdle = True # Stop the status bar clock when the user is idle
self.userIdleTime = 300 # Time of inactivity to consider user idle
## User-Selected Symbols
self.fmtApostrophe = nwUnicode.U_RSQUO
self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO]
@@ -532,6 +535,12 @@ class Config:
self.highlightEmph = self._parseLine(
cnfParse, cnfSec, "highlightemph", self.CNF_BOOL, self.highlightEmph
)
self.stopWhenIdle = self._parseLine(
cnfParse, cnfSec, "stopwhenidle", self.CNF_BOOL, self.stopWhenIdle
)
self.userIdleTime = self._parseLine(
cnfParse, cnfSec, "useridletime", self.CNF_INT, self.userIdleTime
)
## Backup
cnfSec = "Backup"
@@ -672,6 +681,8 @@ class Config:
cnfParse.set(cnfSec, "allowopensquote", str(self.allowOpenSQuote))
cnfParse.set(cnfSec, "allowopendquote", str(self.allowOpenDQuote))
cnfParse.set(cnfSec, "highlightemph", str(self.highlightEmph))
cnfParse.set(cnfSec, "stopwhenidle", str(self.stopWhenIdle))
cnfParse.set(cnfSec, "useridletime", str(self.userIdleTime))
## Backup
cnfSec = "Backup"
+2
View File
@@ -47,6 +47,7 @@ class OptionState():
"widthCol0",
"widthCol1",
"widthCol2",
"widthCol3",
"sortCol",
"sortOrder",
"incNovel",
@@ -54,6 +55,7 @@ class OptionState():
"hideZeros",
"hideNegative",
"groupByDay",
"showIdleTime",
"histMax",
},
"GuiDocSplit": {
+7 -6
View File
@@ -726,12 +726,12 @@ class NWProject():
return True
def closeProject(self):
def closeProject(self, idleTime=0):
"""Close the current project and clear all meta data.
"""
self.optState.saveSettings()
self.projTree.writeToCFile()
self._appendSessionStats()
self._appendSessionStats(idleTime)
self._clearLockFile()
self.clearProject()
self.lockedBy = None
@@ -1389,7 +1389,7 @@ class NWProject():
return True
def _appendSessionStats(self):
def _appendSessionStats(self, idleTime):
"""Append session statistics to the sessions log file.
"""
if not self.ensureFolderStructure():
@@ -1404,15 +1404,16 @@ class NWProject():
# 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("# %-17s %-19s %8s %8s %8s\n" % (
"Start Time", "End Time", "Novel", "Notes", "Idle"
))
outFile.write("%-19s %-19s %8d %8d\n" % (
outFile.write("%-19s %-19s %8d %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(time()),
self.novelWCount,
self.notesWCount,
int(idleTime),
))
except Exception:
+15 -10
View File
@@ -91,6 +91,7 @@ class GuiDocEditor(QTextEdit):
self.wordCount = 0 # Word count
self.paraCount = 0 # Paragraph count
self.lastEdit = 0 # Time stamp of last edit
self.lastActive = 0 # Time stamp of last activity
self.lastFind = None # Position of the last found search word
self.bigDoc = False # Flag for very large document size
self.doReplace = False # Switch to temporarily disable auto-replace
@@ -169,15 +170,16 @@ class GuiDocEditor(QTextEdit):
self.clear()
self.wcTimer.stop()
self.theHandle = None
self.charCount = 0
self.wordCount = 0
self.paraCount = 0
self.lastEdit = 0
self.lastFind = None
self.bigDoc = False
self.doReplace = False
self.queuePos = None
self.theHandle = None
self.charCount = 0
self.wordCount = 0
self.paraCount = 0
self.lastEdit = 0
self.lastActive = 0
self.lastFind = None
self.bigDoc = False
self.doReplace = False
self.queuePos = None
self.setDocumentChanged(False)
self.docHeader.setTitleFromHandle(self.theHandle)
@@ -319,6 +321,7 @@ class GuiDocEditor(QTextEdit):
logger.debug("Document highlighted in %.3f ms" % (1000*(afTime-bfTime)))
self.lastEdit = time()
self.lastActive = time()
self._runCounter()
self.wcTimer.start()
self.theHandle = tHandle
@@ -722,6 +725,7 @@ class GuiDocEditor(QTextEdit):
return False
self._allowAutoReplace(True)
self.lastActive = time()
return True
@@ -839,6 +843,7 @@ class GuiDocEditor(QTextEdit):
* The undo/redo/select all sequences bypasses the docAction
pathway from the menu, so we redirect them back from here.
"""
self.lastActive = time()
isReturn = keyEvent.key() == Qt.Key_Return
isReturn |= keyEvent.key() == Qt.Key_Enter
if isReturn and self.docSearch.anyFocus():
@@ -1083,7 +1088,7 @@ class GuiDocEditor(QTextEdit):
logger.verbose("Word counter is busy")
return
if time() - self.lastEdit < 5*self.wcInterval:
if time() - self.lastEdit < 5 * self.wcInterval:
logger.verbose("Running word counter")
self.theParent.threadPool.start(self.wCounter)
+31
View File
@@ -358,6 +358,33 @@ class GuiPreferencesProjects(QWidget):
"If off, backups will run in the background."
)
# Session Timer
# =============
self.mainForm.addGroupLabel("Session Timer")
## Pause when idle
self.stopWhenIdle = QSwitch()
self.stopWhenIdle.setChecked(self.mainConf.stopWhenIdle)
self.mainForm.addRow(
"Pause the session timer when not writing",
self.stopWhenIdle,
"Also pauses when the application window does not have focus."
)
## Inactive time for idle
self.userIdleTime = QDoubleSpinBox()
self.userIdleTime.setMinimum(0.5)
self.userIdleTime.setMaximum(600.0)
self.userIdleTime.setSingleStep(0.5)
self.userIdleTime.setDecimals(1)
self.userIdleTime.setValue(self.mainConf.userIdleTime/60.0)
self.mainForm.addRow(
"Editor inactive time before pausing timer",
self.userIdleTime,
"User activity includes typing and changing the content.",
theUnit="minutes"
)
return
def saveValues(self):
@@ -372,6 +399,10 @@ class GuiPreferencesProjects(QWidget):
self.mainConf.backupOnClose = self.backupOnClose.isChecked()
self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked()
# Session Timer
self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked()
self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60)
self.mainConf.confChanged = True
return
+27 -14
View File
@@ -29,7 +29,6 @@ import logging
from time import time
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QPainter
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton
@@ -49,6 +48,7 @@ class GuiMainStatus(QStatusBar):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.refTime = None
self.userIdle = False
colNone = QColor(*self.theTheme.statNone)
colTrue = QColor(*self.theTheme.statUnsaved)
@@ -97,9 +97,12 @@ class GuiMainStatus(QStatusBar):
## The Session Clock
### Set the mimimum width so the label doesn't rescale every second
self.timePixmap = self.theTheme.getPixmap("status_time", (iPx, iPx))
self.idlePixmap = self.theTheme.getPixmap("status_idle", (iPx, iPx))
self.timeIcon = QLabel()
self.timeText = QLabel("")
self.timeIcon.setPixmap(self.theTheme.getPixmap("status_time", (iPx, iPx)))
self.timeIcon.setPixmap(self.timePixmap)
self.timeText.setToolTip("Session Time")
self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:"))
self.timeIcon.setContentsMargins(0, 0, 0, 0)
@@ -110,12 +113,6 @@ class GuiMainStatus(QStatusBar):
# Other Settings
self.setSizeGripEnabled(True)
# Start the Clock
self.sessionTimer = QTimer()
self.sessionTimer.setInterval(1000)
self.sessionTimer.timeout.connect(self._updateTime)
self.sessionTimer.start()
logger.debug("GuiMainStatus initialisation complete")
self.clearStatus()
@@ -130,7 +127,7 @@ class GuiMainStatus(QStatusBar):
self.setStats(0, 0)
self.setProjectStatus(None)
self.setDocumentStatus(None)
self._updateTime()
self.updateTime()
return True
##
@@ -182,17 +179,33 @@ class GuiMainStatus(QStatusBar):
self.statsText.setToolTip("Project word count (session change)")
return
##
# Internal Functions
##
def setUserIdle(self, userIdle):
"""Change the idle status icon.
"""
if not self.mainConf.stopWhenIdle:
userIdle = False
def _updateTime(self):
if self.userIdle != userIdle:
if userIdle:
self.timeIcon.setPixmap(self.idlePixmap)
else:
self.timeIcon.setPixmap(self.timePixmap)
self.userIdle = userIdle
return
def updateTime(self, idleTime=0.0):
"""Update the session clock.
"""
if self.refTime is None:
self.timeText.setText("00:00:00")
else:
self.timeText.setText(formatTime(round(time() - self.refTime)))
if self.mainConf.stopWhenIdle:
sessTime = round(time() - self.refTime - idleTime)
else:
sessTime = round(time() - self.refTime)
self.timeText.setText(formatTime(sessTime))
return
# END Class GuiMainStatus
+1
View File
@@ -539,6 +539,7 @@ class GuiIcons:
"proj_nwx" : (None, None),
"status_lang" : (None, None),
"status_time" : (None, None),
"status_idle" : (None, None),
"status_stats" : (None, None),
"status_lines" : (None, None),
"doc_h0" : (QStyle.SP_FileIcon, "x-office-document"),
+79 -26
View File
@@ -48,8 +48,9 @@ class GuiWritingStats(QDialog):
C_TIME = 0
C_LENGTH = 1
C_COUNT = 2
C_BAR = 3
C_IDLE = 2
C_COUNT = 3
C_BAR = 4
FMT_JSON = 0
FMT_CSV = 1
@@ -89,16 +90,21 @@ class GuiWritingStats(QDialog):
wCol2 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol2", 80)
)
wCol3 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol3", 80)
)
self.listBox = QTreeWidget()
self.listBox.setHeaderLabels(["Session Start", "Length", "Words", "Histogram"])
self.listBox.setHeaderLabels(["Session Start", "Length", "Idle", "Words", "Histogram"])
self.listBox.setIndentation(0)
self.listBox.setColumnWidth(self.C_TIME, wCol0)
self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
self.listBox.setColumnWidth(self.C_COUNT, wCol2)
self.listBox.setColumnWidth(self.C_IDLE, wCol2)
self.listBox.setColumnWidth(self.C_COUNT, wCol3)
hHeader = self.listBox.headerItem()
hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight)
hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
@@ -127,6 +133,10 @@ class GuiWritingStats(QDialog):
self.labelTotal.setFont(self.theTheme.guiFontFixed)
self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.labelIdleT = QLabel(formatTime(0))
self.labelIdleT.setFont(self.theTheme.guiFontFixed)
self.labelIdleT.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.labelFilter = QLabel(formatTime(0))
self.labelFilter.setFont(self.theTheme.guiFontFixed)
self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
@@ -144,16 +154,18 @@ class GuiWritingStats(QDialog):
self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.infoForm.addWidget(QLabel("Total Time:"), 0, 0)
self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0)
self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0)
self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0)
self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0)
self.infoForm.addWidget(QLabel("Idle Time:"), 1, 0)
self.infoForm.addWidget(QLabel("Filtered Time:"), 2, 0)
self.infoForm.addWidget(QLabel("Novel Word Count:"), 3, 0)
self.infoForm.addWidget(QLabel("Notes Word Count:"), 4, 0)
self.infoForm.addWidget(QLabel("Total Word Count:"), 5, 0)
self.infoForm.addWidget(self.labelTotal, 0, 1)
self.infoForm.addWidget(self.labelFilter, 1, 1)
self.infoForm.addWidget(self.novelWords, 2, 1)
self.infoForm.addWidget(self.notesWords, 3, 1)
self.infoForm.addWidget(self.totalWords, 4, 1)
self.infoForm.setRowStretch(5, 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
sPx = self.theTheme.baseIconSize
@@ -192,17 +204,25 @@ class GuiWritingStats(QDialog):
)
self.groupByDay.clicked.connect(self._updateListBox)
self.showIdleTime = QSwitch(width=2*sPx, height=sPx)
self.showIdleTime.setChecked(
self.optState.getBool("GuiWritingStats", "showIdleTime", False)
)
self.showIdleTime.clicked.connect(self._updateListBox)
self.filterForm.addWidget(QLabel("Count novel files"), 0, 0)
self.filterForm.addWidget(QLabel("Count note files"), 1, 0)
self.filterForm.addWidget(QLabel("Hide zero word count"), 2, 0)
self.filterForm.addWidget(QLabel("Hide negative word count"), 3, 0)
self.filterForm.addWidget(QLabel("Group entries by day"), 4, 0)
self.filterForm.addWidget(QLabel("Show idle time column"), 5, 0)
self.filterForm.addWidget(self.incNovel, 0, 1)
self.filterForm.addWidget(self.incNotes, 1, 1)
self.filterForm.addWidget(self.hideZeros, 2, 1)
self.filterForm.addWidget(self.hideNegative, 3, 1)
self.filterForm.addWidget(self.groupByDay, 4, 1)
self.filterForm.setRowStretch(5, 1)
self.filterForm.addWidget(self.showIdleTime, 5, 1)
self.filterForm.setRowStretch(6, 1)
# Settings
self.histMax = QSpinBox(self)
@@ -278,6 +298,7 @@ class GuiWritingStats(QDialog):
widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0))
widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1))
widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2))
widthCol3 = self.mainConf.rpxInt(self.listBox.columnWidth(3))
sortCol = self.listBox.sortColumn()
sortOrder = self.listBox.header().sortIndicatorOrder()
incNovel = self.incNovel.isChecked()
@@ -285,6 +306,7 @@ class GuiWritingStats(QDialog):
hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked()
groupByDay = self.groupByDay.isChecked()
showIdleTime = self.showIdleTime.isChecked()
histMax = self.histMax.value()
self.optState.setValue("GuiWritingStats", "winWidth", winWidth)
@@ -292,6 +314,7 @@ class GuiWritingStats(QDialog):
self.optState.setValue("GuiWritingStats", "widthCol0", widthCol0)
self.optState.setValue("GuiWritingStats", "widthCol1", widthCol1)
self.optState.setValue("GuiWritingStats", "widthCol2", widthCol2)
self.optState.setValue("GuiWritingStats", "widthCol3", widthCol3)
self.optState.setValue("GuiWritingStats", "sortCol", sortCol)
self.optState.setValue("GuiWritingStats", "sortOrder", sortOrder)
self.optState.setValue("GuiWritingStats", "incNovel", incNovel)
@@ -299,6 +322,7 @@ class GuiWritingStats(QDialog):
self.optState.setValue("GuiWritingStats", "hideZeros", hideZeros)
self.optState.setValue("GuiWritingStats", "hideNegative", hideNegative)
self.optState.setValue("GuiWritingStats", "groupByDay", groupByDay)
self.optState.setValue("GuiWritingStats", "showIdleTime", showIdleTime)
self.optState.setValue("GuiWritingStats", "histMax", histMax)
self.optState.saveSettings()
@@ -347,23 +371,25 @@ class GuiWritingStats(QDialog):
with open(savePath, mode="w", encoding="utf8") as outFile:
if dataFmt == self.FMT_JSON:
jsonData = []
for _, sD, tT, wD, wA, wB in self.filterData:
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"\n'
'"Date","Length (sec)","Words Changed",'
'"Novel Words","Note Words","Idle Time (sec)"\n'
)
for _, sD, tT, wD, wA, wB in self.filterData:
outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB}\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 e:
@@ -401,6 +427,7 @@ class GuiWritingStats(QDialog):
ttNovel = 0
ttNotes = 0
ttTime = 0
ttIdle = 0
logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
if not os.path.isfile(logFile):
@@ -419,7 +446,7 @@ class GuiWritingStats(QDialog):
continue
inData = inLine.split()
if len(inData) != 6:
if len(inData) < 6:
continue
dStart = datetime.strptime(
@@ -429,16 +456,21 @@ class GuiWritingStats(QDialog):
"%s %s" % (inData[2], inData[3]), nwConst.FMT_TSTAMP
)
sIdle = 0
if len(inData) > 6:
sIdle = checkInt(inData[6], 0)
tDiff = dEnd - dStart
sDiff = tDiff.total_seconds()
ttTime += sDiff
ttIdle += sIdle
wcNovel = int(inData[4])
wcNotes = int(inData[5])
ttNovel = wcNovel
ttNotes = wcNotes
self.logData.append((dStart, sDiff, wcNovel, wcNotes))
self.logData.append((dStart, sDiff, wcNovel, wcNotes, sIdle))
except Exception as e:
self.theParent.makeAlert(
@@ -448,12 +480,17 @@ class GuiWritingStats(QDialog):
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}")
return True
##
# Slots
##
def _updateListBox(self, dummyVar=None):
"""Load/reload the content of the list box. The dummyVar
variable captures the variable sent from the widgets connecting
@@ -474,25 +511,28 @@ class GuiWritingStats(QDialog):
tempData = []
sessDate = None
sessTime = 0
sIdle = 0
lstNovel = 0
lstNotes = 0
for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData):
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))
tempData.append((sessDate, sessTime, lstNovel, lstNotes, sIdle))
sessDate = dStart.date()
sessTime = sDiff
sIdle = sIdle
lstNovel = wcNovel
lstNotes = wcNotes
else:
sessTime += sDiff
sIdle += sIdle
lstNovel = wcNovel
lstNotes = wcNotes
if sessDate is not None:
tempData.append((sessDate, sessTime, lstNovel, lstNotes))
tempData.append((sessDate, sessTime, lstNovel, lstNotes, sIdle))
else:
tempData = self.logData
@@ -502,7 +542,7 @@ class GuiWritingStats(QDialog):
pcTotal = 0
listMax = 0
isFirst = True
for dStart, sDiff, wcNovel, wcNotes in tempData:
for dStart, sDiff, wcNovel, wcNotes, sIdle in tempData:
wcTotal = 0
if incNovel:
@@ -528,16 +568,24 @@ class GuiWritingStats(QDialog):
else:
sStart = dStart.strftime(nwConst.FMT_TSTAMP)
self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes))
self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes, sIdle))
listMax = min(max(listMax, dwTotal), histMax)
pcTotal = wcTotal
# Populate the list
for _, sStart, sDiff, nWords, _, _ in self.filterData:
showIdleTime = self.showIdleTime.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 = "%d %%" % 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:
@@ -550,12 +598,17 @@ class GuiWritingStats(QDialog):
newItem.setData(self.C_BAR, Qt.DecorationRole, theBar)
newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
newItem.setTextAlignment(self.C_IDLE, Qt.AlignRight)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed)
newItem.setFont(self.C_LENGTH, self.theTheme.guiFontFixed)
newItem.setFont(self.C_COUNT, self.theTheme.guiFontFixed)
if showIdleTime:
newItem.setFont(self.C_IDLE, self.theTheme.guiFontFixed)
else:
newItem.setFont(self.C_IDLE, self.theTheme.guiFont)
self.listBox.addTopLevelItem(newItem)
self.timeFilter += sDiff
+38 -2
View File
@@ -86,6 +86,8 @@ class GuiMain(QMainWindow):
self.theIndex = NWIndex(self.theProject, self)
self.hasProject = False
self.isFocusMode = False
self.idleRefTime = time()
self.idleTime = 0.0
# Prepare Main Window
self.resize(*self.mainConf.getWinSize())
@@ -241,6 +243,12 @@ class GuiMain(QMainWindow):
self.asDocTimer = QTimer()
self.asDocTimer.timeout.connect(self._autoSaveDocument)
# Main Clock
self.mainTimer = QTimer()
self.mainTimer.setInterval(1000)
self.mainTimer.timeout.connect(self._timeTick)
self.mainTimer.start()
# Shortcuts and Actions
self._connectMenuActions()
@@ -412,7 +420,11 @@ class GuiMain(QMainWindow):
self.closeDocument()
self.docViewer.clearNavHistory()
self.projView.closeOutline()
self.theProject.closeProject()
self.theProject.closeProject(self.idleTime)
self.idleRefTime = time()
self.idleTime = 0.0
self.theIndex.clearIndex()
self.clearGUI()
self.hasProject = False
@@ -477,7 +489,9 @@ class GuiMain(QMainWindow):
return False
# Project is loaded
self.hasProject = True
self.hasProject = True
self.idleRefTime = time()
self.idleTime = 0.0
# Load the tag index
self.theIndex.loadIndex()
@@ -1414,6 +1428,28 @@ class GuiMain(QMainWindow):
# Slots
##
@pyqtSlot()
def _timeTick(self):
"""Triggered on every tick of the timer.
"""
if not self.hasProject:
return
currTime = time()
editIdle = currTime - self.docEditor.lastActive > self.mainConf.userIdleTime
userIdle = qApp.applicationState() != Qt.ApplicationActive
if editIdle or userIdle:
self.idleTime += currTime - self.idleRefTime
self.statusBar.setUserIdle(True)
else:
self.statusBar.setUserIdle(False)
self.idleRefTime = currTime
self.statusBar.updateTime(idleTime=self.idleTime)
return
@pyqtSlot()
def _treeSingleClick(self):
"""Single click on a project tree item just updates the details
+4 -2
View File
@@ -1,12 +1,12 @@
[Main]
timestamp = 2020-10-11 22:50:45
timestamp = 2021-02-09 21:07:03
theme = default
syntax = default_light
icons = typicons_colour_light
guidark = False
guifont =
guifontsize = 11
lastnotes = 1.0
lastnotes = 0x0
[Sizes]
geometry = 1200, 650
@@ -56,6 +56,8 @@ highlightquotes = True
allowopensquote = False
allowopendquote = True
highlightemph = True
stopwhenidle = True
useridletime = 300
[Backup]
backuppath =
@@ -1,18 +1,18 @@
[Main]
timestamp = 2020-06-29 17:34:15
timestamp = 2021-02-09 21:07:17
theme = default
syntax = default_light
icons = typicons_colour_light
guidark = True
guifont = Cantarell
guifont = Sans
guifontsize = 12
lastnotes = 1.0
lastnotes = 0x0
[Sizes]
geometry = 1100, 650
treecols = 120, 30, 50
geometry = 1200, 650
treecols = 200, 50, 30
novelcols = 200, 50
projcols = 140, 55, 140
projcols = 200, 60, 140
mainpane = 300, 800
docpane = 400, 400
viewpane = 500, 150
@@ -26,7 +26,7 @@ autosaveproject = 40
autosavedoc = 20
[Editor]
textfont = Cantarell
textfont = None
textsize = 13
fixedwidth = False
width = 700
@@ -56,6 +56,8 @@ highlightquotes = False
allowopensquote = False
allowopendquote = True
highlightemph = False
stopwhenidle = True
useridletime = 300
[Backup]
backuppath = some/dir
+5 -5
View File
@@ -859,12 +859,12 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, dummyGUI, tmpDir):
# Session stats
monkeypatch.setattr("os.path.isdir", lambda *args, **kwargs: False)
assert not theProject._appendSessionStats()
assert not theProject._appendSessionStats(idleTime=0)
monkeypatch.undo()
# Block open
monkeypatch.setattr("builtins.open", causeOSError)
assert not theProject._appendSessionStats()
assert not theProject._appendSessionStats(idleTime=0)
monkeypatch.undo()
# Write entry
@@ -876,13 +876,13 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, dummyGUI, tmpDir):
theProject.notesWCount = 100
monkeypatch.setattr("nw.core.project.time", lambda: 1600005600)
assert theProject._appendSessionStats()
assert theProject._appendSessionStats(idleTime=99)
monkeypatch.undo()
assert readFile(statsFile) == (
"# Offset 100\n"
"# Start Time End Time Novel Notes\n"
"%s %s 200 100\n"
"# Start Time End Time Novel Notes Idle\n"
"%s %s 200 100 99\n"
) % (formatTimeStamp(1600002000), formatTimeStamp(1600005600))
# Pack XML Value
+60 -60
View File
@@ -72,7 +72,7 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
# Make a test log file
writeFile(sessFile, (
"# Offset 123\n"
"# Start Time End Time Novel Notes\n"
"# Start Time End Time Novel Notes Idle\n"
"2020-01-01 21:00:00 2020-01-01 21:00:05 6 0\n"
"2020-01-03 21:00:00 2020-01-03 21:00:15 125 0\n"
"2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n"
@@ -86,8 +86,8 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
# Make sure a faulty file can still be read
writeFile(sessFile, (
"# Offset abc123\n"
"# Start Time End Time Novel Notes\n"
"2020-01-01 21:00:00 2020-01-01 21:00:05 6 0\n"
"# Start Time End Time Novel Notes Idle\n"
"2020-01-01 21:00:00 2020-01-01 21:00:05 6 0 50\n"
"2020-01-03 21:00:00 2020-01-03 21:00:15 125 0\n"
"2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n"
"2020-01-06 21:00:00 2020-01-06 21:00:10 125\n"
@@ -101,17 +101,17 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
writeFile(sessFile, (
"# Offset 1075\n"
"# Start Time End Time Novel Notes\n"
"2021-01-31 19:00:00 2021-01-31 19:30:00 700 375\n"
"2021-02-01 19:00:00 2021-02-01 19:30:00 700 375\n"
"2021-02-01 20:00:00 2021-02-01 20:30:00 600 275\n"
"2021-02-02 19:00:00 2021-02-02 19:30:00 750 425\n"
"2021-02-02 20:00:00 2021-02-02 20:30:00 690 365\n"
"2021-02-03 19:00:00 2021-02-03 19:30:00 680 355\n"
"2021-02-04 19:00:00 2021-02-04 19:30:00 700 375\n"
"2021-02-05 19:00:00 2021-02-05 19:30:00 500 175\n"
"2021-02-06 19:00:00 2021-02-06 19:30:00 600 275\n"
"2021-02-07 19:00:00 2021-02-07 19:30:00 600 275\n"
"# Start Time End Time Novel Notes Idle\n"
"2021-01-31 19:00:00 2021-01-31 19:30:00 700 375 0\n"
"2021-02-01 19:00:00 2021-02-01 19:30:00 700 375 10\n"
"2021-02-01 20:00:00 2021-02-01 20:30:00 600 275 20\n"
"2021-02-02 19:00:00 2021-02-02 19:30:00 750 425 30\n"
"2021-02-02 20:00:00 2021-02-02 20:30:00 690 365 40\n"
"2021-02-03 19:00:00 2021-02-03 19:30:00 680 355 50\n"
"2021-02-04 19:00:00 2021-02-04 19:30:00 700 375 60\n"
"2021-02-05 19:00:00 2021-02-05 19:30:00 500 175 70\n"
"2021-02-06 19:00:00 2021-02-06 19:30:00 600 275 80\n"
"2021-02-07 19:00:00 2021-02-07 19:30:00 600 275 90\n"
))
sessLog.populateGUI()
@@ -156,28 +156,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-01 20:00:00", "length": 1800.0,
"newWords": -200, "novelWords": 600, "noteWords": 275
"newWords": -200, "novelWords": 600, "noteWords": 275, "idleTime": 20
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 300, "novelWords": 750, "noteWords": 425
"newWords": 300, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-02 20:00:00", "length": 1800.0,
"newWords": -120, "novelWords": 690, "noteWords": 365
"newWords": -120, "novelWords": 690, "noteWords": 365, "idleTime": 40
}, {
"date": "2021-02-03 19:00:00", "length": 1800.0,
"newWords": -20, "novelWords": 680, "noteWords": 355
"newWords": -20, "novelWords": 680, "noteWords": 355, "idleTime": 50
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 40, "novelWords": 700, "noteWords": 375
"newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-05 19:00:00", "length": 1800.0,
"newWords": -400, "novelWords": 500, "noteWords": 175
"newWords": -400, "novelWords": 500, "noteWords": 175, "idleTime": 70
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 200, "novelWords": 600, "noteWords": 275
"newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80
}
]
@@ -206,28 +206,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-01 20:00:00", "length": 1800.0,
"newWords": -100, "novelWords": 600, "noteWords": 275
"newWords": -100, "novelWords": 600, "noteWords": 275, "idleTime": 20
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 150, "novelWords": 750, "noteWords": 425
"newWords": 150, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-02 20:00:00", "length": 1800.0,
"newWords": -60, "novelWords": 690, "noteWords": 365
"newWords": -60, "novelWords": 690, "noteWords": 365, "idleTime": 40
}, {
"date": "2021-02-03 19:00:00", "length": 1800.0,
"newWords": -10, "novelWords": 680, "noteWords": 355
"newWords": -10, "novelWords": 680, "noteWords": 355, "idleTime": 50
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 20, "novelWords": 700, "noteWords": 375
"newWords": 20, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-05 19:00:00", "length": 1800.0,
"newWords": -200, "novelWords": 500, "noteWords": 175
"newWords": -200, "novelWords": 500, "noteWords": 175, "idleTime": 70
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 100, "novelWords": 600, "noteWords": 275
"newWords": 100, "novelWords": 600, "noteWords": 275, "idleTime": 80
}
]
@@ -254,28 +254,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-01 20:00:00", "length": 1800.0,
"newWords": -100, "novelWords": 600, "noteWords": 275
"newWords": -100, "novelWords": 600, "noteWords": 275, "idleTime": 20
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 150, "novelWords": 750, "noteWords": 425
"newWords": 150, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-02 20:00:00", "length": 1800.0,
"newWords": -60, "novelWords": 690, "noteWords": 365
"newWords": -60, "novelWords": 690, "noteWords": 365, "idleTime": 40
}, {
"date": "2021-02-03 19:00:00", "length": 1800.0,
"newWords": -10, "novelWords": 680, "noteWords": 355
"newWords": -10, "novelWords": 680, "noteWords": 355, "idleTime": 50
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 20, "novelWords": 700, "noteWords": 375
"newWords": 20, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-05 19:00:00", "length": 1800.0,
"newWords": -200, "novelWords": 500, "noteWords": 175
"newWords": -200, "novelWords": 500, "noteWords": 175, "idleTime": 70
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 100, "novelWords": 600, "noteWords": 275
"newWords": 100, "novelWords": 600, "noteWords": 275, "idleTime": 80
}
]
@@ -300,16 +300,16 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 300, "novelWords": 750, "noteWords": 425
"newWords": 300, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 40, "novelWords": 700, "noteWords": 375
"newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 200, "novelWords": 600, "noteWords": 275
"newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80
}
]
@@ -338,34 +338,34 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-01 19:00:00", "length": 1800.0,
"newWords": 0, "novelWords": 700, "noteWords": 375
"newWords": 0, "novelWords": 700, "noteWords": 375, "idleTime": 10
}, {
"date": "2021-02-01 20:00:00", "length": 1800.0,
"newWords": -200, "novelWords": 600, "noteWords": 275
"newWords": -200, "novelWords": 600, "noteWords": 275, "idleTime": 20
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 300, "novelWords": 750, "noteWords": 425
"newWords": 300, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-02 20:00:00", "length": 1800.0,
"newWords": -120, "novelWords": 690, "noteWords": 365
"newWords": -120, "novelWords": 690, "noteWords": 365, "idleTime": 40
}, {
"date": "2021-02-03 19:00:00", "length": 1800.0,
"newWords": -20, "novelWords": 680, "noteWords": 355
"newWords": -20, "novelWords": 680, "noteWords": 355, "idleTime": 50
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 40, "novelWords": 700, "noteWords": 375
"newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-05 19:00:00", "length": 1800.0,
"newWords": -400, "novelWords": 500, "noteWords": 175
"newWords": -400, "novelWords": 500, "noteWords": 175, "idleTime": 70
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 200, "novelWords": 600, "noteWords": 275
"newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80
}, {
"date": "2021-02-07 19:00:00", "length": 1800.0,
"newWords": 0, "novelWords": 600, "noteWords": 275
"newWords": 0, "novelWords": 600, "noteWords": 275, "idleTime": 90
}
]
@@ -391,28 +391,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 10
}, {
"date": "2021-02-01", "length": 3600.0,
"newWords": -200, "novelWords": 600, "noteWords": 275
"newWords": -200, "novelWords": 600, "noteWords": 275, "idleTime": 30
}, {
"date": "2021-02-02", "length": 3600.0,
"newWords": 180, "novelWords": 690, "noteWords": 365
"newWords": 180, "novelWords": 690, "noteWords": 365, "idleTime": 50
}, {
"date": "2021-02-03", "length": 1800.0,
"newWords": -20, "novelWords": 680, "noteWords": 355
"newWords": -20, "novelWords": 680, "noteWords": 355, "idleTime": 60
}, {
"date": "2021-02-04", "length": 1800.0,
"newWords": 40, "novelWords": 700, "noteWords": 375
"newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 70
}, {
"date": "2021-02-05", "length": 1800.0,
"newWords": -400, "novelWords": 500, "noteWords": 175
"newWords": -400, "novelWords": 500, "noteWords": 175, "idleTime": 80
}, {
"date": "2021-02-06", "length": 1800.0,
"newWords": 200, "novelWords": 600, "noteWords": 275
"newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 90
}, {
"date": "2021-02-07", "length": 1800.0,
"newWords": 0, "novelWords": 600, "noteWords": 275
"newWords": 0, "novelWords": 600, "noteWords": 275, "idleTime": 90
}
]