source: sasview/guiframe/local_perspectives/plotting/binder.py @ 4a2b054

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 4a2b054 was 83f4445, checked in by Gervaise Alina <gervyh@…>, 14 years ago

working on documentation

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