unknown il y a 5 mois
Parent
commit
d78c38b65f
18 fichiers modifiés avec 1353 ajouts et 0 suppressions
  1. 40 0
      .gitignore
  2. 97 0
      QUICKSTART.md
  3. 29 0
      main.py
  4. 1 0
      models/__init__.py
  5. 13 0
      models/timer_model.py
  6. 1 0
      requirements.txt
  7. 18 0
      run.bat
  8. 12 0
      run.sh
  9. 33 0
      setup.bat
  10. 31 0
      setup.sh
  11. 1 0
      ui/__init__.py
  12. 115 0
      ui/fullscreen_display.py
  13. 107 0
      ui/fullscreen_timer_widget.py
  14. 281 0
      ui/main_window.py
  15. 65 0
      ui/settings_dialog.py
  16. 134 0
      ui/timer_dialog.py
  17. 186 0
      ui/timer_widget.py
  18. 189 0
      ui/timer_window.py

+ 40 - 0
.gitignore

@@ -0,0 +1,40 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Virtual Environment
+venv/
+ENV/
+env/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Application settings
+*.ini

+ 97 - 0
QUICKSTART.md

@@ -0,0 +1,97 @@
+# Quick Start Guide
+
+## First Time Setup
+
+### Windows
+1. Double-click `setup.bat`
+2. Wait for installation to complete
+3. Double-click `run.bat` to start the app
+
+### Linux
+1. Open terminal in the project folder
+2. Run: `chmod +x setup.sh run.sh`
+3. Run: `./setup.sh`
+4. Run: `./run.sh`
+
+## Basic Usage
+
+### Step 1: Create Your First Timer
+1. Click **"Add Timer"**
+2. Enter a name (e.g., "5 Minute Break")
+3. Set duration using HH:MM:SS format (e.g., 00:05:00 for 5 minutes)
+4. Customize colors and font size if desired
+5. Click **"Save"**
+
+### Step 2: Configure Monitor (Optional)
+1. Click **"Settings"**
+2. Select which monitor to use for fullscreen display
+3. Click **"Save"**
+
+### Step 3: Start Timers
+1. Select a timer from the Timer Library
+2. Click **"Start Selected Timer"**
+3. Timer appears in the Active Timers panel on the right
+4. Start as many timers as you need - they all display together
+5. Use individual controls on each timer or global controls
+
+### Step 4: Control Your Timers
+
+**Individual Timer Controls** (on each timer):
+- **Pause/Resume**: Pause or resume that specific timer
+- **Restart**: Reset the timer to its original duration
+- **Stop**: Stop and remove that timer
+
+**Global Controls** (in the left panel):
+- **Pause All Timers**: Pause all running timers at once
+- **Resume All Timers**: Resume all paused timers
+- **Stop All Timers**: Stop and remove all timers
+
+**Fullscreen Display** (optional):
+- Click **"Show Fullscreen on Monitor"** to display all timers fullscreen
+- Timers split the screen evenly (1 timer = full height, 2 = 50% each, etc.)
+- Font sizes auto-adjust for optimal visibility
+- Perfect for presentations or viewing from a distance
+- Press **ESC** to exit fullscreen mode
+- Control timers from the main window while in fullscreen
+
+## Tips
+
+- **Duration**: Use HH:MM:SS format (00:05:00 = 5 minutes, 01:00:00 = 1 hour)
+- **Multiple Timers**: Run as many timers as you need - all visible in one window
+- **Fullscreen Mode**: Great for presentations, meetings, or viewing on a TV
+- **Multi-Monitor**: Configure which monitor shows fullscreen timers
+- **Individual Controls**: Each timer has Pause, Restart, and Stop buttons
+- **Global Controls**: Use Pause All/Resume All/Stop All for quick management
+- **Pause Feature**: Pause individual timers or all at once when needed
+- **Quick Restart**: Made a mistake? Use the Restart button to start over
+- **Colors**: Use contrasting colors for better visibility
+- **Timer Library**: Save frequently used timers for quick access
+- **Reusable**: Start the same timer configuration multiple times
+
+## Common Durations
+
+- 1 minute = 00:01:00
+- 5 minutes = 00:05:00
+- 10 minutes = 00:10:00
+- 15 minutes = 00:15:00
+- 30 minutes = 00:30:00
+- 1 hour = 01:00:00
+- 2 hours = 02:00:00
+
+## Troubleshooting
+
+**App won't start on Windows:**
+- Make sure Python is installed
+- Run `python --version` in Command Prompt
+- Re-run `setup.bat`
+
+**App won't start on Linux:**
+- Make sure Python 3 is installed
+- Run `python3 --version` in terminal
+- Make scripts executable: `chmod +x setup.sh run.sh`
+- Re-run `./setup.sh`
+
+**Can't see timer text:**
+- Edit the timer and change text color
+- Use high contrast (white on black or vice versa)
+- Adjust font size in timer settings

+ 29 - 0
main.py

@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+"""
+Countdown Timer Application
+A cross-platform PyQt6 application for creating and displaying fullscreen countdown timers.
+"""
+
+import sys
+from PyQt6.QtWidgets import QApplication
+from PyQt6.QtCore import QSettings
+from ui.main_window import MainWindow
+
+
+def main():
+    app = QApplication(sys.argv)
+    app.setOrganizationName("CountdownTimer")
+    app.setApplicationName("CountdownTimer")
+    
+    # Load settings
+    settings = QSettings()
+    
+    # Create and show main window
+    window = MainWindow()
+    window.show()
+    
+    sys.exit(app.exec())
+
+
+if __name__ == "__main__":
+    main()

+ 1 - 0
models/__init__.py

@@ -0,0 +1 @@
+# Models package

+ 13 - 0
models/timer_model.py

@@ -0,0 +1,13 @@
+"""Timer data model."""
+
+from dataclasses import dataclass
+
+
+@dataclass
+class Timer:
+    """Represents a countdown timer configuration."""
+    name: str
+    duration: int  # Duration in seconds
+    font_size: int = 120
+    bg_color: str = "#000000"
+    text_color: str = "#FFFFFF"

+ 1 - 0
requirements.txt

@@ -0,0 +1 @@
+PyQt6>=6.6.0

+ 18 - 0
run.bat

@@ -0,0 +1,18 @@
+@echo off
+echo Starting Countdown Timer Application...
+
+if not exist "venv" (
+    echo Virtual environment not found!
+    echo Please run setup.bat first
+    pause
+    exit /b 1
+)
+
+call venv\Scripts\activate.bat
+python main.py
+
+if errorlevel 1 (
+    echo.
+    echo Application exited with an error
+    pause
+)

+ 12 - 0
run.sh

@@ -0,0 +1,12 @@
+#!/bin/bash
+
+echo "Starting Countdown Timer Application..."
+
+if [ ! -d "venv" ]; then
+    echo "Virtual environment not found!"
+    echo "Please run ./setup.sh first"
+    exit 1
+fi
+
+source venv/bin/activate
+python main.py

+ 33 - 0
setup.bat

@@ -0,0 +1,33 @@
+@echo off
+echo Setting up Countdown Timer Application...
+echo.
+
+REM Check if venv exists
+if not exist "venv" (
+    echo Creating virtual environment...
+    python -m venv venv
+    if errorlevel 1 (
+        echo Error: Failed to create virtual environment
+        echo Please make sure Python is installed and in your PATH
+        pause
+        exit /b 1
+    )
+)
+
+echo Activating virtual environment...
+call venv\Scripts\activate.bat
+
+echo Installing dependencies...
+python -m pip install --upgrade pip
+pip install -r requirements.txt
+
+if errorlevel 1 (
+    echo Error: Failed to install dependencies
+    pause
+    exit /b 1
+)
+
+echo.
+echo Setup complete!
+echo To run the application, use: run.bat
+pause

+ 31 - 0
setup.sh

@@ -0,0 +1,31 @@
+#!/bin/bash
+
+echo "Setting up Countdown Timer Application..."
+echo
+
+# Check if venv exists
+if [ ! -d "venv" ]; then
+    echo "Creating virtual environment..."
+    python3 -m venv venv
+    if [ $? -ne 0 ]; then
+        echo "Error: Failed to create virtual environment"
+        echo "Please make sure Python 3 is installed"
+        exit 1
+    fi
+fi
+
+echo "Activating virtual environment..."
+source venv/bin/activate
+
+echo "Installing dependencies..."
+python -m pip install --upgrade pip
+pip install -r requirements.txt
+
+if [ $? -ne 0 ]; then
+    echo "Error: Failed to install dependencies"
+    exit 1
+fi
+
+echo
+echo "Setup complete!"
+echo "To run the application, use: ./run.sh"

+ 1 - 0
ui/__init__.py

@@ -0,0 +1 @@
+# UI package

+ 115 - 0
ui/fullscreen_display.py

@@ -0,0 +1,115 @@
+"""Fullscreen display window for showing timers on a separate monitor."""
+
+from PyQt6.QtWidgets import QWidget, QVBoxLayout, QScrollArea, QApplication, QLabel
+from PyQt6.QtCore import Qt, pyqtSignal
+from PyQt6.QtGui import QFont
+
+
+class FullscreenDisplay(QWidget):
+    closed = pyqtSignal()
+    
+    def __init__(self, monitor_index=0):
+        super().__init__()
+        self.monitor_index = monitor_index
+        self.timer_widget_mirrors = {}  # Maps original widget to mirror widget
+        self.init_ui()
+        
+    def init_ui(self):
+        # Set window flags for fullscreen
+        self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
+        self.setStyleSheet("background-color: #1a1a1a;")
+        
+        # Main layout - timers will split the screen evenly
+        self.timers_layout = QVBoxLayout(self)
+        self.timers_layout.setContentsMargins(0, 0, 0, 0)
+        self.timers_layout.setSpacing(0)
+        
+        # Position on selected monitor
+        self.position_on_monitor()
+        
+        # Show fullscreen
+        self.showFullScreen()
+        
+    def position_on_monitor(self):
+        screens = QApplication.screens()
+        if self.monitor_index < len(screens):
+            screen = screens[self.monitor_index]
+            self.setGeometry(screen.geometry())
+        else:
+            # Fallback to primary screen
+            screen = QApplication.primaryScreen()
+            self.setGeometry(screen.geometry())
+    
+    def add_timer_widget(self, timer_widget):
+        """Add a mirror of the timer widget to the fullscreen display."""
+        if timer_widget in self.timer_widget_mirrors:
+            return  # Already added
+        
+        # Create a mirror widget that displays the same timer
+        from ui.fullscreen_timer_widget import FullscreenTimerWidget
+        mirror_widget = FullscreenTimerWidget(timer_widget.timer_model)
+        mirror_widget.sync_with(timer_widget)
+        
+        self.timer_widget_mirrors[timer_widget] = mirror_widget
+        self.timers_layout.addWidget(mirror_widget, stretch=1)  # Equal stretch for all timers
+        
+        # Connect to sync updates
+        timer_widget.time_updated.connect(mirror_widget.update_display)
+        timer_widget.state_changed.connect(mirror_widget.update_state)
+        
+        # Update all timer sizes
+        self.update_timer_sizes()
+        
+    def remove_timer_widget(self, timer_widget):
+        """Remove the mirror widget when the original is removed."""
+        if timer_widget in self.timer_widget_mirrors:
+            mirror_widget = self.timer_widget_mirrors[timer_widget]
+            self.timers_layout.removeWidget(mirror_widget)
+            mirror_widget.deleteLater()
+            del self.timer_widget_mirrors[timer_widget]
+            
+            # Update remaining timer sizes
+            self.update_timer_sizes()
+    
+    def update_timer_sizes(self):
+        """Update font sizes based on number of timers."""
+        num_timers = len(self.timer_widget_mirrors)
+        if num_timers == 0:
+            return
+        
+        # Calculate appropriate font size based on number of timers
+        # More timers = smaller font to fit better
+        if num_timers == 1:
+            time_font_size = 120
+            title_font_size = 40
+        elif num_timers == 2:
+            time_font_size = 80
+            title_font_size = 30
+        elif num_timers == 3:
+            time_font_size = 60
+            title_font_size = 24
+        elif num_timers == 4:
+            time_font_size = 50
+            title_font_size = 20
+        else:
+            time_font_size = 40
+            title_font_size = 16
+        
+        # Update all mirror widgets
+        for mirror_widget in self.timer_widget_mirrors.values():
+            mirror_widget.update_font_sizes(time_font_size, title_font_size)
+    
+    def keyPressEvent(self, event):
+        # Allow ESC key to close the fullscreen display
+        if event.key() == Qt.Key.Key_Escape:
+            self.close()
+        super().keyPressEvent(event)
+    
+    def closeEvent(self, event):
+        # Clean up mirror widgets
+        for mirror_widget in list(self.timer_widget_mirrors.values()):
+            mirror_widget.deleteLater()
+        self.timer_widget_mirrors.clear()
+        
+        self.closed.emit()
+        super().closeEvent(event)

+ 107 - 0
ui/fullscreen_timer_widget.py

@@ -0,0 +1,107 @@
+"""Fullscreen timer widget optimized for large display."""
+
+from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QFrame
+from PyQt6.QtCore import Qt
+from PyQt6.QtGui import QFont
+
+
+class FullscreenTimerWidget(QFrame):
+    def __init__(self, timer_model):
+        super().__init__()
+        self.timer_model = timer_model
+        self.remaining_seconds = timer_model.duration
+        self.is_paused = False
+        self.time_font_size = 120
+        self.title_font_size = 40
+        self.init_ui()
+        
+    def init_ui(self):
+        # Frame styling with border between timers
+        self.setStyleSheet(f"""
+            QFrame {{
+                background-color: {self.timer_model.bg_color};
+                border-bottom: 3px solid #333333;
+            }}
+        """)
+        
+        # Main layout
+        layout = QVBoxLayout(self)
+        layout.setContentsMargins(40, 40, 40, 40)
+        
+        # Timer title
+        self.title_label = QLabel(self.timer_model.name)
+        self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        self.title_label.setStyleSheet(f"color: {self.timer_model.text_color}; border: none;")
+        self.title_label.setWordWrap(True)
+        
+        self.title_font = QFont("Arial", self.title_font_size, QFont.Weight.Bold)
+        self.title_label.setFont(self.title_font)
+        
+        layout.addWidget(self.title_label, stretch=1)
+        
+        # Timer display
+        self.time_label = QLabel(self.format_time(self.remaining_seconds))
+        self.time_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        self.time_label.setStyleSheet(f"color: {self.timer_model.text_color}; border: none;")
+        
+        self.time_font = QFont("Arial", self.time_font_size, QFont.Weight.Bold)
+        self.time_label.setFont(self.time_font)
+        
+        layout.addWidget(self.time_label, stretch=3)
+        
+        # Pause indicator (optional, shown when paused)
+        self.pause_indicator = QLabel("⏸ PAUSED")
+        self.pause_indicator.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        self.pause_indicator.setStyleSheet(f"color: {self.timer_model.text_color}; border: none;")
+        pause_font = QFont("Arial", 24, QFont.Weight.Bold)
+        self.pause_indicator.setFont(pause_font)
+        self.pause_indicator.hide()
+        
+        layout.addWidget(self.pause_indicator)
+        
+    def format_time(self, seconds):
+        hours = seconds // 3600
+        minutes = (seconds % 3600) // 60
+        secs = seconds % 60
+        
+        if hours > 0:
+            return f"{hours:02d}:{minutes:02d}:{secs:02d}"
+        else:
+            return f"{minutes:02d}:{secs:02d}"
+    
+    def sync_with(self, source_widget):
+        """Sync this mirror widget with the source widget."""
+        self.remaining_seconds = source_widget.remaining_seconds
+        self.is_paused = source_widget.is_paused
+        self.time_label.setText(self.format_time(self.remaining_seconds))
+        if self.is_paused:
+            self.pause_indicator.show()
+        else:
+            self.pause_indicator.hide()
+    
+    def update_display(self, remaining_seconds):
+        """Update the display with new time (for mirror widgets)."""
+        self.remaining_seconds = remaining_seconds
+        if remaining_seconds <= 0:
+            self.time_label.setText("TIME'S UP!")
+        else:
+            self.time_label.setText(self.format_time(remaining_seconds))
+    
+    def update_state(self, is_paused):
+        """Update the paused state (for mirror widgets)."""
+        self.is_paused = is_paused
+        if is_paused:
+            self.pause_indicator.show()
+        else:
+            self.pause_indicator.hide()
+    
+    def update_font_sizes(self, time_size, title_size):
+        """Update font sizes based on number of timers."""
+        self.time_font_size = time_size
+        self.title_font_size = title_size
+        
+        self.time_font.setPointSize(time_size)
+        self.time_label.setFont(self.time_font)
+        
+        self.title_font.setPointSize(title_size)
+        self.title_label.setFont(self.title_font)

+ 281 - 0
ui/main_window.py

@@ -0,0 +1,281 @@
+"""Main window for the countdown timer application."""
+
+from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
+                             QPushButton, QListWidget, QGroupBox, QLabel,
+                             QScrollArea, QFrame)
+from PyQt6.QtCore import QSettings, Qt, QTimer
+from PyQt6.QtGui import QFont
+from ui.settings_dialog import SettingsDialog
+from ui.timer_dialog import TimerDialog
+from ui.timer_widget import TimerWidget
+from ui.fullscreen_display import FullscreenDisplay
+from models.timer_model import Timer
+
+
+class MainWindow(QMainWindow):
+    def __init__(self):
+        super().__init__()
+        self.settings = QSettings()
+        self.timers = []
+        self.timer_widgets = []
+        self.fullscreen_display = None
+        self.init_ui()
+        self.load_timers()
+        
+    def init_ui(self):
+        self.setWindowTitle("Countdown Timer Manager")
+        self.setMinimumSize(1000, 600)
+        
+        # Central widget
+        central_widget = QWidget()
+        self.setCentralWidget(central_widget)
+        main_layout = QHBoxLayout(central_widget)
+        
+        # Left panel - Timer management
+        left_panel = QWidget()
+        left_layout = QVBoxLayout(left_panel)
+        left_panel.setMaximumWidth(350)
+        
+        # Timer list group
+        timer_group = QGroupBox("Timer Library")
+        timer_layout = QVBoxLayout()
+        
+        self.timer_list = QListWidget()
+        timer_layout.addWidget(self.timer_list)
+        
+        # Timer management buttons
+        timer_btn_layout = QVBoxLayout()
+        self.add_timer_btn = QPushButton("Add Timer")
+        self.edit_timer_btn = QPushButton("Edit Timer")
+        self.delete_timer_btn = QPushButton("Delete Timer")
+        self.start_timer_btn = QPushButton("Start Selected Timer")
+        
+        self.add_timer_btn.clicked.connect(self.add_timer)
+        self.edit_timer_btn.clicked.connect(self.edit_timer)
+        self.delete_timer_btn.clicked.connect(self.delete_timer)
+        self.start_timer_btn.clicked.connect(self.start_timer)
+        
+        timer_btn_layout.addWidget(self.add_timer_btn)
+        timer_btn_layout.addWidget(self.edit_timer_btn)
+        timer_btn_layout.addWidget(self.delete_timer_btn)
+        timer_btn_layout.addWidget(self.start_timer_btn)
+        
+        timer_layout.addLayout(timer_btn_layout)
+        timer_group.setLayout(timer_layout)
+        left_layout.addWidget(timer_group)
+        
+        # Global controls
+        controls_group = QGroupBox("Global Controls")
+        controls_layout = QVBoxLayout()
+        
+        self.stop_all_btn = QPushButton("Stop All Timers")
+        self.pause_all_btn = QPushButton("Pause All Timers")
+        self.resume_all_btn = QPushButton("Resume All Timers")
+        
+        self.stop_all_btn.clicked.connect(self.stop_all_timers)
+        self.pause_all_btn.clicked.connect(self.pause_all_timers)
+        self.resume_all_btn.clicked.connect(self.resume_all_timers)
+        
+        controls_layout.addWidget(self.pause_all_btn)
+        controls_layout.addWidget(self.resume_all_btn)
+        controls_layout.addWidget(self.stop_all_btn)
+        
+        controls_group.setLayout(controls_layout)
+        left_layout.addWidget(controls_group)
+        
+        # Display mode group
+        display_group = QGroupBox("Display Mode")
+        display_layout = QVBoxLayout()
+        
+        self.fullscreen_btn = QPushButton("Show Fullscreen on Monitor")
+        self.fullscreen_btn.clicked.connect(self.show_fullscreen_display)
+        display_layout.addWidget(self.fullscreen_btn)
+        
+        display_group.setLayout(display_layout)
+        left_layout.addWidget(display_group)
+        
+        # Settings button
+        settings_btn = QPushButton("Settings")
+        settings_btn.clicked.connect(self.open_settings)
+        left_layout.addWidget(settings_btn)
+        
+        left_layout.addStretch()
+        main_layout.addWidget(left_panel)
+        
+        # Right panel - Active timers display
+        right_panel = QWidget()
+        right_layout = QVBoxLayout(right_panel)
+        
+        title_label = QLabel("Active Timers")
+        title_font = QFont("Arial", 16, QFont.Weight.Bold)
+        title_label.setFont(title_font)
+        title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        right_layout.addWidget(title_label)
+        
+        # Scroll area for timers
+        scroll_area = QScrollArea()
+        scroll_area.setWidgetResizable(True)
+        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+        
+        self.timers_container = QWidget()
+        self.timers_layout = QVBoxLayout(self.timers_container)
+        self.timers_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
+        
+        scroll_area.setWidget(self.timers_container)
+        right_layout.addWidget(scroll_area)
+        
+        main_layout.addWidget(right_panel, stretch=1)
+        
+        # Status bar
+        self.statusBar().showMessage("Ready")
+        
+    def add_timer(self):
+        dialog = TimerDialog(self)
+        if dialog.exec():
+            timer = dialog.get_timer()
+            self.timers.append(timer)
+            self.timer_list.addItem(timer.name)
+            self.save_timers()
+            self.statusBar().showMessage(f"Timer '{timer.name}' added", 3000)
+            
+    def edit_timer(self):
+        current_row = self.timer_list.currentRow()
+        if current_row >= 0:
+            timer = self.timers[current_row]
+            dialog = TimerDialog(self, timer)
+            if dialog.exec():
+                updated_timer = dialog.get_timer()
+                self.timers[current_row] = updated_timer
+                self.timer_list.item(current_row).setText(updated_timer.name)
+                self.save_timers()
+                self.statusBar().showMessage(f"Timer '{updated_timer.name}' updated", 3000)
+                
+    def delete_timer(self):
+        current_row = self.timer_list.currentRow()
+        if current_row >= 0:
+            timer_name = self.timers[current_row].name
+            del self.timers[current_row]
+            self.timer_list.takeItem(current_row)
+            self.save_timers()
+            self.statusBar().showMessage(f"Timer '{timer_name}' deleted", 3000)
+            
+    def start_timer(self):
+        current_row = self.timer_list.currentRow()
+        if current_row >= 0:
+            timer = self.timers[current_row]
+            
+            # Create timer widget
+            timer_widget = TimerWidget(timer)
+            timer_widget.finished.connect(lambda: self.remove_timer_widget(timer_widget))
+            
+            self.timer_widgets.append(timer_widget)
+            self.timers_layout.addWidget(timer_widget)
+            
+            # If fullscreen display is active, update it
+            if self.fullscreen_display and self.fullscreen_display.isVisible():
+                self.fullscreen_display.add_timer_widget(timer_widget)
+            
+            self.statusBar().showMessage(f"Timer '{timer.name}' started", 3000)
+    
+    def remove_timer_widget(self, timer_widget):
+        if timer_widget in self.timer_widgets:
+            self.timer_widgets.remove(timer_widget)
+            self.timers_layout.removeWidget(timer_widget)
+            timer_widget.deleteLater()
+            
+            # Update fullscreen display if active
+            if self.fullscreen_display and self.fullscreen_display.isVisible():
+                self.fullscreen_display.remove_timer_widget(timer_widget)
+            
+            self.statusBar().showMessage("Timer finished", 3000)
+    
+    def show_fullscreen_display(self):
+        """Show all active timers on a fullscreen display."""
+        if not self.timer_widgets:
+            self.statusBar().showMessage("No timers running. Start a timer first.", 3000)
+            return
+        
+        monitor_index = self.settings.value("monitor_index", 0, type=int)
+        
+        if self.fullscreen_display is None:
+            self.fullscreen_display = FullscreenDisplay(monitor_index)
+            self.fullscreen_display.closed.connect(self.on_fullscreen_closed)
+        
+        # Add all active timer widgets to fullscreen display
+        for timer_widget in self.timer_widgets:
+            self.fullscreen_display.add_timer_widget(timer_widget)
+        
+        self.fullscreen_display.show()
+        self.statusBar().showMessage("Fullscreen display opened on selected monitor", 3000)
+    
+    def on_fullscreen_closed(self):
+        """Handle fullscreen display being closed."""
+        self.fullscreen_display = None
+        self.statusBar().showMessage("Fullscreen display closed", 3000)
+    
+    def stop_all_timers(self):
+        if not self.timer_widgets:
+            self.statusBar().showMessage("No timers running", 3000)
+            return
+        
+        count = len(self.timer_widgets)
+        for timer_widget in list(self.timer_widgets):
+            timer_widget.stop_timer()
+        
+        self.statusBar().showMessage(f"Stopped {count} timer(s)", 3000)
+    
+    def pause_all_timers(self):
+        if not self.timer_widgets:
+            self.statusBar().showMessage("No timers running", 3000)
+            return
+        
+        count = 0
+        for timer_widget in self.timer_widgets:
+            if not timer_widget.is_paused:
+                timer_widget.toggle_pause()
+                count += 1
+        
+        self.statusBar().showMessage(f"Paused {count} timer(s)", 3000)
+    
+    def resume_all_timers(self):
+        if not self.timer_widgets:
+            self.statusBar().showMessage("No timers running", 3000)
+            return
+        
+        count = 0
+        for timer_widget in self.timer_widgets:
+            if timer_widget.is_paused:
+                timer_widget.toggle_pause()
+                count += 1
+        
+        self.statusBar().showMessage(f"Resumed {count} timer(s)", 3000)
+            
+    def open_settings(self):
+        dialog = SettingsDialog(self)
+        dialog.exec()
+        
+    def save_timers(self):
+        self.settings.beginWriteArray("timers")
+        for i, timer in enumerate(self.timers):
+            self.settings.setArrayIndex(i)
+            self.settings.setValue("name", timer.name)
+            self.settings.setValue("duration", timer.duration)
+            self.settings.setValue("font_size", timer.font_size)
+            self.settings.setValue("bg_color", timer.bg_color)
+            self.settings.setValue("text_color", timer.text_color)
+        self.settings.endArray()
+        
+    def load_timers(self):
+        size = self.settings.beginReadArray("timers")
+        for i in range(size):
+            self.settings.setArrayIndex(i)
+            timer = Timer(
+                name=self.settings.value("name", ""),
+                duration=self.settings.value("duration", 60, type=int),
+                font_size=self.settings.value("font_size", 120, type=int),
+                bg_color=self.settings.value("bg_color", "#000000"),
+                text_color=self.settings.value("text_color", "#FFFFFF")
+            )
+            self.timers.append(timer)
+            self.timer_list.addItem(timer.name)
+        self.settings.endArray()

+ 65 - 0
ui/settings_dialog.py

@@ -0,0 +1,65 @@
+"""Settings dialog for monitor selection."""
+
+from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
+                             QComboBox, QPushButton, QGroupBox)
+from PyQt6.QtGui import QScreen
+from PyQt6.QtCore import QSettings
+from PyQt6.QtWidgets import QApplication
+
+
+class SettingsDialog(QDialog):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        self.settings = QSettings()
+        self.init_ui()
+        
+    def init_ui(self):
+        self.setWindowTitle("Settings")
+        self.setMinimumWidth(400)
+        
+        layout = QVBoxLayout(self)
+        
+        # Monitor selection group
+        monitor_group = QGroupBox("Display Settings")
+        monitor_layout = QVBoxLayout()
+        
+        monitor_label = QLabel("Select Monitor for Timers:")
+        monitor_layout.addWidget(monitor_label)
+        
+        self.monitor_combo = QComboBox()
+        self.populate_monitors()
+        monitor_layout.addWidget(self.monitor_combo)
+        
+        # Load saved monitor
+        saved_monitor = self.settings.value("monitor_index", 0, type=int)
+        if saved_monitor < self.monitor_combo.count():
+            self.monitor_combo.setCurrentIndex(saved_monitor)
+        
+        monitor_group.setLayout(monitor_layout)
+        layout.addWidget(monitor_group)
+        
+        # Buttons
+        button_layout = QHBoxLayout()
+        save_btn = QPushButton("Save")
+        cancel_btn = QPushButton("Cancel")
+        
+        save_btn.clicked.connect(self.save_settings)
+        cancel_btn.clicked.connect(self.reject)
+        
+        button_layout.addStretch()
+        button_layout.addWidget(save_btn)
+        button_layout.addWidget(cancel_btn)
+        
+        layout.addLayout(button_layout)
+        
+    def populate_monitors(self):
+        screens = QApplication.screens()
+        for i, screen in enumerate(screens):
+            geometry = screen.geometry()
+            screen_info = f"Monitor {i + 1}: {geometry.width()}x{geometry.height()} at ({geometry.x()}, {geometry.y()})"
+            self.monitor_combo.addItem(screen_info, i)
+            
+    def save_settings(self):
+        monitor_index = self.monitor_combo.currentData()
+        self.settings.setValue("monitor_index", monitor_index)
+        self.accept()

+ 134 - 0
ui/timer_dialog.py

@@ -0,0 +1,134 @@
+"""Dialog for creating and editing timers."""
+
+from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
+                             QLineEdit, QSpinBox, QPushButton, QGroupBox,
+                             QColorDialog, QTimeEdit)
+from PyQt6.QtGui import QColor
+from PyQt6.QtCore import QTime
+from models.timer_model import Timer
+
+
+class TimerDialog(QDialog):
+    def __init__(self, parent=None, timer=None):
+        super().__init__(parent)
+        self.timer = timer
+        self.bg_color = timer.bg_color if timer else "#000000"
+        self.text_color = timer.text_color if timer else "#FFFFFF"
+        self.init_ui()
+        
+    def init_ui(self):
+        self.setWindowTitle("Timer Configuration")
+        self.setMinimumWidth(400)
+        
+        layout = QVBoxLayout(self)
+        
+        # Timer name
+        name_layout = QHBoxLayout()
+        name_layout.addWidget(QLabel("Timer Name:"))
+        self.name_input = QLineEdit()
+        if self.timer:
+            self.name_input.setText(self.timer.name)
+        name_layout.addWidget(self.name_input)
+        layout.addLayout(name_layout)
+        
+        # Duration
+        duration_layout = QHBoxLayout()
+        duration_layout.addWidget(QLabel("Duration (HH:MM:SS):"))
+        self.duration_input = QTimeEdit()
+        self.duration_input.setDisplayFormat("HH:mm:ss")
+        self.duration_input.setMinimumTime(QTime(0, 0, 1))
+        self.duration_input.setMaximumTime(QTime(23, 59, 59))
+        
+        # Set initial value from timer duration in seconds
+        if self.timer:
+            seconds = self.timer.duration
+        else:
+            seconds = 60  # Default 1 minute
+        
+        hours = seconds // 3600
+        minutes = (seconds % 3600) // 60
+        secs = seconds % 60
+        self.duration_input.setTime(QTime(hours, minutes, secs))
+        
+        duration_layout.addWidget(self.duration_input)
+        layout.addLayout(duration_layout)
+        
+        # Appearance group
+        appearance_group = QGroupBox("Appearance")
+        appearance_layout = QVBoxLayout()
+        
+        # Font size
+        font_layout = QHBoxLayout()
+        font_layout.addWidget(QLabel("Font Size:"))
+        self.font_size_input = QSpinBox()
+        self.font_size_input.setMinimum(20)
+        self.font_size_input.setMaximum(500)
+        self.font_size_input.setValue(self.timer.font_size if self.timer else 120)
+        font_layout.addWidget(self.font_size_input)
+        appearance_layout.addLayout(font_layout)
+        
+        # Background color
+        bg_color_layout = QHBoxLayout()
+        bg_color_layout.addWidget(QLabel("Background Color:"))
+        self.bg_color_btn = QPushButton("Choose Color")
+        self.bg_color_btn.clicked.connect(self.choose_bg_color)
+        self.update_bg_color_button()
+        bg_color_layout.addWidget(self.bg_color_btn)
+        appearance_layout.addLayout(bg_color_layout)
+        
+        # Text color
+        text_color_layout = QHBoxLayout()
+        text_color_layout.addWidget(QLabel("Text Color:"))
+        self.text_color_btn = QPushButton("Choose Color")
+        self.text_color_btn.clicked.connect(self.choose_text_color)
+        self.update_text_color_button()
+        text_color_layout.addWidget(self.text_color_btn)
+        appearance_layout.addLayout(text_color_layout)
+        
+        appearance_group.setLayout(appearance_layout)
+        layout.addWidget(appearance_group)
+        
+        # Buttons
+        button_layout = QHBoxLayout()
+        save_btn = QPushButton("Save")
+        cancel_btn = QPushButton("Cancel")
+        
+        save_btn.clicked.connect(self.accept)
+        cancel_btn.clicked.connect(self.reject)
+        
+        button_layout.addStretch()
+        button_layout.addWidget(save_btn)
+        button_layout.addWidget(cancel_btn)
+        
+        layout.addLayout(button_layout)
+        
+    def choose_bg_color(self):
+        color = QColorDialog.getColor(QColor(self.bg_color), self)
+        if color.isValid():
+            self.bg_color = color.name()
+            self.update_bg_color_button()
+            
+    def choose_text_color(self):
+        color = QColorDialog.getColor(QColor(self.text_color), self)
+        if color.isValid():
+            self.text_color = color.name()
+            self.update_text_color_button()
+            
+    def update_bg_color_button(self):
+        self.bg_color_btn.setStyleSheet(f"background-color: {self.bg_color};")
+        
+    def update_text_color_button(self):
+        self.text_color_btn.setStyleSheet(f"background-color: {self.text_color};")
+        
+    def get_timer(self):
+        # Convert QTime to seconds
+        time = self.duration_input.time()
+        duration_seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
+        
+        return Timer(
+            name=self.name_input.text(),
+            duration=duration_seconds,
+            font_size=self.font_size_input.value(),
+            bg_color=self.bg_color,
+            text_color=self.text_color
+        )

+ 186 - 0
ui/timer_widget.py

@@ -0,0 +1,186 @@
+"""Timer widget for displaying in the main window."""
+
+from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame
+from PyQt6.QtCore import QTimer, Qt, pyqtSignal
+from PyQt6.QtGui import QFont
+
+
+class TimerWidget(QFrame):
+    finished = pyqtSignal()
+    time_updated = pyqtSignal(int)  # Emits remaining seconds
+    state_changed = pyqtSignal(bool)  # Emits is_paused state
+    
+    def __init__(self, timer, is_mirror=False):
+        super().__init__()
+        self.timer_model = timer
+        self.remaining_seconds = timer.duration
+        self.is_paused = False
+        self.is_mirror = is_mirror
+        self.countdown_timer = None
+        self.init_ui()
+        if not is_mirror:
+            self.start_countdown()
+        
+    def init_ui(self):
+        # Frame styling
+        self.setFrameStyle(QFrame.Shape.Box | QFrame.Shadow.Raised)
+        self.setLineWidth(2)
+        self.setStyleSheet(f"""
+            QFrame {{
+                background-color: {self.timer_model.bg_color};
+                border: 3px solid {self.timer_model.text_color};
+                border-radius: 10px;
+                margin: 5px;
+            }}
+        """)
+        
+        # Main layout
+        layout = QVBoxLayout(self)
+        layout.setContentsMargins(20, 20, 20, 20)
+        
+        # Timer title
+        self.title_label = QLabel(self.timer_model.name)
+        self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        self.title_label.setStyleSheet(f"color: {self.timer_model.text_color}; border: none;")
+        
+        title_font = QFont("Arial", 18, QFont.Weight.Bold)
+        self.title_label.setFont(title_font)
+        
+        layout.addWidget(self.title_label)
+        
+        # Timer display
+        self.time_label = QLabel(self.format_time(self.remaining_seconds))
+        self.time_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        self.time_label.setStyleSheet(f"color: {self.timer_model.text_color}; border: none;")
+        
+        time_font = QFont("Arial", 48, QFont.Weight.Bold)
+        self.time_label.setFont(time_font)
+        
+        layout.addWidget(self.time_label)
+        
+        # Control buttons
+        control_layout = QHBoxLayout()
+        
+        button_style = f"""
+            QPushButton {{
+                background-color: rgba(255, 255, 255, 0.2);
+                color: {self.timer_model.text_color};
+                border: 2px solid {self.timer_model.text_color};
+                border-radius: 5px;
+                padding: 10px 20px;
+                font-size: 14px;
+                font-weight: bold;
+            }}
+            QPushButton:hover {{
+                background-color: rgba(255, 255, 255, 0.3);
+            }}
+            QPushButton:pressed {{
+                background-color: rgba(255, 255, 255, 0.4);
+            }}
+        """
+        
+        self.pause_btn = QPushButton("Pause")
+        self.pause_btn.setStyleSheet(button_style)
+        self.pause_btn.clicked.connect(self.toggle_pause)
+        control_layout.addWidget(self.pause_btn)
+        
+        self.restart_btn = QPushButton("Restart")
+        self.restart_btn.setStyleSheet(button_style)
+        self.restart_btn.clicked.connect(self.restart_timer)
+        control_layout.addWidget(self.restart_btn)
+        
+        self.stop_btn = QPushButton("Stop")
+        self.stop_btn.setStyleSheet(button_style)
+        self.stop_btn.clicked.connect(self.stop_timer)
+        control_layout.addWidget(self.stop_btn)
+        
+        layout.addLayout(control_layout)
+        
+        # Hide controls for mirror widgets
+        if self.is_mirror:
+            self.pause_btn.hide()
+            self.restart_btn.hide()
+            self.stop_btn.hide()
+        
+        # Set minimum size
+        self.setMinimumHeight(200)
+        
+    def start_countdown(self):
+        self.countdown_timer = QTimer(self)
+        self.countdown_timer.timeout.connect(self.update_timer)
+        self.countdown_timer.start(1000)  # Update every second
+        
+    def update_timer(self):
+        if not self.is_paused:
+            self.remaining_seconds -= 1
+            
+            if self.remaining_seconds <= 0:
+                self.countdown_timer.stop()
+                self.time_label.setText("TIME'S UP!")
+                self.pause_btn.setEnabled(False)
+                self.time_updated.emit(0)
+                QTimer.singleShot(3000, self.close_timer)  # Close after 3 seconds
+            else:
+                self.time_label.setText(self.format_time(self.remaining_seconds))
+                self.time_updated.emit(self.remaining_seconds)
+                
+    def format_time(self, seconds):
+        hours = seconds // 3600
+        minutes = (seconds % 3600) // 60
+        secs = seconds % 60
+        
+        if hours > 0:
+            return f"{hours:02d}:{minutes:02d}:{secs:02d}"
+        else:
+            return f"{minutes:02d}:{secs:02d}"
+    
+    def toggle_pause(self):
+        """Toggle pause/resume state."""
+        self.is_paused = not self.is_paused
+        if self.is_paused:
+            self.pause_btn.setText("Resume")
+        else:
+            self.pause_btn.setText("Pause")
+        self.state_changed.emit(self.is_paused)
+    
+    def restart_timer(self):
+        """Restart the timer from the beginning."""
+        self.remaining_seconds = self.timer_model.duration
+        self.time_label.setText(self.format_time(self.remaining_seconds))
+        self.is_paused = False
+        self.pause_btn.setText("Pause")
+        self.pause_btn.setEnabled(True)
+        
+        if not self.countdown_timer.isActive():
+            self.countdown_timer.start(1000)
+        
+        self.time_updated.emit(self.remaining_seconds)
+        self.state_changed.emit(self.is_paused)
+    
+    def stop_timer(self):
+        """Stop the timer and remove widget."""
+        if self.countdown_timer and self.countdown_timer.isActive():
+            self.countdown_timer.stop()
+        self.close_timer()
+    
+    def close_timer(self):
+        """Emit finished signal to remove this widget."""
+        self.finished.emit()
+    
+    def sync_with(self, source_widget):
+        """Sync this mirror widget with the source widget."""
+        self.remaining_seconds = source_widget.remaining_seconds
+        self.is_paused = source_widget.is_paused
+        self.time_label.setText(self.format_time(self.remaining_seconds))
+    
+    def update_display(self, remaining_seconds):
+        """Update the display with new time (for mirror widgets)."""
+        self.remaining_seconds = remaining_seconds
+        if remaining_seconds <= 0:
+            self.time_label.setText("TIME'S UP!")
+        else:
+            self.time_label.setText(self.format_time(remaining_seconds))
+    
+    def update_state(self, is_paused):
+        """Update the paused state (for mirror widgets)."""
+        self.is_paused = is_paused

+ 189 - 0
ui/timer_window.py

@@ -0,0 +1,189 @@
+"""Fullscreen timer display window."""
+
+from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QApplication
+from PyQt6.QtCore import QTimer, Qt, pyqtSignal
+from PyQt6.QtGui import QFont, QScreen
+
+
+class TimerWindow(QWidget):
+    finished = pyqtSignal()
+    
+    def __init__(self, timer, monitor_index=0, position=0):
+        super().__init__()
+        self.timer_model = timer
+        self.remaining_seconds = timer.duration
+        self.monitor_index = monitor_index
+        self.position = position  # 0 for left half, 1 for right half
+        self.countdown_timer = None
+        self.is_paused = False
+        self.init_ui()
+        self.start_countdown()
+        
+    def init_ui(self):
+        # Set window flags for fullscreen
+        self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
+        
+        # Set background color
+        self.setStyleSheet(f"background-color: {self.timer_model.bg_color};")
+        
+        # Layout
+        layout = QVBoxLayout(self)
+        layout.setContentsMargins(50, 50, 50, 50)
+        
+        # Timer title
+        self.title_label = QLabel(self.timer_model.name)
+        self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        self.title_label.setStyleSheet(f"color: {self.timer_model.text_color};")
+        
+        title_font = QFont("Arial", self.timer_model.font_size // 3, QFont.Weight.Bold)
+        self.title_label.setFont(title_font)
+        
+        layout.addWidget(self.title_label)
+        
+        # Timer label
+        self.time_label = QLabel(self.format_time(self.remaining_seconds))
+        self.time_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+        self.time_label.setStyleSheet(f"color: {self.timer_model.text_color};")
+        
+        font = QFont("Arial", self.timer_model.font_size, QFont.Weight.Bold)
+        self.time_label.setFont(font)
+        
+        layout.addWidget(self.time_label)
+        
+        # Control buttons
+        control_layout = QHBoxLayout()
+        control_layout.addStretch()
+        
+        button_font = QFont("Arial", 16, QFont.Weight.Bold)
+        button_style = f"""
+            QPushButton {{
+                background-color: rgba(255, 255, 255, 0.2);
+                color: {self.timer_model.text_color};
+                border: 2px solid {self.timer_model.text_color};
+                border-radius: 10px;
+                padding: 15px 30px;
+                min-width: 120px;
+            }}
+            QPushButton:hover {{
+                background-color: rgba(255, 255, 255, 0.3);
+            }}
+            QPushButton:pressed {{
+                background-color: rgba(255, 255, 255, 0.4);
+            }}
+        """
+        
+        self.pause_btn = QPushButton("Pause")
+        self.pause_btn.setFont(button_font)
+        self.pause_btn.setStyleSheet(button_style)
+        self.pause_btn.clicked.connect(self.toggle_pause)
+        control_layout.addWidget(self.pause_btn)
+        
+        self.stop_btn = QPushButton("Stop")
+        self.stop_btn.setFont(button_font)
+        self.stop_btn.setStyleSheet(button_style)
+        self.stop_btn.clicked.connect(self.stop_timer)
+        control_layout.addWidget(self.stop_btn)
+        
+        self.restart_btn = QPushButton("Restart")
+        self.restart_btn.setFont(button_font)
+        self.restart_btn.setStyleSheet(button_style)
+        self.restart_btn.clicked.connect(self.restart_timer)
+        control_layout.addWidget(self.restart_btn)
+        
+        control_layout.addStretch()
+        layout.addLayout(control_layout)
+        
+        # Position on selected monitor
+        self.position_on_monitor()
+        
+        # Show fullscreen
+        self.showFullScreen()
+        
+    def position_on_monitor(self):
+        screens = QApplication.screens()
+        if self.monitor_index < len(screens):
+            screen = screens[self.monitor_index]
+            geometry = screen.geometry()
+        else:
+            # Fallback to primary screen
+            screen = QApplication.primaryScreen()
+            geometry = screen.geometry()
+        
+        # Split screen in half if position is 0 or 1
+        if self.position == 0:
+            # Left half
+            self.setGeometry(geometry.x(), geometry.y(), 
+                           geometry.width() // 2, geometry.height())
+        elif self.position == 1:
+            # Right half
+            self.setGeometry(geometry.x() + geometry.width() // 2, geometry.y(),
+                           geometry.width() // 2, geometry.height())
+        else:
+            # Full screen (shouldn't happen with current logic)
+            self.setGeometry(geometry)
+            
+    def start_countdown(self):
+        self.countdown_timer = QTimer(self)
+        self.countdown_timer.timeout.connect(self.update_timer)
+        self.countdown_timer.start(1000)  # Update every second
+        
+    def update_timer(self):
+        if not self.is_paused:
+            self.remaining_seconds -= 1
+            
+            if self.remaining_seconds <= 0:
+                self.countdown_timer.stop()
+                self.time_label.setText("TIME'S UP!")
+                self.pause_btn.setEnabled(False)
+                self.restart_btn.setEnabled(True)
+                QTimer.singleShot(3000, self.close_window)  # Close after 3 seconds
+            else:
+                self.time_label.setText(self.format_time(self.remaining_seconds))
+            
+    def format_time(self, seconds):
+        hours = seconds // 3600
+        minutes = (seconds % 3600) // 60
+        secs = seconds % 60
+        
+        if hours > 0:
+            return f"{hours:02d}:{minutes:02d}:{secs:02d}"
+        else:
+            return f"{minutes:02d}:{secs:02d}"
+            
+    def toggle_pause(self):
+        """Toggle pause/resume state."""
+        self.is_paused = not self.is_paused
+        if self.is_paused:
+            self.pause_btn.setText("Resume")
+        else:
+            self.pause_btn.setText("Pause")
+    
+    def restart_timer(self):
+        """Restart the timer from the beginning."""
+        self.remaining_seconds = self.timer_model.duration
+        self.time_label.setText(self.format_time(self.remaining_seconds))
+        self.is_paused = False
+        self.pause_btn.setText("Pause")
+        self.pause_btn.setEnabled(True)
+        
+        if not self.countdown_timer.isActive():
+            self.countdown_timer.start(1000)
+    
+    def stop_timer(self):
+        """Stop the timer and close the window."""
+        if self.countdown_timer and self.countdown_timer.isActive():
+            self.countdown_timer.stop()
+        self.close_window()
+    
+    def close_window(self):
+        self.finished.emit()
+        self.close()
+        
+    def keyPressEvent(self, event):
+        # Allow ESC key to close the timer
+        if event.key() == Qt.Key.Key_Escape:
+            self.stop_timer()
+        # Space bar to pause/resume
+        elif event.key() == Qt.Key.Key_Space:
+            self.toggle_pause()
+        super().keyPressEvent(event)