Package reflectometry :: Package model1d :: Package profileview :: Module binder

Source Code for Module reflectometry.model1d.profileview.binder

  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      # 0.98 transforms 
8 - def pixel_to_data(transform, x, y):
9 # TODO: suspicious code -- why not use the blended transform? 10 if isinstance(transform, transforms.BlendedGenericTransform): 11 transform = transform._x 12 return transform.inverted().transform( (x,y) )
13 else: 14 # CRUFT 0.91 support
15 - def pixel_to_data(transform, x, y):
16 return transform.inverse_xy_tup((x,y))
17 18
19 -class Selection:
20 """ 21 Store and compare selections. 22 """ 23 # TODO: We need some way to check in prop matches, preferably 24 # TODO: without imposing structure on prop. 25 26 artist=None 27 prop={}
28 - def __init__(self,artist=None,prop={}):
29 self.artist,self.prop = artist,self.prop
30
31 - def __eq__(self,other):
32 return self.artist is other.artist
33
34 - def __ne__(self,other):
35 return self.artist is not other.artist
36
37 - def __nonzero__(self):
38 return self.artist is not None
39 40 41 42 #########################################################################
43 -class BindArtist:
44 # Track keyboard modifiers for events. 45 # TODO: Move keyboard modifier support into the backend. We cannot 46 # TODO: properly support it from outside the windowing system since there 47 # TODO: is no way to recognized whether shift is held down when the mouse 48 # TODO: first clicks on the the application window. 49 control = False 50 shift = False 51 alt = False 52 meta = False 53 54 # Track doubleclick 55 dclick_threshhold = 0.25 56 _last_button = None 57 _last_time = 0 58 59 # Mouse/keyboard events we can bind to 60 events= ['enter','leave','motion','click','dclick','drag','release', 61 'scroll','key','keyup'] 62 # TODO: Need our own event structure 63
64 - def __init__(self, 65 figure 66 ):
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 # Turn off picker if it hasn't already been done 79 try: 80 canvas.mpl_disconnect( canvas.button_pick_id ) 81 canvas.mpl_disconnect( canvas.scroll_pick_id ) 82 except: 83 pass 84 85 #Clear connections to all artists. 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
108 - def clearall(self):
109 """ 110 Clear connections to all artists. 111 112 Use clear(h1,h2,...) to reset specific artists. 113 """ 114 # Don't monitor any actions 115 self._actions = {} 116 for action in self.events: 117 self._actions[action] = {} 118 119 # Need activity state 120 self._artists = [] 121 self._current = Selection() 122 self._hasclick = Selection() 123 self._haskey = Selection()
124 125
126 - def disconnect(self):
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
138 - def __del__(self):
139 self.disconnect()
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 # Check that the trigger is valid 198 if trigger not in self._actions: 199 raise ValueError,"%s invalid --- valid triggers are %s"\ 200 %(trigger,", ".join(self.events)) 201 202 # Register the trigger callback 203 self._actions[trigger][artist]=action 204 #print "==> added",artist,[artist],"to",trigger,":",self._actions[trigger].keys() 205 206 # Maintain a list of all artists 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 # Tag the event with modifiers 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 # Fallback scheme. If the event does not return false, pass to parent. 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
253 - def _find_current(self, event):
254 """ 255 Find the artist who will receive the event. Only search 256 registered artists. All others are invisible to the mouse. 257 """ 258 # TODO: sort by zorder of axes then by zorder within axes 259 self._artists.sort(cmp=lambda x,y: cmp(y.zorder,x.zorder)) 260 # print "search"," ".join([str(h) for h in self._artists]) 261 262 found = Selection() 263 #print "searching in",self._artists 264 for artist in self._artists: 265 266 # TODO: should contains() return false if invisible? 267 if not artist.get_visible(): 268 continue 269 270 # TODO: optimization - exclude artists not inaxes 271 inside,prop = artist.contains(event) 272 if inside: 273 found.artist = artist 274 found.prop = prop 275 break 276 277 #print "found",found.artist 278 279 # TODO: how to check if prop is equal? 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
288 - def _onMotion(self,event):
289 """ 290 Track enter/leave/motion through registered artists; all 291 other artists are invisible. 292 """ 293 ## Can't kill double-click on motion since Windows produces 294 ## spurious motion events. 295 #self._last_button = None 296 297 # Dibs on the motion event for the clicked artist 298 if self._hasclick: 299 # Make sure the x,y data use the coordinate system of the 300 # artist rather than the default axes coordinates. 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 #print "found",found.artist 308 self.trigger(found,'motion',event)
309 310
311 - def _onClick(self,event):
312 """ 313 Process button click 314 """ 315 import time 316 317 # Check for double-click 318 event_time = time.time() 319 #print event_time,self._last_time,self.dclick_threshhold 320 #print (event_time > self._last_time + self.dclick_threshhold) 321 #print event.button,self._last_button 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 # If an artist is already dragging, feed any additional button 333 # presses to that artist. 334 # TODO: do we want to force a single button model on the user? 335 # TODO: that is, once a button is pressed, no other buttons 336 # TODO: can come through? I think this belongs in canvas, not here. 337 if self._hasclick: 338 found = self._hasclick 339 else: 340 found = self._find_current(event) 341 #print "button %d pressed"%event.button 342 # Note: it seems like if "click" returns False then hasclick should 343 # not be set. The problem is that there are two reasons it can 344 # return false: because there is no click action for this artist 345 # or because the click action returned false. A related problem 346 # is that click actions will go to the canvas if there is no click 347 # action for the artist, even if the artist has a drag. I'll leave 348 # it to future maintainers to sort out this problem. For now the 349 # recommendation is that users should define click if they have 350 # drag or release on the artist. 351 self.trigger(found,action,event) 352 self._hasclick = found
353 354
355 - def _onDClick(self,event):
356 """ 357 Process button double click 358 """ 359 # If an artist is already dragging, feed any additional button 360 # presses to that artist. 361 # TODO: do we want to force a single button model on the user? 362 # TODO: that is, once a button is pressed, no other buttons 363 # TODO: can come through? I think this belongs in canvas, not here. 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
372 - def _onRelease(self,event):
373 """ 374 Process release release 375 """ 376 self.trigger(self._hasclick, 'release', event) 377 self._hasclick = Selection()
378 379
380 - def _onKey(self,event):
381 """ 382 Process key click 383 """ 384 # TODO: Do we really want keyboard focus separate from mouse focus? 385 # TODO: Do we need an explicit focus command for keyboard? 386 # TODO: Can we tab between items? 387 # TODO: How do unhandled events get propogated to axes, figure and 388 # TODO: finally to application? Do we need to implement a full tags 389 # TODO: architecture a la Tk? 390 # TODO: Do modifiers cause a grab? Does the artist see the modifiers? 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
403 - def _onKeyRelease(self,event):
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
416 - def _onScroll(self,event):
417 """ 418 Process scroll event 419 """ 420 found = self._find_current(event) 421 self.trigger(found, 'scroll', event)
422