source: sasview/guiframe/local_perspectives/plotting/binder.py @ ba69349

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since ba69349 was 45c1a35, checked in by Gervaise Alina <gervyh@…>, 16 years ago

small bug fixed

  • Property mode set to 100644
File size: 14.8 KB
Line 
1"""
2Extension to MPL to support the binding of artists to key/mouse events.
3"""
4
5class 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
23class 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        try:
46            self._connections = [
47                canvas.mpl_connect('motion_notify_event',self._onMotion),
48                canvas.mpl_connect('button_press_event',self._onClick),
49                canvas.mpl_connect('button_release_event',self._onRelease),
50                canvas.mpl_connect('key_press_event',self._onKey),
51                canvas.mpl_connect('key_release_event',self._onKeyRelease),
52                canvas.mpl_connect('scroll_event',self._onScroll)
53            ]
54        except:
55            print "bypassing scroll_event: wrong matplotlib version"
56            self._connections = [
57                canvas.mpl_connect('motion_notify_event',self._onMotion),
58                canvas.mpl_connect('button_press_event',self._onClick),
59                canvas.mpl_connect('button_release_event',self._onRelease),
60                canvas.mpl_connect('key_press_event',self._onKey),
61                canvas.mpl_connect('key_release_event',self._onKeyRelease),
62            ]
63           
64
65        # Turn off picker if it hasn't already been done
66        try:
67            canvas.mpl_disconnect(canvas.button_pick_id)
68            canvas.mpl_disconnect(canvas.scroll_pick_id)
69        except: 
70            pass
71       
72        self.canvas = canvas
73        self.figure = figure
74        self.clearall()
75       
76    def clear(self, *artists):
77        """
78        self.clear(h1,h2,...)
79            Remove connections for artists h1, h2, ...
80           
81        Use clearall() to reset all connections.
82        """
83
84        for h in artists:
85            for a in self.events:
86                if h in self._actions[a]: del self._actions[a][h]
87            if h in self._artists: self._artists.remove(h)
88        if self._current.artist in artists: self._current = Selection()
89        if self._hasclick.artist in artists: self._hasclick = Selection()
90        if self._haskey.artist in artists: self._haskey = Selection()
91       
92    def clearall(self):
93        """
94        Clear connections to all artists.
95       
96        Use clear(h1,h2,...) to reset specific artists.
97        """
98        # Don't monitor any actions
99        self._actions = {}
100        for action in self.events:
101            self._actions[action] = {}
102
103        # Need activity state
104        self._artists = []
105        self._current = Selection()
106        self._hasclick = Selection()
107        self._haskey = Selection()
108
109    def disconnect(self):
110        """
111        In case we need to disconnect from the canvas...
112        """
113        try: 
114            for cid in self._connections: self.canvas.mpl_disconnect(cid)
115        except: 
116            pass
117        self._connections = []
118
119    def __del__(self):
120        self.disconnect()
121
122    def __call__(self,trigger,artist,action):
123        """Register a callback for an artist to a particular trigger event.
124       
125        usage:
126            self.connect(eventname,artist,action)
127   
128        where:
129            eventname is a string
130            artist is the particular graph object to respond to the event
131            action(event,**kw) is called when the event is triggered
132
133        The action callback is associated with particular artists.
134        Different artists will have different kwargs.  See documentation
135        on the contains() method for each artist.  One common properties
136        are ind for the index of the item under the cursor, which is
137        returned by Line2D and by collections.
138
139        The following events are supported:
140            enter: mouse cursor moves into the artist or to a new index
141            leave: mouse cursor leaves the artist
142            click: mouse button pressed on the artist
143            drag: mouse button pressed on the artist and cursor moves
144            release: mouse button released for the artist
145            key: key pressed when mouse is on the artist
146            keyrelease: key released for the artist
147   
148        The event received by action has a number of attributes:
149            name is the event name which was triggered
150            artist is the object which triggered the event
151            x,y are the screen coordinates of the mouse
152            xdata,ydata are the graph coordinates of the mouse
153            button is the mouse button being pressed/released
154            key is the key being pressed/released
155            shift,control,alt,meta are flags which are true if the
156                corresponding key is pressed at the time of the event.
157            details is a dictionary of artist specific details, such as the
158                id(s) of the point that were clicked.
159               
160        When receiving an event, first check the modifier state to be
161        sure it applies.  E.g., the callback for 'press' might be:
162            if event.button == 1 and event.shift: process Shift-click
163
164        TODO: Only receive events with the correct modifiers (e.g., S-click,
165        TODO:   or *-click for any modifiers).
166        TODO: Only receive button events for the correct button (e.g., click1
167        TODO:   release3, or dclick* for any button)
168        TODO: Support virtual artist, so that and artist can be flagged as
169        TODO:   having a tag list and receive the correct events
170        TODO: Support virtual events for binding to button-3 vs shift button-1
171        TODO:   without changing callback code
172        TODO: Attach multiple callbacks to the same event?
173        TODO: Clean up interaction with toolbar modes
174        TODO: push/pushclear/pop context so that binding changes for the duration
175        TODO:   e.g., to support ? context sensitive help
176        """
177        # Check that the trigger is valid
178        if trigger not in self._actions:
179            raise ValueError,"%s invalid --- valid triggers are %s"\
180                %(trigger,", ".join(self.events))
181
182        # Register the trigger callback
183        self._actions[trigger][artist]=action
184        #print "==> added",artist,[artist],"to",trigger,":",self._actions[trigger].keys()
185
186        # Maintain a list of all artists
187        if artist not in self._artists: 
188            self._artists.append(artist)
189
190    def trigger(self,actor,action,ev):
191        """
192        Trigger a particular event for the artist.  Fallback to axes,
193        to figure, and to 'all' if the event is not processed.
194        """
195        if action not in self.events:
196            raise ValueError, "Trigger expects "+", ".join(self.events)
197       
198        # Tag the event with modifiers
199        for mod in ('alt','control','shift','meta'):
200            setattr(ev,mod,getattr(self,mod))
201        setattr(ev,'artist',None)
202        setattr(ev,'action',action)
203        setattr(ev,'prop',{})
204       
205        # Fallback scheme.  If the event does not return false, pass to parent.
206        processed = False
207        artist,prop = actor.artist,actor.prop
208        if artist in self._actions[action]:
209            ev.artist,ev.prop = artist,prop
210            processed = self._actions[action][artist](ev)
211        if not processed and ev.inaxes in self._actions[action]:
212            ev.artist,ev.prop = ev.inaxes,{}
213            processed = self._actions[action][ev.inaxes](ev)
214        if not processed and self.figure in self._actions[action]:
215            ev.artist,ev.prop = self.figure,{}
216            processed = self._actions[action][self.figure](ev)
217        if not processed and 'all' in self._actions[action]:
218            ev.artist,ev.prop = None,{}
219            processed = self._actions[action]['all'](ev)
220        return processed
221
222    def _find_current(self, event):
223        """
224        Find the artist who will receive the event.  Only search
225        registered artists.  All others are invisible to the mouse.
226        """
227        # TODO: sort by zorder of axes then by zorder within axes
228        self._artists.sort(cmp=lambda x,y: cmp(y.zorder,x.zorder))
229        # print "search"," ".join([str(h) for h in self._artists])
230        found = Selection()
231        #print "searching in",self._artists
232        for artist in self._artists:
233            # TODO: should contains() return false if invisible?
234            if not artist.get_visible(): 
235                continue
236            # TODO: optimization - exclude artists not inaxes
237            try:
238                inside,prop = artist.contains(event)
239            except:
240                # Probably an old version of matplotlib
241                inside = False
242            if inside:
243                found.artist,found.prop = artist,prop
244                break
245        #print "found",found.artist
246       
247        # TODO: how to check if prop is equal?
248        if found != self._current:
249            self.trigger(self._current,'leave',event)
250            self.trigger(found,'enter',event)
251        self._current = found
252
253        return found
254       
255    def _onMotion(self,event):
256        """
257        Track enter/leave/motion through registered artists; all
258        other artists are invisible.
259        """
260        ## Can't kill double-click on motion since Windows produces
261        ## spurious motion events.
262        #self._last_button = None
263       
264        # Dibs on the motion event for the clicked artist
265        if self._hasclick:
266            # Make sure the x,y data use the coordinate system of the
267            # artist rather than the default axes coordinates.
268           
269            transform = self._hasclick.artist.get_transform()
270            #x,y = event.xdata,event.ydata
271            x,y = event.x,event.y
272            try:
273                x,y = transform.inverted().transform_point((x, y))
274
275            except:
276                x,y = transform.inverse_xy_tup((x,y))
277            event.xdata,event.ydata = x,y
278            self.trigger(self._hasclick,'drag',event)
279        else:
280            found = self._find_current(event)
281            #print "found",found.artist
282            self.trigger(found,'motion',event)
283
284    def _onClick(self,event):
285        """
286        Process button click
287        """
288        import time
289       
290        # Check for double-click
291        event_time = time.time()
292        #print event_time,self._last_time,self.dclick_threshhold
293        #print (event_time > self._last_time + self.dclick_threshhold)
294        #print event.button,self._last_button
295        if (event.button != self._last_button) or \
296                (event_time > self._last_time + self.dclick_threshhold):
297            action = 'click'
298        else:
299            action = 'dclick'
300        self._last_button = event.button
301        self._last_time = event_time
302       
303        # If an artist is already dragging, feed any additional button
304        # presses to that artist.
305        # TODO: do we want to force a single button model on the user?
306        # TODO: that is, once a button is pressed, no other buttons
307        # TODO: can come through?  I think this belongs in canvas, not here.
308        if self._hasclick:
309            found = self._hasclick
310        else:
311            found = self._find_current(event)
312        #print "button %d pressed"%event.button
313        # Note: it seems like if "click" returns False then hasclick should
314        # not be set.  The problem is that there are two reasons it can
315        # return false: because there is no click action for this artist
316        # or because the click action returned false.  A related problem
317        # is that click actions will go to the canvas if there is no click
318        # action for the artist, even if the artist has a drag. I'll leave
319        # it to future maintainers to sort out this problem.  For now the
320        # recommendation is that users should define click if they have
321        # drag or release on the artist.
322        self.trigger(found,action,event)
323        self._hasclick = found
324
325    def _onDClick(self,event):
326        """
327        Process button double click
328        """
329        # If an artist is already dragging, feed any additional button
330        # presses to that artist.
331        # TODO: do we want to force a single button model on the user?
332        # TODO: that is, once a button is pressed, no other buttons
333        # TODO: can come through?  I think this belongs in canvas, not here.
334        if self._hasclick:
335            found = self._hasclick
336        else:
337            found = self._find_current(event)
338        self.trigger(found,'dclick',event)
339        self._hasclick = found
340
341    def _onRelease(self,event):
342        """
343        Process release release
344        """
345        self.trigger(self._hasclick,'release',event)
346        self._hasclick = Selection()
347           
348    def _onKey(self,event):
349        """
350        Process key click
351        """
352        # TODO: Do we really want keyboard focus separate from mouse focus?
353        # TODO: Do we need an explicit focus command for keyboard?
354        # TODO: Can we tab between items?
355        # TODO: How do unhandled events get propogated to axes, figure and
356        # TODO: finally to application?  Do we need to implement a full tags
357        # TODO: architecture a la Tk?
358        # TODO: Do modifiers cause a grab?  Does the artist see the modifiers?
359        if event.key in ('alt','meta','control','shift'):
360            setattr(self,event.key,True)
361            return
362
363        if self._haskey:
364            found = self._haskey
365        else:
366            found = self._find_current(event)
367        self.trigger(found,'key',event)
368        self._haskey = found
369   
370    def _onKeyRelease(self,event):
371        """
372        Process key release
373        """
374        if event.key in ('alt','meta','control','shift'):
375            setattr(self,event.key,False)
376            return
377       
378        if self._haskey:
379            self.trigger(self._haskey,'keyup',event)
380        self._haskey = Selection()
381
382    def _onScroll(self,event):
383        """
384        Process scroll event
385        """
386        found = self._find_current(event)
387        self.trigger(found,'scroll',event)
388
Note: See TracBrowser for help on using the repository browser.