Hi all,

A while ago (a year or so), I was looking for a ginput-like function with
matplotlib. For those who don't know what I am talking about, it is a
blocking call that can be used in a script to ask the user to enter
coordinnate by clicking on the figure. This is incredibly handy, as it
allows some simple user interaction without worrying about events loop
and Co.

At the time I gave up on that. I have made progress with GUI and event
loops, and a recent post on the matplotlib-user list put me back to work
on this (
http://www.nabble.com/ginput-is-here%21-%28for-wx-anyway%29-to14960506.html
).

I have a preliminary patch to add this feature to matplotlib that I
attach. I add a method to the backend canvases, to be able to deal with
event-loop-handling, and following Jack Gurkesaft I create my own a small
event-loop to poll for events inside my function. This is a kludge, but it
allows to block the function while still processing events. If you have
better ideas.

I have implemented the functionnality for all the interactive backends I
could test (and thus not cocoa), but I am not too sure my gtk code is
kosher, I wouldn't mind advice on this one.

The code of ginput itself is currently in a seperate file
(matplotlib.ginput, that you can run for a demo) because I wasn't too
sure where to add it.

This is a first version of the patch. I would like comments on it,
especially where to add the code itself, and if the proposed additions to
the backends look all right.

Once I have comments, I plan to:

    * Make right click cancel an input
    * Allow infinite number of inputs, terminated by right-click
    * Add optional display of inputs
    * Rework docstrings

More suggestions welcomed,

This could (and should, IMHO) open the road to other blocking calls for
user interaction, like the selection of a window, as they make life
really easy for people wanting to hack quick data analysis scripts.

Cheers,

Gaƫl
Index: trunk/matplotlib/lib/matplotlib/backend_bases.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/backend_bases.py    (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backend_bases.py    (working copy)
@@ -1151,7 +1151,13 @@
         """
         return self.callbacks.disconnect(cid)
 
+    def flush_events(self):
+        """ Flush the GUI events for the figure. Implemented only for
+        backends with GUIs.
+        """
+        raise NotImplementedError
 
+
 class FigureManagerBase:
     """
     Helper class for matlab mode, wraps everything up into a neat bundle
Index: trunk/matplotlib/lib/matplotlib/ginput.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/ginput.py   (revision 0)
+++ trunk/matplotlib/lib/matplotlib/ginput.py   (revision 0)
@@ -0,0 +1,80 @@
+
+from matplotlib.pyplot import gcf
+import time
+
+class BlockingMouseInput(object):
+    """ Class that creates a callable object to retrieve mouse click 
+        in a blocking way, a la MatLab. 
+    """
+
+    callback = None 
+    verbose = False
+
+    def on_click(self, event):
+        """ Event handler that will be passed to the current figure to
+            retrieve clicks.
+        """
+        # if it's a valid click, append the coordinates to the list
+        if event.inaxes:
+            self.clicks.append((event.xdata, event.ydata))
+            if self.verbose: 
+                print "input %i: %f,%f" % (len(self.clicks), 
+                                    event.xdata, event.ydata)
+
+    def __call__(self, fig, n=1, timeout=30, verbose=False):
+        """ Blocking call to retrieve n coordinate pairs through mouse 
+            clicks.
+        """
+
+        assert isinstance(n, int), "Requires an integer argument"
+
+        # Ensure that the current figure is shown 
+        fig.show()
+        # connect the click events to the on_click function call
+        self.callback = fig.canvas.mpl_connect('button_press_event', 
+                                                    self.on_click)
+
+        # initialize the list of click coordinates
+        self.clicks = []
+
+        self.verbose = verbose
+
+        # wait for n clicks
+        counter = 0
+        while len(self.clicks) < n:
+            fig.canvas.flush_events()
+            # rest for a moment
+            time.sleep(0.01)
+
+            # check for a timeout
+            counter += 1
+            if timeout > 0 and counter > timeout/0.01: 
+                print "ginput timeout"; 
+                break;
+
+        # All done! Disconnect the event and return what we have
+        fig.canvas.mpl_disconnect(self.callback)
+        self.callback = None
+        return self.clicks
+
+
+def ginput(n=1, timeout=30, verbose=False):
+    """
+    Blocking call to interact with the figure. 
+
+    This will wait for n clicks from the user and return a list of the 
+    coordinates of each click.
+
+    If timeout is negative, does not timeout.
+    """
+
+    blocking_mouse_input = BlockingMouseInput()
+    return blocking_mouse_input(gcf(), n, timeout, verbose=verbose)
+
+if __name__ == "__main__":
+    import pylab
+    t = pylab.arange(10)
+    pylab.plot(t, pylab.sin(t))
+    print "Please click"
+    ginput(3, verbose=True)
+
Index: trunk/matplotlib/lib/matplotlib/backends/backend_qt.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/backends/backend_qt.py      (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backends/backend_qt.py      (working copy)
@@ -175,6 +175,9 @@
 
         return key
 
+    def flush_events(self):
+        qt.qApp.processEvents()
+
 class FigureManagerQT( FigureManagerBase ):
     """
     Public attributes
Index: trunk/matplotlib/lib/matplotlib/backends/backend_gtk.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/backends/backend_gtk.py     (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backends/backend_gtk.py     (working copy)
@@ -386,6 +386,13 @@
     def get_default_filetype(self):
         return 'png'
 
+    def flush_events(self):
+        gtk.gdk.threads_enter()        
+        while gtk.events_pending():
+            gtk.main_iteration(True)
+        gtk.gdk.flush()
+        gtk.gdk.threads_leave()
+        
             
 class FigureManagerGTK(FigureManagerBase):
     """
Index: trunk/matplotlib/lib/matplotlib/backends/backend_tkagg.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/backends/backend_tkagg.py   (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backends/backend_tkagg.py   (working copy)
@@ -269,8 +269,9 @@
         key = self._get_key(event)
         FigureCanvasBase.key_release_event(self, key, guiEvent=event)
 
+    def flush_events(self):
+        self._master.update()
 
-
 class FigureManagerTkAgg(FigureManagerBase):
     """
     Public attributes
Index: trunk/matplotlib/lib/matplotlib/backends/backend_qt4.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/backends/backend_qt4.py     (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backends/backend_qt4.py     (working copy)
@@ -13,7 +13,7 @@
 from matplotlib.mathtext import MathTextParser
 from matplotlib.widgets import SubplotTool
 
-from PyQt4 import QtCore, QtGui
+from PyQt4 import QtCore, QtGui, Qt
 
 backend_version = "0.9.1"
 def fn_name(): return sys._getframe(1).f_code.co_name
@@ -174,6 +174,9 @@
 
         return key
 
+    def flush_events(self):
+        Qt.qApp.processEvents()
+
 class FigureManagerQT( FigureManagerBase ):
     """
     Public attributes
-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-devel

Reply via email to