Merge pull request #13 from pikers/kb_ctls

Pager support
kivy_mainline_and_py3.8
goodboy 2018-03-04 21:58:16 -05:00 committed by GitHub
commit 09b16bf3e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 171 additions and 12 deletions

View File

@ -5,3 +5,5 @@ import os
# use the trio async loop # use the trio async loop
os.environ['KIVY_EVENTLOOP'] = 'trio' os.environ['KIVY_EVENTLOOP'] = 'trio'
import kivy
kivy.require('1.10.0')

139
piker/ui/kb.py 100644
View File

@ -0,0 +1,139 @@
"""
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 input.
For each character pattern-tuple in ``patts2funcs`` invoke the
corresponding mapped function(s) or coro(s).
"""
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')
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
)
self.kbctls = kbctls
self._container = parent
# indicate to ``handle_input`` that search is activated on '/'
self.kbctls.update({
('/',): self.handle_input
})
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)
async def handle_input(self):
self._container.add_widget(self) # display it
self.focus = True # focus immediately (doesn't work from __init__)
# wait for <enter> to close search bar
await self.async_bind('on_text_validate').__aiter__().__anext__()
log.debug("Closing search bar")
self._container.remove_widget(self) # stop displaying
return self.text
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

View File

@ -13,14 +13,16 @@ from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout from kivy.uix.gridlayout import GridLayout
from kivy.uix.stacklayout import StackLayout from kivy.uix.stacklayout import StackLayout
from kivy.uix.button import Button from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.lang import Builder from kivy.lang import Builder
from kivy import utils from kivy import utils
from kivy.app import async_runTouchApp from kivy.app import async_runTouchApp
from kivy.core.window import Window
from ..calc import humanize, percent_change from ..calc import humanize, percent_change
from ..log import get_logger from ..log import get_logger
from .kb import PagerView
log = get_logger('watchlist') log = get_logger('watchlist')
@ -86,6 +88,11 @@ _kv = (f'''
# row_force_default: True # row_force_default: True
# row_default_height: 75 # row_default_height: 75
outline_color: [.7]*4 outline_color: [.7]*4
<SearchBar>
size_hint: 1, 0.03
font_size: 25
background_color: [0.13]*3 + [1]
''') ''')
@ -340,6 +347,7 @@ class TickerTable(GridLayout):
self.symbols2rows = {} self.symbols2rows = {}
self.sort_key = sort_key self.sort_key = sort_key
self.quote_cache = quote_cache self.quote_cache = quote_cache
self.row_filter = lambda item: item
# for tracking last clicked column header cell # for tracking last clicked column header cell
self.last_clicked_col_cell = None self.last_clicked_col_cell = None
@ -352,13 +360,18 @@ class TickerTable(GridLayout):
self.add_widget(row) self.add_widget(row)
return 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``. """Sort and render all rows on the ticker grid from ``pairs``.
""" """
self.clear_widgets() self.clear_widgets()
sort_key = sort_key or self.sort_key sort_key = sort_key or self.sort_key
for data, row in reversed( for data, row in filter(
row_filter or self.row_filter,
reversed(
sorted(pairs.values(), key=lambda item: item[0][sort_key]) sorted(pairs.values(), key=lambda item: item[0][sort_key])
)
): ):
self.add_widget(row) # row append 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] qtconvert(quote, symbol_data=sd)[0] for quote in pkts]
# build out UI # build out UI
Window.set_title(f"watchlist: {name}\t(press ? for help)")
Builder.load_string(_kv) 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( header = header_row(
first_quotes[0].keys(), first_quotes[0].keys(),
size_hint=(1, None), size_hint=(1, None),
) )
root.add_widget(header) box.add_widget(header)
grid = ticker_table( grid = ticker_table(
first_quotes, first_quotes,
size_hint=(1, None), size_hint=(1, None),
@ -488,17 +504,19 @@ async def _async_main(name, watchlists, brokermod):
sort_cell.bold = sort_cell.underline = True sort_cell.bold = sort_cell.underline = True
grid.last_clicked_col_cell = sort_cell 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')) grid.bind(minimum_height=grid.setter('height'))
scroll = ScrollView() pager = PagerView(box, nursery)
scroll.add_widget(grid) pager.add_widget(grid)
root.add_widget(scroll) box.add_widget(pager)
widgets = { widgets = {
# 'anchor': anchor,
'root': box,
'grid': grid, 'grid': grid,
'root': root, 'box': box,
'header': header, 'header': header,
'scroll': scroll, 'pager': pager,
} }
nursery.start_soon(run_kivy, widgets['root'], nursery) nursery.start_soon(run_kivy, widgets['root'], nursery)
nursery.start_soon(update_quotes, widgets, queue, sd, pkts) nursery.start_soon(update_quotes, widgets, queue, sd, pkts)