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