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