1 """
2 Extension to MPL to support the binding of artists to key/mouse events.
3 """
4 from matplotlib import transforms
5
6 if hasattr(transforms,'Transform'):
7
9
10 if isinstance(transform, transforms.BlendedGenericTransform):
11 transform = transform._x
12 return transform.inverted().transform( (x,y) )
13 else:
14
16 return transform.inverse_xy_tup((x,y))
17
18
20 """
21 Store and compare selections.
22 """
23
24
25
26 artist=None
27 prop={}
30
33
36
38 return self.artist is not None
39
40
41
42
44
45
46
47
48
49 control = False
50 shift = False
51 alt = False
52 meta = False
53
54
55 dclick_threshhold = 0.25
56 _last_button = None
57 _last_time = 0
58
59
60 events= ['enter','leave','motion','click','dclick','drag','release',
61 'scroll','key','keyup']
62
63
67 self.canvas = figure.canvas
68 self.figure = figure
69
70 self._connections = [
71 self.canvas.mpl_connect('motion_notify_event', self._onMotion),
72 self.canvas.mpl_connect('button_press_event', self._onClick),
73 self.canvas.mpl_connect('button_release_event',self._onRelease),
74 self.canvas.mpl_connect('key_press_event', self._onKey),
75 self.canvas.mpl_connect('key_release_event', self._onKeyRelease),
76 self.canvas.mpl_connect('scroll_event', self._onScroll) ]
77
78
79 try:
80 canvas.mpl_disconnect( canvas.button_pick_id )
81 canvas.mpl_disconnect( canvas.scroll_pick_id )
82 except:
83 pass
84
85
86 self.clearall()
87
88
89 - def clear(self, *artists):
90 """
91 self.clear(h1,h2,...)
92 Remove connections for artists h1, h2, ...
93
94 Use clearall() to reset all connections.
95 """
96 for h in artists:
97 for a in self.events:
98 if h in self._actions[a]:
99 del self._actions[a][h]
100 if h in self._artists:
101 self._artists.remove(h)
102
103 if self._current.artist in artists: self._current = Selection()
104 if self._hasclick.artist in artists: self._hasclick = Selection()
105 if self._haskey.artist in artists: self._haskey = Selection()
106
107
109 """
110 Clear connections to all artists.
111
112 Use clear(h1,h2,...) to reset specific artists.
113 """
114
115 self._actions = {}
116 for action in self.events:
117 self._actions[action] = {}
118
119
120 self._artists = []
121 self._current = Selection()
122 self._hasclick = Selection()
123 self._haskey = Selection()
124
125
127 """
128 In case we need to disconnect from the canvas...
129 """
130 try:
131 for cid in self._connections:
132 self.canvas.mpl_disconnect(cid)
133 except:
134 pass
135 self._connections = []
136
137
140
141
142 - def __call__(self,trigger,artist,action):
143 """Register a callback for an artist to a particular trigger event.
144
145 usage:
146 self.connect(eventname,artist,action)
147
148 where:
149 eventname is a string
150 artist is the particular graph object to respond to the event
151 action(event,**kw) is called when the event is triggered
152
153 The action callback is associated with particular artists.
154 Different artists will have different kwargs. See documentation
155 on the contains() method for each artist. One common properties
156 are ind for the index of the item under the cursor, which is
157 returned by Line2D and by collections.
158
159 The following events are supported:
160 enter: mouse cursor moves into the artist or to a new index
161 leave: mouse cursor leaves the artist
162 click: mouse button pressed on the artist
163 drag: mouse button pressed on the artist and cursor moves
164 release: mouse button released for the artist
165 key: key pressed when mouse is on the artist
166 keyrelease: key released for the artist
167
168 The event received by action has a number of attributes:
169 name is the event name which was triggered
170 artist is the object which triggered the event
171 x,y are the screen coordinates of the mouse
172 xdata,ydata are the graph coordinates of the mouse
173 button is the mouse button being pressed/released
174 key is the key being pressed/released
175 shift,control,alt,meta are flags which are true if the
176 corresponding key is pressed at the time of the event.
177 details is a dictionary of artist specific details, such as the
178 id(s) of the point that were clicked.
179
180 When receiving an event, first check the modifier state to be
181 sure it applies. E.g., the callback for 'press' might be:
182 if event.button == 1 and event.shift: process Shift-click
183
184 TODO: Only receive events with the correct modifiers (e.g., S-click,
185 TODO: or *-click for any modifiers).
186 TODO: Only receive button events for the correct button (e.g., click1
187 TODO: release3, or dclick* for any button)
188 TODO: Support virtual artist, so that and artist can be flagged as
189 TODO: having a tag list and receive the correct events
190 TODO: Support virtual events for binding to button-3 vs shift button-1
191 TODO: without changing callback code
192 TODO: Attach multiple callbacks to the same event?
193 TODO: Clean up interaction with toolbar modes
194 TODO: push/pushclear/pop context so that binding changes for the duration
195 TODO: e.g., to support ? context sensitive help
196 """
197
198 if trigger not in self._actions:
199 raise ValueError,"%s invalid --- valid triggers are %s"\
200 %(trigger,", ".join(self.events))
201
202
203 self._actions[trigger][artist]=action
204
205
206
207 if artist not in self._artists:
208 self._artists.append(artist)
209
210
211 - def trigger(self,actor,action,ev):
212 """
213 Trigger a particular event for the artist. Fallback to axes,
214 to figure, and to 'all' if the event is not processed.
215 """
216 if action not in self.events:
217 raise ValueError, "Trigger expects "+", ".join(self.events)
218
219
220 for mod in ('alt','control','shift','meta'):
221 setattr(ev,mod,getattr(self,mod))
222 setattr(ev,'artist',None)
223 setattr(ev,'action',action)
224 setattr(ev,'prop',{})
225
226
227 processed = False
228 artist = actor.artist
229 prop = actor.prop
230 if artist in self._actions[action]:
231 ev.artist = artist
232 ev.prop = prop
233 processed = self._actions[action][artist](ev)
234
235 if not processed and ev.inaxes in self._actions[action]:
236 ev.artist = ev.inaxes
237 ev.prop = {}
238 processed = self._actions[action][ev.inaxes](ev)
239
240 if not processed and self.figure in self._actions[action]:
241 ev.artist = self.figure
242 ev.prop = {}
243 processed = self._actions[action][self.figure](ev)
244
245 if not processed and 'all' in self._actions[action]:
246 ev.artist = None
247 ev.prop = {}
248 processed = self._actions[action]['all'](ev)
249
250 return processed
251
252
254 """
255 Find the artist who will receive the event. Only search
256 registered artists. All others are invisible to the mouse.
257 """
258
259 self._artists.sort(cmp=lambda x,y: cmp(y.zorder,x.zorder))
260
261
262 found = Selection()
263
264 for artist in self._artists:
265
266
267 if not artist.get_visible():
268 continue
269
270
271 inside,prop = artist.contains(event)
272 if inside:
273 found.artist = artist
274 found.prop = prop
275 break
276
277
278
279
280 if found != self._current:
281 self.trigger(self._current, 'leave', event)
282 self.trigger(found, 'enter', event)
283 self._current = found
284
285 return found
286
287
289 """
290 Track enter/leave/motion through registered artists; all
291 other artists are invisible.
292 """
293
294
295
296
297
298 if self._hasclick:
299
300
301 transform = self._hasclick.artist.get_transform()
302 xy = pixel_to_data(transform, event.x, event.y)
303 event.xdata, event.ydata = xy
304 self.trigger(self._hasclick, 'drag', event)
305 else:
306 found = self._find_current(event)
307
308 self.trigger(found,'motion',event)
309
310
312 """
313 Process button click
314 """
315 import time
316
317
318 event_time = time.time()
319
320
321
322
323 if (event.button != self._last_button) or \
324 (event_time > self._last_time + self.dclick_threshhold):
325 action = 'click'
326 else:
327 action = 'dclick'
328 self._last_button = event.button
329 self._last_time = event_time
330
331
332
333
334
335
336
337 if self._hasclick:
338 found = self._hasclick
339 else:
340 found = self._find_current(event)
341
342
343
344
345
346
347
348
349
350
351 self.trigger(found,action,event)
352 self._hasclick = found
353
354
356 """
357 Process button double click
358 """
359
360
361
362
363
364 if self._hasclick:
365 found = self._hasclick
366 else:
367 found = self._find_current(event)
368 self.trigger(found,'dclick',event)
369 self._hasclick = found
370
371
373 """
374 Process release release
375 """
376 self.trigger(self._hasclick, 'release', event)
377 self._hasclick = Selection()
378
379
381 """
382 Process key click
383 """
384
385
386
387
388
389
390
391 if event.key in ('alt','meta','control','shift'):
392 setattr(self, event.key, True)
393 return
394
395 if self._haskey:
396 found = self._haskey
397 else:
398 found = self._find_current(event)
399 self.trigger(found, 'key', event)
400 self._haskey = found
401
402
404 """
405 Process key release
406 """
407 if event.key in ('alt','meta','control','shift'):
408 setattr(self, event.key, False)
409 return
410
411 if self._haskey:
412 self.trigger(self._haskey, 'keyup', event)
413 self._haskey = Selection()
414
415
422