From 1b34bc30c7a107585949c1d2abfaa11b1af72d1e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:29:07 +0100 Subject: [PATCH 1/5] Add a modified label widget that emits a click signal --- novelwriter/extensions/modified.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index f9062b39..0a6fa6e0 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -30,14 +30,15 @@ from __future__ import annotations from enum import Enum from typing import TYPE_CHECKING -from PyQt5.QtCore import QSize, Qt, pyqtSlot -from PyQt5.QtGui import QWheelEvent +from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtGui import QMouseEvent, QWheelEvent from PyQt5.QtWidgets import ( - QApplication, QComboBox, QDialog, QDoubleSpinBox, QSpinBox, QToolButton, - QWidget + QApplication, QComboBox, QDialog, QDoubleSpinBox, QLabel, QSpinBox, + QToolButton, QWidget ) from novelwriter import CONFIG, SHARED +from novelwriter.types import QtMouseLeft if TYPE_CHECKING: # pragma: no cover from novelwriter.guimain import GuiMain @@ -196,3 +197,14 @@ class NIconToggleButton(QToolButton): iconSize = self.iconSize() self.setIcon(SHARED.theme.getToggleIcon(iconKey, (iconSize.width(), iconSize.height()))) return + + +class NClickableLabel(QLabel): + + mouseClick = pyqtSignal() + + def mousePressEvent(self, event: QMouseEvent) -> None: + """Capture a left mouse click and emit its signal.""" + if event.button() == QtMouseLeft: + self.mouseClick.emit() + return super().mousePressEvent(event) From dc9413fa7e735b5a4cd84b68dbfdbd3b62bf81ca Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:30:03 +0100 Subject: [PATCH 2/5] Show or hide the status bar timer when it is clicked --- novelwriter/config.py | 3 +++ novelwriter/gui/statusbar.py | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index d1ae57b5..bbe8ebd2 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -194,6 +194,7 @@ class Config: # State self.showViewerPanel = True # The panel for the viewer is visible self.showEditToolBar = False # The document editor toolbar visibility + self.showSessionTime = True # Show the session time in the status bar self.viewComments = True # Comments are shown in the viewer self.viewSynopsis = True # Synopsis is shown in the viewer @@ -674,6 +675,7 @@ class Config: sec = "State" self.showViewerPanel = conf.rdBool(sec, "showviewerpanel", self.showViewerPanel) self.showEditToolBar = conf.rdBool(sec, "showedittoolbar", self.showEditToolBar) + self.showSessionTime = conf.rdBool(sec, "showsessiontime", self.showSessionTime) self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments) self.viewSynopsis = conf.rdBool(sec, "viewsynopsis", self.viewSynopsis) self.searchCase = conf.rdBool(sec, "searchcase", self.searchCase) @@ -784,6 +786,7 @@ class Config: conf["State"] = { "showviewerpanel": str(self.showViewerPanel), "showedittoolbar": str(self.showEditToolBar), + "showsessiontime": str(self.showSessionTime), "viewcomments": str(self.viewComments), "viewsynopsis": str(self.viewSynopsis), "searchcase": str(self.searchCase), diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index e7459c53..6b2b727e 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -35,6 +35,7 @@ from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime from novelwriter.constants import nwConst from novelwriter.enum import nwTrinary +from novelwriter.extensions.modified import NClickableLabel from novelwriter.extensions.statusled import StatusLED logger = logging.getLogger(__name__) @@ -92,12 +93,16 @@ class GuiMainStatus(QStatusBar): # The Session Clock # Set the minimum width so the label doesn't rescale every second - self.timeIcon = QLabel(self) - self.timeText = QLabel("", self) + self.timeIcon = NClickableLabel(self) + self.timeIcon.mouseClick.connect(self._onClickTimerLabel) + + self.timeText = NClickableLabel("", self) self.timeText.setToolTip(self.tr("Session Time")) self.timeText.setMinimumWidth(SHARED.theme.getTextWidth("00:00:00:")) self.timeIcon.setContentsMargins(0, 0, 0, 0) self.timeText.setContentsMargins(0, 0, 0, 0) + self.timeText.setVisible(CONFIG.showSessionTime) + self.timeText.mouseClick.connect(self._onClickTimerLabel) self.addPermanentWidget(self.timeIcon) self.addPermanentWidget(self.timeText) @@ -224,6 +229,18 @@ class GuiMainStatus(QStatusBar): self.setDocumentStatus(nwTrinary.NEGATIVE if status else nwTrinary.POSITIVE) return + ## + # Private Slots + ## + + @pyqtSlot() + def _onClickTimerLabel(self) -> None: + """Process mouse click on timer label.""" + state = not CONFIG.showSessionTime + self.timeText.setVisible(state) + CONFIG.showSessionTime = state + return + ## # Debug ## From 2fa1dcccaceee0ce585ea13bb54be5bf911cf5f0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:38:03 +0100 Subject: [PATCH 3/5] Use correct signal naming --- novelwriter/extensions/modified.py | 4 ++-- novelwriter/gui/statusbar.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 0a6fa6e0..1805db5b 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -201,10 +201,10 @@ class NIconToggleButton(QToolButton): class NClickableLabel(QLabel): - mouseClick = pyqtSignal() + mouseClicked = pyqtSignal() def mousePressEvent(self, event: QMouseEvent) -> None: """Capture a left mouse click and emit its signal.""" if event.button() == QtMouseLeft: - self.mouseClick.emit() + self.mouseClicked.emit() return super().mousePressEvent(event) diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 6b2b727e..647e90ba 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -94,7 +94,7 @@ class GuiMainStatus(QStatusBar): # The Session Clock # Set the minimum width so the label doesn't rescale every second self.timeIcon = NClickableLabel(self) - self.timeIcon.mouseClick.connect(self._onClickTimerLabel) + self.timeIcon.mouseClicked.connect(self._onClickTimerLabel) self.timeText = NClickableLabel("", self) self.timeText.setToolTip(self.tr("Session Time")) @@ -102,7 +102,7 @@ class GuiMainStatus(QStatusBar): self.timeIcon.setContentsMargins(0, 0, 0, 0) self.timeText.setContentsMargins(0, 0, 0, 0) self.timeText.setVisible(CONFIG.showSessionTime) - self.timeText.mouseClick.connect(self._onClickTimerLabel) + self.timeText.mouseClicked.connect(self._onClickTimerLabel) self.addPermanentWidget(self.timeIcon) self.addPermanentWidget(self.timeText) From e924d121bff3e21791524f30177df8f624892de9 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:38:21 +0100 Subject: [PATCH 4/5] Update tests --- tests/reference/baseConfig_novelwriter.conf | 3 +- tests/test_ext/test_ext_modified.py | 24 ++++++- tests/test_gui/test_gui_statusbar.py | 76 ++++++++++++--------- 3 files changed, 67 insertions(+), 36 deletions(-) diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index 046ac8c8..3bd03bbe 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -1,5 +1,5 @@ [Meta] -timestamp = 2024-11-03 21:45:08 +timestamp = 2024-12-29 17:30:10 [Main] font = @@ -71,6 +71,7 @@ useridletime = 300 [State] showviewerpanel = True showedittoolbar = False +showsessiontime = True viewcomments = True viewsynopsis = True searchcase = False diff --git a/tests/test_ext/test_ext_modified.py b/tests/test_ext/test_ext_modified.py index 65865b27..354d6e17 100644 --- a/tests/test_ext/test_ext_modified.py +++ b/tests/test_ext/test_ext_modified.py @@ -23,11 +23,13 @@ from __future__ import annotations import pytest from PyQt5.QtCore import QEvent, QPoint, QPointF, Qt -from PyQt5.QtGui import QKeyEvent, QWheelEvent +from PyQt5.QtGui import QKeyEvent, QMouseEvent, QWheelEvent from PyQt5.QtWidgets import QWidget -from novelwriter.extensions.modified import NComboBox, NDialog, NDoubleSpinBox, NSpinBox -from novelwriter.types import QtModNone, QtRejected +from novelwriter.extensions.modified import ( + NClickableLabel, NComboBox, NDialog, NDoubleSpinBox, NSpinBox +) +from novelwriter.types import QtModNone, QtMouseLeft, QtRejected from tests.tools import SimpleDialog @@ -139,3 +141,19 @@ def testExtModified_NDoubleSpinBox(qtbot, monkeypatch): assert event.ignored is True # qtbot.stop() + + +@pytest.mark.gui +def testExtModified_NClickableLabel(qtbot, monkeypatch): + """Test the NClickableLabel class.""" + widget = NClickableLabel() + dialog = SimpleDialog(widget) + dialog.show() + + position = widget.rect().center() + event = QMouseEvent( + QEvent.Type.MouseButtonPress, position, QtMouseLeft, QtMouseLeft, QtModNone + ) + + with qtbot.waitSignal(widget.mouseClicked): + widget.mousePressEvent(event) diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 71d0d0d1..f2813a02 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -39,56 +39,68 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): newDoc.writeDocument("# A Note\n\n") nwGUI.rebuildIndex(beQuiet=True) + status = nwGUI.mainStatus + # Reference Time refTime = time.time() - nwGUI.mainStatus.setRefTime(refTime) - assert nwGUI.mainStatus._refTime == refTime + status.setRefTime(refTime) + assert status._refTime == refTime # Project Status - nwGUI.mainStatus.setProjectStatus(nwTrinary.NEUTRAL) - assert nwGUI.mainStatus.projIcon.state == nwTrinary.NEUTRAL - nwGUI.mainStatus.setProjectStatus(nwTrinary.NEGATIVE) - assert nwGUI.mainStatus.projIcon.state == nwTrinary.NEGATIVE - nwGUI.mainStatus.setProjectStatus(nwTrinary.POSITIVE) - assert nwGUI.mainStatus.projIcon.state == nwTrinary.POSITIVE + status.setProjectStatus(nwTrinary.NEUTRAL) + assert status.projIcon.state == nwTrinary.NEUTRAL + status.setProjectStatus(nwTrinary.NEGATIVE) + assert status.projIcon.state == nwTrinary.NEGATIVE + status.setProjectStatus(nwTrinary.POSITIVE) + assert status.projIcon.state == nwTrinary.POSITIVE # Document Status - nwGUI.mainStatus.setDocumentStatus(nwTrinary.NEUTRAL) - assert nwGUI.mainStatus.docIcon.state == nwTrinary.NEUTRAL - nwGUI.mainStatus.setDocumentStatus(nwTrinary.NEGATIVE) - assert nwGUI.mainStatus.docIcon.state == nwTrinary.NEGATIVE - nwGUI.mainStatus.setDocumentStatus(nwTrinary.POSITIVE) - assert nwGUI.mainStatus.docIcon.state == nwTrinary.POSITIVE + status.setDocumentStatus(nwTrinary.NEUTRAL) + assert status.docIcon.state == nwTrinary.NEUTRAL + status.setDocumentStatus(nwTrinary.NEGATIVE) + assert status.docIcon.state == nwTrinary.NEGATIVE + status.setDocumentStatus(nwTrinary.POSITIVE) + assert status.docIcon.state == nwTrinary.POSITIVE # Idle Status CONFIG.stopWhenIdle = False - nwGUI.mainStatus.setUserIdle(True) - nwGUI.mainStatus.updateTime() - assert nwGUI.mainStatus._userIdle is False - assert nwGUI.mainStatus.timeText.text() == "00:00:00" + status.setUserIdle(True) + status.updateTime() + assert status._userIdle is False + assert status.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.timeText.text() != "00:00:00" + status.setUserIdle(True) + status.updateTime(5) + assert status._userIdle is True + assert status.timeText.text() != "00:00:00" - nwGUI.mainStatus.setUserIdle(False) - nwGUI.mainStatus.updateTime(5) - assert nwGUI.mainStatus._userIdle is False - assert nwGUI.mainStatus.timeText.text() != "00:00:00" + status.setUserIdle(False) + status.updateTime(5) + assert status._userIdle is False + assert status.timeText.text() != "00:00:00" + + # Show/Hide Timer + assert status.timeText.isVisible() is True + assert CONFIG.showSessionTime is True + status._onClickTimerLabel() + assert status.timeText.isVisible() is False + assert CONFIG.showSessionTime is False + status._onClickTimerLabel() + assert status.timeText.isVisible() is True + assert CONFIG.showSessionTime is True # Language - nwGUI.mainStatus.setLanguage("None", "None") - assert nwGUI.mainStatus.langText.text() == "None" - nwGUI.mainStatus.setLanguage("en", "None") - assert nwGUI.mainStatus.langText.text() == "American English" + status.setLanguage("None", "None") + assert status.langText.text() == "None" + status.setLanguage("en", "None") + assert status.langText.text() == "American English" # Project Stats CONFIG.incNotesWCount = False nwGUI._lastTotalCount = 0 nwGUI._updateStatusWordCount() - assert nwGUI.mainStatus.statsText.text() == "Words: 9 (+9)" + assert status.statsText.text() == "Words: 9 (+9)" # Update again, but through time tick with monkeypatch.context() as mp: @@ -96,6 +108,6 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): CONFIG.incNotesWCount = True nwGUI._lastTotalCount = 0 nwGUI._timeTick() - assert nwGUI.mainStatus.statsText.text() == "Words: 11 (+11)" + assert status.statsText.text() == "Words: 11 (+11)" # qtbot.stop() From 46d69c31d1a4a621581d26b739a28fa148e257ee Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:43:54 +0100 Subject: [PATCH 5/5] Update docs --- docs/source/project_overview.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/source/project_overview.rst b/docs/source/project_overview.rst index 6f8d197b..eb169b47 100644 --- a/docs/source/project_overview.rst +++ b/docs/source/project_overview.rst @@ -388,3 +388,16 @@ application like for instance Libre Office Calc or Excel. definition of idle here is that the novelWriter main window loses focus, or the user hasn't made any changes to the currently open document in five minutes. The number of minutes can be altered in **Preferences**. + + +Session Timer +------------- + +A session timer is by default visible in the status bar. The icon will show you a clock icon when +you are active, and a pause icon when you are considered "idle" per the criteria mentioned above. + +If you do not wish to see the timer, you can click on it once to hide it. The icon will still be +visible. Click the icon once more to display the timer again. + +.. versionadded:: 2.6 + As of version 2.6, clicking the timer text or icon in the status bar will toggle its visibility.