-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuttons_dialog.py
More file actions
168 lines (145 loc) · 5.68 KB
/
buttons_dialog.py
File metadata and controls
168 lines (145 loc) · 5.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
from dataclasses import dataclass
from typing import Callable
from PySide6.QtWidgets import (
QDialog,
QWidget,
QVBoxLayout,
QPushButton,
QLabel
)
from PySide6.QtCore import QTimer, Qt
@dataclass
class ButtonSpec:
"""Specification for a dialog button."""
label: str
callable: Callable
enabled: bool = True
class ButtonsDialog(QDialog):
"""
Base modal dialog with buttons that execute immediately on click and closes dialog.
Cancel button or escape hotkey closes dialog.
"""
def __init__(
self,
parent: QWidget,
window_title: str,
button_specs: list[ButtonSpec],
processing_message: str = "",
with_hotkeys: bool = True,
on_complete: Callable | None = None
):
"""
Args:
parent: The parent window (typically App instance)
window_title: Title for the dialog window
processing_message: Message to show while processing
button_specs: List of ButtonSpec objects
on_complete: Callback invoked after an operation completes
"""
super().__init__(parent)
self._on_complete = on_complete
self._processing_message = processing_message
self._with_hotkeys = with_hotkeys
self._button_specs: list[ButtonSpec] = button_specs
self._buttons: list[tuple[QPushButton, ButtonSpec]] = []
self.setWindowTitle(window_title)
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumWidth(200)
self._create_widgets()
self.adjustSize() # Size to fit content
def _create_widgets(self) -> None:
"""Create all dialog widgets."""
# Main layout
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5)
# Operation buttons
for index, spec in enumerate(self._button_specs):
# Prepend hotkey label if enabled
if self._with_hotkeys and index < 9:
display_text = f"[{index + 1}] {spec.label}"
else:
display_text = spec.label
btn = QPushButton(display_text)
btn.setStyleSheet("text-align: left; padding: 0 20px;")
# Disable button if spec says so
if not spec.enabled:
btn.setEnabled(False)
else:
btn.clicked.connect(
lambda checked, b=btn, s=spec: self._on_operation_click(b, s)
)
self._buttons.append((btn, spec))
layout.addWidget(btn)
# Status label (hidden initially, shown when needed)
self._status_label = QLabel("")
self._status_label.setStyleSheet("color: gray;")
self._status_label.hide()
layout.addWidget(self._status_label)
# Cancel button
self._add_cancel_button(layout)
def _add_cancel_button(self, layout: QVBoxLayout) -> None:
"""Add cancel button to the layout."""
cancel_btn = QPushButton("Cancel")
cancel_btn.setStyleSheet("background-color: gray;")
cancel_btn.clicked.connect(self._close)
layout.addWidget(cancel_btn)
def _on_operation_click(
self, button: QPushButton, spec: ButtonSpec, _must_confirm: bool = False
) -> None:
"""Handle operation button click - executes immediately.
Args:
button: The button that was clicked
spec: The ButtonSpec for this button
_must_confirm: Ignored in base class (used by subclass)
"""
if self._processing_message:
self._status_label.setText(self._processing_message)
self._status_label.show()
QTimer.singleShot(100, lambda: self._execute_callback(spec.callable))
def _execute_callback(self, callback: Callable) -> None:
"""Execute the callback and close the dialog."""
callback()
if self._on_complete:
self._on_complete()
self.accept() # Close dialog with Accepted result
def _close(self) -> None:
"""Close the dialog without executing any operation."""
self.reject() # Close dialog with Rejected result
def keyPressEvent(self, event):
"""Handle Escape key to close dialog and digit keys for button hotkeys."""
if event.key() == Qt.Key.Key_Escape:
self._close()
elif self._with_hotkeys:
# Handle digit keys 1-9 for button hotkeys
digit = None
if event.key() == Qt.Key.Key_1:
digit = 1
elif event.key() == Qt.Key.Key_2:
digit = 2
elif event.key() == Qt.Key.Key_3:
digit = 3
elif event.key() == Qt.Key.Key_4:
digit = 4
elif event.key() == Qt.Key.Key_5:
digit = 5
elif event.key() == Qt.Key.Key_6:
digit = 6
elif event.key() == Qt.Key.Key_7:
digit = 7
elif event.key() == Qt.Key.Key_8:
digit = 8
elif event.key() == Qt.Key.Key_9:
digit = 9
if digit is not None and digit <= len(self._buttons):
# Get button and spec
button, spec = self._buttons[digit - 1]
# Only trigger if button is enabled
if spec.enabled:
self._on_operation_click(button, spec)
else:
super().keyPressEvent(event)
else:
super().keyPressEvent(event)
else:
super().keyPressEvent(event)