Skip to content

Commit

Permalink
Hide session timer (#2149)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo authored Dec 29, 2024
2 parents 4d33f08 + 46d69c3 commit c39a5f9
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 43 deletions.
13 changes: 13 additions & 0 deletions docs/source/project_overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 3 additions & 0 deletions novelwriter/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def __init__(self) -> None:
# 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

Expand Down Expand Up @@ -674,6 +675,7 @@ def loadConfig(self) -> bool:
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)
Expand Down Expand Up @@ -784,6 +786,7 @@ def saveConfig(self) -> bool:
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),
Expand Down
20 changes: 16 additions & 4 deletions novelwriter/extensions/modified.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@
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
Expand Down Expand Up @@ -196,3 +197,14 @@ def setThemeIcon(self, iconKey: str) -> None:
iconSize = self.iconSize()
self.setIcon(SHARED.theme.getToggleIcon(iconKey, (iconSize.width(), iconSize.height())))
return


class NClickableLabel(QLabel):

mouseClicked = pyqtSignal()

def mousePressEvent(self, event: QMouseEvent) -> None:
"""Capture a left mouse click and emit its signal."""
if event.button() == QtMouseLeft:
self.mouseClicked.emit()
return super().mousePressEvent(event)
21 changes: 19 additions & 2 deletions novelwriter/gui/statusbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
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__)
Expand Down Expand Up @@ -92,12 +93,16 @@ def __init__(self, parent: QWidget) -> None:

# 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.mouseClicked.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.mouseClicked.connect(self._onClickTimerLabel)
self.addPermanentWidget(self.timeIcon)
self.addPermanentWidget(self.timeText)

Expand Down Expand Up @@ -224,6 +229,18 @@ def updateDocumentStatus(self, status: bool) -> None:
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
##
Expand Down
3 changes: 2 additions & 1 deletion tests/reference/baseConfig_novelwriter.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[Meta]
timestamp = 2024-11-03 21:45:08
timestamp = 2024-12-29 17:30:10

[Main]
font =
Expand Down Expand Up @@ -71,6 +71,7 @@ useridletime = 300
[State]
showviewerpanel = True
showedittoolbar = False
showsessiontime = True
viewcomments = True
viewsynopsis = True
searchcase = False
Expand Down
24 changes: 21 additions & 3 deletions tests/test_ext/test_ext_modified.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
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

Expand Down Expand Up @@ -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)
78 changes: 45 additions & 33 deletions tests/test_gui/test_gui_statusbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,63 +39,75 @@ 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"

nwGUI.mainStatus.setUserIdle(False)
nwGUI.mainStatus.updateTime(5)
assert nwGUI.mainStatus._userIdle is False
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"

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:
mp.setattr("novelwriter.guimain.time", lambda *a: 50.0)
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()

0 comments on commit c39a5f9

Please sign in to comment.