-
Notifications
You must be signed in to change notification settings - Fork 0
/
events.py
265 lines (222 loc) · 10.7 KB
/
events.py
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
# !/usr/bin/python
# coding=utf-8
import re
import logging
from PySide2 import QtCore, QtGui, QtWidgets
class EventFactoryFilter(QtCore.QObject):
"""Event filter for dynamic UI objects.
Forwards events to event handlers dynamically based on the event type.
Parameters:
parent (QWidget, optional): The parent widget for the event filter. Defaults to None.
"""
def __init__(self, parent=None, forward_events_to=None, event_name_prefix=""):
super().__init__(parent)
self.forward_events_to = forward_events_to or self
self.event_name_prefix = event_name_prefix
@staticmethod
def format_event_name(event_type, prefix=""):
"""Get a formatted event method name string from a given event type using a regular expression.
Parameters:
event_type (QEvent.Type): The event type whose method name needs to be generated.
prefix (str, optional): A prefix for the event method names. Defaults to an empty string.
Returns:
str: The formatted event method name.
Examples:
format_event_name(QtCore.QEvent.Type.Enter) returns 'enterEvent'
format_event_name(QtCore.QEvent.Type.MouseButtonPress) returns 'mousePressEvent'
format_event_name(QtCore.QEvent.Type.Enter, prefix='ef_') returns 'ef_enterEvent'
format_event_name(QtCore.QEvent.Type.MouseButtonPress, prefix='ef_') returns 'ef_mousePressEvent'
"""
event_name = re.sub(
r"^.*\.([A-Z])([^B]*)(?:Button)?(.*)$",
lambda m: prefix + m.group(1).lower() + m.group(2) + m.group(3) + "Event",
str(event_type),
)
return event_name
def eventFilter(self, widget, event):
"""Forward widget events to event handlers.
For any event type, the eventFilter will try to connect to a corresponding
method derived from the event type string.
Parameters:
widget (QWidget): The widget that the event filter is applied to.
event (QEvent): The event that needs to be processed.
Returns:
bool: True if the event was handled, False otherwise.
"""
try:
event_handler = getattr(
self.forward_events_to,
self.format_event_name(event.type(), self.event_name_prefix),
)
event_handled = event_handler(widget, event)
if event_handled:
return True
except AttributeError:
pass
return False
class MouseTracking(QtCore.QObject):
"""MouseTracking is a QObject subclass that provides mouse enter and leave events for QWidget child widgets.
It uses event filtering to track the mouse movement and send enter and leave events to the child widgets.
Attributes:
_prev_mouse_over (list): List of widgets that were previously under the mouse cursor.
_mouse_over (list): List of widgets that are currently under the mouse cursor.
_filtered_widgets (set): Set of widgets that have been processed for special handling (widgets with a viewport).
logger (Logger): Instance of a logger that logs mouse tracking events.
Methods:
eventFilter(self, widget, event): Filters events to track mouse move events and button press/release events.
should_capture_mouse(self, widget): Checks if a widget should capture the mouse.
track(self): Updates tracking data and sends enter, leave, and release events to widgets.
Parameters:
parent (QWidget): Parent widget for the MouseTracking object.
log_level (int, optional): Logging level. Defaults to logging.WARNING.
Raises:
TypeError: If parent is not a QWidget derived type.
"""
def __init__(self, parent, log_level=logging.WARNING):
super().__init__(parent)
if not isinstance(parent, QtWidgets.QWidget):
raise TypeError("Parent must be a QWidget derived type")
self._initialize_logger(log_level)
self._prev_mouse_over = []
self._mouse_over = []
self._filtered_widgets = set()
parent.installEventFilter(self)
def _initialize_logger(self, log_level):
"""Initializes logger."""
self.logger = logging.getLogger(__name__)
self.logger.setLevel(log_level)
handler = logging.StreamHandler()
handler.setFormatter(
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)
self.logger.addHandler(handler)
def should_capture_mouse(self, widget):
"""Checks if a widget should capture the mouse."""
widget_conditions = [
(QtWidgets.QPushButton, lambda widget: not widget.isDown()),
(QtWidgets.QComboBox, lambda widget: not widget.view().isVisible()),
(QtWidgets.QSlider, lambda widget: not widget.isSliderDown()),
(QtWidgets.QScrollBar, lambda widget: not widget.isSliderDown()),
]
for widget_type, condition in widget_conditions:
if isinstance(widget, widget_type) and condition(widget):
self.logger.debug(
f"Not capturing mouse for {widget_type.__name__} under specified condition"
)
return False
return True
def _update_widgets_under_cursor(self):
"""Updates the list of widgets currently under the cursor."""
self._get_child_widgets()
top_widget = QtWidgets.QApplication.widgetAt(QtGui.QCursor.pos())
self._mouse_over = (
[top_widget] if top_widget and top_widget in self._widgets else []
)
self.logger.debug(
f"Widgets under cursor: {[f'{w}, {type(w)}' for w in self._mouse_over]}"
)
def _get_child_widgets(self):
"""Updates the list of child widgets of the parent."""
parent = self.parent()
self._widgets = (
parent.currentWidget().findChildren(QtWidgets.QWidget)
if isinstance(parent, QtWidgets.QStackedWidget)
else parent.findChildren(QtWidgets.QWidget)
)
def track(self):
"""Updates tracking data and sends enter and leave events to widgets."""
self.logger.info(f"Previous widgets under cursor: {self._prev_mouse_over}")
self.logger.info(f"Current widgets under cursor: {self._mouse_over}")
self._release_mouse_for_widgets(self._mouse_over)
self._update_widgets_under_cursor()
for widget in self._prev_mouse_over:
if widget not in self._mouse_over:
self._send_leave_event(widget)
for widget in self._mouse_over:
if widget not in self._prev_mouse_over:
self._send_enter_event(widget)
self._handle_mouse_grab()
self._prev_mouse_over = self._mouse_over.copy()
self._filter_viewport_widgets()
def _release_mouse_for_widgets(self, widgets):
"""Releases mouse for given widgets."""
for widget in widgets:
widget.releaseMouse()
def _send_leave_event(self, widget):
"""Sends a leave event to a widget."""
self.logger.info(
f"Sending Leave event to: {widget}, Name: {widget.objectName()}, Parent: {widget.parent().objectName()}"
)
QtGui.QGuiApplication.sendEvent(widget, QtCore.QEvent(QtCore.QEvent.Leave))
def _send_enter_event(self, widget):
"""Sends an enter event to a widget."""
self.logger.info(
f"Sending Enter event to: {widget}, Name: {widget.objectName()}, Parent: {widget.parent().objectName()}"
)
QtGui.QGuiApplication.sendEvent(widget, QtCore.QEvent(QtCore.QEvent.Enter))
def _send_release_event(self, widget, button):
"""Sends a release event to a widget."""
self.logger.info(
f"Sending Release event to: {widget}, Name: {widget.objectName()}, Parent: {widget.parent().objectName()}"
)
release_event = QtGui.QMouseEvent(
QtCore.QEvent.MouseButtonRelease,
QtGui.QCursor.pos(),
button,
button,
QtCore.Qt.NoModifier,
)
QtGui.QGuiApplication.postEvent(widget, release_event)
def _handle_mouse_grab(self):
"""Handles mouse grabbing depending on the widget currently under the cursor."""
top_widget = QtWidgets.QApplication.widgetAt(QtGui.QCursor.pos())
if top_widget:
self.logger.debug(f"Top widget under cursor: {top_widget}")
widget_to_grab = (
top_widget
if self.should_capture_mouse(top_widget)
else QtWidgets.QApplication.activeWindow()
)
self.logger.info(f"Grabbing mouse for widget: {widget_to_grab}")
widget_to_grab.grabMouse()
else:
self.logger.debug(
"No widget under cursor. Grabbing mouse for active window."
)
QtWidgets.QApplication.activeWindow().grabMouse()
def _filter_viewport_widgets(self):
"""Adds special handling for widgets with a viewport."""
for widget in self._widgets:
if hasattr(widget, "viewport") and widget not in self._filtered_widgets:
self._filtered_widgets.add(widget)
self._handle_viewport_widget(widget)
def _handle_viewport_widget(self, widget):
"""Ignores mouse move events for widgets with a viewport."""
original_mouse_move_event = widget.mouseMoveEvent
widget.mouseMoveEvent = lambda event: (
original_mouse_move_event(event),
event.ignore(),
)
def eventFilter(self, widget, event):
"""Calls `track` on each mouse move event and also tracks button press/release for QAbstractButton."""
if event.type() == QtCore.QEvent.MouseMove:
self.logger.info(
f"MouseMove event filter triggered by: {widget} with event: {event.type()}"
)
self.track()
elif event.type() == QtCore.QEvent.MouseButtonRelease:
top_widget = QtWidgets.QApplication.widgetAt(QtGui.QCursor.pos())
if (
isinstance(top_widget, QtWidgets.QAbstractButton)
and not top_widget.isDown()
):
self.logger.info(
f"Mouse button release event detected on: {top_widget}"
)
self._send_release_event(top_widget, event.button())
return super().eventFilter(widget, event)
# --------------------------------------------------------------------------------------------
logging.info(__name__) # module name
# --------------------------------------------------------------------------------------------
# Notes
# --------------------------------------------------------------------------------------------