diff --git a/qt/aqt/flexible_grading_reviewer/widgets.py b/qt/aqt/flexible_grading_reviewer/widgets.py index 6be537d32..e1ae933fb 100644 --- a/qt/aqt/flexible_grading_reviewer/widgets.py +++ b/qt/aqt/flexible_grading_reviewer/widgets.py @@ -139,3 +139,55 @@ class FlexibleBottomBar(FlexibleHorizontalBar): self.add_widget(self.middle_bucket) self.add_stretch() self.add_widget(self.right_bucket) + + +class FlexibleTimerLabel(QLabel): + def __init__(self, parent=None): + super().__init__(parent) + self._time = 0 # current time (seconds) + self._max_time = 0 # maximum time (seconds); 0 means hidden + self._qtimer = QTimer(self) + self._qtimer.setInterval(1000) + qconnect(self._qtimer.timeout, self._on_tick) + self.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.setHidden(True) + + def start(self, max_time: int): + self._time = 0 + self._max_time = max_time + self._update_display() + if self._qtimer.isActive(): + self._qtimer.stop() + self._qtimer.start() + + def stop(self): + if self._qtimer.isActive(): + self._qtimer.stop() + + # Internal tick handler + def _on_tick(self): + self._time += 1 + # clamp to max_time if set (mirrors TS: time = Math.min(maxTime, time)) + if self._time > self._max_time > 0: + self._time = self._max_time + self._update_display() + # if reached max, keep ticking but display in red (TS continues interval) + + def _update_display(self): + if self._max_time <= 0: + super().setText("") # hide when max_time == 0 + self.setHidden(True) + return + + self.setHidden(False) + t = min(self._max_time, self._time) if self._max_time > 0 else self._time + m = t // 60 + s = t % 60 + s_str = f"{s:02d}" + time_string = f"{m}:{s_str}" + + if t >= self._max_time > 0: + # display red when time == maxTime (using simple HTML) + super().setText(f"{time_string}") + else: + super().setText(time_string) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index d6d32a0a6..05c2d4bdf 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -10,7 +10,7 @@ from collections.abc import Generator, Sequence from dataclasses import dataclass from enum import Enum, auto from functools import partial -from typing import Any, Literal, Match, cast +from typing import Any, Literal, Match, Optional, cast import aqt import aqt.browser @@ -38,7 +38,7 @@ from aqt.flexible_grading_reviewer.utils import ( ease_to_answer_key_short, studied_today_count, ) -from aqt.flexible_grading_reviewer.widgets import FlexiblePushButton +from aqt.flexible_grading_reviewer.widgets import FlexiblePushButton, FlexibleTimerLabel from aqt.operations.card import set_card_flag from aqt.operations.note import remove_notes from aqt.operations.scheduling import ( @@ -1257,8 +1257,11 @@ class FlexibleReviewer(Reviewer): QueuedCards.REVIEW: "ForestGreen", } + timer: Optional[FlexibleTimerLabel] = None + def __init__(self, mw: AnkiQt) -> None: super().__init__(mw) + self.timer = None def cleanup(self) -> None: super().cleanup() @@ -1282,6 +1285,8 @@ class FlexibleReviewer(Reviewer): FlexiblePushButton(text=tr.studying_more()), on_clicked=partial(self.showContextMenu), ) + # Right side: add timer + self.timer = self.mw.bottomWidget.right_bucket.add_widget(FlexibleTimerLabel()) def browse_queue(self, queue_type: Union[str, Any]) -> None: if queue_type == QueuedCards.LEARNING: @@ -1354,11 +1359,24 @@ class FlexibleReviewer(Reviewer): def _clear_bottom_web(self) -> None: self.bottom.web.setHtml("") + def _max_time(self) -> int: + if self.card.should_show_timer(): + return self.card.time_limit() // 1000 + else: + return 0 + def _showAnswerButton(self) -> None: self._add_side_buttons() self._add_middle_buttons_for_question_side() self._clear_bottom_web() + assert self.timer, "timer should exist." + self.timer.start(max_time=self._max_time()) + + def _should_stop_timer_on_answer(self) -> bool: + conf = self.mw.col.decks.config_dict_for_deck_id(self.card.current_deck_id()) + return bool(conf["stopTimerOnAnswer"]) + def _showEaseButtons(self) -> None: if not self._states_mutated: self.mw.progress.single_shot(50, self._showEaseButtons) @@ -1367,6 +1385,10 @@ class FlexibleReviewer(Reviewer): self._add_middle_buttons_for_answer_side() self._clear_bottom_web() + assert self.timer, "timer should exist." + if self._should_stop_timer_on_answer(): + self.timer.stop() + def onEnterKey(self) -> None: if self.state == "question": self._getTypedAnswer()