source: sasview/src/sas/sasgui/guiframe/local_perspectives/plotting/binder.py @ c8e1996

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.2ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since c8e1996 was d85c194, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 9 years ago

Remaining modules refactored

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