From 03d1cee2b3334c0fa507c07e20e10910e9b3f017 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Mon, 22 Apr 2019 14:15:28 +0200 Subject: [PATCH] Implemented a working word and char counter that runs in the background at regular intervals. --- nw/config.py | 1 + nw/gui/doceditor.py | 118 +++++++++++++++++++----------------------- nw/gui/wordcounter.py | 73 ++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 65 deletions(-) create mode 100644 nw/gui/wordcounter.py diff --git a/nw/config.py b/nw/config.py index 0ed2b914..82a405ab 100644 --- a/nw/config.py +++ b/nw/config.py @@ -66,6 +66,7 @@ class Config: self.doReplaceDash = True self.doReplaceDots = True self.replaceQuotes = ["“","”"] + self.wordCountTimer = 5.0 # Check if config file exists if path.isfile(path.join(self.confPath,self.confFile)): diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 497d004a..e5ee44a7 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -16,10 +16,11 @@ import nw from time import time from PyQt5.QtWidgets import QWidget, QTextEdit, QHBoxLayout, QVBoxLayout, QFrame, QSplitter, QToolBar, QAction, QScrollArea -from PyQt5.QtCore import Qt, QSize, QSizeF +from PyQt5.QtCore import Qt, QSize, QSizeF, QTimer, QThread, pyqtSignal from PyQt5.QtGui import QIcon, QFont, QTextCursor, QTextFormat, QTextBlockFormat from nw.gui.dochighlight import GuiDocHighlighter +from nw.gui.wordcounter import WordCounter logger = logging.getLogger(__name__) @@ -34,12 +35,7 @@ class GuiDocEditor(QWidget): self.charCount = 0 self.wordCount = 0 self.lineCount = 0 - - # Internal Temps - self.blockWords = {} - self.currEditBlock = -1 - self.currEditStart = 0 - self.currEditFinal = 0 + self.lastEdit = 0 self.outerBox = QVBoxLayout() self.guiEditor = QTextEdit() @@ -68,30 +64,28 @@ class GuiDocEditor(QWidget): self.theDoc.setDocumentMargin(0) self.theDoc.contentsChange.connect(self._docChange) + # Set Up Word Count Thread and Timer + self.wcInterval = self.mainConf.wordCountTimer + self.wcTimer = QTimer() + self.wcTimer.setInterval(int(self.wcInterval*1000)) + self.wcTimer.timeout.connect(self._runCounter) + + self.wCounter = WordCounter(self) + self.wCounter.finished.connect(self._updateCounts) + logger.debug("DocEditor initialisation complete") return + ## + # Class Methods + ## + def setText(self, theText): - self.guiEditor.setPlainText(theText) - - self.charCount = self.theDoc.characterCount() - self.wordCount = 0 - - self.currEditBlock = -1 - self.currEditWords = 0 - - tStart = time() - self.blockWords = {} - for n in range(self.theDoc.blockCount()): - self._countWords(self.theDoc.findBlockByNumber(n)) - self.wordCount = sum(self.blockWords.values()) - logger.verbose("Doc word count took %.3f µs" % ((time()-tStart)*1e6)) - - self.theParent.statusBar.setCharCount(self.charCount) - self.theParent.statusBar.setWordCount(self.wordCount) - + self.lastEdit = time() + self._runCounter() + self.wcTimer.start() return True def getText(self): @@ -110,27 +104,22 @@ class GuiDocEditor(QWidget): self.guiEditor.setViewportMargins(tM,mTB,0,mTB) return + ## + # Document Events and Maintenance + ## + def _docChange(self, thePos, charsRemoved, charsAdded): - - tStart = time() - - self.charCount = self.theDoc.characterCount() - self.lineCount = self.theDoc.lineCount() - self.theParent.statusBar.setCharCount(self.charCount) - - currBlock = self.theDoc.findBlock(thePos) - self._countWords(currBlock) - self.wordCount = sum(self.blockWords.values()) - self.theParent.statusBar.setWordCount(self.wordCount) - + self.lastEdit = time() + if not self.wcTimer.isActive(): + self.wcTimer.start() if self.mainConf.doReplace: - self._docAutoReplace(currBlock) - - logger.verbose("Doc change signal took %.3f µs" % ((time()-tStart)*1e6)) - + self._docAutoReplace(self.theDoc.findBlock(thePos)) + logger.verbose("Doc change signal took %.3f µs" % ((time()-self.lastEdit)*1e6)) return def _docAutoReplace(self, theBlock): + """Autoreplace text elements based on main configuration. + """ if not theBlock.isValid(): return @@ -177,34 +166,33 @@ class GuiDocEditor(QWidget): return - def _countWords(self, theBlock): - """Count the number of words in a given block, but only if it's a text block. I.e. we skip - blocks starting with @ and %, and we also skip the ### in titles. Dashes are replaced with - spaces before the count to ensure, for instance, 'word1—word2' is counted as two words. + def _runCounter(self): + """Decide whether to run the word counter, or stop the timer due to inactivity. """ - - if not theBlock.isValid(): - return - - theText = theBlock.text() - theLen = len(theText) - self.blockWords[theBlock.blockNumber()] = 0 - - if theLen == 0: - return 0 - if theText[0] == "@" or theText[0] == "%": - return 0 - if theText[0] == "#": - nWords = -1 + sinceActive = time()-self.lastEdit + if sinceActive > 5*self.wcInterval: + logger.verbose("Stopping word count timer due to no activity over the last %.3f seconds" % sinceActive) + self.wcTimer.stop() + elif self.wCounter.isRunning(): + logger.verbose("Word counter thread is busy") else: - nWords = 0 + logger.verbose("Starting word counter") + self.wCounter.start() + return - theBuff = theText.replace("–"," ").replace("—"," ") - nWords += len(theBuff.split()) + def _updateCounts(self): + """Slot for the word counter's finished signal + """ + logger.verbose("Updating word counts") + self.charCount = self.wCounter.charCount + self.wordCount = self.wCounter.wordCount + self.theParent.statusBar.setCharCount(self.charCount) + self.theParent.statusBar.setWordCount(self.wordCount) + return - self.blockWords[theBlock.blockNumber()] = nWords - - return nWords + ## + # GUI Builder + ## def _buildTabToolBar(self): toolBar = self.editToolBar diff --git a/nw/gui/wordcounter.py b/nw/gui/wordcounter.py new file mode 100644 index 00000000..f03a834a --- /dev/null +++ b/nw/gui/wordcounter.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +"""novelWriter GUI Document Word Counter + + novelWriter – GUI Document Word Counter +=================================== + A thread for counting words and characters in a document + + File History: + Created: 2019-04-22 [0.0.1] + +""" + +import logging +import nw + +from time import time + +from PyQt5.QtWidgets import QWidget, QTextEdit, QHBoxLayout, QVBoxLayout, QFrame, QSplitter, QToolBar, QAction, QScrollArea +from PyQt5.QtCore import Qt, QSize, QSizeF, QTimer, QThread, pyqtSignal +from PyQt5.QtGui import QIcon, QFont, QTextCursor, QTextFormat, QTextBlockFormat + +from nw.gui.dochighlight import GuiDocHighlighter + +logger = logging.getLogger(__name__) + +class WordCounter(QThread): + + def __init__(self, theParent): + QThread.__init__(self, theParent) + self.theParent = theParent + self.charCount = 0 + self.wordCount = 0 + return + + def run(self): + + self.charCount = 0 + self.wordCount = 0 + + for n in range(self.theParent.theDoc.blockCount()): + + theBlock = self.theParent.theDoc.findBlockByNumber(n) + if not theBlock.isValid(): + continue + + theText = theBlock.text() + theLen = len(theText) + + if theLen == 0: + continue + if theText[0] == "@" or theText[0] == "%": + continue + + if theText[0:5] == "#### ": + self.wordCount -= 1 + self.charCount -= 5 + elif theText[0:4] == "### ": + self.wordCount -= 1 + self.charCount -= 4 + elif theText[0:3] == "## ": + self.wordCount -= 1 + self.charCount -= 3 + elif theText[0:2] == "# ": + self.wordCount -= 1 + self.charCount -= 2 + + theBuff = theText.replace("–"," ").replace("—"," ") + self.wordCount += len(theBuff.split()) + self.charCount += theLen + + pass + +## END Class _WordCounter