Skip to content

Commit 7dcd6ae

Browse files
Fix qt 6.3 keyboard events (#284)
* Fix keyboard events not coming in by setting focus policy * Simplify Qt lib import logic * fix handle_event Co-authored-by: Almar Klein <[email protected]>
1 parent aa382b9 commit 7dcd6ae

File tree

6 files changed

+57
-39
lines changed

6 files changed

+57
-39
lines changed

examples/triangle_qt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# For the sake of making this example Just Work, we try multiple QT libs
1010
for lib in ("PySide6", "PyQt6", "PySide2", "PyQt5"):
1111
try:
12-
QtWidgets = importlib.import_module(f"{lib}.QtWidgets")
12+
QtWidgets = importlib.import_module(".QtWidgets", lib)
1313
break
1414
except ModuleNotFoundError:
1515
pass

examples/triangle_qt_embed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# For the sake of making this example Just Work, we try multiple QT libs
1010
for lib in ("PySide6", "PyQt6", "PySide2", "PyQt5"):
1111
try:
12-
QtWidgets = importlib.import_module(f"{lib}.QtWidgets")
12+
QtWidgets = importlib.import_module(".QtWidgets", lib)
1313
break
1414
except ModuleNotFoundError:
1515
pass

wgpu/gui/base.py

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -216,41 +216,36 @@ def _handle_event_rate_limited(self, ev, call_later_func, match_keys, accum_keys
216216
pending event, the old event is dispatched now. The `accum_keys` keys of
217217
the current and new event are added together (e.g. to accumulate wheel delta).
218218
219-
This method is called in the following cases:
219+
The (accumulated) event is handled in the following cases:
220220
* When the timer runs out.
221221
* When a non-rate-limited event is dispatched.
222222
* When a rate-limited event of the same type is scheduled
223223
that has different match_keys (e.g. modifiers changes).
224+
225+
Subclasses that use this method must use ``_handle_event_and_flush()``
226+
where they would otherwise call ``handle_event()``, to preserve event order.
224227
"""
225228
event_type = ev["event_type"]
226229
# We may need to emit the old event. Otherwise, we need to update the new one.
227230
old = self._pending_events.get(event_type, None)
228231
if old:
229232
if any(ev[key] != old[key] for key in match_keys):
230-
self._dispatch_event(old)
233+
self.handle_event(old)
231234
else:
232235
for key in accum_keys:
233236
ev[key] = old[key] + ev[key]
234237
# Make sure that we have scheduled a moment to handle events
235238
if not self._pending_events:
236-
call_later_func(self._get_event_wait_time(), self._dispatch_pending_events)
239+
call_later_func(self._get_event_wait_time(), self._handle_pending_events)
237240
# Store the event object
238241
self._pending_events[event_type] = ev
239242

240-
def handle_event(self, event):
241-
"""Handle an incoming event.
243+
def _handle_event_and_flush(self, event):
244+
"""Call handle_event after flushing any pending (rate-limited) events."""
245+
self._handle_pending_events()
246+
self.handle_event(event)
242247

243-
Subclasses can overload this method. Events include widget
244-
resize, mouse/touch interaction, key events, and more. An event
245-
is a dict with at least the key event_type. For details, see
246-
https://jupyter-rfb.readthedocs.io/en/latest/events.html
247-
"""
248-
# On any not-rate-limited event, we dispatch any pending events.
249-
# This is to make sure that the original order of events is preserved.
250-
self._dispatch_pending_events()
251-
self._dispatch_event(event)
252-
253-
def _dispatch_pending_events(self):
248+
def _handle_pending_events(self):
254249
"""Handle any pending rate-limited events."""
255250
if self._pending_events:
256251
events = self._pending_events.values()
@@ -259,10 +254,22 @@ def _dispatch_pending_events(self):
259254
for ev in events:
260255
self.handle_event(ev)
261256

262-
def _dispatch_event(self, event):
263-
"""Dispatch event to the event handlers."""
257+
def handle_event(self, event):
258+
"""Handle an incoming event.
259+
260+
Subclasses can overload this method. Events include widget
261+
resize, mouse/touch interaction, key events, and more. An event
262+
is a dict with at least the key event_type. For details, see
263+
https://jupyter-rfb.readthedocs.io/en/latest/events.html
264+
265+
The default implementation dispatches the event to the
266+
registered event handlers.
267+
"""
268+
# Collect callbacks
264269
event_type = event.get("event_type")
265-
for callback in self._event_handlers[event_type].copy():
270+
callbacks = self._event_handlers[event_type] | self._event_handlers["*"]
271+
# Dispatch
272+
for callback in callbacks:
266273
with log_exception(f"Error during handling {event['event_type']} event"):
267274
callback(event)
268275

@@ -295,11 +302,24 @@ def my_handler(event):
295302
@canvas.add_event_handler("pointer_up", "pointer_down")
296303
def my_handler(event):
297304
print(event)
305+
306+
Catch 'm all:
307+
308+
.. code-block:: py
309+
310+
canvas.add_event_handler(my_handler, "*")
311+
298312
"""
299313
decorating = not callable(args[0])
300314
callback = None if decorating else args[0]
301315
types = args if decorating else args[1:]
302316

317+
if not types:
318+
raise TypeError("No event types are given to add_event_handler.")
319+
for type in types:
320+
if not isinstance(type, str):
321+
raise TypeError(f"Event types must be str, but got {type}")
322+
303323
def decorator(_callback):
304324
for type in types:
305325
self._event_handlers[type].add(_callback)

wgpu/gui/glfw.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def _on_size_change(self, *args):
201201
def _on_close(self, *args):
202202
all_glfw_canvases.discard(self)
203203
glfw.hide_window(self._window)
204-
self.handle_event({"event_type": "close"})
204+
self._handle_event_and_flush({"event_type": "close"})
205205

206206
def _on_window_dirty(self, *args):
207207
self._request_draw()
@@ -234,7 +234,7 @@ def _determine_size(self):
234234
"height": self._logical_size[1],
235235
"pixel_ratio": self._pixel_ratio,
236236
}
237-
self.handle_event(ev)
237+
self._handle_event_and_flush(ev)
238238

239239
def _set_logical_size(self, new_logical_size):
240240
# There is unclarity about the window size in "screen pixels".
@@ -358,7 +358,7 @@ def _on_mouse_button(self, window, but, action, mods):
358358
}
359359

360360
# Emit the current event
361-
self.handle_event(ev)
361+
self._handle_event_and_flush(ev)
362362

363363
# Maybe emit a double-click event
364364
self._follow_double_click(action, button)
@@ -410,7 +410,7 @@ def _follow_double_click(self, action, button):
410410
"ntouches": 0, # glfw dows not have touch support
411411
"touches": {},
412412
}
413-
self.handle_event(ev)
413+
self._handle_event_and_flush(ev)
414414

415415
def _on_cursor_pos(self, window, x, y):
416416
# Store pointer position in logical coordinates
@@ -483,7 +483,7 @@ def _on_key(self, window, key, scancode, action, mods):
483483
"key": keyname,
484484
"modifiers": list(self._key_modifiers),
485485
}
486-
self.handle_event(ev)
486+
self._handle_event_and_flush(ev)
487487

488488

489489
# Make available under a name that is the same for all gui backends

wgpu/gui/qt.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@
1313
# Select GUI toolkit
1414
for libname in ("PySide6", "PyQt6", "PySide2", "PyQt5"):
1515
if libname in sys.modules:
16-
QtCore = importlib.import_module(libname + ".QtCore")
17-
widgets_modname = "QtGui" if QtCore.qVersion()[0] == "4" else "QtWidgets"
18-
QtWidgets = importlib.import_module(libname + "." + widgets_modname)
16+
QtCore = importlib.import_module(".QtCore", libname)
17+
QtWidgets = importlib.import_module(".QtWidgets", libname)
1918
try:
2019
WA_PaintOnScreen = QtCore.Qt.WidgetAttribute.WA_PaintOnScreen
2120
PreciseTimer = QtCore.Qt.TimerType.PreciseTimer
2221
KeyboardModifiers = QtCore.Qt.KeyboardModifier
22+
FocusPolicy = QtCore.Qt.FocusPolicy
2323
Keys = QtCore.Qt.Key
2424
except AttributeError:
2525
WA_PaintOnScreen = QtCore.Qt.WA_PaintOnScreen
2626
PreciseTimer = QtCore.Qt.PreciseTimer
2727
KeyboardModifiers = QtCore.Qt
28+
FocusPolicy = QtCore.Qt
2829
Keys = QtCore.Qt
2930
break
3031
else:
@@ -126,6 +127,7 @@ def __init__(self, *args, **kwargs):
126127
self.setAttribute(WA_PaintOnScreen, True)
127128
self.setAutoFillBackground(False)
128129
self.setMouseTracking(True)
130+
self.setFocusPolicy(FocusPolicy.StrongFocus)
129131

130132
# A timer for limiting fps
131133
self._request_draw_timer = QtCore.QTimer()
@@ -209,7 +211,7 @@ def _key_event(self, event_type, event):
209211
"key": KEY_MAP.get(event.key(), event.text()),
210212
"modifiers": modifiers,
211213
}
212-
self.handle_event(ev)
214+
self._handle_event_and_flush(ev)
213215

214216
def keyPressEvent(self, event): # noqa: N802
215217
self._key_event("key_down", event)
@@ -253,7 +255,7 @@ def _mouse_event(self, event_type, event, touches=True):
253255
accum_keys = {}
254256
self._handle_event_rate_limited(ev, call_later, match_keys, accum_keys)
255257
else:
256-
self.handle_event(ev)
258+
self._handle_event_and_flush(ev)
257259

258260
def mousePressEvent(self, event): # noqa: N802
259261
self._mouse_event("pointer_down", event)
@@ -295,10 +297,10 @@ def resizeEvent(self, event): # noqa: N802
295297
"height": float(event.size().height()),
296298
"pixel_ratio": self.get_pixel_ratio(),
297299
}
298-
self.handle_event(ev)
300+
self._handle_event_and_flush(ev)
299301

300302
def closeEvent(self, event): # noqa: N802
301-
self.handle_event({"event_type": "close"})
303+
self._handle_event_and_flush({"event_type": "close"})
302304

303305

304306
class QWgpuCanvas(WgpuAutoGui, WgpuCanvasBase, QtWidgets.QWidget):
@@ -321,6 +323,7 @@ def __init__(self, *, size=None, title=None, max_fps=30, **kwargs):
321323
self.setMouseTracking(True)
322324

323325
self._subwidget = QWgpuWidget(self, max_fps=max_fps)
326+
self._subwidget.add_event_handler(self.handle_event, "*")
324327

325328
layout = QtWidgets.QHBoxLayout(self)
326329
layout.setContentsMargins(0, 0, 0, 0)
@@ -374,12 +377,6 @@ def get_context(self, *args, **kwargs):
374377
def request_draw(self, *args, **kwargs):
375378
return self._subwidget.request_draw(*args, **kwargs)
376379

377-
def add_event_handler(self, *args, **kwargs):
378-
return self._subwidget.add_event_handler(*args, **kwargs)
379-
380-
def remove_event_handler(self, *args, **kwargs):
381-
return self._subwidget.remove_event_handler(*args, **kwargs)
382-
383380

384381
# Make available under a name that is the same for all gui backends
385382
WgpuWidget = QWgpuWidget

wgpu/gui/wx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def __init__(self, *, parent=None, size=None, title=None, max_fps=30, **kwargs):
120120
self.SetTitle(title or "wx wgpu canvas")
121121

122122
self._subwidget = WxWgpuWindow(parent=self, max_fps=max_fps)
123+
self._subwidget.add_event_handler(self.handle_event, "*")
123124
self.Bind(wx.EVT_CLOSE, lambda e: self.Destroy())
124125

125126
self.Show()

0 commit comments

Comments
 (0)