Package reflectometry :: Package reduction :: Module binder

Source Code for Module reflectometry.reduction.binder

  1  """ 
  2  Extension to MPL to support the binding of artists to key/mouse events. 
  3  """ 
  4   
5 -class Selection:
6 """ 7 Store and compare selections. 8 """ 9 # TODO: We need some way to check in prop matches, preferably 10 # TODO: without imposing structure on prop. 11 12 artist=None 13 prop={}
14 - def __init__(self,artist=None,prop={}):
15 self.artist,self.prop = artist,self.prop
16 - def __eq__(self,other):
17 return self.artist is other.artist
18 - def __ne__(self,other):
19 return self.artist is not other.artist
20 - def __nonzero__(self):
21 return self.artist is not None
22
23 -class BindArtist:
24 25 # Track keyboard modifiers for events. 26 # TODO: Move keyboard modifier support into the backend. We cannot 27 # TODO: properly support it from outside the windowing system since there 28 # TODO: is no way to recognized whether shift is held down when the mouse 29 # TODO: first clicks on the the application window. 30 control,shift,alt,meta=False,False,False,False 31 32 # Track doubleclick 33 dclick_threshhold = 0.25 34 _last_button, _last_time = None, 0 35 36 # Mouse/keyboard events we can bind to 37 events= ['enter','leave','motion','click','dclick','drag','release', 38 'scroll','key','keyup'] 39 # TODO: Need our own event structure 40
41 - def __init__(self,figure):
42 canvas = figure.canvas 43 44 # Link to keyboard/mouse 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 # Turn off picker if it hasn't already been done 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
81 - def clearall(self):
82 """ 83 Clear connections to all artists. 84 85 Use clear(h1,h2,...) to reset specific artists. 86 """ 87 # Don't monitor any actions 88 self._actions = {} 89 for action in self.events: 90 self._actions[action] = {} 91 92 # Need activity state 93 self._artists = [] 94 self._current = Selection() 95 self._hasclick = Selection() 96 self._haskey = Selection()
97
98 - def disconnect(self):
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
108 - def __del__(self):
109 self.disconnect()
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 # Check that the trigger is valid 167 if trigger not in self._actions: 168 raise ValueError,"%s invalid --- valid triggers are %s"\ 169 %(trigger,", ".join(self.events)) 170 171 # Register the trigger callback 172 self._actions[trigger][artist]=action 173 #print "==> added",artist,[artist],"to",trigger,":",self._actions[trigger].keys() 174 175 # Maintain a list of all artists 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 # Tag the event with modifiers 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 # Fallback scheme. If the event does not return false, pass to parent. 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
211 - def _find_current(self, event):
212 """ 213 Find the artist who will receive the event. Only search 214 registered artists. All others are invisible to the mouse. 215 """ 216 # TODO: sort by zorder of axes then by zorder within axes 217 self._artists.sort(cmp=lambda x,y: cmp(y.zorder,x.zorder)) 218 # print "search"," ".join([str(h) for h in self._artists]) 219 found = Selection() 220 #print "searching in",self._artists 221 for artist in self._artists: 222 # TODO: should contains() return false if invisible? 223 if not artist.get_visible(): 224 continue 225 # TODO: optimization - exclude artists not inaxes 226 inside,prop = artist.contains(event) 227 if inside: 228 found.artist,found.prop = artist,prop 229 break 230 #print "found",found.artist 231 232 # TODO: how to check if prop is equal? 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
240 - def _onMotion(self,event):
241 """ 242 Track enter/leave/motion through registered artists; all 243 other artists are invisible. 244 """ 245 ## Can't kill double-click on motion since Windows produces 246 ## spurious motion events. 247 #self._last_button = None 248 249 # Dibs on the motion event for the clicked artist 250 if self._hasclick: 251 # Make sure the x,y data use the coordinate system of the 252 # artist rather than the default axes coordinates. 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 #print "found",found.artist 261 self.trigger(found,'motion',event)
262
263 - def _onClick(self,event):
264 """ 265 Process button click 266 """ 267 import time 268 269 # Check for double-click 270 event_time = time.time() 271 #print event_time,self._last_time,self.dclick_threshhold 272 #print (event_time > self._last_time + self.dclick_threshhold) 273 #print event.button,self._last_button 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 # If an artist is already dragging, feed any additional button 283 # presses to that artist. 284 # TODO: do we want to force a single button model on the user? 285 # TODO: that is, once a button is pressed, no other buttons 286 # TODO: can come through? I think this belongs in canvas, not here. 287 if self._hasclick: 288 found = self._hasclick 289 else: 290 found = self._find_current(event) 291 #print "button %d pressed"%event.button 292 # Note: it seems like if "click" returns False then hasclick should 293 # not be set. The problem is that there are two reasons it can 294 # return false: because there is no click action for this artist 295 # or because the click action returned false. A related problem 296 # is that click actions will go to the canvas if there is no click 297 # action for the artist, even if the artist has a drag. I'll leave 298 # it to future maintainers to sort out this problem. For now the 299 # recommendation is that users should define click if they have 300 # drag or release on the artist. 301 self.trigger(found,action,event) 302 self._hasclick = found
303
304 - def _onDClick(self,event):
305 """ 306 Process button double click 307 """ 308 # If an artist is already dragging, feed any additional button 309 # presses to that artist. 310 # TODO: do we want to force a single button model on the user? 311 # TODO: that is, once a button is pressed, no other buttons 312 # TODO: can come through? I think this belongs in canvas, not here. 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
320 - def _onRelease(self,event):
321 """ 322 Process release release 323 """ 324 self.trigger(self._hasclick,'release',event) 325 self._hasclick = Selection()
326
327 - def _onKey(self,event):
328 """ 329 Process key click 330 """ 331 # TODO: Do we really want keyboard focus separate from mouse focus? 332 # TODO: Do we need an explicit focus command for keyboard? 333 # TODO: Can we tab between items? 334 # TODO: How do unhandled events get propogated to axes, figure and 335 # TODO: finally to application? Do we need to implement a full tags 336 # TODO: architecture a la Tk? 337 # TODO: Do modifiers cause a grab? Does the artist see the modifiers? 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
349 - def _onKeyRelease(self,event):
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
361 - def _onScroll(self,event):
362 """ 363 Process scroll event 364 """ 365 found = self._find_current(event) 366 self.trigger(found,'scroll',event)
367