From 385f1b960791bc197d35dbd7d3bb6e954cf27676 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 22 Feb 2018 18:28:50 -0500 Subject: [PATCH] Add a pager widget It's a `ScrollView` but with keyboard controls that allow for paging just like the classic unix `less` program. Add a search bar widget too! --- piker/ui/kb.py | 144 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 31 deletions(-) diff --git a/piker/ui/kb.py b/piker/ui/kb.py index 34dcc8ce..ab13bd7a 100644 --- a/piker/ui/kb.py +++ b/piker/ui/kb.py @@ -1,17 +1,22 @@ """ -VI-like Keyboard controls +Keyboard controls """ +import inspect +from functools import partial + from kivy.core.window import Window +from kivy.uix.textinput import TextInput +from kivy.uix.scrollview import ScrollView from ..log import get_logger log = get_logger('keyboard') async def handle_input(widget, patts2funcs: dict, patt_len_limit=3): - """Handle keyboard inputs. + """Handle keyboard input. - For each character pattern in ``patts2funcs`` invoke the corresponding - mapped functions. + For each character pattern-tuple in ``patts2funcs`` invoke the + corresponding mapped function(s) or coro(s). """ def _kb_closed(): """Teardown handler. @@ -21,37 +26,114 @@ async def handle_input(widget, patts2funcs: dict, patt_len_limit=3): kb.unbind(on_key_down=_kb_closed) last_patt = [] - kb = Window.request_keyboard(_kb_closed, widget, 'text') - async for kb, keycode, text, modifiers in kb.async_bind( - 'on_key_down' - ): - # log.debug( - log.debug( - f"Keyboard input received:\n" - f"key {keycode}\ntext {text}\nmodifiers {modifiers}" + keyq = kb.async_bind('on_key_down') + + while True: + async for kb, keycode, text, modifiers in keyq: + log.debug( + f"Keyboard input received:\n" + f"key {keycode}\ntext {text}\nmodifiers {modifiers}" + ) + code, key = keycode + if modifiers and key in modifiers: + continue + elif modifiers and key not in modifiers: + key = '-'.join(modifiers + [key]) + + # keep track of multi-key patterns + last_patt.append(key) + + func = patts2funcs.get(tuple(last_patt)) + if not func: + func = patts2funcs.get((key,)) + + if inspect.iscoroutinefunction(func): + # stop kb queue to avoid duplicate input processing + keyq.stop() + + log.debug(f'invoking kb coro func {func}') + await func() + last_patt = [] + break # trigger loop restart + + elif func: + log.debug(f'invoking kb func {func}') + func() + last_patt = [] + + if len(last_patt) > patt_len_limit: + last_patt = [] + + log.debug(f"last_patt {last_patt}") + + log.debug("Restarting keyboard handling loop") + # rebind to avoid capturing keys processed by ^ coro + keyq = kb.async_bind('on_key_down') + + log.debug("Exitting keyboard handling loop") + + +class SearchBar(TextInput): + def __init__(self, kbctls: dict, parent, **kwargs): + super(SearchBar, self).__init__( + multiline=False, + # text='/', + **kwargs ) - code, key = keycode - if modifiers and key in modifiers: - continue - elif modifiers and key not in modifiers: - key = '-'.join(modifiers + [key]) + self.kbctls = kbctls + self._container = parent + # indicate to ``handle_input`` that search is activated on '/' + self.kbctls.update({ + ('/',): self.handle_input + }) - # keep track of multi-key patterns - last_patt.append(key) + def on_text(self, instance, value): + # ``scroll.scroll_to()`` looks super awesome here for when we + # our interproc-ticker protocol + # async for item in self.async_bind('text'): + print(value) - func = patts2funcs.get(tuple(last_patt)) - if not func: - func = patts2funcs.get(key) + async def handle_input(self): + self._container.add_widget(self) # display it + self.focus = True # focus immediately (doesn't work from __init__) + # wait for to close search bar + await self.async_bind('on_text_validate').__aiter__().__anext__() - if func: - log.debug(f'invoking func kb {func}') - func() - last_patt = [] - elif key == 'q': - kb.release() + log.debug("Closing search bar") + self._container.remove_widget(self) # stop displaying + return self.text - if len(last_patt) > patt_len_limit: - last_patt = [] - log.debug(f"last_patt {last_patt}") +class PagerView(ScrollView): + """Pager view that adds less-like scrolling and search. + """ + def __init__(self, container, nursery, kbctls: dict = None, **kwargs): + super(PagerView, self).__init__(**kwargs) + self._container = container + self.kbctls = kbctls or {} + self.kbctls.update({ + ('g', 'g'): partial(self.move_y, 1), + ('shift-g',): partial(self.move_y, 0), + ('u',): partial(self.halfpage_y, 'u'), + ('d',): partial(self.halfpage_y, 'd'), + # ('?',): + }) + self.search = SearchBar(self.kbctls, container) + # spawn kb handler task + nursery.start_soon(handle_input, self, self.kbctls) + + def move_y(self, val): + '''Scroll in the y direction [0, 1]. + ''' + self.scroll_y = val + + def halfpage_y(self, direction): + """Scroll a half-page up or down. + """ + pxs = (self.height/2) + _, yscale = self.convert_distance_to_scroll(0, pxs) + new = self.scroll_y + (yscale * {'u': 1, 'd': -1}[direction]) + # bound to near [0, 1] to avoid "over-scrolling" + limited = max(-0.03, min(new, 1.03)) + self.scroll_y = limited