diff --git a/IPython/__init__.py b/IPython/__init__.py index d44f528..d1a36c3 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -63,6 +63,16 @@ Shell = shell from IPython.core import ipapi from IPython.core import iplib +from IPython.lib import ( + enable_wx, disable_wx, + enable_gtk, disable_gtk, + enable_qt4, disable_qt4, + enable_tk, disable_tk, + set_inputhook, clear_inputhook, + current_gui, spin, + appstart_qt4, appstart_wx +) + # Release data from IPython.core import release # do it explicitly so pydoc can see it - pydoc bug __author__ = '%s <%s>\n%s <%s>\n%s <%s>' % \ diff --git a/IPython/lib/__init__.py b/IPython/lib/__init__.py index 8e4a70c..c0d8767 100644 --- a/IPython/lib/__init__.py +++ b/IPython/lib/__init__.py @@ -20,7 +20,9 @@ from IPython.lib.inputhook import ( enable_gtk, disable_gtk, enable_qt4, disable_qt4, enable_tk, disable_tk, - set_inputhook, clear_inputhook + set_inputhook, clear_inputhook, + current_gui, spin, + appstart_qt4, appstart_wx ) #----------------------------------------------------------------------------- diff --git a/IPython/lib/inputhook.py b/IPython/lib/inputhook.py index 2bfc953..5f052a5 100755 --- a/IPython/lib/inputhook.py +++ b/IPython/lib/inputhook.py @@ -19,70 +19,104 @@ import ctypes import sys #----------------------------------------------------------------------------- -# Code +# Constants +#----------------------------------------------------------------------------- + +# Constants for identifying the GUI toolkits. +GUI_WX = 'wx' +GUI_QT4 = 'qt4' +GUI_GTK = 'gtk' +GUI_TK = 'tk' + +#----------------------------------------------------------------------------- +# Utility classes #----------------------------------------------------------------------------- -def appstart_qt4(): - from PyQt4 import QtCore, QtGui - - app = QtCore.QCoreApplication.instance() - print 'qtapp:', app - if app is not None: - if current_gui() == 'qt4': - pass - else: - app.exec_() - class _DummyMainloop(object): """A special manager to hijack GUI mainloops that is mostly a no-op. - This does have, however, special logic. + We are not using this class currently as it breaks GUI code that calls + a mainloop function after the app has started to process pending events. """ def __init__(self, ml, ihm, gui_type): self.ml = ml self.ihm = ihm self.gui_type = gui_type - - + def __call__(self, *args, **kw): - force = kw.pop('force', False) - force = False - if force: - #print 'forced spin' # dbg - self.ml(*args, **kw) - if self.ihm.current_gui() == self.gui_type: pass else: self.ml(*args, **kw) -def spin_qt4(): +#----------------------------------------------------------------------------- +# Appstart and spin functions +#----------------------------------------------------------------------------- + + +def appstart_qt4(app): + """Start the qt4 event loop in a way that plays with IPython. + + When a qt4 app is run interactively in IPython, the event loop should + not be started. This function checks to see if IPython's qt4 integration + is activated and if so, it passes. If not, it will call the :meth:`exec_` + method of the main qt4 app. + + This function should be used by users who want their qt4 scripts to work + both at the command line and in IPython. These users should put the + following logic at the bottom on their script, after they create a + :class:`QApplication` instance (called ``app`` here):: + + try: + from IPython.lib.inputhook import appstart_qt4 + appstart_qt4(app) + except ImportError: + app.exec_() + """ from PyQt4 import QtCore, QtGui - app = QtCore.QCoreApplication.instance() - if (app is not None) and (app.thread() == QtCore.QThread.currentThread()): - ## timer = QtCore.QTimer() - ## QtCore.QObject.connect(timer, - ## QtCore.SIGNAL('timeout()'), - ## app, - ## QtCore.SLOT('quit()')) - ## timer.start(100) - #QtCore.QCoreApplication.exec_(force=True) - QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) - ##timer.stop() - - -def spin_wx(): - app = wx.GetApp() - if app is not None and wx.Thread_IsMain(): - evtloop = wx.EventLoop() - ea = wx.EventLoopActivator(evtloop) - while evtloop.Pending(): - evtloop.Dispatch() - app.ProcessIdle() - del ea + assert isinstance(app, QtCore.QCoreApplication) + if app is not None: + if current_gui() == GUI_QT4: + pass + else: + app.exec_() + + +def appstart_wx(app): + """Start the wx event loop in a way that plays with IPython. + + When a wx app is run interactively in IPython, the event loop should + not be started. This function checks to see if IPython's wx integration + is activated and if so, it passes. If not, it will call the + :meth:`MainLoop` method of the main qt4 app. + + This function should be used by users who want their wx scripts to work + both at the command line and in IPython. These users should put the + following logic at the bottom on their script, after they create a + :class:`App` instance (called ``app`` here):: + + try: + from IPython.lib.inputhook import appstart_wx + appstart_wx(app) + except ImportError: + app.MainLoop() + """ + import wx + + assert isinstance(app, wx.App) + if app is not None: + if current_gui() == GUI_WX: + pass + else: + app.MainLoop() + + +#----------------------------------------------------------------------------- +# Main InputHookManager class +#----------------------------------------------------------------------------- class InputHookManager(object): @@ -95,6 +129,11 @@ class InputHookManager(object): def __init__(self): self.PYFUNC = ctypes.PYFUNCTYPE(ctypes.c_int) self._apps = {} + self._spinner_dict = { + GUI_QT4 : self._spin_qt4, + GUI_WX : self._spin_wx, + GUI_GTK : self._spin_gtk, + GUI_TK : self._spin_tk} self._reset() def _reset(self): @@ -104,7 +143,11 @@ class InputHookManager(object): self._current_gui = None def _hijack_wx(self): - """Hijack the wx mainloop so a user calling it won't cause badness.""" + """Hijack the wx mainloop so a user calling it won't cause badness. + + We are not currently using this as it breaks GUI code that calls a + mainloop at anytime but startup. + """ import wx if hasattr(wx, '_core_'): core = getattr(wx, '_core_') elif hasattr(wx, '_core'): core = getattr(wx, '_core') @@ -114,28 +157,100 @@ class InputHookManager(object): return orig_mainloop def _hijack_qt4(self): - """Hijack the qt4 mainloop so a user calling it won't cause badness.""" + """Hijack the qt4 mainloop so a user calling it won't cause badness. + + We are not currently using this as it breaks GUI code that calls a + mainloop at anytime but startup. + """ from PyQt4 import QtGui, QtCore orig_mainloop = QtGui.qApp.exec_ - dumb_ml = _DummyMainloop(orig_mainloop, self, 'qt4') + dumb_ml = _DummyMainloop(orig_mainloop, self, GUI_QT4) QtGui.qApp.exec_ = dumb_ml QtGui.QApplication.exec_ = dumb_ml QtCore.QCoreApplication.exec_ = dumb_ml return orig_mainloop def _hijack_gtk(self): - """Hijack the gtk mainloop so a user calling it won't cause badness.""" + """Hijack the gtk mainloop so a user calling it won't cause badness. + + We are not currently using this as it breaks GUI code that calls a + mainloop at anytime but startup. + """ import gtk orig_mainloop = gtk.main - gtk.mainloop = _DummyMainloop - gtk.main = _DummyMainloop + dumb_ml = _DummyMainloop(orig_mainloop, self, GUI_GTK) + gtk.mainloop = dumb_ml + gtk.main = dumb_ml return orig_mainloop def _hijack_tk(self): - """Hijack the tk mainloop so a user calling it won't cause badness.""" + """Hijack the tk mainloop so a user calling it won't cause badness. + + We are not currently using this as it breaks GUI code that calls a + mainloop at anytime but startup. + """ import Tkinter - Tkinter.Misc.mainloop = _DummyMainloop - Tkinter.mainloop = _DummyMainloop + orig_mainloop = gtk.main + dumb_ml = _DummyMainloop(orig_mainloop, self, GUI_TK) + Tkinter.Misc.mainloop = dumb_ml + Tkinter.mainloop = dumb_ml + + def _spin_qt4(self): + """Process all pending events in the qt4 event loop. + + This is for internal IPython use only and user code should not call this. + Instead, they should issue the raw GUI calls themselves. + """ + from PyQt4 import QtCore, QtGui + + app = QtCore.QCoreApplication.instance() + if app is not None: + QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents) + + def _spin_wx(self): + """Process all pending events in the wx event loop. + + This is for internal IPython use only and user code should not call this. + Instead, they should issue the raw GUI calls themselves. + """ + import wx + app = wx.GetApp() + if app is not None and wx.Thread_IsMain(): + evtloop = wx.EventLoop() + ea = wx.EventLoopActivator(evtloop) + while evtloop.Pending(): + evtloop.Dispatch() + app.ProcessIdle() + del ea + + def _spin_gtk(self): + """Process all pending events in the gtk event loop. + + This is for internal IPython use only and user code should not call this. + Instead, they should issue the raw GUI calls themselves. + """ + pass + + def _spin_tk(self): + """Process all pending events in the tk event loop. + + This is for internal IPython use only and user code should not call this. + Instead, they should issue the raw GUI calls themselves. + """ + app = self._apps.get(GUI_TK) + if app is not None: + app.update() + + def spin(self): + """Process pending events in the current gui. + + This method is just provided for IPython to use internally if needed + for things like testing. Third party projects should not call this + method, but instead should call the underlying GUI toolkit methods + that we are calling. + """ + spinner = self._spinner_dict.get(self._current_gui, lambda: None) + spinner() def get_pyos_inputhook(self): """Return the current PyOS_InputHook as a ctypes.c_void_p.""" @@ -211,14 +326,13 @@ class InputHookManager(object): """ from IPython.lib.inputhookwx import inputhook_wx self.set_inputhook(inputhook_wx) - self._current_gui = 'wx' - self._hijack_wx() + self._current_gui = GUI_WX if app: import wx app = wx.GetApp() if app is None: app = wx.App(redirect=False, clearSigInt=False) - self._apps['wx'] = app + self._apps[GUI_WX] = app return app def disable_wx(self): @@ -259,14 +373,13 @@ class InputHookManager(object): QtCore.pyqtRestoreInputHook() except AttributeError: pass - self._current_gui = 'qt4' - #self._hijack_qt4() + self._current_gui = GUI_QT4 if app: from PyQt4 import QtGui app = QtCore.QCoreApplication.instance() if app is None: app = QtGui.QApplication(sys.argv) - self._apps['qt4'] = app + self._apps[GUI_QT4] = app return app def disable_qt4(self): @@ -294,13 +407,12 @@ class InputHookManager(object): import gtk try: gtk.set_interactive(True) - self._current_gui = 'gtk' + self._current_gui = GUI_GTK except AttributeError: # For older versions of gtk, use our own ctypes version from IPython.lib.inputhookgtk import inputhook_gtk self.set_inputhook(inputhook_gtk) - self._current_gui = 'gtk' - self._hijack_gtk() + self._current_gui = GUI_GTK def disable_gtk(self): """Disable event loop integration with PyGTK. @@ -322,8 +434,13 @@ class InputHookManager(object): Currently this is a no-op as creating a :class:`Tkinter.Tk` object sets ``PyOS_InputHook``. """ - self._current_gui = 'tk' - self._hijack_tk() + self._current_gui = GUI_TK + if app: + import Tkinter + app = Tkinter.Tk() + app.withdraw() + self._apps[GUI_TK] = app + return app def disable_tk(self): """Disable event loop integration with Tkinter. @@ -350,3 +467,4 @@ clear_inputhook = inputhook_manager.clear_inputhook set_inputhook = inputhook_manager.set_inputhook current_gui = inputhook_manager.current_gui clear_app_refs = inputhook_manager.clear_app_refs +spin = inputhook_manager.spin diff --git a/docs/examples/core/gui-qt.py b/docs/examples/lib/gui-qt.py similarity index 94% rename from docs/examples/core/gui-qt.py rename to docs/examples/lib/gui-qt.py index ce8b870..4a0b935 100755 --- a/docs/examples/core/gui-qt.py +++ b/docs/examples/lib/gui-qt.py @@ -35,8 +35,6 @@ if __name__ == '__main__': sw.show() try: - import IPython.lib.inputhook as i; i.appstart_qt4() + from IPython import appstart_qt4; appstart_qt4(app) except ImportError: app.exec_() - - #import time; time.sleep(10) diff --git a/docs/examples/lib/gui-wx.py b/docs/examples/lib/gui-wx.py new file mode 100644 index 0000000..007d7dc --- /dev/null +++ b/docs/examples/lib/gui-wx.py @@ -0,0 +1,99 @@ +"""A Simple wx example to test IPython's event loop integration. + +To run this do: + +In [5]: %gui wx + +In [6]: %run gui-wx.py + +Ref: Modified from wxPython source code wxPython/samples/simple/simple.py + +This example can only be run once in a given IPython session. +""" + +import wx + + +class MyFrame(wx.Frame): + """ + This is MyFrame. It just shows a few controls on a wxPanel, + and has a simple menu. + """ + def __init__(self, parent, title): + wx.Frame.__init__(self, parent, -1, title, + pos=(150, 150), size=(350, 200)) + + # Create the menubar + menuBar = wx.MenuBar() + + # and a menu + menu = wx.Menu() + + # add an item to the menu, using \tKeyName automatically + # creates an accelerator, the third param is some help text + # that will show up in the statusbar + menu.Append(wx.ID_EXIT, "E&xit\tAlt-X", "Exit this simple sample") + + # bind the menu event to an event handler + self.Bind(wx.EVT_MENU, self.OnTimeToClose, id=wx.ID_EXIT) + + # and put the menu on the menubar + menuBar.Append(menu, "&File") + self.SetMenuBar(menuBar) + + self.CreateStatusBar() + + # Now create the Panel to put the other controls on. + panel = wx.Panel(self) + + # and a few controls + text = wx.StaticText(panel, -1, "Hello World!") + text.SetFont(wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD)) + text.SetSize(text.GetBestSize()) + btn = wx.Button(panel, -1, "Close") + funbtn = wx.Button(panel, -1, "Just for fun...") + + # bind the button events to handlers + self.Bind(wx.EVT_BUTTON, self.OnTimeToClose, btn) + self.Bind(wx.EVT_BUTTON, self.OnFunButton, funbtn) + + # Use a sizer to layout the controls, stacked vertically and with + # a 10 pixel border around each + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(text, 0, wx.ALL, 10) + sizer.Add(btn, 0, wx.ALL, 10) + sizer.Add(funbtn, 0, wx.ALL, 10) + panel.SetSizer(sizer) + panel.Layout() + + + def OnTimeToClose(self, evt): + """Event handler for the button click.""" + print "See ya later!" + self.Close() + + def OnFunButton(self, evt): + """Event handler for the button click.""" + print "Having fun yet?" + + +class MyApp(wx.App): + def OnInit(self): + frame = MyFrame(None, "Simple wxPython App") + self.SetTopWindow(frame) + + print "Print statements go to this stdout window by default." + + frame.Show(True) + return True + +app = wx.GetApp() +if app is None: + app = MyApp(redirect=False, clearSigInt=False) + +try: + from IPython.lib.inputhook import appstart_wx + appstart_wx(app) +except ImportError: + app.MainLoop() + diff --git a/docs/examples/core/switchgui.py b/docs/examples/lib/switchgui.py similarity index 51% rename from docs/examples/core/switchgui.py rename to docs/examples/lib/switchgui.py index 9037329..165a60e 100644 --- a/docs/examples/core/switchgui.py +++ b/docs/examples/lib/switchgui.py @@ -1,6 +1,8 @@ """Test the new %gui command. Run this in ipython as -%run switchgui [backend] +In [1]: %gui [backend] + +In [2]: %run switchgui [backend] where the optional backend can be one of: qt4, gtk, tk, wx. """ @@ -8,23 +10,18 @@ where the optional backend can be one of: qt4, gtk, tk, wx. import sys import time -import IPython.core.ipapi as ipapi -ip = ipapi.get() - from IPython.lib import inputhook -try: - backend = sys.argv[1] - #a = ip.magic('gui -a %s' % backend) - #a = ip.magic('gui %s' % backend) -except IndexError: - backend = 'qt' +gui = inputhook.current_gui() +if gui is None: + gui = 'qt4' + inputhook.enable_qt4(app=True) -backends = dict(wx='wxagg', qt='qt4agg', gtk='gtkagg', tk='tkagg') +backends = dict(wx='wxagg', qt4='qt4agg', gtk='gtkagg', tk='tkagg') import matplotlib -matplotlib.use(backends[backend]) -#matplotlib.interactive(True) +matplotlib.use(backends[gui]) +matplotlib.interactive(True) import matplotlib from matplotlib import pyplot as plt, mlab, pylab @@ -33,25 +30,19 @@ import numpy as np from numpy import * from matplotlib.pyplot import * -x = np.linspace(0,pi,100) +x = np.linspace(0,pi,500) print "A plot has been created" line, = plot(x,sin(2*x)) -plt.show() -inputhook.spin_qt4() +inputhook.spin() -#raw_input("Press Enter to continue") -print "I will now count until 10, please hit Ctrl-C before I'm done..." -print "IPython should stop counting and return to the prompt without crashing." +print "Now, we will update the plot..." print -line_x = line.get_data()[0] for i in range(1,51): print i, sys.stdout.flush() - line.set_data(line_x,sin(x*i)) + line.set_data(x,sin(x*i)) plt.title('i=%d' % i) - #plt.show() plt.draw() - inputhook.spin_qt4() - #time.sleep(0.04) + inputhook.spin()