Skip to content

Commit c39a5f9

Browse files
authored
Hide session timer (#2149)
2 parents 4d33f08 + 46d69c3 commit c39a5f9

File tree

7 files changed

+119
-43
lines changed

7 files changed

+119
-43
lines changed

docs/source/project_overview.rst

+13
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,16 @@ application like for instance Libre Office Calc or Excel.
388388
definition of idle here is that the novelWriter main window loses focus, or the user hasn't made
389389
any changes to the currently open document in five minutes. The number of minutes can be altered
390390
in **Preferences**.
391+
392+
393+
Session Timer
394+
-------------
395+
396+
A session timer is by default visible in the status bar. The icon will show you a clock icon when
397+
you are active, and a pause icon when you are considered "idle" per the criteria mentioned above.
398+
399+
If you do not wish to see the timer, you can click on it once to hide it. The icon will still be
400+
visible. Click the icon once more to display the timer again.
401+
402+
.. versionadded:: 2.6
403+
As of version 2.6, clicking the timer text or icon in the status bar will toggle its visibility.

novelwriter/config.py

+3
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def __init__(self) -> None:
194194
# State
195195
self.showViewerPanel = True # The panel for the viewer is visible
196196
self.showEditToolBar = False # The document editor toolbar visibility
197+
self.showSessionTime = True # Show the session time in the status bar
197198
self.viewComments = True # Comments are shown in the viewer
198199
self.viewSynopsis = True # Synopsis is shown in the viewer
199200

@@ -674,6 +675,7 @@ def loadConfig(self) -> bool:
674675
sec = "State"
675676
self.showViewerPanel = conf.rdBool(sec, "showviewerpanel", self.showViewerPanel)
676677
self.showEditToolBar = conf.rdBool(sec, "showedittoolbar", self.showEditToolBar)
678+
self.showSessionTime = conf.rdBool(sec, "showsessiontime", self.showSessionTime)
677679
self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments)
678680
self.viewSynopsis = conf.rdBool(sec, "viewsynopsis", self.viewSynopsis)
679681
self.searchCase = conf.rdBool(sec, "searchcase", self.searchCase)
@@ -784,6 +786,7 @@ def saveConfig(self) -> bool:
784786
conf["State"] = {
785787
"showviewerpanel": str(self.showViewerPanel),
786788
"showedittoolbar": str(self.showEditToolBar),
789+
"showsessiontime": str(self.showSessionTime),
787790
"viewcomments": str(self.viewComments),
788791
"viewsynopsis": str(self.viewSynopsis),
789792
"searchcase": str(self.searchCase),

novelwriter/extensions/modified.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@
3030
from enum import Enum
3131
from typing import TYPE_CHECKING
3232

33-
from PyQt5.QtCore import QSize, Qt, pyqtSlot
34-
from PyQt5.QtGui import QWheelEvent
33+
from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
34+
from PyQt5.QtGui import QMouseEvent, QWheelEvent
3535
from PyQt5.QtWidgets import (
36-
QApplication, QComboBox, QDialog, QDoubleSpinBox, QSpinBox, QToolButton,
37-
QWidget
36+
QApplication, QComboBox, QDialog, QDoubleSpinBox, QLabel, QSpinBox,
37+
QToolButton, QWidget
3838
)
3939

4040
from novelwriter import CONFIG, SHARED
41+
from novelwriter.types import QtMouseLeft
4142

4243
if TYPE_CHECKING: # pragma: no cover
4344
from novelwriter.guimain import GuiMain
@@ -196,3 +197,14 @@ def setThemeIcon(self, iconKey: str) -> None:
196197
iconSize = self.iconSize()
197198
self.setIcon(SHARED.theme.getToggleIcon(iconKey, (iconSize.width(), iconSize.height())))
198199
return
200+
201+
202+
class NClickableLabel(QLabel):
203+
204+
mouseClicked = pyqtSignal()
205+
206+
def mousePressEvent(self, event: QMouseEvent) -> None:
207+
"""Capture a left mouse click and emit its signal."""
208+
if event.button() == QtMouseLeft:
209+
self.mouseClicked.emit()
210+
return super().mousePressEvent(event)

novelwriter/gui/statusbar.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from novelwriter.common import formatTime
3636
from novelwriter.constants import nwConst
3737
from novelwriter.enum import nwTrinary
38+
from novelwriter.extensions.modified import NClickableLabel
3839
from novelwriter.extensions.statusled import StatusLED
3940

4041
logger = logging.getLogger(__name__)
@@ -92,12 +93,16 @@ def __init__(self, parent: QWidget) -> None:
9293

9394
# The Session Clock
9495
# Set the minimum width so the label doesn't rescale every second
95-
self.timeIcon = QLabel(self)
96-
self.timeText = QLabel("", self)
96+
self.timeIcon = NClickableLabel(self)
97+
self.timeIcon.mouseClicked.connect(self._onClickTimerLabel)
98+
99+
self.timeText = NClickableLabel("", self)
97100
self.timeText.setToolTip(self.tr("Session Time"))
98101
self.timeText.setMinimumWidth(SHARED.theme.getTextWidth("00:00:00:"))
99102
self.timeIcon.setContentsMargins(0, 0, 0, 0)
100103
self.timeText.setContentsMargins(0, 0, 0, 0)
104+
self.timeText.setVisible(CONFIG.showSessionTime)
105+
self.timeText.mouseClicked.connect(self._onClickTimerLabel)
101106
self.addPermanentWidget(self.timeIcon)
102107
self.addPermanentWidget(self.timeText)
103108

@@ -224,6 +229,18 @@ def updateDocumentStatus(self, status: bool) -> None:
224229
self.setDocumentStatus(nwTrinary.NEGATIVE if status else nwTrinary.POSITIVE)
225230
return
226231

232+
##
233+
# Private Slots
234+
##
235+
236+
@pyqtSlot()
237+
def _onClickTimerLabel(self) -> None:
238+
"""Process mouse click on timer label."""
239+
state = not CONFIG.showSessionTime
240+
self.timeText.setVisible(state)
241+
CONFIG.showSessionTime = state
242+
return
243+
227244
##
228245
# Debug
229246
##

tests/reference/baseConfig_novelwriter.conf

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[Meta]
2-
timestamp = 2024-11-03 21:45:08
2+
timestamp = 2024-12-29 17:30:10
33

44
[Main]
55
font =
@@ -71,6 +71,7 @@ useridletime = 300
7171
[State]
7272
showviewerpanel = True
7373
showedittoolbar = False
74+
showsessiontime = True
7475
viewcomments = True
7576
viewsynopsis = True
7677
searchcase = False

tests/test_ext/test_ext_modified.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
import pytest
2424

2525
from PyQt5.QtCore import QEvent, QPoint, QPointF, Qt
26-
from PyQt5.QtGui import QKeyEvent, QWheelEvent
26+
from PyQt5.QtGui import QKeyEvent, QMouseEvent, QWheelEvent
2727
from PyQt5.QtWidgets import QWidget
2828

29-
from novelwriter.extensions.modified import NComboBox, NDialog, NDoubleSpinBox, NSpinBox
30-
from novelwriter.types import QtModNone, QtRejected
29+
from novelwriter.extensions.modified import (
30+
NClickableLabel, NComboBox, NDialog, NDoubleSpinBox, NSpinBox
31+
)
32+
from novelwriter.types import QtModNone, QtMouseLeft, QtRejected
3133

3234
from tests.tools import SimpleDialog
3335

@@ -139,3 +141,19 @@ def testExtModified_NDoubleSpinBox(qtbot, monkeypatch):
139141
assert event.ignored is True
140142

141143
# qtbot.stop()
144+
145+
146+
@pytest.mark.gui
147+
def testExtModified_NClickableLabel(qtbot, monkeypatch):
148+
"""Test the NClickableLabel class."""
149+
widget = NClickableLabel()
150+
dialog = SimpleDialog(widget)
151+
dialog.show()
152+
153+
position = widget.rect().center()
154+
event = QMouseEvent(
155+
QEvent.Type.MouseButtonPress, position, QtMouseLeft, QtMouseLeft, QtModNone
156+
)
157+
158+
with qtbot.waitSignal(widget.mouseClicked):
159+
widget.mousePressEvent(event)

tests/test_gui/test_gui_statusbar.py

+45-33
Original file line numberDiff line numberDiff line change
@@ -39,63 +39,75 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
3939
newDoc.writeDocument("# A Note\n\n")
4040
nwGUI.rebuildIndex(beQuiet=True)
4141

42+
status = nwGUI.mainStatus
43+
4244
# Reference Time
4345
refTime = time.time()
44-
nwGUI.mainStatus.setRefTime(refTime)
45-
assert nwGUI.mainStatus._refTime == refTime
46+
status.setRefTime(refTime)
47+
assert status._refTime == refTime
4648

4749
# Project Status
48-
nwGUI.mainStatus.setProjectStatus(nwTrinary.NEUTRAL)
49-
assert nwGUI.mainStatus.projIcon.state == nwTrinary.NEUTRAL
50-
nwGUI.mainStatus.setProjectStatus(nwTrinary.NEGATIVE)
51-
assert nwGUI.mainStatus.projIcon.state == nwTrinary.NEGATIVE
52-
nwGUI.mainStatus.setProjectStatus(nwTrinary.POSITIVE)
53-
assert nwGUI.mainStatus.projIcon.state == nwTrinary.POSITIVE
50+
status.setProjectStatus(nwTrinary.NEUTRAL)
51+
assert status.projIcon.state == nwTrinary.NEUTRAL
52+
status.setProjectStatus(nwTrinary.NEGATIVE)
53+
assert status.projIcon.state == nwTrinary.NEGATIVE
54+
status.setProjectStatus(nwTrinary.POSITIVE)
55+
assert status.projIcon.state == nwTrinary.POSITIVE
5456

5557
# Document Status
56-
nwGUI.mainStatus.setDocumentStatus(nwTrinary.NEUTRAL)
57-
assert nwGUI.mainStatus.docIcon.state == nwTrinary.NEUTRAL
58-
nwGUI.mainStatus.setDocumentStatus(nwTrinary.NEGATIVE)
59-
assert nwGUI.mainStatus.docIcon.state == nwTrinary.NEGATIVE
60-
nwGUI.mainStatus.setDocumentStatus(nwTrinary.POSITIVE)
61-
assert nwGUI.mainStatus.docIcon.state == nwTrinary.POSITIVE
58+
status.setDocumentStatus(nwTrinary.NEUTRAL)
59+
assert status.docIcon.state == nwTrinary.NEUTRAL
60+
status.setDocumentStatus(nwTrinary.NEGATIVE)
61+
assert status.docIcon.state == nwTrinary.NEGATIVE
62+
status.setDocumentStatus(nwTrinary.POSITIVE)
63+
assert status.docIcon.state == nwTrinary.POSITIVE
6264

6365
# Idle Status
6466
CONFIG.stopWhenIdle = False
65-
nwGUI.mainStatus.setUserIdle(True)
66-
nwGUI.mainStatus.updateTime()
67-
assert nwGUI.mainStatus._userIdle is False
68-
assert nwGUI.mainStatus.timeText.text() == "00:00:00"
67+
status.setUserIdle(True)
68+
status.updateTime()
69+
assert status._userIdle is False
70+
assert status.timeText.text() == "00:00:00"
6971

7072
CONFIG.stopWhenIdle = True
71-
nwGUI.mainStatus.setUserIdle(True)
72-
nwGUI.mainStatus.updateTime(5)
73-
assert nwGUI.mainStatus._userIdle is True
74-
assert nwGUI.mainStatus.timeText.text() != "00:00:00"
75-
76-
nwGUI.mainStatus.setUserIdle(False)
77-
nwGUI.mainStatus.updateTime(5)
78-
assert nwGUI.mainStatus._userIdle is False
79-
assert nwGUI.mainStatus.timeText.text() != "00:00:00"
73+
status.setUserIdle(True)
74+
status.updateTime(5)
75+
assert status._userIdle is True
76+
assert status.timeText.text() != "00:00:00"
77+
78+
status.setUserIdle(False)
79+
status.updateTime(5)
80+
assert status._userIdle is False
81+
assert status.timeText.text() != "00:00:00"
82+
83+
# Show/Hide Timer
84+
assert status.timeText.isVisible() is True
85+
assert CONFIG.showSessionTime is True
86+
status._onClickTimerLabel()
87+
assert status.timeText.isVisible() is False
88+
assert CONFIG.showSessionTime is False
89+
status._onClickTimerLabel()
90+
assert status.timeText.isVisible() is True
91+
assert CONFIG.showSessionTime is True
8092

8193
# Language
82-
nwGUI.mainStatus.setLanguage("None", "None")
83-
assert nwGUI.mainStatus.langText.text() == "None"
84-
nwGUI.mainStatus.setLanguage("en", "None")
85-
assert nwGUI.mainStatus.langText.text() == "American English"
94+
status.setLanguage("None", "None")
95+
assert status.langText.text() == "None"
96+
status.setLanguage("en", "None")
97+
assert status.langText.text() == "American English"
8698

8799
# Project Stats
88100
CONFIG.incNotesWCount = False
89101
nwGUI._lastTotalCount = 0
90102
nwGUI._updateStatusWordCount()
91-
assert nwGUI.mainStatus.statsText.text() == "Words: 9 (+9)"
103+
assert status.statsText.text() == "Words: 9 (+9)"
92104

93105
# Update again, but through time tick
94106
with monkeypatch.context() as mp:
95107
mp.setattr("novelwriter.guimain.time", lambda *a: 50.0)
96108
CONFIG.incNotesWCount = True
97109
nwGUI._lastTotalCount = 0
98110
nwGUI._timeTick()
99-
assert nwGUI.mainStatus.statsText.text() == "Words: 11 (+11)"
111+
assert status.statsText.text() == "Words: 11 (+11)"
100112

101113
# qtbot.stop()

0 commit comments

Comments
 (0)