@@ -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+
15811664class 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