Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b168863

Browse files
committed
Touchscreen support
- support for touch-to-drag and pinch-to-zoom in NavigationToolbar2 - pass touchscreen events from Qt4 and Qt5 backends - add option in matplotlibrc to pass touches as mouse events if desired
1 parent d5132fc commit b168863

File tree

8 files changed

+482
-1
lines changed

8 files changed

+482
-1
lines changed

doc/users/navigation_toolbar.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ The ``Pan/Zoom`` button
5050
mouse button. The radius scale can be zoomed in and out using the
5151
right mouse button.
5252

53+
If your system has a touchscreen, with certain backends the figure can
54+
be panned by touching and dragging, or zoomed by pinching with two fingers.
55+
The Pan/Zoom button does not need to be activated for touchscreen interaction.
56+
As above, the 'x' and 'y' keys will constrain movement to the x or y axes,
57+
and 'CONTROL' preserves aspect ratio.
58+
5359
.. image:: ../../lib/matplotlib/mpl-data/images/zoom_to_rect_large.png
5460

5561
The ``Zoom-to-rectangle`` button
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Touchscreen Support
2+
-------------------
3+
4+
Support for touch-to-drag and pinch-to-zoom have been added for the
5+
Qt4 and Qt5 backends. For other/custom backends, the interface in
6+
`NavigationToolbar2` is general, so that the backends only need to
7+
pass a list of the touch points, and `NavigationToolbar2` will do the rest.
8+
Support is added separately for touch rotating and zooming in `Axes3D`.

lib/matplotlib/backend_bases.py

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,89 @@ def __str__(self):
15781578
self.dblclick, self.inaxes)
15791579

15801580

1581+
class Touch(LocationEvent):
1582+
"""
1583+
A single touch.
1584+
1585+
In addition to the :class:`Event` and :class:`LocationEvent`
1586+
attributes, the following attributes are defined:
1587+
1588+
Attributes
1589+
----------
1590+
ID : int
1591+
A unique ID (generated by the backend) for this touch.
1592+
1593+
"""
1594+
x = None # x position - pixels from left of canvas
1595+
y = None # y position - pixels from right of canvas
1596+
inaxes = None # the Axes instance if mouse us over axes
1597+
xdata = None # x coord of mouse in data coords
1598+
ydata = None # y coord of mouse in data coords
1599+
ID = None # unique ID of touch event
1600+
1601+
def __init__(self, name, canvas, x, y, ID, guiEvent=None):
1602+
"""
1603+
x, y in figure coords, 0,0 = bottom, left
1604+
"""
1605+
LocationEvent.__init__(self, name, canvas, x, y, guiEvent=guiEvent)
1606+
self.ID = ID
1607+
1608+
def __str__(self):
1609+
return ("MPL Touch: xy=(%d,%d) xydata=(%s,%s) inaxes=%s ID=%d" %
1610+
(self.x, self.y, self.xdata, self.ydata, self.inaxes, self.ID))
1611+
1612+
1613+
class TouchEvent(Event):
1614+
"""
1615+
A touch event, with possibly several touches.
1616+
1617+
For
1618+
('touch_begin_event',
1619+
'touch_update_event',
1620+
'touch_end_event')
1621+
1622+
In addition to the :class:`Event` and :class:`LocationEvent`
1623+
attributes, the following attributes are defined:
1624+
1625+
Attributes
1626+
----------
1627+
1628+
touches : None, or list
1629+
A list of the touches (possibly several), which will be of class Touch.
1630+
They are passed to the class as a list of triples of the form (ID,x,y),
1631+
where ID is an integer unique to that touch (usually provided by the
1632+
backend) and x and y are the touch coordinates
1633+
1634+
key : None, or str
1635+
The key depressed when this event triggered.
1636+
1637+
Example
1638+
-------
1639+
Usage::
1640+
1641+
def on_touch(event):
1642+
print('touch at', event.touches[0].x, event.touches[0].y)
1643+
1644+
cid = fig.canvas.mpl_connect('touch_update_event', on_touch)
1645+
1646+
"""
1647+
touches = None
1648+
key = None
1649+
1650+
def __init__(self, name, canvas, touches, key, guiEvent=None):
1651+
"""
1652+
x, y in figure coords, 0,0 = bottom, left
1653+
"""
1654+
Event.__init__(self, name, canvas, guiEvent=guiEvent)
1655+
self.touches = [Touch(name+'_'+str(n), canvas, x, y, ID,
1656+
guiEvent=guiEvent) for n, (ID, x, y) in enumerate(touches)]
1657+
self.key = key
1658+
1659+
def __str__(self):
1660+
return ("MPL TouchEvent: key=" + str(self.key) + ", touches=" +
1661+
' \n'.join(str(t) for t in self.touches))
1662+
1663+
15811664
class PickEvent(Event):
15821665
"""
15831666
a pick event, fired when the user picks a location on the canvas
@@ -1683,6 +1766,9 @@ class FigureCanvasBase(object):
16831766
'button_release_event',
16841767
'scroll_event',
16851768
'motion_notify_event',
1769+
'touch_begin_event',
1770+
'touch_update_event',
1771+
'touch_end_event',
16861772
'pick_event',
16871773
'idle_event',
16881774
'figure_enter_event',
@@ -1937,6 +2023,73 @@ def motion_notify_event(self, x, y, guiEvent=None):
19372023
guiEvent=guiEvent)
19382024
self.callbacks.process(s, event)
19392025

2026+
def touch_begin_event(self, touches, guiEvent=None):
2027+
"""
2028+
Backend derived classes should call this function on first touch.
2029+
2030+
This method will call all functions connected to the
2031+
'touch_begin_event' with a :class:`TouchEvent` instance.
2032+
2033+
Parameters
2034+
----------
2035+
touches : list
2036+
a list of triples of the form (ID,x,y), where ID is a unique
2037+
integer ID for each touch, and x and y are the touch's
2038+
coordinates.
2039+
2040+
guiEvent
2041+
the native UI event that generated the mpl event
2042+
2043+
"""
2044+
s = 'touch_begin_event'
2045+
event = TouchEvent(s, self, touches, key=self._key, guiEvent=guiEvent)
2046+
self.callbacks.process(s, event)
2047+
2048+
def touch_update_event(self, touches, guiEvent=None):
2049+
"""
2050+
Backend derived classes should call this function on all
2051+
touch updates.
2052+
2053+
This method will call all functions connected to the
2054+
'touch_update_event' with a :class:`TouchEvent` instance.
2055+
2056+
Parameters
2057+
----------
2058+
touches : list
2059+
a list of triples of the form (ID,x,y), where ID is a unique
2060+
integer ID for each touch, and x and y are the touch's
2061+
coordinates.
2062+
2063+
guiEvent
2064+
the native UI event that generated the mpl event
2065+
2066+
"""
2067+
s = 'touch_update_event'
2068+
event = TouchEvent(s, self, touches, key=self._key, guiEvent=guiEvent)
2069+
self.callbacks.process(s, event)
2070+
2071+
def touch_end_event(self, touches, guiEvent=None):
2072+
"""
2073+
Backend derived classes should call this function on touch end.
2074+
2075+
This method will be call all functions connected to the
2076+
'touch_end_event' with a :class:`TouchEvent` instance.
2077+
2078+
Parameters
2079+
----------
2080+
touches : list
2081+
a list of triples of the form (ID,x,y), where ID is a unique
2082+
integer ID for each touch, and x and y are the touch's
2083+
coordinates.
2084+
2085+
guiEvent
2086+
the native UI event that generated the mpl event
2087+
2088+
"""
2089+
s = 'touch_end_event'
2090+
event = TouchEvent(s, self, touches, key=self._key, guiEvent=guiEvent)
2091+
self.callbacks.process(s, event)
2092+
19402093
def leave_notify_event(self, guiEvent=None):
19412094
"""
19422095
Backend derived classes should call this function when leaving
@@ -2788,6 +2941,11 @@ def __init__(self, canvas):
27882941
self._idDrag = self.canvas.mpl_connect(
27892942
'motion_notify_event', self.mouse_move)
27902943

2944+
self._idTouchBegin = self.canvas.mpl_connect(
2945+
'touch_begin_event', self.handle_touch)
2946+
self._idTouchUpdate = None
2947+
self._idTouchEnd = None
2948+
27912949
self._ids_zoom = []
27922950
self._zoom_mode = None
27932951

@@ -2871,6 +3029,187 @@ def _set_cursor(self, event):
28713029

28723030
self._lastCursor = cursors.MOVE
28733031

3032+
def handle_touch(self, event, prev=None):
3033+
if len(event.touches) == 1:
3034+
if self._idTouchUpdate is not None:
3035+
self.touch_end_disconnect(event)
3036+
self.touch_pan_begin(event)
3037+
3038+
elif len(event.touches) == 2:
3039+
if self._idTouchUpdate is not None:
3040+
self.touch_end_disconnect(event)
3041+
self.pinch_zoom_begin(event)
3042+
3043+
else:
3044+
if prev == 'pan':
3045+
self.touch_pan_end(event)
3046+
elif prev == 'zoom':
3047+
self.pinch_zoom_end(event)
3048+
if self._idTouchUpdate is None:
3049+
self._idTouchUpdate = self.canvas.mpl_connect(
3050+
'touch_update_event', self.handle_touch)
3051+
self._idTouchEnd = self.canvas.mpl_connect(
3052+
'touch_end_event', self.touch_end_disconnect)
3053+
3054+
def touch_end_disconnect(self, event):
3055+
self._idTouchUpdate = self.canvas.mpl_disconnect(self._idTouchUpdate)
3056+
self._idTouchEnd = self.canvas.mpl_disconnect(self._idTouchEnd)
3057+
3058+
def touch_pan_begin(self, event):
3059+
self._idTouchUpdate = self.canvas.mpl_connect(
3060+
'touch_update_event', self.touch_pan)
3061+
self._idTouchEnd = self.canvas.mpl_connect(
3062+
'touch_end_event', self.touch_pan_end)
3063+
3064+
touch = event.touches[0]
3065+
x, y = touch.x, touch.y
3066+
3067+
# push the current view to define home if stack is empty
3068+
if self._views.empty():
3069+
self.push_current()
3070+
3071+
self._xypress = []
3072+
for i, a in enumerate(self.canvas.figure.get_axes()):
3073+
if (x is not None and y is not None and a.in_axes(touch) and
3074+
a.get_navigate() and a.can_pan()):
3075+
a.start_pan(x, y, 1)
3076+
self._xypress.append((a, i))
3077+
3078+
def touch_pan(self, event):
3079+
if len(event.touches) != 1: # number of touches changed
3080+
self.touch_pan_end(event, push_view=False)
3081+
self.handle_touch(event, prev='pan')
3082+
return
3083+
3084+
touch = event.touches[0]
3085+
3086+
for a, _ in self._xypress:
3087+
a.drag_pan(1, event.key, touch.x, touch.y)
3088+
self.dynamic_update()
3089+
3090+
def touch_pan_end(self, event, push_view=True):
3091+
self.touch_end_disconnect(event)
3092+
3093+
for a, _ in self._xypress:
3094+
a.end_pan()
3095+
3096+
self._xypress = []
3097+
self._button_pressed = None
3098+
if push_view: # don't push when going from pan to pinch-to-zoom
3099+
self.push_current()
3100+
self.draw()
3101+
3102+
def pinch_zoom_begin(self, event):
3103+
3104+
# push the current view to define home if stack is empty
3105+
# this almost never happens because you have a single touch
3106+
# first. but good to be safe
3107+
if self._views.empty():
3108+
self.push_current()
3109+
3110+
self._xypress = []
3111+
for a in self.canvas.figure.get_axes():
3112+
if (all(a.in_axes(t) for t in event.touches) and
3113+
a.get_navigate() and a.can_zoom()):
3114+
3115+
trans = a.transData
3116+
3117+
view = a._get_view()
3118+
transview = trans.transform(list(zip(view[:2], view[2:])))
3119+
3120+
self._xypress.append((a, event.touches,
3121+
trans.inverted(), transview))
3122+
3123+
self._idTouchUpdate = self.canvas.mpl_connect(
3124+
'touch_update_event', self.pinch_zoom)
3125+
self._idTouchEnd = self.canvas.mpl_connect(
3126+
'touch_end_event', self.pinch_zoom_end)
3127+
3128+
def pinch_zoom(self, event):
3129+
if len(event.touches) != 2: # number of touches changed
3130+
self.pinch_zoom_end(event, push_view=False)
3131+
self.handle_touch(event, prev='zoom')
3132+
return
3133+
3134+
if not self._xypress:
3135+
return
3136+
3137+
# check that these are the same two touches!
3138+
e_IDs = {t.ID for t in event.touches}
3139+
orig_IDs = {t.ID for t in self._xypress[0][1]}
3140+
if e_IDs != orig_IDs:
3141+
self.pinch_zoom_end(event)
3142+
self.pinch_zoom_begin(event)
3143+
3144+
last_a = []
3145+
3146+
for cur_xypress in self._xypress:
3147+
a, orig_touches, orig_trans, orig_lims = cur_xypress
3148+
3149+
center = (sum(t.x for t in event.touches)/2,
3150+
sum(t.y for t in event.touches)/2)
3151+
3152+
orig_center = (sum(t.x for t in orig_touches)/2,
3153+
sum(t.y for t in orig_touches)/2)
3154+
3155+
ot1, ot2 = orig_touches
3156+
t1, t2 = event.touches
3157+
if (event.key == 'control' or
3158+
a.get_aspect() not in ['auto', 'normal']):
3159+
global_scale = np.sqrt(((t1.x-t2.x)**2 + (t1.y-t2.y)**2) /
3160+
((ot1.x-ot2.x)**2 + (ot1.y-ot2.y)**2))
3161+
zoom_scales = (global_scale, global_scale)
3162+
else:
3163+
zoom_scales = (abs((t1.x-t2.x)/(ot1.x-ot2.x)),
3164+
abs((t1.y-t2.y)/(ot1.y-ot2.y)))
3165+
3166+
# if the zoom is really extreme, make it not crazy
3167+
zoom_scales = [z if z > 0.01 else 0.01 for z in zoom_scales]
3168+
3169+
if event.key == 'y':
3170+
xlims = orig_lims[:, 0]
3171+
else:
3172+
xlims = [orig_center[0] + (x - center[0])/zoom_scales[0]
3173+
for x in orig_lims[:, 0]]
3174+
3175+
if event.key == 'x':
3176+
ylims = orig_lims[:, 1]
3177+
else:
3178+
ylims = [orig_center[1] + (y - center[1])/zoom_scales[1]
3179+
for y in orig_lims[:, 1]]
3180+
3181+
lims = orig_trans.transform(list(zip(xlims, ylims)))
3182+
xlims = lims[:, 0]
3183+
ylims = lims[:, 1]
3184+
3185+
# detect twinx,y axes and avoid double zooming
3186+
twinx, twiny = False, False
3187+
if last_a:
3188+
for la in last_a:
3189+
if a.get_shared_x_axes().joined(a, la):
3190+
twinx = True
3191+
if a.get_shared_y_axes().joined(a, la):
3192+
twiny = True
3193+
last_a.append(a)
3194+
3195+
xmin, xmax, ymin, ymax = a._get_view()
3196+
3197+
if twinx and not twiny: # and maybe a key
3198+
a._set_view((xmin, xmax, ylims[0], ylims[1]))
3199+
elif twiny and not twinx:
3200+
a._set_view((xlims[0], xlims[1], ymin, ymax))
3201+
elif not twinx and not twiny:
3202+
a._set_view(list(xlims)+list(ylims))
3203+
3204+
self.dynamic_update()
3205+
3206+
def pinch_zoom_end(self, event, push_view=True):
3207+
self.touch_end_disconnect(event)
3208+
3209+
if push_view: # don't push when going from zoom back to pan
3210+
self.push_current()
3211+
self.draw()
3212+
28743213
def mouse_move(self, event):
28753214
self._set_cursor(event)
28763215

0 commit comments

Comments
 (0)