@@ -1,5 +1,8 @@
|
||||
# novelWriter ChangeLog
|
||||
|
||||
## Version 0.10.0 RC1 [2020-xx-xx]
|
||||
|
||||
|
||||
## Version 0.9.2 [2020-06-26]
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
+2
-2
@@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen"
|
||||
author = "Veronica Berglyd Olsen"
|
||||
|
||||
# The short X.Y version
|
||||
version = "0.9.2"
|
||||
version = "0.10.0"
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = "0.9.2"
|
||||
release = "0.10.0rc1"
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Text Analysis Class
|
||||
===================================
|
||||
Class for analysing bits of text.
|
||||
|
||||
File History:
|
||||
Created: 2018-09-22 [0.0.1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2020, Veronica Berglyd Olsen
|
||||
|
||||
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/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from time import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TextAnalysis():
|
||||
|
||||
def __init__(self, theText, langCode):
|
||||
self.theText = theText
|
||||
self.langCode = langCode
|
||||
return
|
||||
|
||||
def getStats(self):
|
||||
tStart = time()
|
||||
wordCount = self._countWords()
|
||||
tEnd = time()-tStart
|
||||
logger.verbose("Words: %7d in %8.3f ms" % (wordCount,tEnd*1e3))
|
||||
tStart = time()
|
||||
sentCount = self._countSentences()
|
||||
tEnd = time()-tStart
|
||||
logger.verbose("Sentences: %7d in %8.3f ms" % (sentCount,tEnd*1e3))
|
||||
tStart = time()
|
||||
paraCount = self._countParagraphs()
|
||||
tEnd = time()-tStart
|
||||
logger.verbose("Paragraphs: %7d in %8.3f ms" % (paraCount,tEnd*1e3))
|
||||
return wordCount, sentCount, paraCount
|
||||
|
||||
def getReadabilityScore(self):
|
||||
"""Calculate Flesch--Kincaid Readability Score.
|
||||
"""
|
||||
tStart = time()
|
||||
wordCount = self._countWords()
|
||||
sentCount = self._countSentences()
|
||||
if self.langCode[:3] == "en_":
|
||||
ratSyllWord = self._countSyllablesEN()
|
||||
else:
|
||||
ratSyllWord = -1.0
|
||||
rScore = 206.835 - 1.015*(wordCount/sentCount) - 84.6*(ratSyllWord)
|
||||
gLevel = -15.59 + 0.390*(wordCount/sentCount) + 11.8*(ratSyllWord)
|
||||
tEnd = time()-tStart
|
||||
logger.verbose("Readability: %7.3f in %8.3f ms" % (rScore,tEnd*1e3))
|
||||
logger.verbose("Grade Level: %7.3f in %8.3f ms" % (gLevel,tEnd*1e3))
|
||||
logger.verbose("Assessment: %s" % self.getReadabilityText(rScore))
|
||||
|
||||
return rScore, gLevel
|
||||
|
||||
def getReadabilityText(self, rScore):
|
||||
if rScore >= 90.0:
|
||||
return "Very Easy"
|
||||
elif rScore >= 80.0:
|
||||
return "Easy"
|
||||
elif rScore >= 70.0:
|
||||
return "Fairly Easy"
|
||||
elif rScore >= 60.0:
|
||||
return "Average"
|
||||
elif rScore >= 50.0:
|
||||
return "Fairly Difficult"
|
||||
elif rScore >= 30.0:
|
||||
return "Difficult"
|
||||
else:
|
||||
return "Very Difficult"
|
||||
|
||||
#
|
||||
# Internal Functions
|
||||
#
|
||||
|
||||
def _countWords(self):
|
||||
"""Counts the number of words in a text by simply splitting on
|
||||
all white spaces.
|
||||
"""
|
||||
return len(self.theText.strip().split())
|
||||
|
||||
def _countSentences(self):
|
||||
"""Counts the number of non-repeated sentence endings seen in
|
||||
the text. Note: This will count filenames and urls as multiple
|
||||
sentences.
|
||||
"""
|
||||
nSent = 0
|
||||
sawEnd = False
|
||||
for ch in self.theText.strip():
|
||||
if ch in ".!?":
|
||||
if not sawEnd:
|
||||
sawEnd = True
|
||||
nSent += 1
|
||||
else:
|
||||
sawEnd = False
|
||||
return nSent
|
||||
|
||||
def _countParagraphs(self, pThreshold=2):
|
||||
"""Counts the number of paragraphs by counting repeated line
|
||||
breaks.
|
||||
"""
|
||||
nPara = 1
|
||||
sawEnd = 0
|
||||
for ch in self.theText.strip():
|
||||
if ch == "\r": # Ignore Windows line end chars
|
||||
continue
|
||||
if ch == "\n": # Count endlines
|
||||
sawEnd += 1
|
||||
else: # If non-endline is encountered, check condition for paragraph
|
||||
if sawEnd >= pThreshold:
|
||||
nPara += 1
|
||||
sawEnd = 0
|
||||
return nPara
|
||||
|
||||
def _countSyllablesEN(self):
|
||||
"""Attempt to count the syllables in a piece of English language
|
||||
text. This function tends to slightly over-estimate the number
|
||||
of syllables as it doesn't handle the complexity of silent
|
||||
vowels in endings very well. It will count them all.
|
||||
"""
|
||||
|
||||
cleanText = ""
|
||||
for ch in self.theText:
|
||||
if ch in "abcdefghijklmnopqrstuvwxyz'’":
|
||||
cleanText += ch
|
||||
else:
|
||||
cleanText += " "
|
||||
|
||||
asVow = "aeiouy'’"
|
||||
dExept = ("ei","ie","ua","ia","eo")
|
||||
theWords = cleanText.lower().split()
|
||||
allSylls = 0
|
||||
for inWord in theWords:
|
||||
nChar = len(inWord)
|
||||
nSyll = 0
|
||||
wasVow = False
|
||||
wasY = False
|
||||
if nChar == 0:
|
||||
continue
|
||||
if inWord[0] in asVow:
|
||||
nSyll += 1
|
||||
wasVow = True
|
||||
wasY = inWord[0] == "y"
|
||||
for c in range(1,nChar):
|
||||
isVow = False
|
||||
if inWord[c] in asVow:
|
||||
nSyll += 1
|
||||
isVow = True
|
||||
if isVow and wasVow:
|
||||
nSyll -= 1
|
||||
if isVow and wasY:
|
||||
nSyll -= 1
|
||||
if inWord[c:c+2] in dExept:
|
||||
nSyll += 1
|
||||
wasVow = isVow
|
||||
wasY = inWord[c] == "y"
|
||||
if inWord.endswith(("e")):
|
||||
nSyll -= 1
|
||||
if inWord.endswith(("le","ea","io")):
|
||||
nSyll += 1
|
||||
if nSyll < 1:
|
||||
nSyll = 1
|
||||
allSylls += nSyll
|
||||
|
||||
return allSylls/len(theWords)
|
||||
|
||||
# END Class TextAnalysis
|
||||
+2
-2
@@ -40,8 +40,8 @@ __package__ = "novelWriter"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
|
||||
__license__ = "GPLv3"
|
||||
__version__ = "0.9.2"
|
||||
__hexversion__ = "0x000902f0"
|
||||
__version__ = "0.10.0rc1"
|
||||
__hexversion__ = "0x001000c1"
|
||||
__date__ = "2020-06-26"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
|
||||
@@ -31,6 +31,7 @@ class nwConst():
|
||||
|
||||
tStampFmt = "%Y-%m-%d %H:%M:%S" # Default format
|
||||
fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format
|
||||
dStampFmt = "%Y-%m-%d" # Date only format
|
||||
|
||||
# END Class nwConst
|
||||
|
||||
@@ -50,7 +51,7 @@ class nwFiles():
|
||||
PROJ_LOCK = "nwProject.lock"
|
||||
TOC_TXT = "ToC.txt"
|
||||
TOC_JSON = "ToC.json"
|
||||
SESS_INFO = "sessionInfo.log"
|
||||
SESS_STATS = "sessionStats.log"
|
||||
INDEX_FILE = "tagsIndex.json"
|
||||
OPTS_FILE = "guiOptions.json"
|
||||
RECENT_FILE = "recentProjects.json"
|
||||
|
||||
+6
-1
@@ -45,14 +45,19 @@ class OptionState():
|
||||
|
||||
self.theState = {}
|
||||
self.validMap = {
|
||||
"GuiSession": {
|
||||
"GuiSessionLog": {
|
||||
"winWidth",
|
||||
"winHeight",
|
||||
"widthCol0",
|
||||
"widthCol1",
|
||||
"widthCol2",
|
||||
"sortCol",
|
||||
"sortOrder",
|
||||
"incNovel",
|
||||
"incNotes",
|
||||
"hideZeros",
|
||||
"hideNegative",
|
||||
"groupByDay",
|
||||
},
|
||||
"GuiDocSplit": {
|
||||
"spLevel",
|
||||
|
||||
+52
-18
@@ -93,6 +93,8 @@ class NWProject():
|
||||
self.lastViewed = None # The handle of the last file to be viewed
|
||||
self.lastWCount = 0 # The project word count from last session
|
||||
self.currWCount = 0 # The project word count in current session
|
||||
self.novelWCount = 0 # Total number of words in novel files
|
||||
self.notesWCount = 0 # Total number of words in note files
|
||||
self.doBackup = True # Run project backup on exit
|
||||
|
||||
# Set Defaults
|
||||
@@ -231,6 +233,8 @@ class NWProject():
|
||||
self.lastViewed = None
|
||||
self.lastWCount = 0
|
||||
self.currWCount = 0
|
||||
self.novelWCount = 0
|
||||
self.notesWCount = 0
|
||||
|
||||
return
|
||||
|
||||
@@ -435,13 +439,25 @@ class NWProject():
|
||||
self.lastViewed = checkString(xItem.text, None, True)
|
||||
elif xItem.tag == "lastWordCount":
|
||||
self.lastWCount = checkInt(xItem.text, 0, False)
|
||||
elif xItem.tag == "novelWordCount":
|
||||
self.novelWCount = checkInt(xItem.text, 0, False)
|
||||
elif xItem.tag == "notesWordCount":
|
||||
self.notesWCount = checkInt(xItem.text, 0, False)
|
||||
elif xItem.tag == "status":
|
||||
self.statusItems.unpackEntries(xItem)
|
||||
elif xItem.tag == "importance":
|
||||
self.importItems.unpackEntries(xItem)
|
||||
elif xItem.tag == "autoReplace":
|
||||
for xEntry in xItem:
|
||||
self.autoReplace[xEntry.tag] = checkString(xEntry.text, None, False)
|
||||
if xEntry.tag == "entry":
|
||||
if "key" in xEntry.attrib:
|
||||
self.autoReplace[xEntry.attrib["key"]] = checkString(
|
||||
xEntry.text, None, False
|
||||
)
|
||||
else: # Old format
|
||||
self.autoReplace[xEntry.tag] = checkString(
|
||||
xEntry.text, None, False
|
||||
)
|
||||
elif xItem.tag == "titleFormat":
|
||||
titleFormat = self.titleFormat.copy()
|
||||
for xEntry in xItem:
|
||||
@@ -493,7 +509,7 @@ class NWProject():
|
||||
|
||||
# Root element and project details
|
||||
logger.debug("Writing project meta")
|
||||
nwXML = etree.Element("novelWriterXML",attrib={
|
||||
nwXML = etree.Element("novelWriterXML", attrib={
|
||||
"appVersion" : str(nw.__version__),
|
||||
"hexVersion" : str(nw.__hexversion__),
|
||||
"fileVersion" : "1.1",
|
||||
@@ -501,6 +517,10 @@ class NWProject():
|
||||
})
|
||||
|
||||
editTime = int(self.editTime + saveTime - self.projOpened)
|
||||
wcNovel, wcNotes = self.projTree.sumWords()
|
||||
self.novelWCount = wcNovel
|
||||
self.notesWCount = wcNotes
|
||||
self.setProjectWordCount(wcNovel + wcNotes)
|
||||
|
||||
# Save Project Meta
|
||||
xProject = etree.SubElement(nwXML, "project")
|
||||
@@ -519,11 +539,9 @@ class NWProject():
|
||||
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
|
||||
self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
|
||||
self._packProjectValue(xSettings, "lastWordCount", self.currWCount)
|
||||
|
||||
xAutoRep = etree.SubElement(xSettings, "autoReplace")
|
||||
for aKey, aValue in self.autoReplace.items():
|
||||
if len(aKey) > 0:
|
||||
self._packProjectValue(xAutoRep, aKey, aValue)
|
||||
self._packProjectValue(xSettings, "novelWordCount", wcNovel)
|
||||
self._packProjectValue(xSettings, "notesWordCount", wcNotes)
|
||||
self._packProjectKeyValue(xSettings, "autoReplace", self.autoReplace)
|
||||
|
||||
xTitleFmt = etree.SubElement(xSettings, "titleFormat")
|
||||
for aKey, aValue in self.titleFormat.items():
|
||||
@@ -1027,10 +1045,20 @@ class NWProject():
|
||||
if not isinstance(aValue, str):
|
||||
aValue = str(aValue)
|
||||
if aValue == "" and not allowNone: continue
|
||||
xItem = etree.SubElement(xParent,theName)
|
||||
xItem = etree.SubElement(xParent, theName)
|
||||
xItem.text = aValue
|
||||
return
|
||||
|
||||
def _packProjectKeyValue(self, xParent, theName, theDict):
|
||||
"""Pack the entries in the auto-replace dictionary.
|
||||
"""
|
||||
xAutoRep = etree.SubElement(xParent, theName)
|
||||
for aKey, aValue in theDict.items():
|
||||
if len(aKey) > 0:
|
||||
xEntry = etree.SubElement(xAutoRep, "entry", attrib={"key": aKey})
|
||||
xEntry.text = aValue
|
||||
return
|
||||
|
||||
def _scanProjectFolder(self):
|
||||
"""Scan the project folder and check that the files in it are
|
||||
also in the project XML file. If they aren't, import them as
|
||||
@@ -1101,18 +1129,24 @@ class NWProject():
|
||||
if not self.ensureFolderStructure():
|
||||
return False
|
||||
|
||||
sessionFile = path.join(self.projMeta, nwFiles.SESS_INFO)
|
||||
sessionFile = path.join(self.projMeta, nwFiles.SESS_STATS)
|
||||
isFile = path.isfile(sessionFile)
|
||||
|
||||
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
|
||||
print((
|
||||
"Start: {opened:s} "
|
||||
"End: {closed:s} "
|
||||
"Words: {words:8d}"
|
||||
).format(
|
||||
opened = formatTimeStamp(self.projOpened),
|
||||
closed = formatTimeStamp(time()),
|
||||
words = self.getSessionWordCount(),
|
||||
), file=outFile)
|
||||
if not isFile:
|
||||
# It's a new file, so add a header
|
||||
if self.lastWCount > 0:
|
||||
outFile.write("# Offset %d\n" % self.lastWCount)
|
||||
outFile.write("# %-17s %-19s %8s %8s\n" % (
|
||||
"Start Time", "End Time", "Novel", "Notes"
|
||||
))
|
||||
|
||||
outFile.write("%-19s %-19s %8d %8d\n" % (
|
||||
formatTimeStamp(self.projOpened),
|
||||
formatTimeStamp(time()),
|
||||
self.novelWCount,
|
||||
self.notesWCount,
|
||||
))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
+18
-1
@@ -36,7 +36,7 @@ from time import time
|
||||
|
||||
from nw.core.item import NWItem
|
||||
from nw.common import checkString
|
||||
from nw.constants import nwFiles, nwItemType, nwItemClass
|
||||
from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -182,6 +182,23 @@ class NWTree():
|
||||
|
||||
return
|
||||
|
||||
def sumWords(self):
|
||||
"""Loops over all entries and adds up the word counts.
|
||||
"""
|
||||
noteWords = 0
|
||||
novelWords = 0
|
||||
for tHandle in self._treeOrder:
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
continue
|
||||
if tItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
||||
pass
|
||||
elif tItem.itemLayout == nwItemLayout.NOTE:
|
||||
noteWords += tItem.wordCount
|
||||
else:
|
||||
novelWords += tItem.wordCount
|
||||
return novelWords, noteWords
|
||||
|
||||
##
|
||||
# Tree Structure Methods
|
||||
##
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ from nw.gui.preferences import GuiPreferences
|
||||
from nw.gui.projload import GuiProjectLoad
|
||||
from nw.gui.projsettings import GuiProjectSettings
|
||||
from nw.gui.projtree import GuiProjectTree
|
||||
from nw.gui.sessionlog import GuiSessionLogView
|
||||
from nw.gui.sessionlog import GuiSessionLog
|
||||
from nw.gui.statusbar import GuiMainStatus
|
||||
from nw.gui.theme import GuiIcons, GuiTheme
|
||||
|
||||
@@ -36,7 +36,7 @@ __all__ = [
|
||||
"GuiProjectLoad",
|
||||
"GuiProjectSettings",
|
||||
"GuiProjectTree",
|
||||
"GuiSessionLogView",
|
||||
"GuiSessionLog",
|
||||
"GuiMainStatus",
|
||||
"GuiIcons",
|
||||
"GuiTheme",
|
||||
|
||||
@@ -568,9 +568,6 @@ class GuiBuildNovel(QDialog):
|
||||
def _saveDocument(self, theFormat):
|
||||
"""Save the document to various formats.
|
||||
"""
|
||||
|
||||
# FMT_PDF
|
||||
|
||||
byteFmt = QByteArray()
|
||||
fileExt = ""
|
||||
textFmt = ""
|
||||
|
||||
@@ -54,6 +54,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
QTreeWidget.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiProjectTree ...")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
|
||||
+411
-110
@@ -26,78 +26,106 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtGui import QFont, QPixmap
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox,
|
||||
QGridLayout, QLabel, QGroupBox, QCheckBox
|
||||
qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout,
|
||||
QLabel, QGroupBox, QMenu, QAction, QFileDialog
|
||||
)
|
||||
|
||||
from nw.constants import nwConst, nwFiles, nwAlert
|
||||
from nw.gui.custom import QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiSessionLogView(QDialog):
|
||||
class GuiSessionLog(QDialog):
|
||||
|
||||
C_TIME = 0
|
||||
C_LENGTH = 1
|
||||
C_COUNT = 2
|
||||
C_BAR = 3
|
||||
|
||||
FMT_JSON = 0
|
||||
FMT_CSV = 1
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising SessionLogView ...")
|
||||
logger.debug("Initialising GuiSessionLog ...")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.optState = self.theProject.optState
|
||||
self.theProject = theProject
|
||||
self.theTheme = theParent.theTheme
|
||||
self.optState = theProject.optState
|
||||
|
||||
self.logData = []
|
||||
self.filterData = []
|
||||
self.timeFilter = 0.0
|
||||
self.timeTotal = 0.0
|
||||
|
||||
self.outerBox = QGridLayout()
|
||||
self.bottomBox = QHBoxLayout()
|
||||
self.wordOffset = 0
|
||||
|
||||
self.setWindowTitle("Session Log")
|
||||
self.setMinimumWidth(self.mainConf.pxInt(420))
|
||||
self.setMinimumHeight(self.mainConf.pxInt(400))
|
||||
self.resize(
|
||||
self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "winWidth", 550)),
|
||||
self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "winHeight", 500))
|
||||
)
|
||||
|
||||
wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol0", 180))
|
||||
wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol1", 80))
|
||||
wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol2", 80))
|
||||
# List Box
|
||||
wCol0 = self.mainConf.pxInt(
|
||||
self.optState.getInt("GuiSessionLog", "widthCol0", 180)
|
||||
)
|
||||
wCol1 = self.mainConf.pxInt(
|
||||
self.optState.getInt("GuiSessionLog", "widthCol1", 80)
|
||||
)
|
||||
wCol2 = self.mainConf.pxInt(
|
||||
self.optState.getInt("GuiSessionLog", "widthCol2", 80)
|
||||
)
|
||||
|
||||
self.listBox = QTreeWidget()
|
||||
self.listBox.setHeaderLabels(["Session Start","Length","Words",""])
|
||||
self.listBox.setHeaderLabels(["Session Start","Length","Words","Histogram"])
|
||||
self.listBox.setIndentation(0)
|
||||
self.listBox.setColumnWidth(0, wCol0)
|
||||
self.listBox.setColumnWidth(1, wCol1)
|
||||
self.listBox.setColumnWidth(2, wCol2)
|
||||
self.listBox.setColumnWidth(3, 0)
|
||||
self.listBox.setColumnWidth(self.C_TIME, wCol0)
|
||||
self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
|
||||
self.listBox.setColumnWidth(self.C_COUNT, wCol2)
|
||||
self.listBox.resizeColumnToContents(self.C_COUNT)
|
||||
|
||||
hHeader = self.listBox.headerItem()
|
||||
hHeader.setTextAlignment(1,Qt.AlignRight)
|
||||
hHeader.setTextAlignment(2,Qt.AlignRight)
|
||||
hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
|
||||
hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
|
||||
self.monoFont = QFont("Monospace", 10)
|
||||
self.monoFont = QFont()
|
||||
self.monoFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
|
||||
self.monoFont.setFamily(self.theTheme.guiFontFixed.family())
|
||||
|
||||
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
|
||||
sortCol = self.optState.validIntRange(
|
||||
self.optState.getInt("GuiSession", "sortCol", 0), 0, 2, 0
|
||||
self.optState.getInt("GuiSessionLog", "sortCol", 0), 0, 2, 0
|
||||
)
|
||||
sortOrder = self.optState.validIntTuple(
|
||||
self.optState.getInt("GuiSession", "sortOrder", Qt.DescendingOrder),
|
||||
self.optState.getInt("GuiSessionLog", "sortOrder", Qt.DescendingOrder),
|
||||
sortValid, Qt.DescendingOrder
|
||||
)
|
||||
|
||||
self.listBox.sortByColumn(sortCol, sortOrder)
|
||||
self.listBox.setSortingEnabled(True)
|
||||
|
||||
# Word Bar
|
||||
self.barHeight = int(round(0.5*self.theTheme.fontPixelSize))
|
||||
self.barWidth = self.mainConf.pxInt(200)
|
||||
self.barImage = QPixmap(self.barHeight, self.barHeight)
|
||||
self.barImage.fill(self.palette().highlight().color())
|
||||
|
||||
# Session Info
|
||||
self.infoBox = QGroupBox("Sum Total Time", self)
|
||||
self.infoBoxForm = QGridLayout(self)
|
||||
self.infoBox.setLayout(self.infoBoxForm)
|
||||
self.infoBox = QGroupBox("Sum Totals", self)
|
||||
self.infoForm = QGridLayout(self)
|
||||
self.infoBox.setLayout(self.infoForm)
|
||||
|
||||
self.labelTotal = QLabel(self._formatTime(0))
|
||||
self.labelTotal.setFont(self.monoFont)
|
||||
@@ -107,105 +135,300 @@ class GuiSessionLogView(QDialog):
|
||||
self.labelFilter.setFont(self.monoFont)
|
||||
self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.infoBoxForm.addWidget(QLabel("All:"), 0, 0)
|
||||
self.infoBoxForm.addWidget(self.labelTotal, 0, 1)
|
||||
self.infoBoxForm.addWidget(QLabel("Filtered:"), 1, 0)
|
||||
self.infoBoxForm.addWidget(self.labelFilter, 1, 1)
|
||||
self.novelWords = QLabel("0")
|
||||
self.novelWords.setFont(self.monoFont)
|
||||
self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.notesWords = QLabel("0")
|
||||
self.notesWords.setFont(self.monoFont)
|
||||
self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.totalWords = QLabel("0")
|
||||
self.totalWords.setFont(self.monoFont)
|
||||
self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.infoForm.addWidget(QLabel("Total Time:"), 0, 0)
|
||||
self.infoForm.addWidget(self.labelTotal, 0, 1)
|
||||
self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0)
|
||||
self.infoForm.addWidget(self.labelFilter, 1, 1)
|
||||
self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0)
|
||||
self.infoForm.addWidget(self.novelWords, 2, 1)
|
||||
self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0)
|
||||
self.infoForm.addWidget(self.notesWords, 3, 1)
|
||||
self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0)
|
||||
self.infoForm.addWidget(self.totalWords, 4, 1)
|
||||
self.infoForm.setRowStretch(5, 1)
|
||||
|
||||
# Filter Options
|
||||
self.filterBox = QGroupBox("Filters", self)
|
||||
self.filterBoxForm = QGridLayout(self)
|
||||
self.filterBox.setLayout(self.filterBoxForm)
|
||||
sPx = self.theTheme.baseIconSize
|
||||
|
||||
self.hideZeros = QCheckBox("Hide zero word count", self)
|
||||
self.filterBox = QGroupBox("Filters", self)
|
||||
self.filterForm = QGridLayout(self)
|
||||
self.filterBox.setLayout(self.filterForm)
|
||||
|
||||
self.labelNovel = QLabel("Count novel files")
|
||||
self.incNovel = QSwitch(width=2*sPx, height=sPx)
|
||||
self.incNovel.setChecked(
|
||||
self.optState.getBool("GuiSessionLog", "incNovel", True)
|
||||
)
|
||||
self.incNovel.clicked.connect(self._updateListBox)
|
||||
|
||||
self.labelNotes = QLabel("Count note files")
|
||||
self.incNotes = QSwitch(width=2*sPx, height=sPx)
|
||||
self.incNotes.setChecked(
|
||||
self.optState.getBool("GuiSessionLog", "incNotes", True)
|
||||
)
|
||||
self.incNotes.clicked.connect(self._updateListBox)
|
||||
|
||||
self.labelZeros = QLabel("Hide zero word count")
|
||||
self.hideZeros = QSwitch(width=2*sPx, height=sPx)
|
||||
self.hideZeros.setChecked(
|
||||
self.optState.getBool("GuiSession", "hideZeros", True)
|
||||
self.optState.getBool("GuiSessionLog", "hideZeros", True)
|
||||
)
|
||||
self.hideZeros.stateChanged.connect(self._doHideZeros)
|
||||
self.hideZeros.clicked.connect(self._updateListBox)
|
||||
|
||||
self.hideNegative = QCheckBox("Hide negative word count", self)
|
||||
self.labelNegative = QLabel("Hide negative word count")
|
||||
self.hideNegative = QSwitch(width=2*sPx, height=sPx)
|
||||
self.hideNegative.setChecked(
|
||||
self.optState.getBool("GuiSession", "hideNegative", False)
|
||||
self.optState.getBool("GuiSessionLog", "hideNegative", False)
|
||||
)
|
||||
self.hideNegative.stateChanged.connect(self._doHideNegative)
|
||||
self.hideNegative.clicked.connect(self._updateListBox)
|
||||
|
||||
self.filterBoxForm.addWidget(self.hideZeros, 0, 0)
|
||||
self.filterBoxForm.addWidget(self.hideNegative, 1, 0)
|
||||
self.labelByDay = QLabel("Group entries by day")
|
||||
self.groupByDay = QSwitch(width=2*sPx, height=sPx)
|
||||
self.groupByDay.setChecked(
|
||||
self.optState.getBool("GuiSessionLog", "groupByDay", False)
|
||||
)
|
||||
self.groupByDay.clicked.connect(self._updateListBox)
|
||||
|
||||
self.filterForm.addWidget(self.labelNovel, 0, 0)
|
||||
self.filterForm.addWidget(self.incNovel, 0, 1)
|
||||
self.filterForm.addWidget(self.labelNotes, 1, 0)
|
||||
self.filterForm.addWidget(self.incNotes, 1, 1)
|
||||
self.filterForm.addWidget(self.labelZeros, 2, 0)
|
||||
self.filterForm.addWidget(self.hideZeros, 2, 1)
|
||||
self.filterForm.addWidget(self.labelNegative, 3, 0)
|
||||
self.filterForm.addWidget(self.hideNegative, 3, 1)
|
||||
self.filterForm.addWidget(self.labelByDay, 4, 0)
|
||||
self.filterForm.addWidget(self.groupByDay, 4, 1)
|
||||
|
||||
# Buttons
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
|
||||
self.btnSave = self.buttonBox.addButton("Save As", QDialogButtonBox.ActionRole)
|
||||
self.saveMenu = QMenu(self)
|
||||
self.btnSave.setMenu(self.saveMenu)
|
||||
|
||||
self.saveJSON = QAction("JSON Data File (.json)", self)
|
||||
self.saveJSON.triggered.connect(lambda: self._saveData(self.FMT_JSON))
|
||||
self.saveMenu.addAction(self.saveJSON)
|
||||
|
||||
self.saveCSV = QAction("CSV Data File (.csv)", self)
|
||||
self.saveCSV.triggered.connect(lambda: self._saveData(self.FMT_CSV))
|
||||
self.saveMenu.addAction(self.saveCSV)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QGridLayout()
|
||||
self.outerBox.addWidget(self.listBox, 0, 0, 1, 2)
|
||||
self.outerBox.addWidget(self.infoBox, 1, 0)
|
||||
self.outerBox.addWidget(self.filterBox, 1, 1)
|
||||
self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2)
|
||||
self.outerBox.setRowStretch(0, 1)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.show()
|
||||
logger.debug("GuiSessionLog initialisation complete")
|
||||
|
||||
logger.debug("SessionLogView initialisation complete")
|
||||
|
||||
self._loadSessionLog()
|
||||
qApp.processEvents()
|
||||
self._loadLogFile()
|
||||
self._updateListBox()
|
||||
|
||||
return
|
||||
|
||||
def _loadSessionLog(self):
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO)
|
||||
if not path.isfile(logFile):
|
||||
logger.warning("No session log file found for this project.")
|
||||
return False
|
||||
|
||||
self.listBox.clear()
|
||||
|
||||
self.timeFilter = 0.0
|
||||
self.timeTotal = 0.0
|
||||
def _doClose(self):
|
||||
"""Save the state of the window, clear cache, end close.
|
||||
"""
|
||||
self.logData = []
|
||||
|
||||
winWidth = self.mainConf.rpxInt(self.width())
|
||||
winHeight = self.mainConf.rpxInt(self.height())
|
||||
widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0))
|
||||
widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1))
|
||||
widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2))
|
||||
sortCol = self.listBox.sortColumn()
|
||||
sortOrder = self.listBox.header().sortIndicatorOrder()
|
||||
incNovel = self.incNovel.isChecked()
|
||||
incNotes = self.incNotes.isChecked()
|
||||
hideZeros = self.hideZeros.isChecked()
|
||||
hideNegative = self.hideNegative.isChecked()
|
||||
groupByDay = self.groupByDay.isChecked()
|
||||
|
||||
self.optState.setValue("GuiSessionLog", "winWidth", winWidth)
|
||||
self.optState.setValue("GuiSessionLog", "winHeight", winHeight)
|
||||
self.optState.setValue("GuiSessionLog", "widthCol0", widthCol0)
|
||||
self.optState.setValue("GuiSessionLog", "widthCol1", widthCol1)
|
||||
self.optState.setValue("GuiSessionLog", "widthCol2", widthCol2)
|
||||
self.optState.setValue("GuiSessionLog", "sortCol", sortCol)
|
||||
self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder)
|
||||
self.optState.setValue("GuiSessionLog", "incNovel", incNovel)
|
||||
self.optState.setValue("GuiSessionLog", "incNotes", incNotes)
|
||||
self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros)
|
||||
self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative)
|
||||
self.optState.setValue("GuiSessionLog", "groupByDay", groupByDay)
|
||||
|
||||
self.optState.saveSettings()
|
||||
self.close()
|
||||
|
||||
return
|
||||
|
||||
def _saveData(self, dataFmt):
|
||||
"""Save the content of the list box to a file.
|
||||
"""
|
||||
fileExt = ""
|
||||
textFmt = ""
|
||||
|
||||
if dataFmt == self.FMT_JSON:
|
||||
fileExt = "json"
|
||||
textFmt = "JSON Data File"
|
||||
|
||||
elif dataFmt == self.FMT_CSV:
|
||||
fileExt = "csv"
|
||||
textFmt = "CSV Data File"
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
# Generate the file name
|
||||
if fileExt:
|
||||
fileName = "sessionStats.%s" % fileExt
|
||||
saveDir = self.mainConf.lastPath
|
||||
savePath = path.join(saveDir, fileName)
|
||||
if not path.isdir(saveDir):
|
||||
saveDir = self.mainConf.homePath
|
||||
|
||||
if self.mainConf.showGUI:
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
saveTo = QFileDialog.getSaveFileName(
|
||||
self, "Save Document As", savePath, options=dlgOpt
|
||||
)
|
||||
if saveTo[0]:
|
||||
savePath = saveTo[0]
|
||||
else:
|
||||
return False
|
||||
|
||||
self.mainConf.setLastPath(savePath)
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
# Do the actual writing
|
||||
wSuccess = False
|
||||
errMsg = ""
|
||||
|
||||
logger.debug("Loading session log file")
|
||||
try:
|
||||
with open(logFile,mode="r",encoding="utf8") as inFile:
|
||||
with open(savePath, mode="w", encoding="utf8") as outFile:
|
||||
if dataFmt == self.FMT_JSON:
|
||||
jsonData = []
|
||||
for _, sD, tT, wD, wA, wB in self.filterData:
|
||||
jsonData.append({
|
||||
"date": sD,
|
||||
"length": tT,
|
||||
"newWords": wD,
|
||||
"novelWords": wA,
|
||||
"noteWords": wB,
|
||||
})
|
||||
outFile.write(json.dumps(jsonData, indent=2))
|
||||
wSuccess = True
|
||||
|
||||
elif dataFmt == self.FMT_CSV:
|
||||
outFile.write(
|
||||
"\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n" % (
|
||||
"Date", "Length (sec)", "Words Changed", "Novel Words", "Note Words"
|
||||
)
|
||||
)
|
||||
for _, sD, tT, wD, wA, wB in self.filterData:
|
||||
outFile.write(
|
||||
"\"%s\",%d,%d,%d,%d\n" % (sD, tT, wD, wA, wB)
|
||||
)
|
||||
wSuccess = True
|
||||
|
||||
else:
|
||||
errMsg = "Unknown format"
|
||||
|
||||
except Exception as e:
|
||||
errMsg = str(e)
|
||||
|
||||
# Report to user
|
||||
if self.mainConf.showGUI:
|
||||
if wSuccess:
|
||||
self.theParent.makeAlert(
|
||||
"%s file successfully written to:<br> %s" % (
|
||||
textFmt, savePath
|
||||
), nwAlert.INFO
|
||||
)
|
||||
else:
|
||||
self.theParent.makeAlert(
|
||||
"Failed to write %s file. %s" % (
|
||||
textFmt, errMsg
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _loadLogFile(self):
|
||||
"""Load the content of the log file into a buffer.
|
||||
"""
|
||||
logger.debug("Loading session log file")
|
||||
|
||||
self.logData = []
|
||||
|
||||
ttNovel = 0
|
||||
ttNotes = 0
|
||||
ttTime = 0
|
||||
|
||||
try:
|
||||
logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
|
||||
with open(logFile, mode="r", encoding="utf8") as inFile:
|
||||
for inLine in inFile:
|
||||
inData = inLine.split()
|
||||
if len(inData) != 8:
|
||||
if inLine.startswith("#"):
|
||||
if inLine.startswith("# Offset"):
|
||||
self.wordOffset = int(inLine[9:].strip())
|
||||
logger.verbose(
|
||||
"Initial word count when log was started is %d" % self.wordOffset
|
||||
)
|
||||
continue
|
||||
|
||||
inData = inLine.split()
|
||||
if len(inData) != 6:
|
||||
continue
|
||||
|
||||
dStart = datetime.strptime(
|
||||
"%s %s" % (inData[1],inData[2]), nwConst.tStampFmt
|
||||
"%s %s" % (inData[0], inData[1]), nwConst.tStampFmt
|
||||
)
|
||||
dEnd = datetime.strptime(
|
||||
"%s %s" % (inData[4],inData[5]), nwConst.tStampFmt
|
||||
"%s %s" % (inData[2], inData[3]), nwConst.tStampFmt
|
||||
)
|
||||
nWords = int(inData[7])
|
||||
|
||||
tDiff = dEnd - dStart
|
||||
sDiff = tDiff.total_seconds()
|
||||
ttTime += sDiff
|
||||
|
||||
self.timeTotal += sDiff
|
||||
if abs(nWords) > 0:
|
||||
self.timeFilter += sDiff
|
||||
wcNovel = int(inData[4])
|
||||
wcNotes = int(inData[5])
|
||||
ttNovel = wcNovel
|
||||
ttNotes = wcNotes
|
||||
|
||||
if hideZeros and nWords == 0:
|
||||
continue
|
||||
|
||||
if hideNegative and nWords < 0:
|
||||
continue
|
||||
|
||||
newItem = QTreeWidgetItem(
|
||||
[str(dStart), self._formatTime(sDiff), str(nWords), ""]
|
||||
)
|
||||
|
||||
newItem.setTextAlignment(1,Qt.AlignRight)
|
||||
newItem.setTextAlignment(2,Qt.AlignRight)
|
||||
|
||||
newItem.setFont(0,self.monoFont)
|
||||
newItem.setFont(1,self.monoFont)
|
||||
newItem.setFont(2,self.monoFont)
|
||||
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
self.logData.append((dStart, sDiff, wcNovel, wcNotes))
|
||||
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
@@ -213,47 +436,125 @@ class GuiSessionLogView(QDialog):
|
||||
)
|
||||
return False
|
||||
|
||||
self.labelFilter.setText(self._formatTime(self.timeFilter))
|
||||
self.labelTotal.setText(self._formatTime(self.timeTotal))
|
||||
self.labelTotal.setText(self._formatTime(ttTime))
|
||||
self.novelWords.setText("{:n}".format(ttNovel))
|
||||
self.notesWords.setText("{:n}".format(ttNotes))
|
||||
self.totalWords.setText("{:n}".format(ttNovel + ttNotes))
|
||||
|
||||
return True
|
||||
|
||||
def _doClose(self):
|
||||
def _updateListBox(self):
|
||||
"""Load/reload the content of the list box.
|
||||
"""
|
||||
self.listBox.clear()
|
||||
self.timeFilter = 0.0
|
||||
|
||||
widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0))
|
||||
widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1))
|
||||
widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2))
|
||||
sortCol = self.listBox.sortColumn()
|
||||
sortOrder = self.listBox.header().sortIndicatorOrder()
|
||||
incNovel = self.incNovel.isChecked()
|
||||
incNotes = self.incNotes.isChecked()
|
||||
hideZeros = self.hideZeros.isChecked()
|
||||
hideNegative = self.hideNegative.isChecked()
|
||||
groupByDay = self.groupByDay.isChecked()
|
||||
|
||||
self.optState.setValue("GuiSession", "widthCol0", widthCol0)
|
||||
self.optState.setValue("GuiSession", "widthCol1", widthCol1)
|
||||
self.optState.setValue("GuiSession", "widthCol2", widthCol2)
|
||||
self.optState.setValue("GuiSession", "sortCol", sortCol)
|
||||
self.optState.setValue("GuiSession", "sortOrder", sortOrder)
|
||||
self.optState.setValue("GuiSession", "hideZeros", hideZeros)
|
||||
self.optState.setValue("GuiSession", "hideNegative", hideNegative)
|
||||
# Group the data
|
||||
if groupByDay:
|
||||
tempData = []
|
||||
sessDate = None
|
||||
sessTime = 0
|
||||
lstNovel = 0
|
||||
lstNotes = 0
|
||||
|
||||
self.optState.saveSettings()
|
||||
self.close()
|
||||
for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData):
|
||||
if n == 0:
|
||||
sessDate = dStart.date()
|
||||
if sessDate != dStart.date():
|
||||
tempData.append((sessDate, sessTime, lstNovel, lstNotes))
|
||||
sessDate = dStart.date()
|
||||
sessTime = sDiff
|
||||
lstNovel = wcNovel
|
||||
lstNotes = wcNotes
|
||||
else:
|
||||
sessTime += sDiff
|
||||
lstNovel = wcNovel
|
||||
lstNotes = wcNotes
|
||||
|
||||
return
|
||||
if sessDate is not None:
|
||||
tempData.append((sessDate, sessTime, lstNovel, lstNotes))
|
||||
|
||||
def _doHideZeros(self, newState):
|
||||
self._loadSessionLog()
|
||||
return
|
||||
else:
|
||||
tempData = self.logData
|
||||
|
||||
def _doHideNegative(self, newState):
|
||||
self._loadSessionLog()
|
||||
return
|
||||
# Calculate Word Diff
|
||||
self.filterData = []
|
||||
pcTotal = 0
|
||||
listMax = 0
|
||||
isFirst = True
|
||||
for dStart, sDiff, wcNovel, wcNotes in tempData:
|
||||
|
||||
wcTotal = 0
|
||||
if incNovel:
|
||||
wcTotal += wcNovel
|
||||
if incNotes:
|
||||
wcTotal += wcNotes
|
||||
|
||||
dwTotal = wcTotal - pcTotal
|
||||
if hideZeros and dwTotal == 0:
|
||||
continue
|
||||
if hideNegative and dwTotal < 0:
|
||||
continue
|
||||
|
||||
if isFirst:
|
||||
# Subtract the offset from the first list entry
|
||||
dwTotal -= self.wordOffset
|
||||
isFirst = False
|
||||
|
||||
if groupByDay:
|
||||
sStart = dStart.strftime(nwConst.dStampFmt)
|
||||
else:
|
||||
sStart = dStart.strftime(nwConst.tStampFmt)
|
||||
|
||||
self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes))
|
||||
listMax = max(listMax, dwTotal)
|
||||
pcTotal = wcTotal
|
||||
|
||||
# Populate the list
|
||||
for _, sStart, sDiff, nWords, _, _ in self.filterData:
|
||||
|
||||
newItem = QTreeWidgetItem()
|
||||
newItem.setText(self.C_TIME, sStart)
|
||||
newItem.setText(self.C_LENGTH, self._formatTime(sDiff))
|
||||
newItem.setText(self.C_COUNT, "{:n}".format(nWords))
|
||||
|
||||
if nWords > 0 and listMax > 0:
|
||||
theBar = self.barImage.scaled(
|
||||
int(200*nWords/listMax),
|
||||
self.barHeight,
|
||||
Qt.IgnoreAspectRatio,
|
||||
Qt.FastTransformation
|
||||
)
|
||||
newItem.setData(self.C_BAR, Qt.DecorationRole, theBar)
|
||||
|
||||
newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter)
|
||||
|
||||
newItem.setFont(self.C_TIME, self.monoFont)
|
||||
newItem.setFont(self.C_LENGTH, self.monoFont)
|
||||
newItem.setFont(self.C_COUNT, self.monoFont)
|
||||
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
self.timeFilter += sDiff
|
||||
|
||||
self.labelFilter.setText(self._formatTime(self.timeFilter))
|
||||
|
||||
return True
|
||||
|
||||
def _formatTime(self, tS):
|
||||
"""Format the time spent in 00:00:00 format.
|
||||
"""
|
||||
tM = int(tS/60)
|
||||
tH = int(tM/60)
|
||||
tM = tM - tH*60
|
||||
tS = tS - tM*60 - tH*3600
|
||||
return "%02d:%02d:%02d" % (tH,tM,tS)
|
||||
|
||||
# END Class GuiSessionLogView
|
||||
# END Class GuiSessionLog
|
||||
|
||||
@@ -139,6 +139,8 @@ class GuiTheme:
|
||||
logger.verbose("GUI Scale: %.2f" % self.guiScale)
|
||||
|
||||
self.guiFont = qApp.font()
|
||||
self.guiFontFixed = QFontDatabase.systemFont(QFontDatabase.FixedFont)
|
||||
|
||||
qMetric = QFontMetrics(self.guiFont)
|
||||
self.fontPointSize = self.guiFont.pointSizeF()
|
||||
self.fontPixelSize = int(round(qMetric.height()))
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ from nw.gui import (
|
||||
GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails,
|
||||
GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus,
|
||||
GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme,
|
||||
GuiProjectSettings, GuiProjectTree, GuiSessionLogView
|
||||
GuiProjectSettings, GuiProjectTree, GuiSessionLog
|
||||
)
|
||||
from nw.core import NWProject, NWDoc, NWIndex
|
||||
from nw.constants import nwFiles, nwItemType, nwAlert
|
||||
@@ -810,7 +810,7 @@ class GuiMain(QMainWindow):
|
||||
"""Open the session log dialog.
|
||||
"""
|
||||
if self.hasProject:
|
||||
dlgTLine = GuiSessionLogView(self, self.theProject)
|
||||
dlgTLine = GuiSessionLog(self, self.theProject)
|
||||
dlgTLine.exec_()
|
||||
return True
|
||||
|
||||
|
||||
@@ -21,4 +21,4 @@ Thin spaces and thin non-breaking spaces are also supported from the Insert menu
|
||||
|
||||
If you need to split a scene file up into further pieces, you can do so with the level four heading, like above. This is referred to as a section.
|
||||
|
||||
Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the export dialog.
|
||||
Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the Build Novel Project dialog. You can also have them replaced with scene separators like “* * *”.
|
||||
|
||||
+14
-12
@@ -1,25 +1,27 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.9.2" hexVersion="0x000902f0" fileVersion="1.1" timeStamp="2020-06-26 21:20:24">
|
||||
<novelWriterXML appVersion="0.10.0rc1" hexVersion="0x001000c1" fileVersion="1.1" timeStamp="2020-06-26 19:29:32">
|
||||
<project>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
<author>Jay Doh</author>
|
||||
<saveCount>408</saveCount>
|
||||
<autoCount>71</autoCount>
|
||||
<editTime>15120</editTime>
|
||||
<saveCount>589</saveCount>
|
||||
<autoCount>102</autoCount>
|
||||
<editTime>24247</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>False</doBackup>
|
||||
<spellCheck>True</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>636b6aa9b697b</lastEdited>
|
||||
<lastViewed>bb2c23b3c42cc</lastViewed>
|
||||
<lastWordCount>967</lastWordCount>
|
||||
<lastViewed>b3e74dbc1f584</lastViewed>
|
||||
<lastWordCount>982</lastWordCount>
|
||||
<novelWordCount>606</novelWordCount>
|
||||
<notesWordCount>376</notesWordCount>
|
||||
<autoReplace>
|
||||
<A>B</A>
|
||||
<B>E</B>
|
||||
<C>D</C>
|
||||
<entry key="A">B</entry>
|
||||
<entry key="B">E</entry>
|
||||
<entry key="C">D</entry>
|
||||
</autoReplace>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
@@ -114,10 +116,10 @@
|
||||
<status>1st Draft</status>
|
||||
<exported>True</exported>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>1483</charCount>
|
||||
<wordCount>263</wordCount>
|
||||
<charCount>1564</charCount>
|
||||
<wordCount>278</wordCount>
|
||||
<paraCount>8</paraCount>
|
||||
<cursorPos>1086</cursorPos>
|
||||
<cursorPos>1633</cursorPos>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
|
||||
<name>Another Scene</name>
|
||||
|
||||
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
|
||||
|
||||
setuptools.setup(
|
||||
name = "novelWriter",
|
||||
version = "0.9.2",
|
||||
version = "0.10.0",
|
||||
author = "Veronica Berglyd Olsen",
|
||||
author_email = "code@vkbo.net",
|
||||
description = "A markdown-like document editor for writing novels",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<lastEdited>31489056e0916</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>86</lastWordCount>
|
||||
<novelWordCount>59</novelWordCount>
|
||||
<notesWordCount>27</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:07:03">
|
||||
<novelWriterXML appVersion="0.10.0rc1" hexVersion="0x001000c1" fileVersion="1.1" timeStamp="2020-06-26 19:32:47">
|
||||
<project>
|
||||
<name>Project Name</name>
|
||||
<title>Project Title</title>
|
||||
@@ -16,8 +16,10 @@
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace>
|
||||
<This>With This Stuff </This>
|
||||
<entry key="This">With This Stuff </entry>
|
||||
</autoReplace>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<lastEdited>31489056e0916</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>59</lastWordCount>
|
||||
<novelWordCount>59</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
<novelWordCount>0</novelWordCount>
|
||||
<notesWordCount>0</notesWordCount>
|
||||
<autoReplace/>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
|
||||
+111
-2
@@ -2,7 +2,7 @@
|
||||
"""novelWriter Main GUI Class Tester
|
||||
"""
|
||||
|
||||
import nw, pytest, sys
|
||||
import nw, pytest, sys, json
|
||||
from nwtools import *
|
||||
|
||||
from os import path, unlink
|
||||
@@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt
|
||||
|
||||
from nw.gui import (
|
||||
GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel,
|
||||
GuiDocMerge, GuiDocSplit
|
||||
GuiDocMerge, GuiDocSplit, GuiSessionLog
|
||||
)
|
||||
from nw.constants import *
|
||||
|
||||
@@ -409,6 +409,115 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI.closeMain()
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testSessionLogExport(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
assert nwGUI.openProject(nwTempGUI)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
nwGUI.mainConf.lastPath = nwTempGUI
|
||||
sessLog = GuiSessionLog(nwGUI, nwGUI.theProject)
|
||||
sessLog.show()
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
assert sessLog._saveData(sessLog.FMT_CSV)
|
||||
qtbot.wait(stepDelay)
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = path.join(nwTempGUI, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
assert len(jsonData) == 3
|
||||
assert jsonData[0]["length"] > 0
|
||||
assert jsonData[0]["newWords"] == 86
|
||||
assert jsonData[0]["novelWords"] == 59
|
||||
assert jsonData[0]["noteWords"] == 27
|
||||
|
||||
# No Novel Files
|
||||
qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton)
|
||||
qtbot.wait(stepDelay)
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = path.join(nwTempGUI, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
assert len(jsonData) == 2
|
||||
assert jsonData[0]["length"] > 0
|
||||
assert jsonData[0]["newWords"] == 27
|
||||
assert jsonData[0]["novelWords"] == 59
|
||||
assert jsonData[0]["noteWords"] == 27
|
||||
|
||||
# No Note Files
|
||||
qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton)
|
||||
qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton)
|
||||
qtbot.wait(stepDelay)
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = path.join(nwTempGUI, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
assert len(jsonData) == 3
|
||||
assert jsonData[0]["length"] > 0
|
||||
assert jsonData[0]["newWords"] == 59
|
||||
assert jsonData[0]["novelWords"] == 59
|
||||
assert jsonData[0]["noteWords"] == 27
|
||||
|
||||
# No Negative Entries
|
||||
qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton)
|
||||
qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton)
|
||||
qtbot.wait(stepDelay)
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = path.join(nwTempGUI, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
assert len(jsonData) == 1
|
||||
|
||||
# Un-hide Zero Entries
|
||||
qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton)
|
||||
qtbot.mouseClick(sessLog.hideZeros, Qt.LeftButton)
|
||||
qtbot.wait(stepDelay)
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = path.join(nwTempGUI, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
assert len(jsonData) == 4
|
||||
|
||||
# Group by Day
|
||||
qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton)
|
||||
qtbot.wait(stepDelay)
|
||||
assert sessLog._saveData(sessLog.FMT_JSON)
|
||||
qtbot.wait(stepDelay)
|
||||
|
||||
jsonStats = path.join(nwTempGUI, "sessionStats.json")
|
||||
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
|
||||
jsonData = json.loads(inFile.read())
|
||||
|
||||
# Check against both 1 and 2 as this can be 2 if test was started just before midnight.
|
||||
# A failed test should in any case produce a 4
|
||||
assert len(jsonData) in (1, 2)
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
sessLog._doClose()
|
||||
nwGUI.closeMain()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testAboutBox(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
|
||||
Reference in New Issue
Block a user