Search code examples
pythonmatplotlibwxwidgets

matplotlib event.mouseevent.key gets "stuck"


I have boiled down my problem to the following simple app in wxWidgets (I think it is about as small as I can get it...):

import wx
import matplotlib

matplotlib.use('WXAgg')
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas

class GraphPanel(wx.Panel):
   def __init__(self, *args, **kwargs):
      super(GraphPanel, self).__init__(*args, **kwargs)
      self._win             = None
      self._figure          = matplotlib.figure.Figure()
      self._canvas          = FigureCanvas(self, -1, self._figure) 
      self._ax              = self._figure.add_subplot(111)
      self._canvas.mpl_connect('pick_event', self._OnGraphPickEvent)
      x = [1,2,3]
      y = [ 3,1,2]
      self._ax.scatter(x, y, picker=True)

   def _OnGraphPickEvent(self, event):
      print event.mouseevent.key, event.guiEvent.ControlDown()
      if event.mouseevent.key == 'control':
         if self._win is None:
            self._win = SecondFrame(self)
            self._win.Show()
         self._win.SetFocus()
         self._win.Raise()

class SecondFrame(wx.Frame):
   def __init__(self, *args, **kwargs):
      wx.Frame.__init__(self, *args, **kwargs)
      self.Show()

class FirstFrame(wx.Frame):
   def __init__(self, *args, **kwargs):
      wx.Frame.__init__(self, *args, **kwargs)
      self._graph = GraphPanel(self, wx.ID_ANY)
      szr = wx.BoxSizer(wx.VERTICAL)
      szr.Add(self._graph, 1, wx.EXPAND)
      self.SetSizerAndFit(szr)
      self.Show()

if __name__ == "__main__":
   app = wx.App(False)
   frame = FirstFrame(None)
   app.MainLoop()

Now the interesting thing to me is that it appears that the value for event.mouseevent.key gets stuck. The first time I click a scatter point holding down ctrl I get a value of None. If I re-press ctrl then I get the expected value and a new window is shown. But then if I click, without holding ctrl, any other scatter points, event.mouseevent.key gets stuck and continually reports control.

The only work around I have found is to go down the the wx layer and use event.guiEvent.ControlDown(), which always appears to be accurate.

It has something to do with displaying a new Frame because if you remove the code that creates and shows the second frame, this problem does not occur.

Does any one know if I am using the matplotlib events incorrectly here? I've just been following online examples so perhaps I've misunderstood something or am missing something to do with new frames and event loops??

I am using wxWidgets 2.8.12.1 with python 2.7.3 on Ubuntu 12.04.3 LTS

Thanks :)


Solution

  • tl;dr

    Okay, so I have done a little bit of digging. The reason the key is getting "stuck" in the matplotlib event is because the matplotlib backend uses the key down and key up events to figure out what key is currently pressed when the mouse is clicked. When a key is pressed it is remembered until it is released, so if a user does key press --> mouse click --> key release, they key pressed is remembered and passed to the mouse click event handler.

    So when I launch a new window and give it focus from the click event, the subsequent key release event is "lost": the first frame never receives it, thus the backend doesn't reset its cached key press and reports a stale value for all subsequent mouse clicks until a new key press event is generated.

    There doesn't appear to be a good solution to the specific problem where the main window looses focus in between a key press and release event...

    The rest of the explanation

    This key caching can be found in backend_bases.py, which on my Ubuntu system is found in /usr/local/lib/python2.7/dist-packages/matplotlib/backends. The key press event stashed the current key in self._key and resets this to None when the key is released.

    The stack trace from my first frame's pick event is this:

    File "test.py", line 57, in <module>
        app.MainLoop()
    File "/usr/lib/python2.7/dist-packages/wx-2.8-gtk2-unicode/wx/_core.py", line 8010, in MainLoop
        wx.PyApp.MainLoop(self)
    File "/usr/lib/python2.7/dist-packages/wx-2.8-gtk2-unicode/wx/_core.py", line 7306, in MainLoop
        return _core_.PyApp_MainLoop(*args, **kwargs)
    File "/usr/local/lib/python2.7/dist-packages/matplotlib/backends/backend_wx.py", line 1069, in _onLeftButtonDown
        FigureCanvasBase.button_press_event(self, x, y, 1, guiEvent=evt)
    File "/usr/local/lib/python2.7/dist-packages/matplotlib/backend_bases.py", line 1910, in button_press_event
        self.callbacks.process(s, mouseevent)
    <snip>
    File "/usr/local/lib/python2.7/dist-packages/matplotlib/backend_bases.py", line 1787, in pick
        self.figure.pick(mouseevent)
    File "/usr/local/lib/python2.7/dist-packages/matplotlib/artist.py", line 450, in pick
        a.pick(mouseevent)
    <snip>
    File "/usr/local/lib/python2.7/dist-packages/matplotlib/artist.py", line 436, in pick
        self.figure.canvas.pick_event(mouseevent, self, **prop)
    File "/usr/local/lib/python2.7/dist-packages/matplotlib/backend_bases.py", line 1874, in pick_event
        self.callbacks.process(s, event)
    File "/usr/local/lib/python2.7/dist-packages/matplotlib/cbook.py", line 563, in process
        proxy(*args, **kwargs)
    File "/usr/local/lib/python2.7/dist-packages/matplotlib/cbook.py", line 430, in __call__
        return mtd(*args, **kwargs)
    File "test.py", line 22, in _OnGraphPickEvent
        for line in traceback.format_stack():
    

    Going down a level, in the class FigureCanvasWx the functions _onKeyDown() and _onKeyUp() are bound to the wxWidgets key press event. These call the base class FigureCanvasBase corresponding event handler functions. As said it is the FigureCanvasBase that then caches the key press information so that it can pass the currently key pressed to its mouse events, which eventually reach the pick event.

    I added a bit of debug prints into the above classes. Gives the following...

    Test 1: First frame is shown. I click on the graph, but not on a point because I don't want to fire the pick event which shows a new frame.

    FigureCanvasWx:onLeftButtonDown: Ctrl down is "False"
    FigureCanvasBase:button_press_event: KEY IS None
    Artist:Pick:MPL MouseEvent: xy=(475,156) xydata=(2.88911290323,1.34375) button=1 dblclick=False inaxes=Axes(0.125,0.1;0.775x0.8) None
    

    Okay that works as expected. wxPython and matplot lib agree on the control key not being pressed.

    Test 2: Now I press the control key and click on the graph, but not on a point, just a blank space, again to see the effect without firing the pick event in which a new frame is shown.

    FigureCanvasBase:key_press_event: SETTING KEY TO control
    FigureCanvasWx:onLeftButtonDown: Ctrl down is "True"
    FigureCanvasBase:button_press_event: KEY IS control
    Artist:Pick:MPL MouseEvent: xy=(440,144) xydata=(2.67741935484,1.25) button=1 dblclick=False inaxes=Axes(0.125,0.1;0.775x0.8) control
    FigureCanvasBase:key_release_event: RESETTING KEY #<<!! THE IMPORTANT BIT
    

    When I press the control key the FigureCanvasBase caches the key press. Then when the mouse event from wxPython is wrapped into the matplotlib event this cached key value is added to the matplotlib event. At this point matplotlib and wxpython still agree.

    After my click I release the control key and the FigureCanvasBase correctly resets is key cache. Therefore when I click again without holding the control key the matplotlib event correctly reports no key being pressed.

    Test 3:* Now I hold the control key down and click on an actual point on my graph which fires my pick event in which a new window is created, displayed and given focus:

    FigureCanvasBase:key_press_event: SETTING KEY TO control
    FigureCanvasWx:onLeftButtonDown: Ctrl down is "True"
    FigureCanvasBase:button_press_event: KEY IS control
    Artist:Pick:MPL MouseEvent: xy=(494,238) xydata=(3.00403225806,1.984375) button=1 dblclick=False inaxes=Axes(0.125,0.1;0.775x0.8) control
    FigureCanvasBase:pick_event: CREATED PICK EVENT <matplotlib.backend_bases.PickEvent object at 0xaf631ec> control
    control True
    

    This way okay... matplotlib and wxpython still agree.... but wait...

    Oh no... I release the control key but the first frame no longer has focus... there is no key release event

    So now when I click any point on the graph, event blank space, i will see the control key as being pressed!

    FigureCanvasWx:onLeftButtonDown: Ctrl down is "False" ##<< !!!!
    FigureCanvasBase:button_press_event: KEY IS control   ##<< !!!!
    ARTIST PICK MPL MouseEvent: xy=(475,475) xydata=(None,None) button=1 dblclick=False inaxes=None control
    

    The only way to clear this is to give the first frame focus and deliberately press a key so that the reset event can clear the cache.

    Interestingly the second frame does not appear to receive the release event either unless I deliberately click on it before releasing the control key which stops me being able to send this event back to the first window and working around it that way.

    So... it seems the key caching scheme was a problem in this specific scenario. The work around seems to be the only way to really handle this. If we could force wxPython to re-read the keyboard state manually we could correct this but in most cases, even for the wx.GetKeyState() function, it will only report if control keys are down and not any key, which is what matplotlib wants.