swissclock 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
swissclock/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ # swissclock package
@@ -0,0 +1,94 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ from PySide6.QtCore import QRectF, Qt, QTimer
3
+ from PySide6.QtGui import QColor, QPainter
4
+ from PySide6.QtWidgets import QMenu, QMessageBox, QWidget
5
+
6
+
7
+ class BaseDial(QWidget):
8
+ def __init__(self, parent=None):
9
+ super().__init__(parent)
10
+
11
+ self.timer = QTimer(self)
12
+ self.timer.timeout.connect(self.update_dial)
13
+ self.timer.start(30) # Smooth animation
14
+
15
+ self.black_color = QColor(0, 0, 0)
16
+ self.red_color = QColor(220, 0, 0) # Swiss red
17
+ self.green_color = QColor(0, 180, 0) # Normal tracking green
18
+ self.orange_color = QColor(255, 165, 0) # Warning orange
19
+
20
+ def update_dial(self):
21
+ self.update()
22
+
23
+ def paintEvent(self, event):
24
+ side = min(self.width(), self.height())
25
+ painter = QPainter(self)
26
+ painter.setRenderHint(QPainter.Antialiasing)
27
+ painter.translate(self.width() / 2, self.height() / 2)
28
+ painter.scale(side / 240.0, side / 240.0)
29
+
30
+ # White background face
31
+ painter.setPen(Qt.NoPen)
32
+ painter.setBrush(Qt.white)
33
+ painter.drawEllipse(-98, -98, 196, 196)
34
+
35
+ self.draw_face(painter)
36
+ self.draw_hands(painter)
37
+
38
+ def draw_face(self, painter):
39
+ pass
40
+
41
+ def draw_hands(self, painter):
42
+ pass
43
+
44
+ def draw_dial(self, painter):
45
+ # To be implemented by subclasses
46
+ pass
47
+
48
+ def contextMenuEvent(self, event):
49
+ menu = QMenu(self)
50
+ about_action = menu.addAction("About")
51
+ help_action = menu.addAction("Help (Shortcuts)")
52
+ action = menu.exec(event.globalPos())
53
+ if action == about_action:
54
+ self.show_about()
55
+ elif action == help_action:
56
+ self.show_help()
57
+
58
+ def show_help(self):
59
+ QMessageBox.information(self, "SwissClock Help",
60
+ "<h2>Keyboard Shortcuts</h2>"
61
+ "<ul>"
62
+ "<li><b>T</b>: Switch between Clock and Stopwatch</li>"
63
+ "<li><b>Space</b>: Start / Stop / Resume Stopwatch</li>"
64
+ "<li><b>R</b>: Reset Stopwatch</li>"
65
+ "<li><b>D</b>: Toggle Digital / Analog mode (in Stopwatch)</li>"
66
+ "<li><b>F</b>: Toggle Fullscreen mode</li>"
67
+ "</ul>")
68
+
69
+ def show_about(self):
70
+ QMessageBox.about(self, "About SwissClock",
71
+ "<h2>Swiss Railway Time</h2>"
72
+ "<p>A Swiss Railways inspired unified analog clock and morphing digital stopwatch.</p>"
73
+ "<p><b>Author:</b> Alin Elena &lt;alin@elena.re&gt;</p>"
74
+ "<p><b>License:</b> GPL-3.0-or-later</p>"
75
+ "<p><b>Dependencies:</b> PySide6</p>")
76
+
77
+ def draw_capsule_hand(self, painter, angle, color, rect_args):
78
+ painter.save()
79
+ painter.rotate(angle)
80
+ painter.setBrush(color)
81
+ painter.setPen(Qt.NoPen)
82
+ # rect_args: (x, y, w, h, xRadius, yRadius)
83
+ painter.drawRoundedRect(*rect_args)
84
+ painter.restore()
85
+
86
+ def draw_swiss_second_hand(self, painter, angle):
87
+ painter.save()
88
+ painter.rotate(angle)
89
+ painter.setBrush(self.red_color)
90
+ painter.setPen(Qt.NoPen)
91
+ painter.drawRect(-1, -76, 2, 101)
92
+ painter.drawEllipse(QRectF(-6, -82, 12, 12))
93
+ painter.drawEllipse(QRectF(-3, -3, 6, 6))
94
+ painter.restore()
swissclock/clock.py ADDED
@@ -0,0 +1,108 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ import math
3
+ import sys
4
+
5
+ from PySide6.QtCore import QRectF, Qt, QTime
6
+ from PySide6.QtWidgets import QApplication
7
+
8
+ from .base_dial import BaseDial
9
+
10
+
11
+ class AnalogClock(BaseDial):
12
+ def __init__(self, parent=None):
13
+ super().__init__(parent)
14
+ self.setWindowTitle("Analog Clock")
15
+ self.resize(300, 300)
16
+
17
+ def draw_face(self, painter):
18
+ # Draw 24 hour markers (thick lines for even, red dots for odd)
19
+ painter.save()
20
+ for i in range(24):
21
+ if i % 2 == 0:
22
+ painter.setBrush(self.black_color)
23
+ painter.setPen(Qt.NoPen)
24
+ painter.drawEllipse(QRectF(83, -4, 8, 8))
25
+ else:
26
+ painter.setBrush(self.red_color)
27
+ painter.setPen(Qt.NoPen)
28
+ painter.drawEllipse(QRectF(85, -2, 4, 4))
29
+ painter.rotate(15.0)
30
+ painter.restore()
31
+
32
+ # Draw 60 minute markers (small black circles)
33
+ painter.save()
34
+ painter.setBrush(self.black_color)
35
+ painter.setPen(Qt.NoPen)
36
+ for i in range(60):
37
+ painter.drawEllipse(QRectF(85, -2, 4, 4))
38
+ painter.rotate(6.0)
39
+ painter.restore()
40
+
41
+ # Draw numbers (even inside, odd outside)
42
+ painter.save()
43
+ font = painter.font()
44
+ font.setFamily("Verdana")
45
+ font.setPointSize(6)
46
+ font.setBold(True)
47
+ painter.setFont(font)
48
+
49
+ window_text_color = self.palette().windowText().color()
50
+
51
+ for h in range(1, 25):
52
+ angle_deg = -90 + h * 15
53
+ angle_rad = math.radians(angle_deg)
54
+ if h % 2 == 0:
55
+ x = 65 * math.cos(angle_rad)
56
+ y = 65 * math.sin(angle_rad)
57
+ painter.setPen(self.black_color)
58
+ else:
59
+ x = 108 * math.cos(angle_rad)
60
+ y = 108 * math.sin(angle_rad)
61
+ painter.setPen(window_text_color)
62
+
63
+ rect = QRectF(x - 10, y - 10, 20, 20)
64
+ painter.drawText(rect, Qt.AlignCenter, str(h))
65
+ painter.restore()
66
+
67
+ # Draw minute labels outside the clock
68
+ painter.save()
69
+ font = painter.font()
70
+ font.setFamily("Courier New")
71
+ font.setPointSize(4)
72
+ font.setBold(True)
73
+ painter.setFont(font)
74
+ painter.setPen(self.palette().windowText().color())
75
+ for m in range(0, 60, 5):
76
+ angle_deg = -90 + m * 6
77
+ angle_rad = math.radians(angle_deg)
78
+ x = 108 * math.cos(angle_rad)
79
+ y = 108 * math.sin(angle_rad)
80
+ rect = QRectF(x - 10, y - 10, 20, 20)
81
+ painter.drawText(rect, Qt.AlignCenter, f"{m:02d}")
82
+ painter.restore()
83
+
84
+ def draw_hands(self, painter):
85
+ time = QTime.currentTime()
86
+
87
+ # Hour hand
88
+ hour_angle = 15.0 * (time.hour() + time.minute() / 60.0 + time.second() / 3600.0)
89
+ self.draw_capsule_hand(painter, hour_angle, self.black_color, (-4, -55, 8, 70, 4.0, 4.0))
90
+
91
+ # Minute hand (Swiss clocks jump the minute hand at the exact minute)
92
+ minute_angle = 6.0 * time.minute()
93
+ self.draw_capsule_hand(painter, minute_angle, self.black_color, (-3, -75, 6, 95, 3.0, 3.0))
94
+
95
+ # Second hand (waits for 1.5s at 60, then jumps directly to 1.5 seconds)
96
+ ms = time.second() * 1000 + time.msec()
97
+ if ms < 1500:
98
+ second_angle = 0.0
99
+ else:
100
+ second_angle = ms * (360.0 / 60000.0)
101
+
102
+ self.draw_swiss_second_hand(painter, second_angle)
103
+
104
+ if __name__ == '__main__':
105
+ app = QApplication(sys.argv)
106
+ clock = AnalogClock()
107
+ clock.show()
108
+ sys.exit(app.exec())
swissclock/main.py ADDED
@@ -0,0 +1,138 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ import sys
3
+
4
+ from PySide6.QtGui import QKeySequence, QShortcut
5
+ from PySide6.QtWidgets import (
6
+ QApplication,
7
+ QHBoxLayout,
8
+ QPushButton,
9
+ QStackedWidget,
10
+ QVBoxLayout,
11
+ QWidget,
12
+ )
13
+
14
+ from .clock import AnalogClock
15
+ from .stop import StopWatch
16
+
17
+
18
+ class UnifiedApp(QWidget):
19
+ def __init__(self):
20
+ super().__init__()
21
+ self.setWindowTitle("Swiss Railway Time")
22
+ self.resize(320, 420)
23
+
24
+ layout = QVBoxLayout(self)
25
+
26
+ self.stacked = QStackedWidget()
27
+
28
+ self.clock_widget = AnalogClock()
29
+ self.stopwatch_widget = StopWatch(self)
30
+
31
+ self.stacked.addWidget(self.clock_widget)
32
+ self.stacked.addWidget(self.stopwatch_widget)
33
+
34
+ layout.addWidget(self.stacked, 1) # Expanding
35
+
36
+ # Action Buttons Layout (Start / Reset)
37
+ self.action_layout = QHBoxLayout()
38
+ self.btn_start = QPushButton("Start (Space)")
39
+ self.btn_start.clicked.connect(self.toggle_stopwatch)
40
+
41
+ self.btn_reset = QPushButton("Reset (R)")
42
+ self.btn_reset.clicked.connect(self.reset_stopwatch)
43
+
44
+ self.action_layout.addWidget(self.btn_start)
45
+ self.action_layout.addWidget(self.btn_reset)
46
+
47
+ # Only show action buttons when in stopwatch mode
48
+ self.action_widget = QWidget()
49
+ self.action_widget.setLayout(self.action_layout)
50
+ sp = self.action_widget.sizePolicy()
51
+ sp.setRetainSizeWhenHidden(True)
52
+ self.action_widget.setSizePolicy(sp)
53
+ self.action_widget.setVisible(False)
54
+ layout.addWidget(self.action_widget)
55
+
56
+ # Main Toggle Button
57
+ self.btn_toggle = QPushButton("Switch to Stopwatch (T)")
58
+ self.btn_toggle.clicked.connect(self.toggle_view)
59
+ layout.addWidget(self.btn_toggle)
60
+
61
+ # Bulletproof window-level shortcuts
62
+ self.shortcut_toggle = QShortcut(QKeySequence("T"), self)
63
+ self.shortcut_toggle.activated.connect(self.toggle_view)
64
+
65
+ self.shortcut_space = QShortcut(QKeySequence("Space"), self)
66
+ self.shortcut_space.activated.connect(self.handle_space)
67
+
68
+ self.shortcut_reset = QShortcut(QKeySequence("R"), self)
69
+ self.shortcut_reset.activated.connect(self.handle_reset)
70
+
71
+ self.shortcut_digital = QShortcut(QKeySequence("D"), self)
72
+ self.shortcut_digital.activated.connect(self.handle_digital)
73
+
74
+ self.shortcut_fullscreen = QShortcut(QKeySequence("F"), self)
75
+ self.shortcut_fullscreen.activated.connect(self.toggle_fullscreen)
76
+
77
+ def toggle_fullscreen(self):
78
+ if self.isFullScreen():
79
+ self.showNormal()
80
+ else:
81
+ self.showFullScreen()
82
+
83
+ def handle_space(self):
84
+ if self.stacked.currentIndex() == 1:
85
+ self.toggle_stopwatch()
86
+
87
+ def handle_reset(self):
88
+ if self.stacked.currentIndex() == 1:
89
+ self.reset_stopwatch()
90
+
91
+ def handle_digital(self):
92
+ if self.stacked.currentIndex() == 1:
93
+ self.stopwatch_widget.toggle_digital_mode()
94
+
95
+ def toggle_stopwatch(self):
96
+ self.stopwatch_widget.toggle_timer()
97
+ self.update_buttons()
98
+
99
+ def reset_stopwatch(self):
100
+ self.stopwatch_widget.reset_timer()
101
+ self.update_buttons()
102
+
103
+ def update_buttons(self):
104
+ if self.stopwatch_widget.running:
105
+ self.btn_start.setText("Stop (Space)")
106
+ else:
107
+ if self.stopwatch_widget.total_elapsed > 0:
108
+ self.btn_start.setText("Resume (Space)")
109
+ else:
110
+ self.btn_start.setText("Start (Space)")
111
+
112
+ def toggle_view(self):
113
+ if self.stacked.currentIndex() == 0:
114
+ self.stacked.setCurrentIndex(1)
115
+ self.btn_toggle.setText("Switch to Clock (T)")
116
+ self.action_widget.setVisible(True)
117
+ self.update_buttons()
118
+ else:
119
+ self.stacked.setCurrentIndex(0)
120
+ self.btn_toggle.setText("Switch to Stopwatch (T)")
121
+ self.action_widget.setVisible(False)
122
+
123
+ def main():
124
+ app = QApplication.instance() or QApplication(sys.argv)
125
+
126
+ # Optional: Set an application-wide stylesheet for a modern dark look if desired
127
+ app.setStyleSheet("""
128
+ QWidget {
129
+ background-color: #121212;
130
+ }
131
+ """)
132
+
133
+ clock = UnifiedApp()
134
+ clock.show()
135
+ sys.exit(app.exec())
136
+
137
+ if __name__ == "__main__":
138
+ main()
@@ -0,0 +1,87 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ from PySide6.QtCore import QPointF, Qt
3
+ from PySide6.QtGui import QColor, QPen
4
+
5
+ # Define the 7 standard segments on a 1x2 grid
6
+ SEGS = [
7
+ (0,0, 1,0), # 0: T
8
+ (1,0, 1,1), # 1: TR
9
+ (1,1, 1,2), # 2: BR
10
+ (1,2, 0,2), # 3: B
11
+ (0,2, 0,1), # 4: BL
12
+ (0,1, 0,0), # 5: TL
13
+ (0,1, 1,1) # 6: M
14
+ ]
15
+
16
+ # Standard 7-segment bitmasks for digits 0-9
17
+ # Order: T, TR, BR, B, BL, TL, M
18
+ STATES = [
19
+ [1, 1, 1, 1, 1, 1, 0], # 0
20
+ [0, 1, 1, 0, 0, 0, 0], # 1
21
+ [1, 1, 0, 1, 1, 0, 1], # 2
22
+ [1, 1, 1, 1, 0, 0, 1], # 3
23
+ [0, 1, 1, 0, 0, 1, 1], # 4
24
+ [1, 0, 1, 1, 0, 1, 1], # 5
25
+ [1, 0, 1, 1, 1, 1, 1], # 6
26
+ [1, 1, 1, 0, 0, 0, 0], # 7
27
+ [1, 1, 1, 1, 1, 1, 1], # 8
28
+ [1, 1, 1, 1, 0, 1, 1], # 9
29
+ ]
30
+
31
+ class MorphDigit:
32
+ def __init__(self):
33
+ self.target = 0
34
+ self.opacities = [float(x) for x in STATES[0]]
35
+
36
+ def set_target(self, digit):
37
+ if self.target != digit:
38
+ self.target = digit
39
+
40
+ def update(self):
41
+ # Lerp each segment's opacity towards its target state (smooth fade)
42
+ target_state = STATES[self.target]
43
+ for i in range(7):
44
+ current = self.opacities[i]
45
+ target = float(target_state[i])
46
+
47
+ # Smooth 30fps fade factor (lower is slower)
48
+ factor = 0.1
49
+ self.opacities[i] = current + (target - current) * factor
50
+
51
+ def draw(self, painter, x_offset, y_offset, scale_x, scale_y, color):
52
+ pen_width = max(2.0, scale_x * 0.15)
53
+
54
+ # 10% gap to ensure the segments are completely distinct from each other
55
+ gap = 0.1
56
+
57
+ for i in range(7):
58
+ opacity = self.opacities[i]
59
+ if opacity < 0.01:
60
+ continue
61
+
62
+ x1, y1, x2, y2 = SEGS[i]
63
+
64
+ dx = x2 - x1
65
+ dy = y2 - y1
66
+ length = (dx**2 + dy**2)**0.5
67
+
68
+ # Apply gap to shrink the line slightly and make it distinct
69
+ nx = dx / length
70
+ ny = dy / length
71
+
72
+ start_x = x1 + nx * gap
73
+ start_y = y1 + ny * gap
74
+ end_x = x2 - nx * gap
75
+ end_y = y2 - ny * gap
76
+
77
+ # Create a color with the current opacity for fading
78
+ c = QColor(color)
79
+ c.setAlphaF(opacity)
80
+
81
+ pen = QPen(c)
82
+ pen.setWidthF(pen_width)
83
+ pen.setCapStyle(Qt.FlatCap) # Flat caps for that distinct LCD look
84
+ painter.setPen(pen)
85
+
86
+ painter.drawLine(QPointF(x_offset + start_x * scale_x, y_offset + start_y * scale_y),
87
+ QPointF(x_offset + end_x * scale_x, y_offset + end_y * scale_y))
swissclock/stop.py ADDED
@@ -0,0 +1,273 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ import math
3
+ import sys
4
+
5
+ from PySide6.QtCore import QElapsedTimer, QRectF, Qt
6
+ from PySide6.QtGui import QPainter, QPen
7
+ from PySide6.QtWidgets import QApplication, QInputDialog, QMenu
8
+
9
+ from .base_dial import BaseDial
10
+ from .morph_digit import MorphDigit
11
+
12
+
13
+ class StopWatch(BaseDial):
14
+ def __init__(self, parent=None):
15
+ super().__init__(parent)
16
+
17
+ self.running = False
18
+ self.elapsed_timer = QElapsedTimer()
19
+ self.total_elapsed = 0
20
+ self.countdown_target_ms = 0
21
+ self.is_digital = False
22
+
23
+ # 5 digits for H:MM:SS
24
+ self.digits = [MorphDigit() for _ in range(5)]
25
+
26
+ self.setWindowTitle("Stopwatch")
27
+
28
+ self.setContextMenuPolicy(Qt.CustomContextMenu)
29
+ self.customContextMenuRequested.connect(self.show_context_menu)
30
+
31
+
32
+ def show_context_menu(self, pos):
33
+ menu = QMenu(self)
34
+
35
+ action_stopwatch = menu.addAction("Stopwatch Mode")
36
+ action_1min = menu.addAction("1 Minute")
37
+ action_5min = menu.addAction("5 Minutes")
38
+ action_10min = menu.addAction("10 Minutes")
39
+ action_custom = menu.addAction("Custom...")
40
+ menu.addSeparator()
41
+ action_help = menu.addAction("Help (Shortcuts)")
42
+ action_about = menu.addAction("About")
43
+
44
+ action = menu.exec(self.mapToGlobal(pos))
45
+
46
+ if action == action_stopwatch:
47
+ self.set_countdown(0)
48
+ elif action == action_1min:
49
+ self.set_countdown(60 * 1000)
50
+ elif action == action_5min:
51
+ self.set_countdown(5 * 60 * 1000)
52
+ elif action == action_10min:
53
+ self.set_countdown(10 * 60 * 1000)
54
+ elif action == action_custom:
55
+ mins, ok = QInputDialog.getInt(self, "Custom Countdown", "Enter minutes:", 1, 1, 999)
56
+ if ok:
57
+ self.set_countdown(mins * 60 * 1000)
58
+ elif action == action_help:
59
+ self.show_help()
60
+ elif action == action_about:
61
+ self.show_about()
62
+
63
+ def toggle_digital_mode(self):
64
+ self.is_digital = not self.is_digital
65
+ self.update()
66
+
67
+ def update_dial(self):
68
+ if self.is_digital:
69
+ ms = self.get_elapsed_ms()
70
+ abs_ms = abs(ms)
71
+ s = (abs_ms % 60000) // 1000
72
+ m = (abs_ms % 3600000) // 60000
73
+ h = abs_ms // 3600000
74
+
75
+ self.digits[0].set_target(h % 10)
76
+ self.digits[1].set_target(m // 10)
77
+ self.digits[2].set_target(m % 10)
78
+ self.digits[3].set_target(s // 10)
79
+ self.digits[4].set_target(s % 10)
80
+
81
+ for digit in self.digits:
82
+ digit.update()
83
+
84
+ super().update_dial()
85
+
86
+ def paintEvent(self, event):
87
+ if not self.is_digital:
88
+ super().paintEvent(event)
89
+ return
90
+
91
+ ms = self.get_elapsed_ms()
92
+
93
+ if self.total_elapsed == 0 and not self.running:
94
+ color = Qt.white
95
+ elif self.countdown_target_ms > 0:
96
+ if ms < 0:
97
+ color = self.red_color
98
+ elif ms <= 0.10 * self.countdown_target_ms:
99
+ color = self.orange_color
100
+ else:
101
+ color = self.green_color
102
+ else:
103
+ color = self.green_color
104
+
105
+ painter = QPainter(self)
106
+ painter.setRenderHint(QPainter.Antialiasing)
107
+ painter.fillRect(self.rect(), Qt.black)
108
+
109
+ # H:MM:SS layout scaling
110
+ digit_w = self.width() * 0.1
111
+ digit_h = digit_w * 2.0
112
+ gap = digit_w * 0.4
113
+ colon_gap = digit_w * 0.8
114
+
115
+ total_w = digit_w * 5 + gap * 2 + colon_gap * 2
116
+
117
+ start_x = (self.width() - total_w) / 2
118
+ start_y = (self.height() - digit_h) / 2
119
+
120
+ curr_x = start_x
121
+
122
+ # Draw Negative sign if in overtime
123
+ if ms < 0:
124
+ pen = QPen(color)
125
+ pen.setWidthF(max(2.0, digit_w * 0.15))
126
+ pen.setCapStyle(Qt.RoundCap)
127
+ painter.setPen(pen)
128
+ painter.drawLine(start_x - digit_w * 0.8, start_y + digit_h / 2, start_x - digit_w * 0.2, start_y + digit_h / 2)
129
+
130
+ # Draw H
131
+ self.digits[0].draw(painter, curr_x, start_y, digit_w, digit_h / 2.0, color)
132
+ curr_x += digit_w + colon_gap / 2
133
+
134
+ # Draw Colon 1
135
+ painter.setPen(Qt.NoPen)
136
+ painter.setBrush(color)
137
+ radius = digit_w * 0.15
138
+ painter.drawEllipse(curr_x - radius, start_y + digit_h * 0.35 - radius, radius*2, radius*2)
139
+ painter.drawEllipse(curr_x - radius, start_y + digit_h * 0.65 - radius, radius*2, radius*2)
140
+ curr_x += colon_gap / 2
141
+
142
+ # Draw M1, M2
143
+ self.digits[1].draw(painter, curr_x, start_y, digit_w, digit_h / 2.0, color)
144
+ curr_x += digit_w + gap
145
+ self.digits[2].draw(painter, curr_x, start_y, digit_w, digit_h / 2.0, color)
146
+ curr_x += digit_w + colon_gap / 2
147
+
148
+ # Draw Colon 2
149
+ painter.drawEllipse(curr_x - radius, start_y + digit_h * 0.35 - radius, radius*2, radius*2)
150
+ painter.drawEllipse(curr_x - radius, start_y + digit_h * 0.65 - radius, radius*2, radius*2)
151
+ curr_x += colon_gap / 2
152
+
153
+ # Draw S1, S2
154
+ self.digits[3].draw(painter, curr_x, start_y, digit_w, digit_h / 2.0, color)
155
+ curr_x += digit_w + gap
156
+ self.digits[4].draw(painter, curr_x, start_y, digit_w, digit_h / 2.0, color)
157
+
158
+ def draw_face(self, painter):
159
+ # Draw 60 second markers (big for multiples of 5, small for others)
160
+ painter.save()
161
+ for i in range(60):
162
+ if i % 5 == 0:
163
+ painter.setBrush(self.black_color)
164
+ painter.setPen(Qt.NoPen)
165
+ painter.drawEllipse(QRectF(83, -4, 8, 8))
166
+ else:
167
+ painter.setBrush(self.black_color)
168
+ painter.setPen(Qt.NoPen)
169
+ painter.drawEllipse(QRectF(85, -2, 4, 4))
170
+ painter.rotate(6.0)
171
+ painter.restore()
172
+
173
+ # Draw numbers every 1 second outside
174
+ painter.save()
175
+ font = painter.font()
176
+ font.setFamily("Courier New")
177
+ font.setPointSize(4)
178
+ font.setBold(True)
179
+ painter.setFont(font)
180
+
181
+ window_text_color = self.palette().windowText().color()
182
+ painter.setPen(window_text_color)
183
+
184
+ for m in range(60):
185
+ angle_deg = -90 + m * 6
186
+ angle_rad = math.radians(angle_deg)
187
+ x = 108 * math.cos(angle_rad)
188
+ y = 108 * math.sin(angle_rad)
189
+
190
+ rect = QRectF(x - 10, y - 10, 20, 20)
191
+ painter.drawText(rect, Qt.AlignCenter, f"{m:02d}")
192
+ painter.restore()
193
+
194
+ def draw_hands(self, painter):
195
+ ms = self.get_elapsed_ms()
196
+
197
+ # Minute hand (smooth rotation)
198
+ minute_angle = (ms % 3600000) * (360.0 / 3600000.0)
199
+
200
+ if self.total_elapsed == 0 and not self.running:
201
+ hand_color = self.black_color
202
+ elif self.countdown_target_ms > 0:
203
+ if ms < 0:
204
+ hand_color = self.red_color
205
+ elif ms <= 0.10 * self.countdown_target_ms:
206
+ hand_color = self.orange_color
207
+ else:
208
+ hand_color = self.green_color
209
+ else:
210
+ hand_color = self.green_color
211
+
212
+ self.draw_capsule_hand(painter, minute_angle, hand_color, (-3, -75, 6, 95, 3.0, 3.0))
213
+
214
+ # Draw second hand (Swiss railway animation: wait at top for 1.5s)
215
+ sec_ms = ms % 60000
216
+
217
+ raw_elapsed = self.total_elapsed
218
+ if self.running:
219
+ raw_elapsed += self.elapsed_timer.elapsed()
220
+
221
+ if raw_elapsed >= 1500:
222
+ if self.countdown_target_ms > 0:
223
+ if sec_ms > 58500:
224
+ second_angle = 0.0
225
+ else:
226
+ second_angle = sec_ms * (360.0 / 60000.0)
227
+ else:
228
+ if sec_ms < 1500:
229
+ second_angle = 0.0
230
+ else:
231
+ second_angle = sec_ms * (360.0 / 60000.0)
232
+ else:
233
+ second_angle = sec_ms * (360.0 / 60000.0)
234
+
235
+ self.draw_swiss_second_hand(painter, second_angle)
236
+
237
+ def toggle_timer(self):
238
+ if self.running:
239
+ # Pause
240
+ self.total_elapsed += self.elapsed_timer.elapsed()
241
+ self.running = False
242
+ else:
243
+ # Start
244
+ self.elapsed_timer.start()
245
+ self.running = True
246
+ self.update()
247
+
248
+ def reset_timer(self):
249
+ self.running = False
250
+ self.total_elapsed = 0
251
+ self.update()
252
+
253
+ def set_countdown(self, ms):
254
+ self.countdown_target_ms = ms
255
+ self.reset_timer()
256
+ if hasattr(self.parent(), 'update_buttons'):
257
+ self.parent().update_buttons()
258
+
259
+ def get_elapsed_ms(self):
260
+ ms = self.total_elapsed
261
+ if self.running:
262
+ ms += self.elapsed_timer.elapsed()
263
+
264
+ if self.countdown_target_ms > 0:
265
+ return self.countdown_target_ms - ms
266
+ return ms
267
+
268
+ if __name__ == '__main__':
269
+ app = QApplication(sys.argv)
270
+ stopwatch = StopWatch()
271
+ stopwatch.resize(300, 300)
272
+ stopwatch.show()
273
+ sys.exit(app.exec())