Merge pull request #343 from vkbo/dev

Dev to Master for 0.10
This commit is contained in:
Veronica K. Berglyd Olsen
2020-06-27 16:54:09 +02:00
committed by GitHub
25 changed files with 646 additions and 346 deletions
+3
View File
@@ -1,5 +1,8 @@
# novelWriter ChangeLog
## Version 0.10.0 RC1 [2020-xx-xx]
## Version 0.9.2 [2020-06-26]
**Bugfixes**
+2 -2
View File
@@ -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 ---------------------------------------------------
-186
View File
@@ -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
View File
@@ -40,8 +40,8 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, 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"
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
-3
View File
@@ -568,9 +568,6 @@ class GuiBuildNovel(QDialog):
def _saveDocument(self, theFormat):
"""Save the document to various formats.
"""
# FMT_PDF
byteFmt = QByteArray()
fileExt = ""
textFmt = ""
+1
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -21,4 +21,4 @@ Thin spaces and thin non-breaking spaces are also supported from the Insert menu
If you need to split a scene file up into further pieces, you can do so with the level four heading, like above. This is referred to as a section.
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
View File
@@ -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>
+1 -1
View File
@@ -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",
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>86</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>27</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+4 -2
View File
@@ -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>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>59</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -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
View File
@@ -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])