diff --git a/novelwriter/constants.py b/novelwriter/constants.py index f74ce319..016a30e9 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -52,6 +52,9 @@ class nwConst: URL_HELP = "https://github.com/vkbo/novelWriter/discussions" URL_RELEASE = "https://github.com/vkbo/novelWriter/releases/latest" + # Gui Settings + STATUS_MSG_TIMEOUT = 15000 # milliseconds + # END Class nwConst diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 27ac56af..fd5e0b0a 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -58,7 +58,8 @@ logger = logging.getLogger(__name__) class NWProject(QObject): - projectStatusChanged = pyqtSignal(bool) + statusChanged = pyqtSignal(bool) + statusMessage = pyqtSignal(str) def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) @@ -335,7 +336,7 @@ class NWProject(QObject): self.setProjectChanged(False) self._valid = True - self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self._data.name)) + self.statusMessage.emit(self.tr("Opened Project: {0}").format(self._data.name)) return True @@ -389,7 +390,7 @@ class NWProject(QObject): ) self._storage.writeLockFile() - self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name)) + self.statusMessage.emit(self.tr("Saved Project: {0}").format(self._data.name)) self.setProjectChanged(False) return True @@ -411,7 +412,7 @@ class NWProject(QObject): return False logger.info("Backing up project") - self.mainGui.setStatus(self.tr("Backing up project ...")) + self.statusMessage.emit(self.tr("Backing up project ...")) if not self._data.name: self.mainGui.makeAlert(self.tr( @@ -446,9 +447,7 @@ class NWProject(QObject): ), level=nwAlert.ERROR) return False - self.mainGui.setStatus(self.tr( - "Project backed up to '{0}'" - ).format(str(archName))) + self.statusMessage.emit(self.tr("Project backed up to '{0}'").format(str(archName))) return True @@ -502,7 +501,7 @@ class NWProject(QObject): """ if isinstance(status, bool): self._changed = status - self.projectStatusChanged.emit(self._changed) + self.statusChanged.emit(self._changed) return self._changed ## diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 4caa855e..633805d8 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -71,9 +71,10 @@ class GuiDocEditor(QTextEdit): ) # Custom Signals - spellDictionaryChanged = pyqtSignal(str, str) - docEditedStatusChanged = pyqtSignal(bool) + statusMessage = pyqtSignal(str) docCountsChanged = pyqtSignal(str, int, int, int) + editedStatusChanged = pyqtSignal(bool) + spellDictionaryChanged = pyqtSignal(str, str) loadDocumentTagRequest = pyqtSignal(str, Enum) novelStructureChanged = pyqtSignal() novelItemMetaChanged = pyqtSignal(str) @@ -454,9 +455,7 @@ class GuiDocEditor(QTextEdit): # Update the status bar if self._nwItem is not None: - self.mainGui.setStatus( - self.tr("Opened Document: {0}").format(self._nwItem.itemName) - ) + self.statusMessage.emit(self.tr("Opened Document: {0}").format(self._nwItem.itemName)) return True @@ -562,9 +561,7 @@ class GuiDocEditor(QTextEdit): self.docFooter.updateInfo() # Update the status bar - self.mainGui.setStatus( - self.tr("Saved Document: {0}").format(self._nwItem.itemName) - ) + self.statusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName)) return True @@ -639,7 +636,7 @@ class GuiDocEditor(QTextEdit): document change signal. """ self._docChanged = bValue - self.docEditedStatusChanged.emit(self._docChanged) + self.editedStatusChanged.emit(self._docChanged) return self._docChanged def setCursorPosition(self, position): @@ -747,7 +744,7 @@ class GuiDocEditor(QTextEdit): return True - def spellCheckDocument(self): + def spellCheckDocument(self) -> None: """Rerun the highlighter to update spell checking status of the currently loaded text. The fastest way to do this, at least as of Qt 5.13, is to clear the text and put it back. This clears @@ -763,9 +760,8 @@ class GuiDocEditor(QTextEdit): self.highLight.rehighlight() qApp.restoreOverrideCursor() logger.debug("Document highlighted in %.3f ms", 1000*(time() - start)) - self.mainGui.mainStatus.setStatus(self.tr("Spell check complete")) - - return True + self.statusMessage.emit(self.tr("Spell check complete")) + return ## # General Class Methods diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 20152367..597124de 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -27,6 +27,7 @@ from __future__ import annotations import logging from time import time +from typing import TYPE_CHECKING, Literal from PyQt5.QtCore import pyqtSlot, QLocale from PyQt5.QtGui import QColor @@ -34,21 +35,24 @@ from PyQt5.QtWidgets import qApp, QStatusBar, QLabel from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime +from novelwriter.constants import nwConst from novelwriter.extensions.statusled import StatusLED +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) class GuiMainStatus(QStatusBar): - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiMainStatus") - self.mainGui = mainGui - self.refTime = None - self.userIdle = False + self._refTime = -1.0 + self._userIdle = False colNone = QColor(*SHARED.theme.statNone) colSaved = QColor(*SHARED.theme.statSaved) @@ -114,80 +118,59 @@ class GuiMainStatus(QStatusBar): return - def clearStatus(self): - """Reset all widgets on the status bar to default values. - """ - self.setRefTime(None) + def clearStatus(self) -> None: + """Reset all widgets on the status bar to default values.""" + self.setRefTime(-1.0) self.setLanguage(None, "") self.setProjectStats(0, 0) self.setProjectStatus(StatusLED.S_NONE) self.setDocumentStatus(StatusLED.S_NONE) self.updateTime() - return True + return - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" iPx = SHARED.theme.baseIconSize - self.langIcon.setPixmap(SHARED.theme.getPixmap("status_lang", (iPx, iPx))) self.statsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (iPx, iPx))) - self.timePixmap = SHARED.theme.getPixmap("status_time", (iPx, iPx)) self.idlePixmap = SHARED.theme.getPixmap("status_idle", (iPx, iPx)) - self.timeIcon.setPixmap(self.timePixmap) - return ## # Setters ## - def setRefTime(self, theTime): - """Set the reference time for the status bar clock. - """ - self.refTime = theTime + def setRefTime(self, refTime: float) -> None: + """Set the reference time for the status bar clock.""" + self._refTime = refTime return - def setStatus(self, theMessage, timeOut=20.0): - """Set the status bar message to display for 'timeOut' seconds. - """ - self.showMessage(theMessage, int(timeOut*1000)) - qApp.processEvents() + def setProjectStatus(self, state: Literal[0, 1, 2]) -> None: + """Set the project status colour icon.""" + self.projIcon.setState(state) return - def setProjectStatus(self, theState): - """Set the project status colour icon. - """ - self.projIcon.setState(theState) + def setDocumentStatus(self, state: Literal[0, 1, 2]) -> None: + """Set the document status colour icon.""" + self.docIcon.setState(state) return - def setDocumentStatus(self, theState): - """Set the document status colour icon. - """ - self.docIcon.setState(theState) - return - - def setUserIdle(self, userIdle): - """Change the idle status icon. - """ + def setUserIdle(self, idle: bool) -> None: + """Change the idle status icon.""" if not CONFIG.stopWhenIdle: - userIdle = False - - if self.userIdle != userIdle: - if userIdle: + idle = False + if self._userIdle != idle: + if idle: self.timeIcon.setPixmap(self.idlePixmap) else: self.timeIcon.setPixmap(self.timePixmap) - - self.userIdle = userIdle - + self._userIdle = idle return - def setProjectStats(self, pWC, sWC): - """Update the current project statistics. - """ + def setProjectStats(self, pWC: int, sWC: int) -> None: + """Update the current project statistics.""" self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}")) if CONFIG.incNotesWCount: self.statsText.setToolTip(self.tr("Project word count (session change)")) @@ -195,53 +178,55 @@ class GuiMainStatus(QStatusBar): self.statsText.setToolTip(self.tr("Novel word count (session change)")) return - def updateTime(self, idleTime=0.0): - """Update the session clock. - """ - if self.refTime is None: + def updateTime(self, idleTime: float = 0.0) -> None: + """Update the session clock.""" + if self._refTime < 0.0: self.timeText.setText("00:00:00") else: if CONFIG.stopWhenIdle: - sessTime = round(time() - self.refTime - idleTime) + sessTime = round(time() - self._refTime - idleTime) else: - sessTime = round(time() - self.refTime) + sessTime = round(time() - self._refTime) self.timeText.setText(formatTime(sessTime)) return ## - # Slots + # Public Slots ## + @pyqtSlot(str) + def setStatusMessage(self, message: str) -> None: + """Set the status bar message to display.""" + self.showMessage(message, nwConst.STATUS_MSG_TIMEOUT) + qApp.processEvents() + return + @pyqtSlot(str, str) - def setLanguage(self, theLanguage, theProvider): - """Set the language code for the spell checker. - """ - if theLanguage == "None": + def setLanguage(self, language: str, provider: str) -> None: + """Set the language code for the spell checker.""" + if language == "None": self.langText.setText(self.tr("None")) self.langText.setToolTip("") else: - qLocal = QLocale(theLanguage) + qLocal = QLocale(language) spLang = qLocal.nativeLanguageName().title() self.langText.setText(spLang) - if theProvider: - self.langText.setToolTip("%s (%s)" % (theLanguage, theProvider)) + if provider: + self.langText.setToolTip("%s (%s)" % (language, provider)) else: - self.langText.setToolTip(theLanguage) - + self.langText.setToolTip(language) return @pyqtSlot(bool) - def doUpdateProjectStatus(self, isChanged): - """Slot for updating the project status. - """ - self.setProjectStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD) + def updateProjectStatus(self, status: bool) -> None: + """Update the project status.""" + self.setProjectStatus(StatusLED.S_BAD if status else StatusLED.S_GOOD) return @pyqtSlot(bool) - def doUpdateDocumentStatus(self, isChanged): - """Slot for updating the document status. - """ - self.setDocumentStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD) + def updateDocumentStatus(self, status: bool) -> None: + """Update the document status.""" + self.setDocumentStatus(StatusLED.S_BAD if status else StatusLED.S_GOOD) return # END Class GuiMainStatus diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 7eab0b8f..cabfeda9 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -236,7 +236,8 @@ class GuiMain(QMainWindow): # Connect Signals # =============== - SHARED.projectStatusChanged.connect(self.mainStatus.doUpdateProjectStatus) + SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus) + SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage) self.viewsBar.viewChangeRequested.connect(self._changeView) @@ -255,12 +256,13 @@ class GuiMain(QMainWindow): self.novelView.openDocumentRequest.connect(self._openDocument) self.docEditor.spellDictionaryChanged.connect(self.mainStatus.setLanguage) - self.docEditor.docEditedStatusChanged.connect(self.mainStatus.doUpdateDocumentStatus) + self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus) self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) self.docEditor.docCountsChanged.connect(self.projView.updateCounts) self.docEditor.loadDocumentTagRequest.connect(self._followTag) self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree) self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta) + self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage) self.docViewer.loadDocumentTagRequest.connect(self._followTag) @@ -299,9 +301,6 @@ class GuiMain(QMainWindow): keyEscape.setKey(QKeySequence(Qt.Key_Escape)) keyEscape.activated.connect(self._keyPressEscape) - # Forward Functions - self.setStatus = self.mainStatus.setStatus - # Cache Alert Pixmaps pxSize = (2*iPx, 2*iPx) self.alertPix: dict[nwAlert, QPixmap] = { @@ -333,7 +332,7 @@ class GuiMain(QMainWindow): ), level=nwAlert.WARN) logger.info("novelWriter is ready ...") - self.setStatus(self.tr("novelWriter is ready ...")) + self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ...")) return @@ -838,7 +837,7 @@ class GuiMain(QMainWindow): self.novelView.refreshTree() tEnd = time() - self.setStatus( + self.mainStatus.setStatusMessage( self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}") ) self.docEditor.updateTagHighLighting() diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 48cb5eb7..8ad3e3f3 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -41,6 +41,7 @@ logger = logging.getLogger(__name__) class SharedData(QObject): projectStatusChanged = pyqtSignal(bool) + projectStatusMessage = pyqtSignal(str) def __init__(self) -> None: super().__init__() @@ -131,11 +132,17 @@ class SharedData(QObject): ## @pyqtSlot(bool) - def _processProjectStatusChange(self, state: bool) -> None: + def _emitProjectStatusChange(self, state: bool) -> None: """Forward the project status slot.""" self.projectStatusChanged.emit(state) return + @pyqtSlot(str) + def _emitProjectStatusMeesage(self, message: str) -> None: + """Forward the project message slot.""" + self.projectStatusMessage.emit(message) + return + ## # Internal Functions ## @@ -144,10 +151,12 @@ class SharedData(QObject): """Create a new project instance.""" from novelwriter.core.project import NWProject if isinstance(self._project, NWProject): - self._project.projectStatusChanged.disconnect() + self._project.statusChanged.disconnect() + self._project.statusMessage.disconnect() self._project.deleteLater() self._project = NWProject(self.mainGui) - self._project.projectStatusChanged.connect(self._processProjectStatusChange) + self._project.statusChanged.connect(self._emitProjectStatusChange) + self._project.statusMessage.connect(self._emitProjectStatusMeesage) return # END Class SharedData diff --git a/tests/mocked.py b/tests/mocked.py index f4a85e4b..3c748236 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -92,7 +92,7 @@ class MockStatusBar: def setStatus(self, theText): return - def doUpdateProjectStatus(self, theStatus): + def updateProjectStatus(self, theStatus): return # END Class MockStatusBar diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 1096f8e2..ea8a9f4f 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -213,7 +213,7 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): # Document Changed Signal nwGUI.docEditor._docChanged = False - with qtbot.waitSignal(nwGUI.docEditor.docEditedStatusChanged, raising=True, timeout=100): + with qtbot.waitSignal(nwGUI.docEditor.editedStatusChanged, raising=True, timeout=100): nwGUI.docEditor.setDocumentChanged(True) assert nwGUI.docEditor._docChanged is True diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index b227fb61..701a2d07 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -30,8 +30,7 @@ from novelwriter.extensions.statusled import StatusLED @pytest.mark.gui def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): - """Test the the various features of the status bar. - """ + """Test the the various features of the status bar.""" buildTestProject(nwGUI, projPath) cHandle = nwGUI.project.newFile("A Note", C.hCharRoot) newDoc = nwGUI.project.storage.getDocument(cHandle) @@ -42,7 +41,7 @@ def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): # Reference Time refTime = time.time() nwGUI.mainStatus.setRefTime(refTime) - assert nwGUI.mainStatus.refTime == refTime + assert nwGUI.mainStatus._refTime == refTime # Project Status nwGUI.mainStatus.setProjectStatus(StatusLED.S_NONE) @@ -64,18 +63,18 @@ def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): CONFIG.stopWhenIdle = False nwGUI.mainStatus.setUserIdle(True) nwGUI.mainStatus.updateTime() - assert nwGUI.mainStatus.userIdle is False + assert nwGUI.mainStatus._userIdle is False assert nwGUI.mainStatus.timeText.text() == "00:00:00" CONFIG.stopWhenIdle = True nwGUI.mainStatus.setUserIdle(True) nwGUI.mainStatus.updateTime(5) - assert nwGUI.mainStatus.userIdle is True + assert nwGUI.mainStatus._userIdle is True assert nwGUI.mainStatus.timeText.text() != "00:00:00" nwGUI.mainStatus.setUserIdle(False) nwGUI.mainStatus.updateTime(5) - assert nwGUI.mainStatus.userIdle is False + assert nwGUI.mainStatus._userIdle is False assert nwGUI.mainStatus.timeText.text() != "00:00:00" # Language