@@ -1578,6 +1578,95 @@ def __str__(self):
1578
1578
self .dblclick , self .inaxes )
1579
1579
1580
1580
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
+
1581
1670
class PickEvent (Event ):
1582
1671
"""
1583
1672
a pick event, fired when the user picks a location on the canvas
@@ -1683,6 +1772,9 @@ class FigureCanvasBase(object):
1683
1772
'button_release_event' ,
1684
1773
'scroll_event' ,
1685
1774
'motion_notify_event' ,
1775
+ 'touch_begin_event' ,
1776
+ 'touch_update_event' ,
1777
+ 'touch_end_event' ,
1686
1778
'pick_event' ,
1687
1779
'idle_event' ,
1688
1780
'figure_enter_event' ,
@@ -1937,6 +2029,73 @@ def motion_notify_event(self, x, y, guiEvent=None):
1937
2029
guiEvent = guiEvent )
1938
2030
self .callbacks .process (s , event )
1939
2031
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
+
1940
2099
def leave_notify_event (self , guiEvent = None ):
1941
2100
"""
1942
2101
Backend derived classes should call this function when leaving
@@ -2788,6 +2947,11 @@ def __init__(self, canvas):
2788
2947
self ._idDrag = self .canvas .mpl_connect (
2789
2948
'motion_notify_event' , self .mouse_move )
2790
2949
2950
+ self ._idTouchBegin = self .canvas .mpl_connect (
2951
+ 'touch_begin_event' , self .handle_touch )
2952
+ self ._idTouchUpdate = None
2953
+ self ._idTouchEnd = None
2954
+
2791
2955
self ._ids_zoom = []
2792
2956
self ._zoom_mode = None
2793
2957
@@ -2871,6 +3035,181 @@ def _set_cursor(self, event):
2871
3035
2872
3036
self ._lastCursor = cursors .MOVE
2873
3037
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
+
2874
3213
def mouse_move (self , event ):
2875
3214
self ._set_cursor (event )
2876
3215
0 commit comments