source: sasview/src/sas/plottools/binder.py @ 1ee0772

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 1ee0772 was 79492222, checked in by krzywon, 10 years ago

Changed the file and folder names to remove all SANS references.

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