From 9e0f58ea6b4e74a819ffd6a0c620b33342970b92 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 17 Feb 2018 15:09:22 -0500 Subject: [PATCH 1/3] Add a keyboard input handling task --- piker/ui/__init__.py | 2 ++ piker/ui/kb.py | 57 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 piker/ui/kb.py diff --git a/piker/ui/__init__.py b/piker/ui/__init__.py index a1006428..941b7bb6 100644 --- a/piker/ui/__init__.py +++ b/piker/ui/__init__.py @@ -5,3 +5,5 @@ import os # use the trio async loop os.environ['KIVY_EVENTLOOP'] = 'trio' +import kivy +kivy.require('1.10.0') diff --git a/piker/ui/kb.py b/piker/ui/kb.py new file mode 100644 index 00000000..34dcc8ce --- /dev/null +++ b/piker/ui/kb.py @@ -0,0 +1,57 @@ +""" +VI-like Keyboard controls +""" +from kivy.core.window import Window + +from ..log import get_logger +log = get_logger('keyboard') + + +async def handle_input(widget, patts2funcs: dict, patt_len_limit=3): + """Handle keyboard inputs. + + For each character pattern in ``patts2funcs`` invoke the corresponding + mapped functions. + """ + def _kb_closed(): + """Teardown handler. + """ + log.debug('Closing keyboard controls') + print('Closing keyboard controls') + 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}" + ) + 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 func: + log.debug(f'invoking func kb {func}') + func() + last_patt = [] + elif key == 'q': + kb.release() + + if len(last_patt) > patt_len_limit: + last_patt = [] + + log.debug(f"last_patt {last_patt}") From 385f1b960791bc197d35dbd7d3bb6e954cf27676 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 22 Feb 2018 18:28:50 -0500 Subject: [PATCH 2/3] 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 From 6e0209ac6bb3a9ed40c177daa115da3e6f757d26 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 22 Feb 2018 18:39:21 -0500 Subject: [PATCH 3/3] Add pager support to watchlist --- piker/ui/watchlist.py | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/piker/ui/watchlist.py b/piker/ui/watchlist.py index f2d8a8ff..5019b414 100644 --- a/piker/ui/watchlist.py +++ b/piker/ui/watchlist.py @@ -13,14 +13,16 @@ from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.stacklayout import StackLayout from kivy.uix.button import Button -from kivy.uix.scrollview import ScrollView from kivy.lang import Builder from kivy import utils from kivy.app import async_runTouchApp +from kivy.core.window import Window from ..calc import humanize, percent_change from ..log import get_logger +from .kb import PagerView + log = get_logger('watchlist') @@ -86,6 +88,11 @@ _kv = (f''' # row_force_default: True # row_default_height: 75 outline_color: [.7]*4 + + + size_hint: 1, 0.03 + font_size: 25 + background_color: [0.13]*3 + [1] ''') @@ -340,6 +347,7 @@ class TickerTable(GridLayout): self.symbols2rows = {} self.sort_key = sort_key self.quote_cache = quote_cache + self.row_filter = lambda item: item # for tracking last clicked column header cell self.last_clicked_col_cell = None @@ -352,13 +360,18 @@ class TickerTable(GridLayout): self.add_widget(row) return row - def render_rows(self, pairs: (dict, Row), sort_key: str = None): + def render_rows( + self, pairs: (dict, Row), sort_key: str = None, row_filter=None, + ): """Sort and render all rows on the ticker grid from ``pairs``. """ self.clear_widgets() sort_key = sort_key or self.sort_key - for data, row in reversed( - sorted(pairs.values(), key=lambda item: item[0][sort_key]) + for data, row in filter( + row_filter or self.row_filter, + reversed( + sorted(pairs.values(), key=lambda item: item[0][sort_key]) + ) ): self.add_widget(row) # row append @@ -469,13 +482,16 @@ async def _async_main(name, watchlists, brokermod): qtconvert(quote, symbol_data=sd)[0] for quote in pkts] # build out UI + Window.set_title(f"watchlist: {name}\t(press ? for help)") Builder.load_string(_kv) - root = BoxLayout(orientation='vertical', padding=5, spacing=5) + # anchor = AnchorLayout(anchor_x='right', anchor_y='bottom') + box = BoxLayout(orientation='vertical', padding=5, spacing=5) + # anchor.add_widget(box) header = header_row( first_quotes[0].keys(), size_hint=(1, None), ) - root.add_widget(header) + box.add_widget(header) grid = ticker_table( first_quotes, size_hint=(1, None), @@ -488,17 +504,19 @@ async def _async_main(name, watchlists, brokermod): sort_cell.bold = sort_cell.underline = True grid.last_clicked_col_cell = sort_cell - # set up a scroll view for large ticker lists + # set up a pager view for large ticker lists grid.bind(minimum_height=grid.setter('height')) - scroll = ScrollView() - scroll.add_widget(grid) - root.add_widget(scroll) + pager = PagerView(box, nursery) + pager.add_widget(grid) + box.add_widget(pager) widgets = { + # 'anchor': anchor, + 'root': box, 'grid': grid, - 'root': root, + 'box': box, 'header': header, - 'scroll': scroll, + 'pager': pager, } nursery.start_soon(run_kivy, widgets['root'], nursery) nursery.start_soon(update_quotes, widgets, queue, sd, pkts)