bouquin/bouquin/pomodoro_timer.py

204 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import math
from typing import Optional
from PySide6.QtCore import Qt, QTimer, Signal, Slot, QSignalBlocker
from PySide6.QtWidgets import (
QFrame,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
QWidget,
)
from . import strings
from .db import DBManager
from .time_log import TimeLogDialog
class PomodoroTimer(QFrame):
"""A simple timer for tracking work time on a specific task."""
timerStopped = Signal(int, str) # Emits (elapsed_seconds, task_text)
def __init__(self, task_text: str, parent: Optional[QWidget] = None):
super().__init__(parent)
self._task_text = task_text
self._elapsed_seconds = 0
self._running = False
layout = QVBoxLayout(self)
# Task label
task_label = QLabel(task_text)
task_label.setWordWrap(True)
layout.addWidget(task_label)
# Timer display
self.time_label = QLabel("00:00:00")
font = self.time_label.font()
font.setPointSize(20)
font.setBold(True)
self.time_label.setFont(font)
self.time_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.time_label)
# Control buttons
btn_layout = QHBoxLayout()
self.start_pause_btn = QPushButton(strings._("start"))
self.start_pause_btn.clicked.connect(self._toggle_timer)
btn_layout.addWidget(self.start_pause_btn)
self.stop_btn = QPushButton(strings._("stop_and_log"))
self.stop_btn.clicked.connect(self._stop_and_log)
self.stop_btn.setEnabled(False)
btn_layout.addWidget(self.stop_btn)
layout.addLayout(btn_layout)
# Internal timer (ticks every second)
self._timer = QTimer(self)
self._timer.timeout.connect(self._tick)
@Slot()
def _toggle_timer(self):
"""Start or pause the timer."""
if self._running:
# Pause
self._running = False
self._timer.stop()
self.start_pause_btn.setText(strings._("resume"))
else:
# Start/Resume
self._running = True
self._timer.start(1000) # 1 second
self.start_pause_btn.setText(strings._("pause"))
self.stop_btn.setEnabled(True)
@Slot()
def _tick(self):
"""Update the elapsed time display."""
self._elapsed_seconds += 1
self._update_display()
def _update_display(self):
"""Update the time display label."""
hours = self._elapsed_seconds // 3600
minutes = (self._elapsed_seconds % 3600) // 60
seconds = self._elapsed_seconds % 60
self.time_label.setText(f"{hours:02d}:{minutes:02d}:{seconds:02d}")
@Slot()
def _stop_and_log(self):
"""Stop the timer and emit signal to open time log."""
if self._running:
self._running = False
self._timer.stop()
self.timerStopped.emit(self._elapsed_seconds, self._task_text)
self.close()
class PomodoroManager:
"""Manages Pomodoro timers and integrates with time log."""
def __init__(self, db: DBManager, parent_window):
self._db = db
self._parent = parent_window
self._active_timer: Optional[PomodoroTimer] = None
def start_timer_for_line(self, line_text: str, date_iso: str):
"""
Start a new timer for the given line of text and embed it into the
TimeLogWidget in the main window sidebar.
"""
# Cancel any existing timer first
self.cancel_timer()
# The timer lives inside the TimeLogWidget in the sidebar
time_log_widget = getattr(self._parent, "time_log", None)
if time_log_widget is None:
return
self._active_timer = PomodoroTimer(line_text, time_log_widget)
self._active_timer.timerStopped.connect(
lambda seconds, text: self._on_timer_stopped(seconds, text, date_iso)
)
# Ask the TimeLogWidget to own and display the widget
if hasattr(time_log_widget, "show_pomodoro_widget"):
time_log_widget.show_pomodoro_widget(self._active_timer)
else:
# Fallback just attach it as a child widget
self._active_timer.setParent(time_log_widget)
self._active_timer.show()
def cancel_timer(self):
"""Cancel any running timer without logging and remove it from the sidebar."""
if not self._active_timer:
return
time_log_widget = getattr(self._parent, "time_log", None)
if time_log_widget is not None and hasattr(
time_log_widget, "clear_pomodoro_widget"
):
time_log_widget.clear_pomodoro_widget()
else:
# Fallback if the widget API doesn't exist
self._active_timer.setParent(None)
self._active_timer.deleteLater()
self._active_timer = None
def _on_timer_stopped(self, elapsed_seconds: int, task_text: str, date_iso: str):
"""Handle timer stop - open time log dialog with pre-filled data."""
# Convert seconds to decimal hours, rounding up to the nearest 0.25 hour (15 minutes)
quarter_hours = math.ceil(elapsed_seconds / 900)
hours = quarter_hours * 0.25
# Ensure minimum of 0.25 hours
if hours < 0.25:
hours = 0.25
# Untoggle the toolbar button without retriggering the slot
tool_bar = getattr(self._parent, "toolBar", None)
if tool_bar is not None and hasattr(tool_bar, "actTimer"):
blocker = QSignalBlocker(tool_bar.actTimer)
tool_bar.actTimer.setChecked(False)
del blocker
# Remove the embedded widget
self.cancel_timer()
# Open time log dialog
dlg = TimeLogDialog(
self._db,
date_iso,
self._parent,
True,
themes=self._parent.themes,
close_after_add=True,
)
# Pre-fill the hours
dlg.hours_spin.setValue(hours)
# Pre-fill the note with task text
dlg.note.setText(task_text)
# Show the dialog
dlg.exec()
time_log_widget = getattr(self._parent, "time_log", None)
if time_log_widget is not None:
# Same behaviour as TimeLogWidget._open_dialog/_open_dialog_log_only:
# reload the summary so the TimeLogWidget in sidebar updates its totals
time_log_widget._reload_summary()
if not time_log_widget.toggle_btn.isChecked():
time_log_widget.summary_label.setText(
strings._("time_log_collapsed_hint")
)