@@ -1578,6 +1578,89 @@ 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" %
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
+
1581
1664
class PickEvent (Event ):
1582
1665
"""
1583
1666
a pick event, fired when the user picks a location on the canvas
@@ -1683,6 +1766,9 @@ class FigureCanvasBase(object):
1683
1766
'button_release_event' ,
1684
1767
'scroll_event' ,
1685
1768
'motion_notify_event' ,
1769
+ 'touch_begin_event' ,
1770
+ 'touch_update_event' ,
1771
+ 'touch_end_event' ,
1686
1772
'pick_event' ,
1687
1773
'idle_event' ,
1688
1774
'figure_enter_event' ,
@@ -1937,6 +2023,73 @@ def motion_notify_event(self, x, y, guiEvent=None):
1937
2023
guiEvent = guiEvent )
1938
2024
self .callbacks .process (s , event )
1939
2025
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
+
1940
2093
def leave_notify_event (self , guiEvent = None ):
1941
2094
"""
1942
2095
Backend derived classes should call this function when leaving
@@ -2788,6 +2941,11 @@ def __init__(self, canvas):
2788
2941
self ._idDrag = self .canvas .mpl_connect (
2789
2942
'motion_notify_event' , self .mouse_move )
2790
2943
2944
+ self ._idTouchBegin = self .canvas .mpl_connect (
2945
+ 'touch_begin_event' , self .handle_touch )
2946
+ self ._idTouchUpdate = None
2947
+ self ._idTouchEnd = None
2948
+
2791
2949
self ._ids_zoom = []
2792
2950
self ._zoom_mode = None
2793
2951
@@ -2871,6 +3029,187 @@ def _set_cursor(self, event):
2871
3029
2872
3030
self ._lastCursor = cursors .MOVE
2873
3031
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
+
2874
3213
def mouse_move (self , event ):
2875
3214
self ._set_cursor (event )
2876
3215
0 commit comments