source: sasview/sansguiframe/src/sans/guiframe/local_perspectives/plotting/binder.py @ 31ac4a1

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 31ac4a1 was 8c347a6, checked in by Gervaise Alina <gervyh@…>, 13 years ago

moving guiframe under sansguiframe

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