Qt threading done right (thread pool for word counter)

This commit is contained in:
Veronica K. B. Olsen
2020-10-07 15:04:13 +02:00
parent b6a4568254
commit 0c1aaecd78
2 changed files with 46 additions and 38 deletions
+44 -37
View File
@@ -12,6 +12,7 @@
Created: 2020-04-25 [0.4.5] GuiDocEditHeader Created: 2020-04-25 [0.4.5] GuiDocEditHeader
Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch
Created: 2020-06-27 [0.10.0] GuiDocEditFooter Created: 2020-06-27 [0.10.0] GuiDocEditFooter
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182020, Veronica Berglyd Olsen Copyright 20182020, Veronica Berglyd Olsen
@@ -36,7 +37,8 @@ import logging
from time import time from time import time
from PyQt5.QtCore import ( from PyQt5.QtCore import (
Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp, QRegularExpression, QPointF Qt, QSize, QTimer, pyqtSlot, pyqtSignal, QRegExp, QRegularExpression,
QPointF, QObject, QRunnable
) )
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette,
@@ -135,14 +137,15 @@ class GuiDocEditor(QTextEdit):
activated=self._followTag activated=self._followTag
) )
# Set Up Word Count Thread and Timer # Set Up Word Counter
self.wcInterval = self.mainConf.wordCountTimer self.wcInterval = self.mainConf.wordCountTimer
self.wcTimer = QTimer() self.wcTimer = QTimer()
self.wcTimer.setInterval(int(self.wcInterval*1000)) self.wcTimer.setInterval(int(self.wcInterval*1000))
self.wcTimer.timeout.connect(self._runCounter) self.wcTimer.timeout.connect(self._runCounter)
self.wCounter = BackgroundWordCounter(self) self.wCounter = BackgroundWordCounter(self)
self.wCounter.finished.connect(self._updateCounts) self.wCounter.setAutoDelete(False)
self.wCounter.signals.countsReady.connect(self._updateCounts)
self.initEditor() self.initEditor()
@@ -912,21 +915,18 @@ class GuiDocEditor(QTextEdit):
"""Decide whether to run the word counter, or stop the timer due """Decide whether to run the word counter, or stop the timer due
to inactivity. to inactivity.
""" """
sinceActive = time()-self.lastEdit if self.wCounter.isRunning():
if sinceActive > 5*self.wcInterval: logger.verbose("Word counter is busy")
logger.debug( return
"Stopping word count timer: no activity last %.1f seconds" % sinceActive
) if time() - self.lastEdit < 5*self.wcInterval:
self.wcTimer.stop() logger.verbose("Running word counter")
elif self.wCounter.isRunning(): self.theParent.threadPool.start(self.wCounter)
logger.verbose("Word counter thread is busy")
else:
logger.verbose("Starting word counter")
self.wCounter.start()
return return
@pyqtSlot() @pyqtSlot(int, int, int)
def _updateCounts(self): def _updateCounts(self, cCount, wCount, pCount):
"""Slot for the word counter's finished signal """Slot for the word counter's finished signal
""" """
theItem = self.nwDocument.getCurrentItem() theItem = self.nwDocument.getCurrentItem()
@@ -935,19 +935,17 @@ class GuiDocEditor(QTextEdit):
logger.verbose("Updating word count") logger.verbose("Updating word count")
self.charCount = self.wCounter.charCount self.charCount = cCount
self.wordCount = self.wCounter.wordCount self.wordCount = wCount
self.paraCount = self.wCounter.paraCount self.paraCount = pCount
theItem.setCharCount(self.charCount) theItem.setCharCount(cCount)
theItem.setWordCount(self.wordCount) theItem.setWordCount(wCount)
theItem.setParaCount(self.paraCount) theItem.setParaCount(pCount)
self.theParent.treeView.propagateCount(self.theHandle, self.wordCount) self.theParent.treeView.propagateCount(self.theHandle, wCount)
self.theParent.treeView.projectWordCount() self.theParent.treeView.projectWordCount()
self.theParent.treeMeta.updateCounts( self.theParent.treeMeta.updateCounts(self.theHandle, cCount, wCount, pCount)
self.theHandle, self.charCount, self.wordCount, self.paraCount self._checkDocSize(self.qDocument.characterCount())
)
self._checkDocSize(self.charCount)
self.docFooter.updateCounts() self.docFooter.updateCounts()
return return
@@ -1521,33 +1519,42 @@ class GuiDocEditor(QTextEdit):
# END Class GuiDocEditor # END Class GuiDocEditor
# =============================================================================================== # # =============================================================================================== #
# The Off GUI Thread Word Counter # The Off-GUI Thread Word Counter
# Runs the word counter in the background for the DocEditor # A runnable for the word counter to be run in the thread pool off the main GUI thread.
# =============================================================================================== # # =============================================================================================== #
class BackgroundWordCounter(QThread): class BackgroundWordCounter(QRunnable):
def __init__(self, docEditor): def __init__(self, docEditor):
QThread.__init__(self, docEditor) QRunnable.__init__(self)
self.docEditor = docEditor self.docEditor = docEditor
self.charCount = 0 self.signals = BackgroundWordCounterSignals()
self.wordCount = 0 self._isRunning = False
self.paraCount = 0
return return
def isRunning(self):
return self._isRunning
@pyqtSlot()
def run(self): def run(self):
"""Overloaded run function for the word counter, forwarding the """Overloaded run function for the word counter, forwarding the
call to the function that does the actual counting. call to the function that does the actual counting.
""" """
self._isRunning = True
theText = self.docEditor.getText() theText = self.docEditor.getText()
cC, wC, pC = countWords(theText) cC, wC, pC = countWords(theText)
self.charCount = cC self.signals.countsReady.emit(cC, wC, pC)
self.wordCount = wC self._isRunning = False
self.paraCount = pC
return return
## END Class BackgroundWordCounter ## END Class BackgroundWordCounter
class BackgroundWordCounterSignals(QObject):
countsReady = pyqtSignal(int, int, int)
# END Class BackgroundWordCounterSignals
# =============================================================================================== # # =============================================================================================== #
# The Embedded Document Search/Replace Feature # The Embedded Document Search/Replace Feature
# Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport # Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport
+2 -1
View File
@@ -32,7 +32,7 @@ import os
from datetime import datetime from datetime import datetime
from time import time from time import time
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer, QThreadPool
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut, qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
@@ -60,6 +60,7 @@ class GuiMain(QMainWindow):
logger.debug("Initialising GUI ...") logger.debug("Initialising GUI ...")
self.setObjectName("GuiMain") self.setObjectName("GuiMain")
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.threadPool = QThreadPool()
# Some runtime info useful for debugging # Some runtime info useful for debugging
logger.info("OS: %s" % self.mainConf.osType) logger.info("OS: %s" % self.mainConf.osType)