diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py new file mode 100644 index 0000000..6ad6919 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -0,0 +1,16 @@ +import importlib +import os + +aliases = { + 'qt4': 'qt' +} + +def get_inputhook_func(gui): + if gui in aliases: + return get_inputhook_func(aliases[gui]) + + if gui == 'qt5': + os.environ['QT_API'] = 'pyqt5' + + mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui) + return mod.inputhook diff --git a/IPython/terminal/pt_inputhooks/gtk.py b/IPython/terminal/pt_inputhooks/gtk.py new file mode 100644 index 0000000..49bfeb3 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/gtk.py @@ -0,0 +1,55 @@ +# Code borrowed from python-prompt-toolkit examples +# https://github.com/jonathanslenders/python-prompt-toolkit/blob/77cdcfbc7f4b4c34a9d2f9a34d422d7152f16209/examples/inputhook.py + +# Copyright (c) 2014, Jonathan Slenders +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# * Neither the name of the {organization} nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +PyGTK input hook for prompt_toolkit. + +Listens on the pipe prompt_toolkit sets up for a notification that it should +return control to the terminal event loop. +""" + +import gtk, gobject + +def inputhook(context): + """ + When the eventloop of prompt-toolkit is idle, call this inputhook. + + This will run the GTK main loop until the file descriptor + `context.fileno()` becomes ready. + + :param context: An `InputHookContext` instance. + """ + def _main_quit(*a, **kw): + gtk.main_quit() + return False + + gobject.io_add_watch(context.fileno(), gobject.IO_IN, _main_quit) + gtk.main() diff --git a/IPython/terminal/pt_inputhooks/gtk3.py b/IPython/terminal/pt_inputhooks/gtk3.py new file mode 100644 index 0000000..5c6c545 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/gtk3.py @@ -0,0 +1,12 @@ +"""prompt_toolkit input hook for GTK 3 +""" + +from gi.repository import Gtk, GLib + +def _main_quit(*args, **kwargs): + Gtk.main_quit() + return False + +def inputhook(context): + GLib.io_add_watch(context.fileno(), GLib.IO_IN, _main_quit) + Gtk.main() diff --git a/IPython/terminal/pt_inputhooks/qt.py b/IPython/terminal/pt_inputhooks/qt.py new file mode 100644 index 0000000..1fd4e92 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/qt.py @@ -0,0 +1,11 @@ +from IPython.external.qt_for_kernel import QtCore, QtGui + +def inputhook(context): + app = QtCore.QCoreApplication.instance() + if not app: + return + event_loop = QtCore.QEventLoop(app) + notifier = QtCore.QSocketNotifier(context.fileno(), QtCore.QSocketNotifier.Read) + notifier.setEnabled(True) + notifier.activated.connect(event_loop.exit) + event_loop.exec_() diff --git a/IPython/terminal/pt_inputhooks/tk.py b/IPython/terminal/pt_inputhooks/tk.py new file mode 100644 index 0000000..24313a8 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/tk.py @@ -0,0 +1,93 @@ +# Code borrowed from ptpython +# https://github.com/jonathanslenders/ptpython/blob/86b71a89626114b18898a0af463978bdb32eeb70/ptpython/eventloop.py + +# Copyright (c) 2015, Jonathan Slenders +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# * Neither the name of the {organization} nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Wrapper around the eventloop that gives some time to the Tkinter GUI to process +events when it's loaded and while we are waiting for input at the REPL. This +way we don't block the UI of for instance ``turtle`` and other Tk libraries. + +(Normally Tkinter registeres it's callbacks in ``PyOS_InputHook`` to integrate +in readline. ``prompt-toolkit`` doesn't understand that input hook, but this +will fix it for Tk.) +""" +import time + +import _tkinter +try: + import tkinter +except ImportError: + import Tkinter as tkinter # Python 2 + +def inputhook(inputhook_context): + """ + Inputhook for Tk. + Run the Tk eventloop until prompt-toolkit needs to process the next input. + """ + # Get the current TK application. + root = tkinter._default_root + + def wait_using_filehandler(): + """ + Run the TK eventloop until the file handler that we got from the + inputhook becomes readable. + """ + # Add a handler that sets the stop flag when `prompt-toolkit` has input + # to process. + stop = [False] + def done(*a): + stop[0] = True + + root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done) + + # Run the TK event loop as long as we don't receive input. + while root.dooneevent(_tkinter.ALL_EVENTS): + if stop[0]: + break + + root.deletefilehandler(inputhook_context.fileno()) + + def wait_using_polling(): + """ + Windows TK doesn't support 'createfilehandler'. + So, run the TK eventloop and poll until input is ready. + """ + while not inputhook_context.input_is_ready(): + while root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT): + pass + # Sleep to make the CPU idle, but not too long, so that the UI + # stays responsive. + time.sleep(.01) + + if root is not None: + if hasattr(root, 'createfilehandler'): + wait_using_filehandler() + else: + wait_using_polling() diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 5742ddb..babaa07 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -9,7 +9,7 @@ from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import HasFocus, HasSelection from prompt_toolkit.history import InMemoryHistory -from prompt_toolkit.shortcuts import create_prompt_application +from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.key_binding.vi_state import InputMode @@ -22,6 +22,8 @@ from pygments.styles import get_style_by_name from pygments.lexers import Python3Lexer, PythonLexer from pygments.token import Token +from .pt_inputhooks import get_inputhook_func + class IPythonPTCompleter(Completer): """Adaptor to provide IPython completions to prompt_toolkit""" @@ -40,7 +42,6 @@ class IPythonPTCompleter(Completer): for m in matches: yield Completion(m, start_position=start_pos) - class PTInteractiveShell(InteractiveShell): colors_force = True @@ -70,7 +71,6 @@ class PTInteractiveShell(InteractiveShell): (Token.Prompt, (' ' * (width - 2)) + ': '), ] - def init_prompt_toolkit_cli(self): kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode) insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT) @@ -135,7 +135,8 @@ class PTInteractiveShell(InteractiveShell): style=style, ) - self.pt_cli = CommandLineInterface(app) + self.pt_cli = CommandLineInterface(app, + eventloop=create_eventloop(self.inputhook)) def __init__(self, *args, **kwargs): super(PTInteractiveShell, self).__init__(*args, **kwargs) @@ -158,6 +159,16 @@ class PTInteractiveShell(InteractiveShell): if document: self.run_cell(document.text, store_history=True) + _inputhook = None + def inputhook(self, context): + if self._inputhook is not None: + self._inputhook(context) + + def enable_gui(self, gui=None): + if gui: + self._inputhook = get_inputhook_func(gui) + else: + self._inputhook = None if __name__ == '__main__': PTInteractiveShell.instance().interact()