-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwidget_utilities.py
More file actions
295 lines (226 loc) · 10.7 KB
/
Copy pathwidget_utilities.py
File metadata and controls
295 lines (226 loc) · 10.7 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
"""
Widget utilities base class for PySide6 widgets.
Provides standard initialization patterns and debug utilities.
"""
from PySide6 import QtWidgets, QtCore, QtGui
from contextlib import contextmanager
import sys
import base64
from io import BytesIO
from PIL import Image
import matplotlib.pyplot as plt
from loguru import logger
# For demo purposes, we'll redefine the class here in this notebook. In practical usage, you'd import:
# from pyside_refrances.widget_utilities import WidgetUtilities
# Helper functions:
def close_qt_windows():
"""
Close all visible top-level Qt windows.
Iterates through all top-level widgets in the application and closes
any that are currently visible. Useful for cleaning up during
interactive development in notebooks.
Returns:
int: Number of windows closed
"""
closed_count = 0
for widget in QtWidgets.QApplication.topLevelWidgets():
if widget.isVisible():
logger.debug(f"Closing widget: {widget.objectName() or widget.__class__.__name__}")
widget.close()
closed_count += 1
if closed_count:
logger.info(f"Closed {closed_count} Qt window(s)")
else:
logger.debug("No visible Qt windows to close")
return closed_count
# For demo purposes, we'll redefine the class here in this notebook. In practical usage, you'd import:
# from pyside_refrances.widget_utilities import WidgetUtilities
class WidgetUtilities:
"""
Abstract base class providing common widget utilities and initialization structure.
Provides debug tools for widget inspection and a standard initialization sequence
for GUI widgets. Subclasses must implement the abstract initialization methods.
"""
# ==================================
# === GUI Setup Abstract Methods ===
# ==================================
def _widget_startup_init_calls(self):
"""Call all GUI initialization methods in the proper sequence."""
self.gui_layout_init()
self.gui_sizing_init()
self.gui_styling_init()
self.gui_wiring_init()
def gui_layout_init(self):
"""Initialize and arrange all GUI elements."""
raise NotImplementedError("Subclass must implement gui_layout_init")
def gui_sizing_init(self):
"""Set widget size constraints and properties."""
raise NotImplementedError("Subclass must implement gui_sizing_init")
def gui_styling_init(self):
"""Apply widget styling and appearance settings."""
raise NotImplementedError("Subclass must implement gui_styling_init")
def gui_wiring_init(self):
"""Connect widget signals to their slots."""
raise NotImplementedError("Subclass must implement gui_wiring_init")
# ========================
# === Usage Utilities ===
# ========================
@contextmanager
def temp_block_widgets_signals(self, *widgets):
"""
Context manager to temporarily block signals for multiple widgets.
Blocks all signals from the specified widgets for the duration of the
context, then restores their previous signal state. Useful for preventing
circular signal emission when programmatically updating widget values.
Args:
*widgets: Variable number of QWidget instances to block
Example:
with self.temp_block_widgets_signals(self.lineEdit, self.comboBox):
self.lineEdit.setText("new value")
self.comboBox.setCurrentIndex(2)
# No signals emitted from these widgets during this block
Note:
If a widget was already blocking signals before entering the context,
it will remain blocked after exiting.
"""
# Store original signal blocking states
original_states = []
for widget in widgets:
if isinstance(widget, QtCore.QObject): # Check it's a Qt object
original_states.append((widget, widget.blockSignals(True)))
else:
logger.warning(f"Skipping non-QObject: {widget}")
original_states.append((widget, None))
try:
yield
finally:
# Restore original signal blocking states
for widget, original_state in original_states:
if original_state is not None: # Was a valid QObject
widget.blockSignals(original_state)
# ===================
# === Debug Tools ===
# ===================
def set_debug_tooltips(self):
"""
Set debug tooltips on all widgets showing their variable names.
If a widget already has a tooltip, appends debug info to it.
Otherwise, sets the debug tooltip directly.
"""
for attr_name in dir(self):
attr = getattr(self, attr_name, None)
if isinstance(attr, QtWidgets.QWidget):
debug_info = f"Widget: {attr_name}\nType: {type(attr).__name__}"
existing_tooltip = attr.toolTip()
if existing_tooltip:
# Append debug info to existing tooltip
attr.setToolTip(f"{existing_tooltip}\n\n--- Debug Info ---\n{debug_info}")
else:
# No existing tooltip, just set debug info
attr.setToolTip(debug_info)
if isinstance(self, QtWidgets.QWidget):
debug_info = f"Widget: {self.__class__.__name__}"
existing_tooltip = self.toolTip()
if existing_tooltip:
self.setToolTip(f"{existing_tooltip}\n\n--- Debug Info ---\n{debug_info}")
else:
self.setToolTip(debug_info)
def capture_widget_info(self):
"""
Capture widget images and locations for automation.
Iterates through all visible QWidget attributes and captures their
screen position, size, and appearance. Intended for future use with
pyautogui or similar automation tools.
Returns:
dict: Widget information indexed by attribute name. Each entry contains:
{
'location': tuple[int, int], # (x, y) global screen coordinates of the top-left corner
'size': tuple[int, int], # (width, height) in pixels
'image_base64': str, # Base64 encoded PNG image
'type': str # Widget class name (e.g., 'QPushButton')
}
Example:
>>> info = widget.capture_widget_info()
>>> info['self']['type'] # The parent widget itself
'FileFolderSelectorQWidget'
>>> info['submit_QPushButton']['location']
(100, 200)
"""
widget_info = {}
# First, capture the parent widget itself if it's a QWidget
if isinstance(self, QtWidgets.QWidget) and self.isVisible():
# Get screen position of widget's top-left corner
global_pos = self.mapToGlobal(QtCore.QPoint(0, 0))
location = (global_pos.x(), global_pos.y())
size = (self.width(), self.height())
# Create a pixmap (off-screen image) of the widget's size
pixmap = QtGui.QPixmap(self.size())
# Render the widget's visual appearance into the pixmap
self.render(pixmap)
# Convert pixmap to base64 string for storage/transmission
byte_array = QtCore.QByteArray()
buffer = QtCore.QBuffer(byte_array)
buffer.open(QtCore.QIODevice.WriteOnly)
pixmap.save(buffer, "PNG") # Save pixmap as PNG into buffer
base64_str = base64.b64encode(byte_array.data()).decode()
widget_info['self'] = {
'location': location,
'size': size,
'image_base64': base64_str,
'type': type(self).__name__
}
# Then capture all child widgets
for attr_name in dir(self):
attr = getattr(self, attr_name, None)
if isinstance(attr, QtWidgets.QWidget) and attr.isVisible():
# Get widget's position in global screen coordinates
global_pos = attr.mapToGlobal(QtCore.QPoint(0, 0))
location = (global_pos.x(), global_pos.y())
size = (attr.width(), attr.height())
# Create pixmap and render widget appearance
pixmap = QtGui.QPixmap(attr.size())
attr.render(pixmap)
# Convert to base64 for easy storage/transmission
byte_array = QtCore.QByteArray()
buffer = QtCore.QBuffer(byte_array)
buffer.open(QtCore.QIODevice.WriteOnly)
pixmap.save(buffer, "PNG")
base64_str = base64.b64encode(byte_array.data()).decode()
widget_info[attr_name] = {
'location': location,
'size': size,
'image_base64': base64_str,
'type': type(attr).__name__
}
return widget_info
@staticmethod
def show_widget_image(widget_info, widget_name):
"""
Display captured widget image with formatted metadata.
Args:
widget_info: Dictionary from capture_widget_info()
widget_name: Name of widget to display
"""
img_data = base64.b64decode(widget_info[widget_name]['image_base64'])
img = Image.open(BytesIO(img_data))
# Create figure with better spacing
fig, ax = plt.subplots(figsize=(8, 5))
# Display image
ax.imshow(img)
ax.axis('off')
# Add border around image
ax.add_patch(plt.Rectangle((0, 0), img.width-1, img.height-1,
fill=False, edgecolor='gray', linewidth=2))
# Format widget info
info = widget_info[widget_name]
# Main title - widget name and type
ax.set_title(f"{widget_name} ({info['type']})",
fontsize=14, fontweight='bold', pad=10)
# Subtitle with location and size info
location_text = f"Widget Top-Left corner location in screen space: {info['location']} • Size: {info['size'][0]}×{info['size'][1]}px"
fig.text(0.5, 0.92, location_text,
ha='center', fontsize=10, color='gray')
# Tighten layout
plt.tight_layout()
plt.subplots_adjust(top=0.85)
plt.show()