source: sasview/plottools/src/danse/common/plottools/binder.py @ 8542bf3

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 8542bf3 was 0e553fd, checked in by Jae Cho <jhjcho@…>, 12 years ago

1)fixed y-axis unit when it includes x and x-axis has power
2)now enable to drag the legend

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