Project tree status and total word count changes (#882)

* Fix some inconsistencies in source file docstrings
* Remove the option to show status text in project tree
* Change how total word counts are calculated and propagated
* Fix tests and improve coverage
* Add setting in preferences
* Add missing word count timer to config file
This commit is contained in:
Veronica Berglyd Olsen
2021-09-10 00:38:45 +02:00
committed by GitHub
parent b9311e84ce
commit 3c8cb34b0a
19 changed files with 246 additions and 134 deletions
+12 -4
View File
@@ -4,7 +4,7 @@ novelWriter Common Functions
Various common functions
File History:
Created: 2019-05-12 [0.1.0]
Created: 2019-05-12 [0.1]
This file is a part of novelWriter
Copyright 20182021, Veronica Berglyd Olsen
@@ -441,9 +441,10 @@ class NWConfigParser(ConfigParser):
CNF_STR = 0
CNF_INT = 1
CNF_BOOL = 2
CNF_S_LST = 3
CNF_I_LST = 4
CNF_FLOAT = 2
CNF_BOOL = 3
CNF_S_LST = 4
CNF_I_LST = 5
def __init__(self):
super().__init__()
@@ -458,6 +459,11 @@ class NWConfigParser(ConfigParser):
"""
return self._parseLine(section, option, default, self.CNF_INT)
def rdFlt(self, section, option, default):
"""Read float value.
"""
return self._parseLine(section, option, default, self.CNF_FLOAT)
def rdBool(self, section, option, default):
"""Read boolean value.
"""
@@ -503,6 +509,8 @@ class NWConfigParser(ConfigParser):
return self.get(section, option)
elif type == self.CNF_INT:
return self.getint(section, option)
elif type == self.CNF_FLOAT:
return self.getfloat(section, option)
elif type == self.CNF_BOOL:
return self.getboolean(section, option)
elif type in (self.CNF_I_LST, self.CNF_S_LST):
+13 -11
View File
@@ -108,7 +108,6 @@ class Config:
# Features
self.hideVScroll = False # Hide vertical scroll bars on main widgets
self.hideHScroll = False # Hide horizontal scroll bars on main widgets
self.fullStatus = True # Show the full status text in the project tree
self.emphLabels = True # Add emphasis to H1 and H2 item labels
# Project
@@ -145,6 +144,7 @@ class Config:
self.wordCountTimer = 5.0 # Interval for word count update in seconds
self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes
self.incNotesWCount = True # The status bar word count includes notes
self.highlightQuotes = True # Highlight text in quotes
self.allowOpenSQuote = False # Allow open-ended single quotes
@@ -179,9 +179,9 @@ class Config:
self.askBeforeBackup = True
# State
self.showRefPanel = True
self.viewComments = True
self.viewSynopsis = True
self.showRefPanel = True # The reference panel for the viewer is visible
self.viewComments = True # Comments are shown in the viewer
self.viewSynopsis = True # Synopsis is shown in the viewer
# Check Qt5 Versions
verQt = splitVersionNumber(QT_VERSION_STR)
@@ -466,7 +466,6 @@ class Config:
cnfSec = "Project"
self.autoSaveProj = theConf.rdInt(cnfSec, "autosaveproject", self.autoSaveProj)
self.autoSaveDoc = theConf.rdInt(cnfSec, "autosavedoc", self.autoSaveDoc)
self.fullStatus = theConf.rdBool(cnfSec, "fullstatus", self.fullStatus)
self.emphLabels = theConf.rdBool(cnfSec, "emphlabels", self.emphLabels)
# Editor
@@ -498,7 +497,9 @@ class Config:
self.showTabsNSpaces = theConf.rdBool(cnfSec, "showtabsnspaces", self.showTabsNSpaces)
self.showLineEndings = theConf.rdBool(cnfSec, "showlineendings", self.showLineEndings)
self.showMultiSpaces = theConf.rdBool(cnfSec, "showmultispaces", self.showMultiSpaces)
self.wordCountTimer = theConf.rdFlt(cnfSec, "wordcounttimer", self.wordCountTimer)
self.bigDocLimit = theConf.rdInt(cnfSec, "bigdoclimit", self.bigDocLimit)
self.incNotesWCount = theConf.rdBool(cnfSec, "incnoteswcount", self.incNotesWCount)
self.showFullPath = theConf.rdBool(cnfSec, "showfullpath", self.showFullPath)
self.highlightQuotes = theConf.rdBool(cnfSec, "highlightquotes", self.highlightQuotes)
self.allowOpenSQuote = theConf.rdBool(cnfSec, "allowopensquote", self.allowOpenSQuote)
@@ -588,7 +589,6 @@ class Config:
theConf["Project"] = {
"autosaveproject": str(self.autoSaveProj),
"autosavedoc": str(self.autoSaveDoc),
"fullstatus": str(self.fullStatus),
"emphlabels": str(self.emphLabels),
}
@@ -620,7 +620,9 @@ class Config:
"showtabsnspaces": str(self.showTabsNSpaces),
"showlineendings": str(self.showLineEndings),
"showmultispaces": str(self.showMultiSpaces),
"wordcounttimer": str(self.wordCountTimer),
"bigdoclimit": str(self.bigDocLimit),
"incnoteswcount": str(self.incNotesWCount),
"showfullpath": str(self.showFullPath),
"highlightquotes": str(self.highlightQuotes),
"allowopensquote": str(self.allowOpenSQuote),
@@ -821,7 +823,7 @@ class Config:
return True
def setDocPanePos(self, panePos):
self.docPanePos = [int(x/self.guiScale) for x in panePos]
self.docPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
@@ -832,22 +834,22 @@ class Config:
def setOutlinePanePos(self, panePos):
self.outlnPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
self.confChanged = True
return True
def setShowRefPanel(self, checkState):
self.showRefPanel = checkState
self.confChanged = True
self.confChanged = True
return self.showRefPanel
def setViewComments(self, viewState):
self.viewComments = viewState
self.confChanged = True
self.confChanged = True
return self.viewComments
def setViewSynopsis(self, viewState):
self.viewSynopsis = viewState
self.confChanged = True
self.confChanged = True
return self.viewSynopsis
##
+29 -29
View File
@@ -99,9 +99,11 @@ class NWProject():
self.lastEdited = None # The handle of the last file to be edited
self.lastViewed = None # The handle of the last file to be viewed
self.lastWCount = 0 # The project word count from last session
self.lastNovelWC = 0 # The novel files word count from last session
self.lastNotesWC = 0 # The note files 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.currNovelWC = 0 # The novel files word count in cutrent session
self.currNotesWC = 0 # The note files word count in cutrent session
self.doBackup = True # Run project backup on exit
# Internal Mapping
@@ -226,9 +228,11 @@ class NWProject():
self.lastEdited = None
self.lastViewed = None
self.lastWCount = 0
self.lastNovelWC = 0
self.lastNotesWC = 0
self.currWCount = 0
self.novelWCount = 0
self.notesWCount = 0
self.currNovelWC = 0
self.currNotesWC = 0
return
@@ -566,9 +570,9 @@ class NWProject():
elif xItem.tag == "lastWordCount":
self.lastWCount = checkInt(xItem.text, 0, False)
elif xItem.tag == "novelWordCount":
self.novelWCount = checkInt(xItem.text, 0, False)
self.lastNovelWC = checkInt(xItem.text, 0, False)
elif xItem.tag == "notesWordCount":
self.notesWCount = checkInt(xItem.text, 0, False)
self.lastNotesWC = checkInt(xItem.text, 0, False)
elif xItem.tag == "status":
self.statusItems.unpackXML(xItem)
elif xItem.tag == "importance":
@@ -610,8 +614,8 @@ class NWProject():
self._scanProjectFolder()
self._loadProjectLocalisation()
self.updateWordCounts()
self.currWCount = self.lastWCount
self.projOpened = time()
self.projAltered = False
@@ -652,11 +656,8 @@ class NWProject():
"timeStamp": formatTimeStamp(saveTime),
})
self.updateWordCounts()
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")
@@ -677,8 +678,8 @@ class NWProject():
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
self._packProjectValue(xSettings, "lastWordCount", self.currWCount)
self._packProjectValue(xSettings, "novelWordCount", wcNovel)
self._packProjectValue(xSettings, "notesWordCount", wcNotes)
self._packProjectValue(xSettings, "novelWordCount", self.currNovelWC)
self._packProjectValue(xSettings, "notesWordCount", self.currNotesWC)
self._packProjectKeyValue(xSettings, "autoReplace", self.autoReplace)
xTitleFmt = etree.SubElement(xSettings, "titleFormat")
@@ -1067,14 +1068,6 @@ class NWProject():
self.setProjectChanged(True)
return True
def setProjectWordCount(self, theCount):
"""Set the current project word count.
"""
if self.currWCount != theCount:
self.currWCount = theCount
self.setProjectChanged(True)
return True
def setStatusColours(self, newCols):
"""Update the list of novel file status flags. Also iterate
through the project and replace keys that have been renamed.
@@ -1145,11 +1138,6 @@ class NWProject():
return authString
def getSessionWordCount(self):
"""Returns the number of words added or removed this session.
"""
return self.currWCount - self.lastWCount
def getCurrentEditTime(self):
"""Get the total project edit time, including the time spent in
the current session.
@@ -1202,6 +1190,18 @@ class NWProject():
# Class Methods
##
def updateWordCounts(self):
"""Update the total word count values.
"""
wcNovel, wcNotes = self.projTree.sumWords()
wcTotal = wcNovel + wcNotes
if wcTotal != self.currWCount:
self.currNovelWC = wcNovel
self.currNotesWC = wcNotes
self.currWCount = wcTotal
self.setProjectChanged(True)
return
def countStatus(self):
"""Count how many times the various status flags are used in the
project tree. The counts themselves are kept in the NWStatus
@@ -1460,7 +1460,7 @@ class NWProject():
isFile = os.path.isfile(sessionFile)
nowTime = time()
sessDiff = self.getSessionWordCount()
sessDiff = self.currWCount - self.lastWCount
sessTime = nowTime - self.projOpened
logger.info("The session lasted %d sec and added %d words", int(sessTime), sessDiff)
@@ -1481,8 +1481,8 @@ class NWProject():
outFile.write("%-19s %-19s %8d %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(nowTime),
self.novelWCount,
self.notesWCount,
self.currNovelWC,
self.currNotesWC,
int(idleTime),
))
+1 -1
View File
@@ -190,7 +190,7 @@ class NWTree():
return True
def sumWords(self):
"""Loops over all entries and adds up the word counts.
"""Loop over all entries and add up the word counts.
"""
noteWords = 0
novelWords = 0
+10 -11
View File
@@ -244,14 +244,6 @@ class GuiPreferencesGeneral(QWidget):
# ============
self.mainForm.addGroupLabel(self.tr("GUI Settings"))
self.fullStatus = QSwitch()
self.fullStatus.setChecked(self.mainConf.fullStatus)
self.mainForm.addRow(
self.tr("Show status text in project tree"),
self.fullStatus,
self.tr("If disabled, only the icon is shown."),
)
self.emphLabels = QSwitch()
self.emphLabels.setChecked(self.mainConf.emphLabels)
self.mainForm.addRow(
@@ -295,7 +287,6 @@ class GuiPreferencesGeneral(QWidget):
guiDark = self.guiDark.isChecked()
guiFont = self.guiFont.text()
guiFontSize = self.guiFontSize.value()
fullStatus = self.fullStatus.isChecked()
emphLabels = self.emphLabels.isChecked()
# Check if restart is needed
@@ -309,7 +300,6 @@ class GuiPreferencesGeneral(QWidget):
# Check if refreshing project tree is needed
refreshTree = False
refreshTree |= self.mainConf.fullStatus != fullStatus
refreshTree |= self.mainConf.emphLabels != emphLabels
self.mainConf.guiLang = guiLang
@@ -318,7 +308,6 @@ class GuiPreferencesGeneral(QWidget):
self.mainConf.guiDark = guiDark
self.mainConf.guiFont = guiFont
self.mainConf.guiFontSize = guiFontSize
self.mainConf.fullStatus = fullStatus
self.mainConf.emphLabels = emphLabels
self.mainConf.showFullPath = self.showFullPath.isChecked()
self.mainConf.hideVScroll = self.hideVScroll.isChecked()
@@ -758,6 +747,15 @@ class GuiPreferencesEditor(QWidget):
theUnit=self.tr("seconds")
)
# Include Notes in Word Count
self.incNotesWCount = QSwitch()
self.incNotesWCount.setChecked(self.mainConf.incNotesWCount)
self.mainForm.addRow(
self.tr("Include project notes in total word count"),
self.incNotesWCount,
self.tr("Affects the word count shown on the status bar.")
)
# Writing Guides
# ==============
self.mainForm.addGroupLabel(self.tr("Writing Guides"))
@@ -826,6 +824,7 @@ class GuiPreferencesEditor(QWidget):
# Word Count
self.mainConf.wordCountTimer = self.wordCountTimer.value()
self.mainConf.incNotesWCount = self.incNotesWCount.isChecked()
# Writing Guides
self.mainConf.showTabsNSpaces = self.showTabsNSpaces.isChecked()
+1 -1
View File
@@ -4,7 +4,7 @@ novelWriter GUI Updates
A dialog box for checking for latest updates
File History:
Created: 2021-08-21 [1.5-alpah0]
Created: 2021-08-21 [1.5a0]
This file is a part of novelWriter
Copyright 20182021, Veronica Berglyd Olsen
+2 -2
View File
@@ -8,8 +8,8 @@ Created: 2018-09-29 [0.0.1] GuiDocEditor
Created: 2019-04-22 [0.0.1] BackgroundWordCounter
Created: 2019-09-29 [0.2.1] GuiDocEditSearch
Created: 2020-04-25 [0.4.5] GuiDocEditHeader
Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch
Created: 2020-06-27 [0.10.0] GuiDocEditFooter
Rewritten: 2020-06-15 [0.9] GuiDocEditSearch
Created: 2020-06-27 [0.10] GuiDocEditFooter
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
This file is a part of novelWriter
+1 -1
View File
@@ -7,7 +7,7 @@ File History:
Created: 2019-05-10 [0.0.1] GuiDocViewer
Created: 2019-10-31 [0.3.2] GuiDocViewDetails
Created: 2020-04-25 [0.4.5] GuiDocViewHeader
Created: 2020-06-09 [0.8.0] GuiDocViewFooter
Created: 2020-06-09 [0.8] GuiDocViewFooter
Created: 2020-09-08 [1.0b1] GuiDocViewHistory
This file is a part of novelWriter
+7 -36
View File
@@ -5,7 +5,7 @@ GUI classes for the main window project tree
File History:
Created: 2018-09-29 [0.0.1] GuiProjectTree
Created: 2020-06-04 [0.7.0] GuiProjectTreeMenu
Created: 2020-06-04 [0.7] GuiProjectTreeMenu
This file is a part of novelWriter
Copyright 20182021, Veronica Berglyd Olsen
@@ -51,7 +51,7 @@ class GuiProjectTree(QTreeWidget):
novelItemChanged = pyqtSignal()
noteItemChanged = pyqtSignal()
projectWordCountChanged = pyqtSignal(int, int)
wordCountsChanged = pyqtSignal()
def __init__(self, theParent):
QTreeWidget.__init__(self, theParent)
@@ -86,8 +86,7 @@ class GuiProjectTree(QTreeWidget):
self.setIndentation(iPx)
self.setColumnCount(4)
self.setHeaderLabels([
self.tr("Project Tree"), self.tr("Words"), "",
self.tr("Status") if self.mainConf.fullStatus else ""
self.tr("Project Tree"), self.tr("Words"), "", ""
])
treeHeadItem = self.headerItem()
@@ -305,7 +304,7 @@ class GuiProjectTree(QTreeWidget):
nwItem.setWordCount(wC)
nwItem.setParaCount(pC)
self.propagateCount(tHandle, wC)
self.projectWordCount()
self.wordCountsChanged.emit()
return True
@@ -533,7 +532,7 @@ class GuiProjectTree(QTreeWidget):
self.theIndex.deleteHandle(tHandle)
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
self.projectWordCount()
self.wordCountsChanged.emit()
else:
# The file is not already in the trash folder, so we
@@ -630,11 +629,7 @@ class GuiProjectTree(QTreeWidget):
trItem.setText(self.C_NAME, nwItem.itemName)
trItem.setIcon(self.C_EXPORT, expIcon)
trItem.setIcon(self.C_STATUS, statIcon)
if self.mainConf.fullStatus:
trItem.setText(self.C_STATUS, nwItem.itemStatus)
else:
trItem.setToolTip(self.C_STATUS, nwItem.itemStatus)
trItem.setToolTip(self.C_STATUS, nwItem.itemStatus)
if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT:
if hLevel in ("H1", "H2"):
@@ -674,25 +669,6 @@ class GuiProjectTree(QTreeWidget):
return
def projectWordCount(self):
"""Sum up the word counts for all root items and set the
relevant values in the project and on the status bar. This call
is a fast way of getting this number, and depends on the
propagateCount function being called when it should to maintain
the correct count.
"""
nWords = 0
for n in range(self.topLevelItemCount()):
tItem = self.topLevelItem(n)
nWords += int(tItem.data(self.C_COUNT, Qt.UserRole))
self.theProject.setProjectWordCount(nWords)
sWords = self.theProject.getSessionWordCount()
self.projectWordCountChanged.emit(nWords, sWords)
return
def buildTree(self):
"""Build the entire project tree from scratch. This depends on
the save project item iterator in the project class which will
@@ -702,11 +678,6 @@ class GuiProjectTree(QTreeWidget):
logger.debug("Building the project tree ...")
self.clearTree()
self.setHeaderLabels([
self.tr("Project Tree"), self.tr("Words"), "",
self.tr("Status") if self.mainConf.fullStatus else ""
])
iCount = 0
for nwItem in self.theProject.getProjectItems():
iCount += 1
@@ -819,7 +790,7 @@ class GuiProjectTree(QTreeWidget):
"""Slot for updating the word count of a specific item.
"""
self.propagateCount(tHandle, wCount)
self.projectWordCount()
self.wordCountsChanged.emit()
return
##
+14 -11
View File
@@ -4,7 +4,8 @@ novelWriter GUI Main Window Status Bar
GUI class for the main window status bar
File History:
Created: 2019-04-20 [0.0.1]
Created: 2019-04-20 [0.0.1] GuiMainStatus
Created: 2020-05-17 [0.5.1] StatusLED
This file is a part of novelWriter
Copyright 20182021, Veronica Berglyd Olsen
@@ -28,7 +29,7 @@ import novelwriter
from time import time
from PyQt5.QtCore import QLocale, pyqtSlot
from PyQt5.QtCore import pyqtSlot, QLocale
from PyQt5.QtGui import QColor, QPainter
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton
@@ -125,7 +126,7 @@ class GuiMainStatus(QStatusBar):
"""
self.setRefTime(None)
self.setLanguage(None, "")
self.doUpdateProjectStats(0, 0)
self.setProjectStats(0, 0)
self.setProjectStatus(nwState.NONE)
self.setDocumentStatus(nwState.NONE)
self.updateTime()
@@ -176,6 +177,16 @@ class GuiMainStatus(QStatusBar):
return
def setProjectStats(self, pWC, sWC):
"""Update the current project statistics.
"""
self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}"))
if self.mainConf.incNotesWCount:
self.statsText.setToolTip(self.tr("Project word count (session change)"))
else:
self.statsText.setToolTip(self.tr("Novel word count (session change)"))
return
def updateTime(self, idleTime=0.0):
"""Update the session clock.
"""
@@ -211,14 +222,6 @@ class GuiMainStatus(QStatusBar):
return
@pyqtSlot(int, int)
def doUpdateProjectStats(self, pWC, sWC):
"""Update the current project statistics.
"""
self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}"))
self.statsText.setToolTip(self.tr("Project word count (session change)"))
return
@pyqtSlot(bool)
def doUpdateProjectStatus(self, isChanged):
"""Slot for updating the project status.
+1 -1
View File
@@ -5,7 +5,7 @@ Classes managing and caching themes and icons
File History:
Created: 2019-05-18 [0.1.3] GuiTheme
Created: 2019-11-08 [0.4.0] GuiIcons
Created: 2019-11-08 [0.4] GuiIcons
This file is a part of novelWriter
Copyright 20182021, Veronica Berglyd Olsen
+24 -3
View File
@@ -124,7 +124,7 @@ class GuiMain(QMainWindow):
self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
self.treeView.projectWordCountChanged.connect(self.statusBar.doUpdateProjectStats)
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
# Minor GUI Elements
self.statusIcons = []
@@ -529,7 +529,7 @@ class GuiMain(QMainWindow):
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.doUpdateProjectStats(self.theProject.currWCount, 0)
self._updateStatusWordCount()
# Restore previously open documents, if any
if self.theProject.lastEdited is not None:
@@ -903,13 +903,13 @@ class GuiMain(QMainWindow):
tItem.setParaCount(pC)
self.treeView.propagateCount(tItem.itemHandle, wC)
self.treeView.setTreeItemValues(tItem.itemHandle)
self.treeView.projectWordCount()
tEnd = time()
self.setStatus(
self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
)
self.docEditor.updateTagHighLighting()
self._updateStatusWordCount()
qApp.restoreOverrideCursor()
if not beQuiet:
@@ -980,6 +980,7 @@ class GuiMain(QMainWindow):
self.novelView.initTree()
self.projView.initOutline()
self.projMeta.initDetails()
self._updateStatusWordCount()
return
@@ -1527,6 +1528,26 @@ class GuiMain(QMainWindow):
return
@pyqtSlot()
def _updateStatusWordCount(self):
"""Update the word count on the status bar.
"""
if not self.hasProject:
self.statusBar.setProjectStats(0, 0)
logger.verbose("Updating total word count")
self.theProject.updateWordCounts()
if self.mainConf.incNotesWCount:
currWords = self.theProject.currWCount
diffWords = currWords - self.theProject.lastWCount
else:
currWords = self.theProject.currNovelWC
diffWords = currWords - self.theProject.lastNovelWC
self.statusBar.setProjectStats(currWords, diffWords)
return
@pyqtSlot()
def _treeSingleClick(self):
"""Single click on a project tree item just updates the details