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

Skip to content

Commit 908f70b

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 908f70b

File tree

8 files changed

+484
-1
lines changed

8 files changed

+484
-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,95 @@ 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") % (self.x,
1610+
self.y,
1611+
self.xdata,
1612+
self.ydata,
1613+
self.inaxes,
1614+
self.ID)
1615+
1616+
1617+
class TouchEvent(Event):
1618+
"""
1619+
A touch event, with possibly several touches.
1620+
1621+
For
1622+
('touch_begin_event',
1623+
'touch_update_event',
1624+
'touch_end_event')
1625+
1626+
In addition to the :class:`Event` and :class:`LocationEvent`
1627+
attributes, the following attributes are defined:
1628+
1629+
Attributes
1630+
----------
1631+
1632+
touches : None, or list
1633+
A list of the touches (possibly several), which will be of class Touch.
1634+
They are passed to the class as a list of triples of the form (ID,x,y),
1635+
where ID is an integer unique to that touch (usually provided by the
1636+
backend) and x and y are the touch coordinates
1637+
1638+
key : None, or str
1639+
The key depressed when this event triggered.
1640+
1641+
Example
1642+
-------
1643+
Usage::
1644+
1645+
def on_touch(event):
1646+
print('touch at', event.touches[0].x, event.touches[0].y)
1647+
1648+
cid = fig.canvas.mpl_connect('touch_update_event', on_touch)
1649+
1650+
"""
1651+
touches = None
1652+
key = None
1653+
1654+
def __init__(self, name, canvas, touches, key, guiEvent=None):
1655+
"""
1656+
x, y in figure coords, 0,0 = bottom, left
1657+
"""
1658+
Event.__init__(self, name, canvas, guiEvent=guiEvent)
1659+
self.touches = [ Touch(name+'_%d'%n,
1660+
canvas,
1661+
x,y,ID,
1662+
guiEvent=guiEvent) for n,(ID,x,y) in enumerate(touches)]
1663+
self.key = key
1664+
1665+
def __str__(self):
1666+
return "MPL TouchEvent: key="+str(self.key)+", touches=" + ' \n'.join(str(t) for t in self.touches)
1667+
1668+
1669+
15811670
class PickEvent(Event):
15821671
"""
15831672
a pick event, fired when the user picks a location on the canvas
@@ -1683,6 +1772,9 @@ class FigureCanvasBase(object):
16831772
'button_release_event',
16841773
'scroll_event',
16851774
'motion_notify_event',
1775+
'touch_begin_event',
1776+
'touch_update_event',
1777+
'touch_end_event',
16861778
'pick_event',
16871779
'idle_event',
16881780
'figure_enter_event',
@@ -1937,6 +2029,73 @@ def motion_notify_event(self, x, y, guiEvent=None):
19372029
guiEvent=guiEvent)
19382030
self.callbacks.process(s, event)
19392031

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

2950+
self._idTouchBegin = self.canvas.mpl_connect(
2951+
'touch_begin_event', self.handle_touch)
2952+
self._idTouchUpdate = None
2953+
self._idTouchEnd = None
2954+
27912955
self._ids_zoom = []
27922956
self._zoom_mode = None
27932957

@@ -2871,6 +3035,181 @@ def _set_cursor(self, event):
28713035

28723036
self._lastCursor = cursors.MOVE
28733037

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