Skip to content

Commit

Permalink
Merge pull request #2456 from aganders3/qnativegestureevent
Browse files Browse the repository at this point in the history
  • Loading branch information
djhoese committed May 2, 2023
2 parents 525504a + d6e31b5 commit ce20572
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 56 deletions.
131 changes: 78 additions & 53 deletions vispy/app/backends/_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from __future__ import division

from time import sleep, time
import math
import os
import sys
import atexit
Expand Down Expand Up @@ -410,17 +411,10 @@ def __init__(self, vispy_canvas, **kwargs):
# either not PyQt5 backend or no parent window available
pass

# Activate touch and gesture.
# NOTE: we only activate touch on OS X because there seems to be
# problems on Ubuntu computers with touchscreen.
# See https://github.com/vispy/vispy/pull/1143
if sys.platform == 'darwin':
if PYQT6_API:
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents)
self.grabGesture(QtCore.Qt.GestureType.PinchGesture)
else:
self.setAttribute(QtCore.Qt.WA_AcceptTouchEvents)
self.grabGesture(QtCore.Qt.PinchGesture)
# QNativeGestureEvent does not keep track of last or total
# values like QGestureEvent does
self._native_gesture_scale_values = []
self._native_gesture_rotation_values = []

def screen_changed(self, new_screen):
"""Window moved from one display to another, resize canvas.
Expand Down Expand Up @@ -563,50 +557,81 @@ def keyPressEvent(self, ev):
def keyReleaseEvent(self, ev):
self._keyEvent(self._vispy_canvas.events.key_release, ev)

def _handle_native_gesture_event(self, ev):
if self._vispy_canvas is None:
return
t = ev.gestureType()
# this is a workaround for what looks like a Qt bug where
# QNativeGestureEvent gives the wrong local position.
# See: https://bugreports.qt.io/browse/QTBUG-59595
try:
pos = self.mapFromGlobal(ev.globalPosition())
except AttributeError:
# globalPos is deprecated in Qt6
pos = self.mapFromGlobal(ev.globalPos())
pos = pos.x(), pos.y()

if t == QtCore.Qt.NativeGestureType.BeginNativeGesture:
self._vispy_canvas.events.touch(
type='gesture_begin',
pos=_get_event_xy(ev),
)
elif t == QtCore.Qt.NativeGestureType.EndNativeGesture:
self._native_touch_total_rotation = []
self._native_touch_total_scale = []
self._vispy_canvas.events.touch(
type='gesture_end',
pos=_get_event_xy(ev),
)
elif t == QtCore.Qt.NativeGestureType.RotateNativeGesture:
angle = ev.value()
last_angle = (
self._native_gesture_rotation_values[-1]
if self._native_gesture_rotation_values
else None
)
self._native_gesture_rotation_values.append(angle)
total_rotation_angle = math.fsum(self._native_gesture_rotation_values)
self._vispy_canvas.events.touch(
type="gesture_rotate",
pos=pos,
rotation=angle,
last_rotation=last_angle,
total_rotation_angle=total_rotation_angle,
)
elif t == QtCore.Qt.NativeGestureType.ZoomNativeGesture:
scale = ev.value()
last_scale = (
self._native_gesture_scale_values[-1]
if self._native_gesture_scale_values
else None
)
self._native_gesture_scale_values.append(scale)
total_scale_factor = math.fsum(self._native_gesture_scale_values)
self._vispy_canvas.events.touch(
type="gesture_zoom",
pos=pos,
last_scale=last_scale,
scale=scale,
total_scale_factor=total_scale_factor,
)
# QtCore.Qt.NativeGestureType.PanNativeGesture
# Qt6 docs seem to imply this is only supported on Wayland but I have
# not been able to test it.
# Two finger pan events are anyway converted to scroll/wheel events.
# On macOS, more fingers are usually swallowed by the OS (by spaces,
# mission control, etc.).

def event(self, ev):
out = super(QtBaseCanvasBackend, self).event(ev)
t = ev.type()

qt_event_types = QtCore.QEvent.Type if PYQT6_API else QtCore.QEvent
# Two-finger pinch.
if t == qt_event_types.TouchBegin:
self._vispy_canvas.events.touch(type='begin')
if t == qt_event_types.TouchEnd:
self._vispy_canvas.events.touch(type='end')
if t == qt_event_types.Gesture:
pinch_gesture = QtCore.Qt.GestureType.PinchGesture if PYQT6_API else QtCore.Qt.PinchGesture
gesture = ev.gesture(pinch_gesture)
if gesture:
(x, y) = _get_qpoint_pos(gesture.centerPoint())
scale = gesture.scaleFactor()
last_scale = gesture.lastScaleFactor()
rotation = gesture.rotationAngle()
self._vispy_canvas.events.touch(
type="pinch",
pos=(x, y),
last_pos=None,
scale=scale,
last_scale=last_scale,
rotation=rotation,
total_rotation_angle=gesture.totalRotationAngle(),
total_scale_factor=gesture.totalScaleFactor(),
)
# General touch event.
elif t == qt_event_types.TouchUpdate:
if qt_lib == 'pyqt6' or qt_lib == 'pyside6':
points = ev.points()
# These variables are lists of (x, y) coordinates.
pos = [_get_qpoint_pos(p.position()) for p in points]
lpos = [_get_qpoint_pos(p.lastPosition()) for p in points]
else:
points = ev.touchPoints()
# These variables are lists of (x, y) coordinates.
pos = [_get_qpoint_pos(p.pos()) for p in points]
lpos = [_get_qpoint_pos(p.lastPos()) for p in points]
self._vispy_canvas.events.touch(type='touch',
pos=pos,
last_pos=lpos,
)

# QNativeGestureEvent is Qt 5+
if (
(QT5_NEW_API or PYSIDE6_API or PYQT6_API)
and isinstance(ev, QtGui.QNativeGestureEvent)
):
self._handle_native_gesture_event(ev)

return out

def _keyEvent(self, func, ev):
Expand Down
4 changes: 4 additions & 0 deletions vispy/scene/cameras/base_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ def _viewbox_set(self, viewbox):
viewbox.events.mouse_release.connect(self.viewbox_mouse_event)
viewbox.events.mouse_move.connect(self.viewbox_mouse_event)
viewbox.events.mouse_wheel.connect(self.viewbox_mouse_event)
viewbox.events.gesture_zoom.connect(self.viewbox_mouse_event)
viewbox.events.gesture_rotate.connect(self.viewbox_mouse_event)
viewbox.events.resize.connect(self.viewbox_resize_event)
# todo: also add key events! (and also on viewbox (they're missing)

Expand All @@ -144,6 +146,8 @@ def _viewbox_unset(self, viewbox):
viewbox.events.mouse_release.disconnect(self.viewbox_mouse_event)
viewbox.events.mouse_move.disconnect(self.viewbox_mouse_event)
viewbox.events.mouse_wheel.disconnect(self.viewbox_mouse_event)
viewbox.events.gesture_zoom.disconnect(self.viewbox_mouse_event)
viewbox.events.gesture_rotate.disconnect(self.viewbox_mouse_event)
viewbox.events.resize.disconnect(self.viewbox_resize_event)

@property
Expand Down
5 changes: 4 additions & 1 deletion vispy/scene/cameras/panzoom.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,10 @@ def viewbox_mouse_event(self, event):
center = self._scene_transform.imap(event.pos)
self.zoom((1 + self.zoom_factor)**(-event.delta[1] * 30), center)
event.handled = True

elif event.type == 'gesture_zoom':
center = self._scene_transform.imap(event.pos)
self.zoom(1 - event.scale, center)
event.handled = True
elif event.type == 'mouse_move':
if event.press_event is None:
return
Expand Down
6 changes: 6 additions & 0 deletions vispy/scene/cameras/perspective.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ def viewbox_mouse_event(self, event):
if self._distance is not None:
self._distance *= s
self.view_changed()
elif event.type == 'gesture_zoom':
s = 1 - event.scale
self._scale_factor *= s
if self._distance is not None:
self._distance *= s
self.view_changed()

@property
def scale_factor(self):
Expand Down
37 changes: 37 additions & 0 deletions vispy/scene/cameras/tests/test_perspective.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,41 @@ def test_panzoom_center():
assert v.camera.center == (-12.8, -12.8, 0)


@requires_application()
def test_panzoom_gesture_zoom():
with TestingCanvas(size=(120, 200)) as canvas:
view = canvas.central_widget.add_view()
imdata = io.load_crate().astype('float32') / 255
scene.visuals.Image(imdata, parent=view.scene)
view.camera = scene.PanZoomCamera(aspect=1)

assert view.camera.rect.size == (1, 1)

canvas.events.touch(
type="gesture_zoom",
pos=(60, 100),
scale=-1.0,
)

assert view.camera.rect.size == (2, 2)


@requires_application()
def test_turntable_gesture_zoom():
with TestingCanvas(size=(120, 200)) as canvas:
view = canvas.central_widget.add_view()
imdata = io.load_crate().astype('float32') / 255
scene.visuals.Image(imdata, parent=view.scene)
view.camera = scene.TurntableCamera()

initial_scale_factor = view.camera.scale_factor
canvas.events.touch(
type="gesture_zoom",
pos=(60, 100),
scale=-1.0,
)

assert view.camera.scale_factor == 2 * initial_scale_factor


run_tests_if_main()
9 changes: 8 additions & 1 deletion vispy/scene/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def __init__(self, title='VisPy canvas', size=(800, 600), position=None,
self.events.mouse_move.connect(self._process_mouse_event)
self.events.mouse_release.connect(self._process_mouse_event)
self.events.mouse_wheel.connect(self._process_mouse_event)
self.events.touch.connect(self._process_mouse_event)

self.scene = SubScene()
self.freeze()
Expand Down Expand Up @@ -344,7 +345,12 @@ def _update_scenegraph(self, event):

def _process_mouse_event(self, event):
prof = Profiler() # noqa
deliver_types = ['mouse_press', 'mouse_wheel']
deliver_types = [
'mouse_press',
'mouse_wheel',
'gesture_zoom',
'gesture_rotate',
]
if self._send_hover_events:
deliver_types += ['mouse_move']

Expand Down Expand Up @@ -524,6 +530,7 @@ def on_close(self, event):
self.events.mouse_move.disconnect(self._process_mouse_event)
self.events.mouse_release.disconnect(self._process_mouse_event)
self.events.mouse_wheel.disconnect(self._process_mouse_event)
self.events.touch.disconnect(self._process_mouse_event)

# -------------------------------------------------- transform handling ---
def push_viewport(self, viewport):
Expand Down
9 changes: 9 additions & 0 deletions vispy/scene/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ def delta(self):
"""The increment by which the mouse wheel has moved."""
return self.mouse_event.delta

@property
def scale(self):
"""The scale of a gesture_zoom event"""
try:
return self.mouse_event.scale
except AttributeError:
errmsg = f"SceneMouseEvent type '{self.type}' has no scale"
raise TypeError(errmsg)

def copy(self):
ev = self.__class__(self.mouse_event, self.visual)
return ev
3 changes: 2 additions & 1 deletion vispy/scene/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def __init__(self, parent=None, name=None, transforms=None):
# Add some events to the emitter groups:
events = ['canvas_change', 'parent_change', 'children_change',
'transform_change', 'mouse_press', 'mouse_move',
'mouse_release', 'mouse_wheel', 'key_press', 'key_release']
'mouse_release', 'mouse_wheel', 'key_press', 'key_release',
'gesture_zoom', 'gesture_rotate']
# Create event emitter if needed (in subclasses that inherit from
# Visual, we already have an emitter to share)
if not hasattr(self, 'events'):
Expand Down

0 comments on commit ce20572

Please sign in to comment.