inputhooks
This commit is contained in:
parent
9c862c21bc
commit
1cff18d1c0
@ -0,0 +1,49 @@
|
|||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
aliases = {
|
||||||
|
'qt4': 'qt',
|
||||||
|
'gtk2': 'gtk',
|
||||||
|
}
|
||||||
|
|
||||||
|
backends = [
|
||||||
|
'qt', 'qt4', 'qt5',
|
||||||
|
'gtk', 'gtk2', 'gtk3',
|
||||||
|
'tk',
|
||||||
|
'wx',
|
||||||
|
'pyglet', 'glut',
|
||||||
|
'osx',
|
||||||
|
]
|
||||||
|
|
||||||
|
registered = {}
|
||||||
|
|
||||||
|
def register(name, inputhook):
|
||||||
|
"""Register the function *inputhook* as an event loop integration."""
|
||||||
|
registered[name] = inputhook
|
||||||
|
|
||||||
|
class UnknownBackend(KeyError):
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("No event loop integration for {!r}. "
|
||||||
|
"Supported event loops are: {}").format(self.name,
|
||||||
|
', '.join(backends + sorted(registered)))
|
||||||
|
|
||||||
|
def get_inputhook_name_and_func(gui):
|
||||||
|
if gui in registered:
|
||||||
|
return gui, registered[gui]
|
||||||
|
|
||||||
|
if gui not in backends:
|
||||||
|
raise UnknownBackend(gui)
|
||||||
|
|
||||||
|
if gui in aliases:
|
||||||
|
return get_inputhook_name_and_func(aliases[gui])
|
||||||
|
|
||||||
|
gui_mod = gui
|
||||||
|
if gui == 'qt5':
|
||||||
|
os.environ['QT_API'] = 'pyqt5'
|
||||||
|
gui_mod = 'qt'
|
||||||
|
|
||||||
|
mod = importlib.import_module('yap_ipython.terminal.pt_inputhooks.'+gui_mod)
|
||||||
|
return gui, mod.inputhook
|
@ -0,0 +1,140 @@
|
|||||||
|
"""GLUT Input hook for interactive use with prompt_toolkit
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# GLUT is quite an old library and it is difficult to ensure proper
|
||||||
|
# integration within yap_ipython since original GLUT does not allow to handle
|
||||||
|
# events one by one. Instead, it requires for the mainloop to be entered
|
||||||
|
# and never returned (there is not even a function to exit he
|
||||||
|
# mainloop). Fortunately, there are alternatives such as freeglut
|
||||||
|
# (available for linux and windows) and the OSX implementation gives
|
||||||
|
# access to a glutCheckLoop() function that blocks itself until a new
|
||||||
|
# event is received. This means we have to setup the idle callback to
|
||||||
|
# ensure we got at least one event that will unblock the function.
|
||||||
|
#
|
||||||
|
# Furthermore, it is not possible to install these handlers without a window
|
||||||
|
# being first created. We choose to make this window invisible. This means that
|
||||||
|
# display mode options are set at this level and user won't be able to change
|
||||||
|
# them later without modifying the code. This should probably be made available
|
||||||
|
# via yap_ipython options system.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import signal
|
||||||
|
import OpenGL.GLUT as glut
|
||||||
|
import OpenGL.platform as platform
|
||||||
|
from timeit import default_timer as clock
|
||||||
|
|
||||||
|
# Frame per second : 60
|
||||||
|
# Should probably be an yap_ipython option
|
||||||
|
glut_fps = 60
|
||||||
|
|
||||||
|
# Display mode : double buffeed + rgba + depth
|
||||||
|
# Should probably be an yap_ipython option
|
||||||
|
glut_display_mode = (glut.GLUT_DOUBLE |
|
||||||
|
glut.GLUT_RGBA |
|
||||||
|
glut.GLUT_DEPTH)
|
||||||
|
|
||||||
|
glutMainLoopEvent = None
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
try:
|
||||||
|
glutCheckLoop = platform.createBaseFunction(
|
||||||
|
'glutCheckLoop', dll=platform.GLUT, resultType=None,
|
||||||
|
argTypes=[],
|
||||||
|
doc='glutCheckLoop( ) -> None',
|
||||||
|
argNames=(),
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
raise RuntimeError(
|
||||||
|
'''Your glut implementation does not allow interactive sessions'''
|
||||||
|
'''Consider installing freeglut.''')
|
||||||
|
glutMainLoopEvent = glutCheckLoop
|
||||||
|
elif glut.HAVE_FREEGLUT:
|
||||||
|
glutMainLoopEvent = glut.glutMainLoopEvent
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
'''Your glut implementation does not allow interactive sessions. '''
|
||||||
|
'''Consider installing freeglut.''')
|
||||||
|
|
||||||
|
|
||||||
|
def glut_display():
|
||||||
|
# Dummy display function
|
||||||
|
pass
|
||||||
|
|
||||||
|
def glut_idle():
|
||||||
|
# Dummy idle function
|
||||||
|
pass
|
||||||
|
|
||||||
|
def glut_close():
|
||||||
|
# Close function only hides the current window
|
||||||
|
glut.glutHideWindow()
|
||||||
|
glutMainLoopEvent()
|
||||||
|
|
||||||
|
def glut_int_handler(signum, frame):
|
||||||
|
# Catch sigint and print the defaultipyt message
|
||||||
|
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||||
|
print('\nKeyboardInterrupt')
|
||||||
|
# Need to reprint the prompt at this stage
|
||||||
|
|
||||||
|
# Initialisation code
|
||||||
|
glut.glutInit( sys.argv )
|
||||||
|
glut.glutInitDisplayMode( glut_display_mode )
|
||||||
|
# This is specific to freeglut
|
||||||
|
if bool(glut.glutSetOption):
|
||||||
|
glut.glutSetOption( glut.GLUT_ACTION_ON_WINDOW_CLOSE,
|
||||||
|
glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS )
|
||||||
|
glut.glutCreateWindow( b'ipython' )
|
||||||
|
glut.glutReshapeWindow( 1, 1 )
|
||||||
|
glut.glutHideWindow( )
|
||||||
|
glut.glutWMCloseFunc( glut_close )
|
||||||
|
glut.glutDisplayFunc( glut_display )
|
||||||
|
glut.glutIdleFunc( glut_idle )
|
||||||
|
|
||||||
|
|
||||||
|
def inputhook(context):
|
||||||
|
"""Run the pyglet event loop by processing pending events only.
|
||||||
|
|
||||||
|
This keeps processing pending events until stdin is ready. After
|
||||||
|
processing all pending events, a call to time.sleep is inserted. This is
|
||||||
|
needed, otherwise, CPU usage is at 100%. This sleep time should be tuned
|
||||||
|
though for best performance.
|
||||||
|
"""
|
||||||
|
# We need to protect against a user pressing Control-C when yap_ipython is
|
||||||
|
# idle and this is running. We trap KeyboardInterrupt and pass.
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, glut_int_handler)
|
||||||
|
|
||||||
|
try:
|
||||||
|
t = clock()
|
||||||
|
|
||||||
|
# Make sure the default window is set after a window has been closed
|
||||||
|
if glut.glutGetWindow() == 0:
|
||||||
|
glut.glutSetWindow( 1 )
|
||||||
|
glutMainLoopEvent()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
while not context.input_is_ready():
|
||||||
|
glutMainLoopEvent()
|
||||||
|
# We need to sleep at this point to keep the idle CPU load
|
||||||
|
# low. However, if sleep to long, GUI response is poor. As
|
||||||
|
# a compromise, we watch how often GUI events are being processed
|
||||||
|
# and switch between a short and long sleep time. Here are some
|
||||||
|
# stats useful in helping to tune this.
|
||||||
|
# time CPU load
|
||||||
|
# 0.001 13%
|
||||||
|
# 0.005 3%
|
||||||
|
# 0.01 1.5%
|
||||||
|
# 0.05 0.5%
|
||||||
|
used_time = clock() - t
|
||||||
|
if used_time > 10.0:
|
||||||
|
# print 'Sleep for 1 s' # dbg
|
||||||
|
time.sleep(1.0)
|
||||||
|
elif used_time > 0.1:
|
||||||
|
# Few GUI events coming in, so we can sleep longer
|
||||||
|
# print 'Sleep for 0.05 s' # dbg
|
||||||
|
time.sleep(0.05)
|
||||||
|
else:
|
||||||
|
# Many GUI events coming in, so sleep only very little
|
||||||
|
time.sleep(0.001)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
@ -0,0 +1,58 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# Enable threading in GTK. (Otherwise, GTK will keep the GIL.)
|
||||||
|
gtk.gdk.threads_init()
|
||||||
|
|
||||||
|
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()
|
@ -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.PRIORITY_DEFAULT, GLib.IO_IN, _main_quit)
|
||||||
|
Gtk.main()
|
@ -0,0 +1,137 @@
|
|||||||
|
"""Inputhook for OS X
|
||||||
|
|
||||||
|
Calls NSApp / CoreFoundation APIs via ctypes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# obj-c boilerplate from appnope, used under BSD 2-clause
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
from threading import Event
|
||||||
|
|
||||||
|
objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc'))
|
||||||
|
|
||||||
|
void_p = ctypes.c_void_p
|
||||||
|
|
||||||
|
objc.objc_getClass.restype = void_p
|
||||||
|
objc.sel_registerName.restype = void_p
|
||||||
|
objc.objc_msgSend.restype = void_p
|
||||||
|
objc.objc_msgSend.argtypes = [void_p, void_p]
|
||||||
|
|
||||||
|
msg = objc.objc_msgSend
|
||||||
|
|
||||||
|
def _utf8(s):
|
||||||
|
"""ensure utf8 bytes"""
|
||||||
|
if not isinstance(s, bytes):
|
||||||
|
s = s.encode('utf8')
|
||||||
|
return s
|
||||||
|
|
||||||
|
def n(name):
|
||||||
|
"""create a selector name (for ObjC methods)"""
|
||||||
|
return objc.sel_registerName(_utf8(name))
|
||||||
|
|
||||||
|
def C(classname):
|
||||||
|
"""get an ObjC Class by name"""
|
||||||
|
return objc.objc_getClass(_utf8(classname))
|
||||||
|
|
||||||
|
# end obj-c boilerplate from appnope
|
||||||
|
|
||||||
|
# CoreFoundation C-API calls we will use:
|
||||||
|
CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation'))
|
||||||
|
|
||||||
|
CFFileDescriptorCreate = CoreFoundation.CFFileDescriptorCreate
|
||||||
|
CFFileDescriptorCreate.restype = void_p
|
||||||
|
CFFileDescriptorCreate.argtypes = [void_p, ctypes.c_int, ctypes.c_bool, void_p]
|
||||||
|
|
||||||
|
CFFileDescriptorGetNativeDescriptor = CoreFoundation.CFFileDescriptorGetNativeDescriptor
|
||||||
|
CFFileDescriptorGetNativeDescriptor.restype = ctypes.c_int
|
||||||
|
CFFileDescriptorGetNativeDescriptor.argtypes = [void_p]
|
||||||
|
|
||||||
|
CFFileDescriptorEnableCallBacks = CoreFoundation.CFFileDescriptorEnableCallBacks
|
||||||
|
CFFileDescriptorEnableCallBacks.restype = None
|
||||||
|
CFFileDescriptorEnableCallBacks.argtypes = [void_p, ctypes.c_ulong]
|
||||||
|
|
||||||
|
CFFileDescriptorCreateRunLoopSource = CoreFoundation.CFFileDescriptorCreateRunLoopSource
|
||||||
|
CFFileDescriptorCreateRunLoopSource.restype = void_p
|
||||||
|
CFFileDescriptorCreateRunLoopSource.argtypes = [void_p, void_p, void_p]
|
||||||
|
|
||||||
|
CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent
|
||||||
|
CFRunLoopGetCurrent.restype = void_p
|
||||||
|
|
||||||
|
CFRunLoopAddSource = CoreFoundation.CFRunLoopAddSource
|
||||||
|
CFRunLoopAddSource.restype = None
|
||||||
|
CFRunLoopAddSource.argtypes = [void_p, void_p, void_p]
|
||||||
|
|
||||||
|
CFRelease = CoreFoundation.CFRelease
|
||||||
|
CFRelease.restype = None
|
||||||
|
CFRelease.argtypes = [void_p]
|
||||||
|
|
||||||
|
CFFileDescriptorInvalidate = CoreFoundation.CFFileDescriptorInvalidate
|
||||||
|
CFFileDescriptorInvalidate.restype = None
|
||||||
|
CFFileDescriptorInvalidate.argtypes = [void_p]
|
||||||
|
|
||||||
|
# From CFFileDescriptor.h
|
||||||
|
kCFFileDescriptorReadCallBack = 1
|
||||||
|
kCFRunLoopCommonModes = void_p.in_dll(CoreFoundation, 'kCFRunLoopCommonModes')
|
||||||
|
|
||||||
|
|
||||||
|
def _NSApp():
|
||||||
|
"""Return the global NSApplication instance (NSApp)"""
|
||||||
|
return msg(C('NSApplication'), n('sharedApplication'))
|
||||||
|
|
||||||
|
|
||||||
|
def _wake(NSApp):
|
||||||
|
"""Wake the Application"""
|
||||||
|
event = msg(C('NSEvent'),
|
||||||
|
n('otherEventWithType:location:modifierFlags:'
|
||||||
|
'timestamp:windowNumber:context:subtype:data1:data2:'),
|
||||||
|
15, # Type
|
||||||
|
0, # location
|
||||||
|
0, # flags
|
||||||
|
0, # timestamp
|
||||||
|
0, # window
|
||||||
|
None, # context
|
||||||
|
0, # subtype
|
||||||
|
0, # data1
|
||||||
|
0, # data2
|
||||||
|
)
|
||||||
|
msg(NSApp, n('postEvent:atStart:'), void_p(event), True)
|
||||||
|
|
||||||
|
|
||||||
|
_triggered = Event()
|
||||||
|
|
||||||
|
def _input_callback(fdref, flags, info):
|
||||||
|
"""Callback to fire when there's input to be read"""
|
||||||
|
_triggered.set()
|
||||||
|
CFFileDescriptorInvalidate(fdref)
|
||||||
|
CFRelease(fdref)
|
||||||
|
NSApp = _NSApp()
|
||||||
|
msg(NSApp, n('stop:'), NSApp)
|
||||||
|
_wake(NSApp)
|
||||||
|
|
||||||
|
_c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p, void_p)
|
||||||
|
_c_input_callback = _c_callback_func_type(_input_callback)
|
||||||
|
|
||||||
|
|
||||||
|
def _stop_on_read(fd):
|
||||||
|
"""Register callback to stop eventloop when there's data on fd"""
|
||||||
|
_triggered.clear()
|
||||||
|
fdref = CFFileDescriptorCreate(None, fd, False, _c_input_callback, None)
|
||||||
|
CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack)
|
||||||
|
source = CFFileDescriptorCreateRunLoopSource(None, fdref, 0)
|
||||||
|
loop = CFRunLoopGetCurrent()
|
||||||
|
CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes)
|
||||||
|
CFRelease(source)
|
||||||
|
|
||||||
|
|
||||||
|
def inputhook(context):
|
||||||
|
"""Inputhook for Cocoa (NSApp)"""
|
||||||
|
NSApp = _NSApp()
|
||||||
|
_stop_on_read(context.fileno())
|
||||||
|
msg(NSApp, n('run'))
|
||||||
|
if not _triggered.is_set():
|
||||||
|
# app closed without firing callback,
|
||||||
|
# probably due to last window being closed.
|
||||||
|
# Run the loop manually in this case,
|
||||||
|
# since there may be events still to process (#9734)
|
||||||
|
CoreFoundation.CFRunLoopRun()
|
@ -0,0 +1,66 @@
|
|||||||
|
"""Enable pyglet to be used interacively with prompt_toolkit
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from timeit import default_timer as clock
|
||||||
|
import pyglet
|
||||||
|
|
||||||
|
# On linux only, window.flip() has a bug that causes an AttributeError on
|
||||||
|
# window close. For details, see:
|
||||||
|
# http://groups.google.com/group/pyglet-users/browse_thread/thread/47c1aab9aa4a3d23/c22f9e819826799e?#c22f9e819826799e
|
||||||
|
|
||||||
|
if sys.platform.startswith('linux'):
|
||||||
|
def flip(window):
|
||||||
|
try:
|
||||||
|
window.flip()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
def flip(window):
|
||||||
|
window.flip()
|
||||||
|
|
||||||
|
|
||||||
|
def inputhook(context):
|
||||||
|
"""Run the pyglet event loop by processing pending events only.
|
||||||
|
|
||||||
|
This keeps processing pending events until stdin is ready. After
|
||||||
|
processing all pending events, a call to time.sleep is inserted. This is
|
||||||
|
needed, otherwise, CPU usage is at 100%. This sleep time should be tuned
|
||||||
|
though for best performance.
|
||||||
|
"""
|
||||||
|
# We need to protect against a user pressing Control-C when yap_ipython is
|
||||||
|
# idle and this is running. We trap KeyboardInterrupt and pass.
|
||||||
|
try:
|
||||||
|
t = clock()
|
||||||
|
while not context.input_is_ready():
|
||||||
|
pyglet.clock.tick()
|
||||||
|
for window in pyglet.app.windows:
|
||||||
|
window.switch_to()
|
||||||
|
window.dispatch_events()
|
||||||
|
window.dispatch_event('on_draw')
|
||||||
|
flip(window)
|
||||||
|
|
||||||
|
# We need to sleep at this point to keep the idle CPU load
|
||||||
|
# low. However, if sleep to long, GUI response is poor. As
|
||||||
|
# a compromise, we watch how often GUI events are being processed
|
||||||
|
# and switch between a short and long sleep time. Here are some
|
||||||
|
# stats useful in helping to tune this.
|
||||||
|
# time CPU load
|
||||||
|
# 0.001 13%
|
||||||
|
# 0.005 3%
|
||||||
|
# 0.01 1.5%
|
||||||
|
# 0.05 0.5%
|
||||||
|
used_time = clock() - t
|
||||||
|
if used_time > 10.0:
|
||||||
|
# print 'Sleep for 1 s' # dbg
|
||||||
|
time.sleep(1.0)
|
||||||
|
elif used_time > 0.1:
|
||||||
|
# Few GUI events coming in, so we can sleep longer
|
||||||
|
# print 'Sleep for 0.05 s' # dbg
|
||||||
|
time.sleep(0.05)
|
||||||
|
else:
|
||||||
|
# Many GUI events coming in, so sleep only very little
|
||||||
|
time.sleep(0.001)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
@ -0,0 +1,35 @@
|
|||||||
|
import sys
|
||||||
|
from yap_ipython.external.qt_for_kernel import QtCore, QtGui
|
||||||
|
|
||||||
|
# If we create a QApplication, keep a reference to it so that it doesn't get
|
||||||
|
# garbage collected.
|
||||||
|
_appref = None
|
||||||
|
|
||||||
|
|
||||||
|
def inputhook(context):
|
||||||
|
global _appref
|
||||||
|
app = QtCore.QCoreApplication.instance()
|
||||||
|
if not app:
|
||||||
|
_appref = app = QtGui.QApplication([" "])
|
||||||
|
event_loop = QtCore.QEventLoop(app)
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
# The QSocketNotifier method doesn't appear to work on Windows.
|
||||||
|
# Use polling instead.
|
||||||
|
timer = QtCore.QTimer()
|
||||||
|
timer.timeout.connect(event_loop.quit)
|
||||||
|
while not context.input_is_ready():
|
||||||
|
timer.start(50) # 50 ms
|
||||||
|
event_loop.exec_()
|
||||||
|
timer.stop()
|
||||||
|
else:
|
||||||
|
# On POSIX platforms, we can use a file descriptor to quit the event
|
||||||
|
# loop when there is input ready to read.
|
||||||
|
notifier = QtCore.QSocketNotifier(context.fileno(),
|
||||||
|
QtCore.QSocketNotifier.Read)
|
||||||
|
# connect the callback we care about before we turn it on
|
||||||
|
notifier.activated.connect(event_loop.exit)
|
||||||
|
notifier.setEnabled(True)
|
||||||
|
# only start the event loop we are not already flipped
|
||||||
|
if not context.input_is_ready():
|
||||||
|
event_loop.exec_()
|
@ -0,0 +1,90 @@
|
|||||||
|
# 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 registers 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
|
||||||
|
import tkinter
|
||||||
|
|
||||||
|
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()
|
@ -0,0 +1,147 @@
|
|||||||
|
"""Enable wxPython to be used interacively in prompt_toolkit
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
from timeit import default_timer as clock
|
||||||
|
import wx
|
||||||
|
|
||||||
|
|
||||||
|
def inputhook_wx1(context):
|
||||||
|
"""Run the wx event loop by processing pending events only.
|
||||||
|
|
||||||
|
This approach seems to work, but its performance is not great as it
|
||||||
|
relies on having PyOS_InputHook called regularly.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
app = wx.GetApp()
|
||||||
|
if app is not None:
|
||||||
|
assert wx.Thread_IsMain()
|
||||||
|
|
||||||
|
# Make a temporary event loop and process system events until
|
||||||
|
# there are no more waiting, then allow idle events (which
|
||||||
|
# will also deal with pending or posted wx events.)
|
||||||
|
evtloop = wx.EventLoop()
|
||||||
|
ea = wx.EventLoopActivator(evtloop)
|
||||||
|
while evtloop.Pending():
|
||||||
|
evtloop.Dispatch()
|
||||||
|
app.ProcessIdle()
|
||||||
|
del ea
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class EventLoopTimer(wx.Timer):
|
||||||
|
|
||||||
|
def __init__(self, func):
|
||||||
|
self.func = func
|
||||||
|
wx.Timer.__init__(self)
|
||||||
|
|
||||||
|
def Notify(self):
|
||||||
|
self.func()
|
||||||
|
|
||||||
|
class EventLoopRunner(object):
|
||||||
|
|
||||||
|
def Run(self, time, input_is_ready):
|
||||||
|
self.input_is_ready = input_is_ready
|
||||||
|
self.evtloop = wx.EventLoop()
|
||||||
|
self.timer = EventLoopTimer(self.check_stdin)
|
||||||
|
self.timer.Start(time)
|
||||||
|
self.evtloop.Run()
|
||||||
|
|
||||||
|
def check_stdin(self):
|
||||||
|
if self.input_is_ready():
|
||||||
|
self.timer.Stop()
|
||||||
|
self.evtloop.Exit()
|
||||||
|
|
||||||
|
def inputhook_wx2(context):
|
||||||
|
"""Run the wx event loop, polling for stdin.
|
||||||
|
|
||||||
|
This version runs the wx eventloop for an undetermined amount of time,
|
||||||
|
during which it periodically checks to see if anything is ready on
|
||||||
|
stdin. If anything is ready on stdin, the event loop exits.
|
||||||
|
|
||||||
|
The argument to elr.Run controls how often the event loop looks at stdin.
|
||||||
|
This determines the responsiveness at the keyboard. A setting of 1000
|
||||||
|
enables a user to type at most 1 char per second. I have found that a
|
||||||
|
setting of 10 gives good keyboard response. We can shorten it further,
|
||||||
|
but eventually performance would suffer from calling select/kbhit too
|
||||||
|
often.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
app = wx.GetApp()
|
||||||
|
if app is not None:
|
||||||
|
assert wx.Thread_IsMain()
|
||||||
|
elr = EventLoopRunner()
|
||||||
|
# As this time is made shorter, keyboard response improves, but idle
|
||||||
|
# CPU load goes up. 10 ms seems like a good compromise.
|
||||||
|
elr.Run(time=10, # CHANGE time here to control polling interval
|
||||||
|
input_is_ready=context.input_is_ready)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def inputhook_wx3(context):
|
||||||
|
"""Run the wx event loop by processing pending events only.
|
||||||
|
|
||||||
|
This is like inputhook_wx1, but it keeps processing pending events
|
||||||
|
until stdin is ready. After processing all pending events, a call to
|
||||||
|
time.sleep is inserted. This is needed, otherwise, CPU usage is at 100%.
|
||||||
|
This sleep time should be tuned though for best performance.
|
||||||
|
"""
|
||||||
|
# We need to protect against a user pressing Control-C when yap_ipython is
|
||||||
|
# idle and this is running. We trap KeyboardInterrupt and pass.
|
||||||
|
try:
|
||||||
|
app = wx.GetApp()
|
||||||
|
if app is not None:
|
||||||
|
assert wx.Thread_IsMain()
|
||||||
|
|
||||||
|
# The import of wx on Linux sets the handler for signal.SIGINT
|
||||||
|
# to 0. This is a bug in wx or gtk. We fix by just setting it
|
||||||
|
# back to the Python default.
|
||||||
|
if not callable(signal.getsignal(signal.SIGINT)):
|
||||||
|
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||||
|
|
||||||
|
evtloop = wx.EventLoop()
|
||||||
|
ea = wx.EventLoopActivator(evtloop)
|
||||||
|
t = clock()
|
||||||
|
while not context.input_is_ready():
|
||||||
|
while evtloop.Pending():
|
||||||
|
t = clock()
|
||||||
|
evtloop.Dispatch()
|
||||||
|
app.ProcessIdle()
|
||||||
|
# We need to sleep at this point to keep the idle CPU load
|
||||||
|
# low. However, if sleep to long, GUI response is poor. As
|
||||||
|
# a compromise, we watch how often GUI events are being processed
|
||||||
|
# and switch between a short and long sleep time. Here are some
|
||||||
|
# stats useful in helping to tune this.
|
||||||
|
# time CPU load
|
||||||
|
# 0.001 13%
|
||||||
|
# 0.005 3%
|
||||||
|
# 0.01 1.5%
|
||||||
|
# 0.05 0.5%
|
||||||
|
used_time = clock() - t
|
||||||
|
if used_time > 10.0:
|
||||||
|
# print 'Sleep for 1 s' # dbg
|
||||||
|
time.sleep(1.0)
|
||||||
|
elif used_time > 0.1:
|
||||||
|
# Few GUI events coming in, so we can sleep longer
|
||||||
|
# print 'Sleep for 0.05 s' # dbg
|
||||||
|
time.sleep(0.05)
|
||||||
|
else:
|
||||||
|
# Many GUI events coming in, so sleep only very little
|
||||||
|
time.sleep(0.001)
|
||||||
|
del ea
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
# On OSX, evtloop.Pending() always returns True, regardless of there being
|
||||||
|
# any events pending. As such we can't use implementations 1 or 3 of the
|
||||||
|
# inputhook as those depend on a pending/dispatch loop.
|
||||||
|
inputhook = inputhook_wx2
|
||||||
|
else:
|
||||||
|
# This is our default implementation
|
||||||
|
inputhook = inputhook_wx3
|
Reference in New Issue
Block a user