summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2014-03-24 21:57:41 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2014-05-11 21:29:49 -0700
commitba5e8ae2724d5069f2f95193ab89310bc7eb4b35 (patch)
tree1d66973e660d825f2f0c93f2bca7245fa5e6aa3a
parent1a7964d4d509df88f61054513e3f7f074a627b71 (diff)
downloadurwid-ba5e8ae2724d5069f2f95193ab89310bc7eb4b35.tar.gz
Have the Screen call back into MainLoop on new input.
This should be much more readily extended by async loops like asyncio and Twisted.
-rwxr-xr-xurwid/main_loop.py39
-rw-r--r--urwid/raw_display.py155
2 files changed, 103 insertions, 91 deletions
diff --git a/urwid/main_loop.py b/urwid/main_loop.py
index 9b9cee0..75972c3 100755
--- a/urwid/main_loop.py
+++ b/urwid/main_loop.py
@@ -124,7 +124,6 @@ class MainLoop(object):
event_loop = SelectEventLoop()
self.event_loop = event_loop
- self._input_timeout = None
self._watch_pipes = {}
def _set_widget(self, widget):
@@ -297,12 +296,12 @@ class MainLoop(object):
screen.get_cols_rows()
widget.render((20, 10), focus=True)
screen.draw_screen((20, 10), 'fake canvas')
- screen.get_input_descriptors()
- event_loop.watch_file(42, <bound method ...>)
+ screen.unhook_event_loop(...)
+ screen.hook_event_loop(...)
event_loop.enter_idle(<bound method ...>)
event_loop.run()
event_loop.remove_enter_idle(1)
- event_loop.remove_watch_file(2)
+ screen.unhook_event_loop(...)
>>> scr.started = False
>>> ml.run() # doctest:+ELLIPSIS
screen.run_wrapper(<bound method ...>)
@@ -339,49 +338,25 @@ class MainLoop(object):
signals.disconnect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED,
reset_input_descriptors)
self.screen.unhook_event_loop(self.event_loop)
- if self._input_timeout is not None:
- self.event_loop.remove_alarm(self._input_timeout)
- def _update(self, timeout=False):
+ def _update(self, keys, raw):
"""
>>> w = _refl("widget")
>>> w.selectable_rval = True
>>> w.mouse_event_rval = True
>>> scr = _refl("screen")
>>> scr.get_cols_rows_rval = (15, 5)
- >>> scr.get_input_nonblocking_rval = 1, ['y'], [121]
>>> evl = _refl("event_loop")
>>> ml = MainLoop(w, [], scr, event_loop=evl)
>>> ml._input_timeout = "old timeout"
- >>> ml._update() # doctest:+ELLIPSIS
- event_loop.remove_alarm('old timeout')
- screen.get_input_nonblocking()
- event_loop.alarm(1, <function ...>)
+ >>> ml._update(['y'], [121]) # doctest:+ELLIPSIS
screen.get_cols_rows()
widget.selectable()
widget.keypress((15, 5), 'y')
- >>> scr.get_input_nonblocking_rval = None, [("mouse press", 1, 5, 4)
- ... ], []
- >>> ml._update()
- screen.get_input_nonblocking()
+ >>> ml._update([("mouse press", 1, 5, 4)], [])
widget.mouse_event((15, 5), 'mouse press', 1, 5, 4, focus=True)
- >>> scr.get_input_nonblocking_rval = None, [], []
- >>> ml._update()
- screen.get_input_nonblocking()
+ >>> ml._update([], [])
"""
- if self._input_timeout is not None and not timeout:
- # cancel the timeout, something else triggered the update
- self.event_loop.remove_alarm(self._input_timeout)
- self._input_timeout = None
-
- max_wait, keys, raw = self.screen.get_input_nonblocking()
-
- if max_wait is not None:
- # if get_input_nonblocking wants to be called back
- # make sure it happens with an alarm
- self._input_timeout = self.event_loop.alarm(max_wait,
- lambda: self._update(timeout=True))
-
keys = self.input_filter(keys, raw)
if keys:
diff --git a/urwid/raw_display.py b/urwid/raw_display.py
index 27cf60a..610709e 100644
--- a/urwid/raw_display.py
+++ b/urwid/raw_display.py
@@ -209,7 +209,6 @@ class Screen(BaseScreen, RealTerminal):
self.signal_init()
self._alternate_buffer = alternate_buffer
- self._input_iter = self._run_input_iter()
self._next_timeout = self.max_wait
if not self._signal_keys_set:
@@ -251,7 +250,6 @@ class Screen(BaseScreen, RealTerminal):
+ escape.SI
+ move_cursor
+ escape.SHOW_CURSOR)
- self._input_iter = self._fake_input_iter()
if self._old_signal_keys:
self.tty_signal_keys(*(self._old_signal_keys + (fd,)))
@@ -367,81 +365,120 @@ class Screen(BaseScreen, RealTerminal):
_current_event_loop_handles = ()
def unhook_event_loop(self, event_loop):
+ """
+ Remove any hooks added by hook_event_loop.
+ """
for handle in self._current_event_loop_handles:
event_loop.remove_watch_file(handle)
+ if self._input_timeout:
+ event_loop.remove_alarm(self._input_timeout)
+ self._input_timeout = None
+
def hook_event_loop(self, event_loop, callback):
+ """
+ Register the given callback with the event loop, to be called with new
+ input whenever it's available. The callback should be passed a list of
+ processed keys and a list of unprocessed keycodes.
+
+ Subclasses may wish to use parse_input to wrap the callback.
+ """
+ if hasattr(self, 'get_input_nonblocking'):
+ wrapper = self._make_legacy_input_wrapper(event_loop, callback)
+ else:
+ wrapper = lambda: self.parse_input(event_loop, callback)
fds = self.get_input_descriptors()
handles = []
for fd in fds:
- event_loop.watch_file(fd, callback)
+ event_loop.watch_file(fd, wrapper)
self._current_event_loop_handles = handles
- def get_input_nonblocking(self):
+ _input_timeout = None
+ _partial_codes = None
+
+ def _make_legacy_input_wrapper(self, event_loop, callback):
"""
- Return a (next_input_timeout, keys_pressed, raw_keycodes)
- tuple.
+ Support old Screen classes that still have a get_input_nonblocking and
+ expect it to work.
+ """
+ def wrapper():
+ if self._input_timeout:
+ event_loop.remove_alarm(self._input_timeout)
+ self._input_timeout = None
+ timeout, keys, raw = self.get_input_nonblocking()
+ if timeout is not None:
+ self._input_timeout = event_loop.alarm(timeout, wrapper)
- Use this method if you are implementing your own event loop.
+ callback(keys, raw)
- When there is input waiting on one of the descriptors returned
- by get_input_descriptors() this method should be called to
- read and process the input.
+ return wrapper
- This method expects to be called in next_input_timeout seconds
- (a floating point number) if there is no input waiting.
+ def get_available_raw_input(self):
"""
- return self._input_iter.next()
+ Return any currently-available input. Does not block.
- def _run_input_iter(self):
- def empty_resize_pipe():
- # clean out the pipe used to signal external event loops
- # that a resize has occurred
- try:
- while True: os.read(self._resize_pipe_rd, 1)
- except OSError:
- pass
+ This method is only used by parse_input; you can safely ignore it if
+ you implement your own parse_input.
+ """
+ codes = self._get_gpm_codes() + self._get_keyboard_codes()
- while True:
- processed = []
- codes = self._get_gpm_codes() + \
- self._get_keyboard_codes()
+ if self._partial_codes:
+ codes = self._partial_codes + codes
+ self._partial_codes = None
- original_codes = codes
- try:
- while codes:
- run, codes = escape.process_keyqueue(
- codes, True)
- processed.extend(run)
- except escape.MoreInputRequired:
- k = len(original_codes) - len(codes)
- yield (self.complete_wait, processed,
- original_codes[:k])
- empty_resize_pipe()
- original_codes = codes
- processed = []
-
- codes += self._get_keyboard_codes() + \
- self._get_gpm_codes()
- while codes:
- run, codes = escape.process_keyqueue(
- codes, False)
- processed.extend(run)
-
- if self._resized:
- processed.append('window resize')
- self._resized = False
-
- yield (self.max_wait, processed, original_codes)
- empty_resize_pipe()
-
- def _fake_input_iter(self):
- """
- This generator is a placeholder for when the screen is stopped
- to always return that no input is available.
+ # clean out the pipe used to signal external event loops
+ # that a resize has occurred
+ try:
+ while True: os.read(self._resize_pipe_rd, 1)
+ except OSError:
+ pass
+
+ return codes
+
+ def parse_input(self, event_loop, callback, wait_for_more=True):
"""
- while True:
- yield (self.max_wait, [], [])
+ Read any available input from get_available_raw_input, parses it into
+ keys, and calls the given callback.
+
+ The current implementation tries to avoid any assumptions about what
+ the screen or event loop look like; it only deals with parsing keycodes
+ and setting a timeout when an incomplete one is detected.
+ """
+ if self._input_timeout:
+ event_loop.remove_alarm(self._input_timeout)
+ self._input_timeout = None
+
+ codes = self.get_available_raw_input()
+ original_codes = codes
+ processed = []
+ try:
+ while codes:
+ run, codes = escape.process_keyqueue(
+ codes, wait_for_more)
+ processed.extend(run)
+ except escape.MoreInputRequired:
+ # Set a timer to wait for the rest of the input; if it goes off
+ # without any new input having come in, use the partial input
+ k = len(original_codes) - len(codes)
+ processed_codes = original_codes[:k]
+ self._partial_codes = codes
+
+ def _parse_incomplete_input():
+ self._input_timeout = None
+ self.parse_input(
+ event_loop, callback, wait_for_more=False)
+ self._input_timeout = event_loop.alarm(
+ self.complete_wait, self._parse_incomplete_input)
+
+ else:
+ processed_codes = original_codes
+ self._partial_codes = None
+
+ if self._resized:
+ processed.append('window resize')
+ self._resized = False
+
+ callback(processed, processed_codes)
def _get_keyboard_codes(self):
codes = []