commit
09b16bf3e7
|
@ -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')
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue