1# coding: utf-8
2"""
3Inputhook management for GUI event loop integration.
4"""
5
6#-----------------------------------------------------------------------------
7#  Copyright (C) 2008-2011  The IPython Development Team
8#
9#  Distributed under the terms of the BSD License.  The full license is in
10#  the file COPYING, distributed as part of this software.
11#-----------------------------------------------------------------------------
12
13#-----------------------------------------------------------------------------
14# Imports
15#-----------------------------------------------------------------------------
16
17import sys
18import select
19
20#-----------------------------------------------------------------------------
21# Constants
22#-----------------------------------------------------------------------------
23
24# Constants for identifying the GUI toolkits.
25GUI_WX = 'wx'
26GUI_QT = 'qt'
27GUI_QT4 = 'qt4'
28GUI_QT5 = 'qt5'
29GUI_GTK = 'gtk'
30GUI_TK = 'tk'
31GUI_OSX = 'osx'
32GUI_GLUT = 'glut'
33GUI_PYGLET = 'pyglet'
34GUI_GTK3 = 'gtk3'
35GUI_NONE = 'none'  # i.e. disable
36
37#-----------------------------------------------------------------------------
38# Utilities
39#-----------------------------------------------------------------------------
40
41def ignore_CTRL_C():
42    """Ignore CTRL+C (not implemented)."""
43    pass
44
45def allow_CTRL_C():
46    """Take CTRL+C into account (not implemented)."""
47    pass
48
49#-----------------------------------------------------------------------------
50# Main InputHookManager class
51#-----------------------------------------------------------------------------
52
53
54class InputHookManager(object):
55    """Manage PyOS_InputHook for different GUI toolkits.
56
57    This class installs various hooks under ``PyOSInputHook`` to handle
58    GUI event loop integration.
59    """
60
61    def __init__(self):
62        self._return_control_callback = None
63        self._apps = {}
64        self._reset()
65        self.pyplot_imported = False
66
67    def _reset(self):
68        self._callback_pyfunctype = None
69        self._callback = None
70        self._current_gui = None
71
72    def set_return_control_callback(self, return_control_callback):
73        self._return_control_callback = return_control_callback
74
75    def get_return_control_callback(self):
76        return self._return_control_callback
77
78    def return_control(self):
79        return self._return_control_callback()
80
81    def get_inputhook(self):
82        return self._callback
83
84    def set_inputhook(self, callback):
85        """Set inputhook to callback."""
86        # We don't (in the context of PyDev console) actually set PyOS_InputHook, but rather
87        # while waiting for input on xmlrpc we run this code
88        self._callback = callback
89
90    def clear_inputhook(self, app=None):
91        """Clear input hook.
92
93        Parameters
94        ----------
95        app : optional, ignored
96          This parameter is allowed only so that clear_inputhook() can be
97          called with a similar interface as all the ``enable_*`` methods.  But
98          the actual value of the parameter is ignored.  This uniform interface
99          makes it easier to have user-level entry points in the main IPython
100          app like :meth:`enable_gui`."""
101        self._reset()
102
103    def clear_app_refs(self, gui=None):
104        """Clear IPython's internal reference to an application instance.
105
106        Whenever we create an app for a user on qt4 or wx, we hold a
107        reference to the app.  This is needed because in some cases bad things
108        can happen if a user doesn't hold a reference themselves.  This
109        method is provided to clear the references we are holding.
110
111        Parameters
112        ----------
113        gui : None or str
114            If None, clear all app references.  If ('wx', 'qt4') clear
115            the app for that toolkit.  References are not held for gtk or tk
116            as those toolkits don't have the notion of an app.
117        """
118        if gui is None:
119            self._apps = {}
120        elif gui in self._apps:
121            del self._apps[gui]
122
123    def enable_wx(self, app=None):
124        """Enable event loop integration with wxPython.
125
126        Parameters
127        ----------
128        app : WX Application, optional.
129            Running application to use.  If not given, we probe WX for an
130            existing application object, and create a new one if none is found.
131
132        Notes
133        -----
134        This methods sets the ``PyOS_InputHook`` for wxPython, which allows
135        the wxPython to integrate with terminal based applications like
136        IPython.
137
138        If ``app`` is not given we probe for an existing one, and return it if
139        found.  If no existing app is found, we create an :class:`wx.App` as
140        follows::
141
142            import wx
143            app = wx.App(redirect=False, clearSigInt=False)
144        """
145        import wx
146        from distutils.version import LooseVersion as V
147        wx_version = V(wx.__version__).version  # @UndefinedVariable
148
149        if wx_version < [2, 8]:
150            raise ValueError("requires wxPython >= 2.8, but you have %s" % wx.__version__)  # @UndefinedVariable
151
152        from pydev_ipython.inputhookwx import inputhook_wx
153        self.set_inputhook(inputhook_wx)
154        self._current_gui = GUI_WX
155
156        if app is None:
157            app = wx.GetApp()  # @UndefinedVariable
158        if app is None:
159            app = wx.App(redirect=False, clearSigInt=False)  # @UndefinedVariable
160        app._in_event_loop = True
161        self._apps[GUI_WX] = app
162        return app
163
164    def disable_wx(self):
165        """Disable event loop integration with wxPython.
166
167        This merely sets PyOS_InputHook to NULL.
168        """
169        if GUI_WX in self._apps:
170            self._apps[GUI_WX]._in_event_loop = False
171        self.clear_inputhook()
172
173    def enable_qt(self, app=None):
174        from pydev_ipython.qt_for_kernel import QT_API, QT_API_PYQT5
175        if QT_API == QT_API_PYQT5:
176            self.enable_qt5(app)
177        else:
178            self.enable_qt4(app)
179
180    def enable_qt4(self, app=None):
181        """Enable event loop integration with PyQt4.
182
183        Parameters
184        ----------
185        app : Qt Application, optional.
186            Running application to use.  If not given, we probe Qt for an
187            existing application object, and create a new one if none is found.
188
189        Notes
190        -----
191        This methods sets the PyOS_InputHook for PyQt4, which allows
192        the PyQt4 to integrate with terminal based applications like
193        IPython.
194
195        If ``app`` is not given we probe for an existing one, and return it if
196        found.  If no existing app is found, we create an :class:`QApplication`
197        as follows::
198
199            from PyQt4 import QtCore
200            app = QtGui.QApplication(sys.argv)
201        """
202        from pydev_ipython.inputhookqt4 import create_inputhook_qt4
203        app, inputhook_qt4 = create_inputhook_qt4(self, app)
204        self.set_inputhook(inputhook_qt4)
205
206        self._current_gui = GUI_QT4
207        app._in_event_loop = True
208        self._apps[GUI_QT4] = app
209        return app
210
211    def disable_qt4(self):
212        """Disable event loop integration with PyQt4.
213
214        This merely sets PyOS_InputHook to NULL.
215        """
216        if GUI_QT4 in self._apps:
217            self._apps[GUI_QT4]._in_event_loop = False
218        self.clear_inputhook()
219
220    def enable_qt5(self, app=None):
221        from pydev_ipython.inputhookqt5 import create_inputhook_qt5
222        app, inputhook_qt5 = create_inputhook_qt5(self, app)
223        self.set_inputhook(inputhook_qt5)
224
225        self._current_gui = GUI_QT5
226        app._in_event_loop = True
227        self._apps[GUI_QT5] = app
228        return app
229
230    def disable_qt5(self):
231        if GUI_QT5 in self._apps:
232            self._apps[GUI_QT5]._in_event_loop = False
233        self.clear_inputhook()
234
235    def enable_gtk(self, app=None):
236        """Enable event loop integration with PyGTK.
237
238        Parameters
239        ----------
240        app : ignored
241           Ignored, it's only a placeholder to keep the call signature of all
242           gui activation methods consistent, which simplifies the logic of
243           supporting magics.
244
245        Notes
246        -----
247        This methods sets the PyOS_InputHook for PyGTK, which allows
248        the PyGTK to integrate with terminal based applications like
249        IPython.
250        """
251        from pydev_ipython.inputhookgtk import create_inputhook_gtk
252        self.set_inputhook(create_inputhook_gtk(self._stdin_file))
253        self._current_gui = GUI_GTK
254
255    def disable_gtk(self):
256        """Disable event loop integration with PyGTK.
257
258        This merely sets PyOS_InputHook to NULL.
259        """
260        self.clear_inputhook()
261
262    def enable_tk(self, app=None):
263        """Enable event loop integration with Tk.
264
265        Parameters
266        ----------
267        app : toplevel :class:`Tkinter.Tk` widget, optional.
268            Running toplevel widget to use.  If not given, we probe Tk for an
269            existing one, and create a new one if none is found.
270
271        Notes
272        -----
273        If you have already created a :class:`Tkinter.Tk` object, the only
274        thing done by this method is to register with the
275        :class:`InputHookManager`, since creating that object automatically
276        sets ``PyOS_InputHook``.
277        """
278        self._current_gui = GUI_TK
279        if app is None:
280            try:
281                import Tkinter as _TK
282            except:
283                # Python 3
284                import tkinter as _TK  # @UnresolvedImport
285            app = _TK.Tk()
286            app.withdraw()
287            self._apps[GUI_TK] = app
288
289        from pydev_ipython.inputhooktk import create_inputhook_tk
290        self.set_inputhook(create_inputhook_tk(app))
291        return app
292
293    def disable_tk(self):
294        """Disable event loop integration with Tkinter.
295
296        This merely sets PyOS_InputHook to NULL.
297        """
298        self.clear_inputhook()
299
300
301    def enable_glut(self, app=None):
302        """ Enable event loop integration with GLUT.
303
304        Parameters
305        ----------
306
307        app : ignored
308            Ignored, it's only a placeholder to keep the call signature of all
309            gui activation methods consistent, which simplifies the logic of
310            supporting magics.
311
312        Notes
313        -----
314
315        This methods sets the PyOS_InputHook for GLUT, which allows the GLUT to
316        integrate with terminal based applications like IPython. Due to GLUT
317        limitations, it is currently not possible to start the event loop
318        without first creating a window. You should thus not create another
319        window but use instead the created one. See 'gui-glut.py' in the
320        docs/examples/lib directory.
321
322        The default screen mode is set to:
323        glut.GLUT_DOUBLE | glut.GLUT_RGBA | glut.GLUT_DEPTH
324        """
325
326        import OpenGL.GLUT as glut  # @UnresolvedImport
327        from pydev_ipython.inputhookglut import glut_display_mode, \
328            glut_close, glut_display, \
329            glut_idle, inputhook_glut
330
331        if GUI_GLUT not in self._apps:
332            glut.glutInit(sys.argv)
333            glut.glutInitDisplayMode(glut_display_mode)
334            # This is specific to freeglut
335            if bool(glut.glutSetOption):
336                glut.glutSetOption(glut.GLUT_ACTION_ON_WINDOW_CLOSE,
337                                   glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS)
338            glut.glutCreateWindow(sys.argv[0])
339            glut.glutReshapeWindow(1, 1)
340            glut.glutHideWindow()
341            glut.glutWMCloseFunc(glut_close)
342            glut.glutDisplayFunc(glut_display)
343            glut.glutIdleFunc(glut_idle)
344        else:
345            glut.glutWMCloseFunc(glut_close)
346            glut.glutDisplayFunc(glut_display)
347            glut.glutIdleFunc(glut_idle)
348        self.set_inputhook(inputhook_glut)
349        self._current_gui = GUI_GLUT
350        self._apps[GUI_GLUT] = True
351
352
353    def disable_glut(self):
354        """Disable event loop integration with glut.
355
356        This sets PyOS_InputHook to NULL and set the display function to a
357        dummy one and set the timer to a dummy timer that will be triggered
358        very far in the future.
359        """
360        import OpenGL.GLUT as glut  # @UnresolvedImport
361        from glut_support import glutMainLoopEvent  # @UnresolvedImport
362
363        glut.glutHideWindow()  # This is an event to be processed below
364        glutMainLoopEvent()
365        self.clear_inputhook()
366
367    def enable_pyglet(self, app=None):
368        """Enable event loop integration with pyglet.
369
370        Parameters
371        ----------
372        app : ignored
373           Ignored, it's only a placeholder to keep the call signature of all
374           gui activation methods consistent, which simplifies the logic of
375           supporting magics.
376
377        Notes
378        -----
379        This methods sets the ``PyOS_InputHook`` for pyglet, which allows
380        pyglet to integrate with terminal based applications like
381        IPython.
382
383        """
384        from pydev_ipython.inputhookpyglet import inputhook_pyglet
385        self.set_inputhook(inputhook_pyglet)
386        self._current_gui = GUI_PYGLET
387        return app
388
389    def disable_pyglet(self):
390        """Disable event loop integration with pyglet.
391
392        This merely sets PyOS_InputHook to NULL.
393        """
394        self.clear_inputhook()
395
396    def enable_gtk3(self, app=None):
397        """Enable event loop integration with Gtk3 (gir bindings).
398
399        Parameters
400        ----------
401        app : ignored
402           Ignored, it's only a placeholder to keep the call signature of all
403           gui activation methods consistent, which simplifies the logic of
404           supporting magics.
405
406        Notes
407        -----
408        This methods sets the PyOS_InputHook for Gtk3, which allows
409        the Gtk3 to integrate with terminal based applications like
410        IPython.
411        """
412        from pydev_ipython.inputhookgtk3 import create_inputhook_gtk3
413        self.set_inputhook(create_inputhook_gtk3(self._stdin_file))
414        self._current_gui = GUI_GTK
415
416    def disable_gtk3(self):
417        """Disable event loop integration with PyGTK.
418
419        This merely sets PyOS_InputHook to NULL.
420        """
421        self.clear_inputhook()
422
423    def enable_mac(self, app=None):
424        """ Enable event loop integration with MacOSX.
425
426        We call function pyplot.pause, which updates and displays active
427        figure during pause. It's not MacOSX-specific, but it enables to
428        avoid inputhooks in native MacOSX backend.
429        Also we shouldn't import pyplot, until user does it. Cause it's
430        possible to choose backend before importing pyplot for the first
431        time only.
432        """
433        def inputhook_mac(app=None):
434            if self.pyplot_imported:
435                pyplot = sys.modules['matplotlib.pyplot']
436                try:
437                    pyplot.pause(0.01)
438                except:
439                    pass
440            else:
441                if 'matplotlib.pyplot' in sys.modules:
442                    self.pyplot_imported = True
443
444        self.set_inputhook(inputhook_mac)
445        self._current_gui = GUI_OSX
446
447    def disable_mac(self):
448        self.clear_inputhook()
449
450    def current_gui(self):
451        """Return a string indicating the currently active GUI or None."""
452        return self._current_gui
453
454inputhook_manager = InputHookManager()
455
456enable_wx = inputhook_manager.enable_wx
457disable_wx = inputhook_manager.disable_wx
458enable_qt = inputhook_manager.enable_qt
459enable_qt4 = inputhook_manager.enable_qt4
460disable_qt4 = inputhook_manager.disable_qt4
461enable_qt5 = inputhook_manager.enable_qt5
462disable_qt5 = inputhook_manager.disable_qt5
463enable_gtk = inputhook_manager.enable_gtk
464disable_gtk = inputhook_manager.disable_gtk
465enable_tk = inputhook_manager.enable_tk
466disable_tk = inputhook_manager.disable_tk
467enable_glut = inputhook_manager.enable_glut
468disable_glut = inputhook_manager.disable_glut
469enable_pyglet = inputhook_manager.enable_pyglet
470disable_pyglet = inputhook_manager.disable_pyglet
471enable_gtk3 = inputhook_manager.enable_gtk3
472disable_gtk3 = inputhook_manager.disable_gtk3
473enable_mac = inputhook_manager.enable_mac
474disable_mac = inputhook_manager.disable_mac
475clear_inputhook = inputhook_manager.clear_inputhook
476set_inputhook = inputhook_manager.set_inputhook
477current_gui = inputhook_manager.current_gui
478clear_app_refs = inputhook_manager.clear_app_refs
479
480# We maintain this as stdin_ready so that the individual inputhooks
481# can diverge as little as possible from their IPython sources
482stdin_ready = inputhook_manager.return_control
483set_return_control_callback = inputhook_manager.set_return_control_callback
484get_return_control_callback = inputhook_manager.get_return_control_callback
485get_inputhook = inputhook_manager.get_inputhook
486
487# Convenience function to switch amongst them
488def enable_gui(gui=None, app=None):
489    """Switch amongst GUI input hooks by name.
490
491    This is just a utility wrapper around the methods of the InputHookManager
492    object.
493
494    Parameters
495    ----------
496    gui : optional, string or None
497      If None (or 'none'), clears input hook, otherwise it must be one
498      of the recognized GUI names (see ``GUI_*`` constants in module).
499
500    app : optional, existing application object.
501      For toolkits that have the concept of a global app, you can supply an
502      existing one.  If not given, the toolkit will be probed for one, and if
503      none is found, a new one will be created.  Note that GTK does not have
504      this concept, and passing an app if ``gui=="GTK"`` will raise an error.
505
506    Returns
507    -------
508    The output of the underlying gui switch routine, typically the actual
509    PyOS_InputHook wrapper object or the GUI toolkit app created, if there was
510    one.
511    """
512
513    if get_return_control_callback() is None:
514        raise ValueError("A return_control_callback must be supplied as a reference before a gui can be enabled")
515
516    guis = {GUI_NONE: clear_inputhook,
517            GUI_OSX: enable_mac,
518            GUI_TK: enable_tk,
519            GUI_GTK: enable_gtk,
520            GUI_WX: enable_wx,
521            GUI_QT: enable_qt,
522            GUI_QT4: enable_qt4,
523            GUI_QT5: enable_qt5,
524            GUI_GLUT: enable_glut,
525            GUI_PYGLET: enable_pyglet,
526            GUI_GTK3: enable_gtk3,
527            }
528    try:
529        gui_hook = guis[gui]
530    except KeyError:
531        if gui is None or gui == '':
532            gui_hook = clear_inputhook
533        else:
534            e = "Invalid GUI request %r, valid ones are:%s" % (gui, guis.keys())
535            raise ValueError(e)
536    return gui_hook(app)
537
538__all__ = [
539    "GUI_WX",
540    "GUI_QT",
541    "GUI_QT4",
542    "GUI_QT5",
543    "GUI_GTK",
544    "GUI_TK",
545    "GUI_OSX",
546    "GUI_GLUT",
547    "GUI_PYGLET",
548    "GUI_GTK3",
549    "GUI_NONE",
550
551
552    "ignore_CTRL_C",
553    "allow_CTRL_C",
554
555    "InputHookManager",
556
557    "inputhook_manager",
558
559    "enable_wx",
560    "disable_wx",
561    "enable_qt",
562    "enable_qt4",
563    "disable_qt4",
564    "enable_qt5",
565    "disable_qt5",
566    "enable_gtk",
567    "disable_gtk",
568    "enable_tk",
569    "disable_tk",
570    "enable_glut",
571    "disable_glut",
572    "enable_pyglet",
573    "disable_pyglet",
574    "enable_gtk3",
575    "disable_gtk3",
576    "enable_mac",
577    "disable_mac",
578    "clear_inputhook",
579    "set_inputhook",
580    "current_gui",
581    "clear_app_refs",
582
583    "stdin_ready",
584    "set_return_control_callback",
585    "get_return_control_callback",
586    "get_inputhook",
587
588    "enable_gui"]
589