-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.py
More file actions
380 lines (321 loc) · 12.3 KB
/
utils.py
File metadata and controls
380 lines (321 loc) · 12.3 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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
"""Utility functions for RetroClamp.
This module provides common utility functions used throughout the application.
"""
import logging
import os
from typing import Any, List, Optional, Tuple, TypeVar, cast
from PySide6.QtCore import QBuffer, QByteArray, QIODevice, Qt
from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtWidgets import (
QApplication, # Added import for QApplication
QFileDialog,
QInputDialog,
QLineEdit, # Added import for QLineEdit to use QLineEdit.EchoMode
QMessageBox,
QWidget,
)
# Type variable for generic type hinting
T = TypeVar("T")
def setup_logging(level=logging.INFO, log_file: Optional[str] = None) -> None:
"""Set up basic logging configuration.
Args:
level: Logging level (default: logging.INFO)
log_file: Optional path to log file. If None, logs to console only.
"""
handlers: List[logging.Handler] = [logging.StreamHandler()]
if log_file:
os.makedirs(os.path.dirname(os.path.abspath(log_file)), exist_ok=True)
handlers.append(logging.FileHandler(log_file))
logging.basicConfig(
level=level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=handlers,
)
# Helper function to satisfy mypy's strict parent widget requirement
def _get_effective_parent(parent: Optional[QWidget]) -> QWidget:
"""
Returns the provided parent if not None, otherwise QApplication.activeWindow().
This helps satisfy mypy's strictness for QWidget arguments in some static methods
while still allowing Optional[QWidget] in the function signature.
"""
if parent is None:
# Fallback to the currently active window if no parent is explicitly provided.
# This is a common pattern for dialogs that need a parent for modality/positioning.
active_window = QApplication.activeWindow()
if active_window:
return active_window
# As a last resort, if no active window is available, create a dummy widget.
# This case should be rare in a running application.
return QWidget()
return parent
def show_info(
message: str, title: str = "Information", parent: Optional[QWidget] = None
) -> None:
"""Show an information message box.
Args:
message: Message to display
title: Window title (default: "Information")
parent: Parent widget (optional)
"""
# Fix: Pass the effective parent to satisfy mypy
QMessageBox.information(_get_effective_parent(parent), title, message)
def show_error(
message: str, title: str = "Error", parent: Optional[QWidget] = None
) -> None:
"""Show an error message box.
Args:
message: Error message to display
title: Window title (default: "Error")
parent: Parent widget (optional)
"""
# Fix: Pass the effective parent to satisfy mypy
QMessageBox.critical(_get_effective_parent(parent), title, message)
def show_warning(
message: str, title: str = "Warning", parent: Optional[QWidget] = None
) -> None:
"""Show a warning message box.
Args:
message: Warning message to display
title: Window title (default: "Warning")
parent: Parent widget (optional)
"""
# Fix: Pass the effective parent to satisfy mypy
QMessageBox.warning(_get_effective_parent(parent), title, message)
def show_question(
message: str,
title: str = "Confirm",
# Fix: Use QMessageBox.StandardButton enum for buttons
buttons: QMessageBox.StandardButton = QMessageBox.StandardButton.Yes
| QMessageBox.StandardButton.No,
# Fix: Use QMessageBox.StandardButton enum for default_button
default_button: QMessageBox.StandardButton = QMessageBox.StandardButton.NoButton,
parent: Optional[QWidget] = None,
) -> QMessageBox.StandardButton:
"""Show a question dialog with Yes/No buttons.
Args:
message: Question to ask
title: Window title (default: "Confirm")
buttons: Buttons to show (default: Yes/No)
default_button: Default button (default: NoButton)
parent: Parent widget (optional)
Returns:
QMessageBox.StandardButton: The button that was clicked
"""
# Fix: Pass the effective parent to satisfy mypy
return QMessageBox.question(
_get_effective_parent(parent), title, message, buttons, default_button
)
def get_open_file_name(
parent: Optional[QWidget] = None,
caption: str = "Open File",
dir: str = "",
filter: str = "All Files (*)",
selected_filter: str = "",
# Fix: Type hint must be Optional because default is None (mypy's no_implicit_optional)
options: Optional[QFileDialog.Option] = None,
) -> Tuple[str, str]:
"""Show a file open dialog and return the selected file.
Args:
parent: Parent widget (optional)
caption: Dialog caption (default: "Open File")
dir: Starting directory (default: current directory)
filter: File filter string (default: "All Files (*)")
selected_filter: The selected filter (default: "")
options: Additional options (default: 0)
Returns:
Tuple[str, str]: (selected file, selected filter) or ("", "") if canceled
"""
if options is None:
options = QFileDialog.Option(0)
result = QFileDialog.getOpenFileName(
parent, caption, dir, filter, selected_filter, options
)
return cast(Tuple[str, str], result)
def get_save_file_name(
parent: Optional[QWidget] = None,
caption: str = "Save File",
dir: str = "",
filter: str = "All Files (*)",
selected_filter: str = "",
# Fix: Type hint must be Optional because default is None
options: Optional[QFileDialog.Option] = None,
) -> Tuple[str, str]:
"""Show a file save dialog and return the selected file.
Args:
parent: Parent widget (optional)
caption: Dialog caption (default: "Save File")
dir: Starting directory (default: current directory)
filter: File filter string (default: "All Files (*)")
selected_filter: The selected filter (default: "")
options: Additional options (default: 0)
Returns:
Tuple[str, str]: (selected file, selected filter) or ("", "") if canceled
"""
if options is None:
options = QFileDialog.Option(0)
result = QFileDialog.getSaveFileName(
parent, caption, dir, filter, selected_filter, options
)
return cast(Tuple[str, str], result)
def get_existing_directory(
parent: Optional[QWidget] = None,
caption: str = "Select Directory",
dir: str = "",
# Fix: Type hint must be Optional because default is None
options: Optional[QFileDialog.Option] = None,
) -> str:
"""Show a directory selection dialog and return the selected directory.
Args:
parent: Parent widget (optional)
caption: Dialog caption (default: "Select Directory")
dir: Starting directory (default: current directory)
options: Additional options (default: ShowDirsOnly)
Returns:
str: Selected directory or "" if canceled
"""
if options is None:
options = QFileDialog.Option(QFileDialog.Option.ShowDirsOnly)
return str(QFileDialog.getExistingDirectory(parent, caption, dir, options))
def get_text_input(
parent: Optional[QWidget] = None,
title: str = "Input",
label: str = "Enter text:",
text: str = "",
# Fix: Use QLineEdit.EchoMode enum instead of int
echo: QLineEdit.EchoMode = QLineEdit.EchoMode.Normal,
# Fix: Type hint must be Optional because default is None
flags: Optional[Qt.WindowType] = None,
# Fix: Type hint must be Optional because default is None
input_method_hints: Optional[Qt.InputMethodHint] = None,
) -> str:
"""Show a text input dialog and return the entered text.
Args:
parent: Parent widget (optional)
title: Dialog title (default: "Input")
label: Input label (default: "Enter text:")
text: Default text (default: "")
echo: Echo mode (default: QLineEdit.Normal)
flags: Window flags (default: 0)
input_method_hints: Input method hints (default: ImhNone)
Returns:
str: Entered text or "" if canceled
"""
if flags is None:
flags = Qt.WindowType(0)
if input_method_hints is None:
input_method_hints = Qt.InputMethodHint(0)
# Fix: Pass the effective parent to satisfy mypy
text, ok = QInputDialog.getText(
_get_effective_parent(parent),
title,
label,
echo,
text,
flags,
input_method_hints,
)
return str(text) if ok else ""
def get_item_input(
parent: Optional[QWidget] = None,
title: str = "Select Item",
label: str = "Select an item:",
items: Optional[List[Any]] = None,
current: int = 0,
editable: bool = True,
# Fix: Type hint must be Optional because default is None
flags: Optional[Qt.WindowType] = None,
# Fix: Type hint must be Optional because default is None
input_method_hints: Optional[Qt.InputMethodHint] = None,
) -> Tuple[str, bool]:
"""Show an item selection dialog and return the selected item.
Args:
parent: Parent widget (optional)
title: Dialog title (default: "Select Item")
label: Selection label (default: "Select an item:")
items: List of items to select from
current: Index of current item (default: 0)
editable: Whether the input is editable (default: True)
flags: Window flags (default: 0)
input_method_hints: Input method hints (default: ImhNone)
Returns:
Tuple[str, bool]: (selected item, ok) where ok is True if user pressed OK
"""
if items is None:
items = []
if flags is None:
flags = Qt.WindowType(0)
if input_method_hints is None:
input_method_hints = Qt.InputMethodHint.ImhNone
# Fix: Pass the effective parent to satisfy mypy
result = QInputDialog.getItem(
_get_effective_parent(parent),
title,
label,
items,
current,
editable,
flags,
input_method_hints,
)
return (str(result[0]), bool(result[1]))
def create_icon_from_svg(svg_data: str, color: Optional[QColor] = None) -> QIcon:
"""Create a QIcon from SVG data with optional color.
Args:
svg_data: SVG data as a string
color: Optional color to apply to the icon
Returns:
QIcon: The created icon
"""
# Create a buffer with the SVG data
svg_bytes = QByteArray(svg_data.encode("utf-8"))
buffer = QBuffer(svg_bytes)
# Fix: Use QIODevice.OpenModeFlag enum for ReadOnly
buffer.open(QIODevice.OpenModeFlag.ReadOnly)
# Create a pixmap from the SVG
renderer = QSvgRenderer()
# Fix: QSvgRenderer.load expects QByteArray or str, not QBuffer
if not renderer.load(svg_bytes):
return QIcon()
# Create a pixmap and paint the SVG onto it
pixmap = QPixmap(renderer.defaultSize())
# Fix: Use Qt.GlobalColor enum for transparent
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
# Apply color if specified
if color is not None:
image = pixmap.toImage()
for x in range(image.width()):
for y in range(image.height()):
pixel_color = image.pixelColor(x, y)
if pixel_color.alpha() > 0: # Only recolor non-transparent pixels
# Preserve original alpha when applying new color
new_color = QColor(
color.red(), color.green(), color.blue(), pixel_color.alpha()
)
image.setPixelColor(x, y, new_color)
pixmap = QPixmap.fromImage(image)
return QIcon(pixmap)
def human_readable_size(size_bytes: int) -> str:
"""Convert a size in bytes to a human-readable string.
Args:
size_bytes: Size in bytes
Returns:
str: Human-readable size string (e.g., "1.5 MB")
"""
if size_bytes == 0:
return "0 B"
units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
i = 0
size = float(size_bytes)
while size >= 1024 and i < len(units) - 1:
size /= 1024.0
i += 1
# Fix: Format the 'size' variable (float) to 2 decimal places for larger units.
# For 'B' (bytes), format as an integer.
if i == 0:
return f"{int(size)} {units[i]}"
else:
return f"{size:.2f} {units[i]}"