Merge pull request #14 from pikers/searchbar

Searchbar
kivy_mainline_and_py3.8
goodboy 2018-03-07 23:52:32 -05:00 committed by GitHub
commit d2e1605bf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 51 deletions

View File

@ -122,7 +122,13 @@ def watch(loglevel, broker, watchlist_name):
'APH.TO', 'MARI.CN', 'WMD.VN', 'LEAF.TO', 'THCX.VN', 'APH.TO', 'MARI.CN', 'WMD.VN', 'LEAF.TO', 'THCX.VN',
'WEED.TO', 'NINE.VN', 'RTI.VN', 'SNN.CN', 'ACB.TO', 'WEED.TO', 'NINE.VN', 'RTI.VN', 'SNN.CN', 'ACB.TO',
'OGI.VN', 'IMH.VN', 'FIRE.VN', 'EAT.CN', 'NUU.VN', 'OGI.VN', 'IMH.VN', 'FIRE.VN', 'EAT.CN', 'NUU.VN',
'WMD.VN', 'HEMP.VN', 'CALI.CN', 'RBQ.CN', 'WMD.VN', 'HEMP.VN', 'CALI.CN', 'RQB.CN', 'MPX.CN',
'SEED.TO', 'HMJR.TO', 'CMED.TO', 'PAS.VN',
'CRON',
],
'dad': [
'GM', 'TSLA', 'DOL.TO', 'CIM', 'SPY',
'SHOP.TO',
], ],
} }
# broker_conf_path = os.path.join( # broker_conf_path = os.path.join(

View File

@ -1,5 +1,5 @@
""" """
Keyboard controls Pager widget + kb controls
""" """
import inspect import inspect
from functools import partial from functools import partial
@ -75,30 +75,74 @@ async def handle_input(widget, patts2funcs: dict, patt_len_limit=3):
class SearchBar(TextInput): class SearchBar(TextInput):
def __init__(self, kbctls: dict, parent, **kwargs): def __init__(self, kbctls: dict, container: 'Widget', pager: 'PagerView',
searcher, **kwargs):
super(SearchBar, self).__init__( super(SearchBar, self).__init__(
multiline=False, multiline=False,
# text='/', hint_text='Ticker Search',
cursor_blink=False,
**kwargs **kwargs
) )
self.foreground_color = self.hint_text_color # actually readable
self.kbctls = kbctls self.kbctls = kbctls
self._container = parent self._container = container
self._pager = pager
self._searcher = searcher
# indicate to ``handle_input`` that search is activated on '/' # indicate to ``handle_input`` that search is activated on '/'
self.kbctls.update({ self.kbctls.update({
('/',): self.handle_input ('/',): self.handle_input
}) })
self._sugg_template = ' '*4 + '{} matches: '
self._matched = []
def suggest(self, matches):
self.suggestion_text = ''
suffix = self._sugg_template.format(len(matches) or "No")
self.suggestion_text = suffix + ' '.join(matches)
def on_text(self, instance, value): def on_text(self, instance, value):
# ``scroll.scroll_to()`` looks super awesome here for when we
# our interproc-ticker protocol def unhighlight(cells):
# async for item in self.async_bind('text'): for cell in cells:
print(value) cell.outline_color = [0]*3
cell.outline_width = 0
self._matched.remove(cell)
if not value:
if self._matched:
unhighlight(self._matched.copy())
return
if not value.isupper():
self.text = value.upper()
return
text = self.text
matches = self._searcher.search(text)
self.suggest(matches.keys())
if matches:
# unhighlight old matches
unmatched = set(self._matched) - set(matches.keys())
unhighlight(unmatched)
for key, widget in matches.items():
cell = widget.get_cell('symbol')
# cell.background_color = [0.4]*3 + [1]
cell.outline_width = 2
cell.outline_color = [0.6] * 3
self._matched.append(cell)
key, widget = list(matches.items())[0]
# ensure first match widget is displayed
self._pager.scroll_to(widget)
async def handle_input(self): async def handle_input(self):
self._container.add_widget(self) # display it self._container.add_widget(self) # display it
self.focus = True # focus immediately (doesn't work from __init__) self.focus = True # focus immediately (doesn't work from __init__)
# wait for <enter> to close search bar # wait for <enter> to close search bar
await self.async_bind('on_text_validate').__aiter__().__anext__() await self.async_bind('on_text_validate').__aiter__().__anext__()
log.debug(f"Seach text is {self.text}")
log.debug("Closing search bar") log.debug("Closing search bar")
self._container.remove_widget(self) # stop displaying self._container.remove_widget(self) # stop displaying
@ -108,7 +152,8 @@ class SearchBar(TextInput):
class PagerView(ScrollView): class PagerView(ScrollView):
"""Pager view that adds less-like scrolling and search. """Pager view that adds less-like scrolling and search.
""" """
def __init__(self, container, nursery, kbctls: dict = None, **kwargs): def __init__(self, container, contained, nursery, kbctls: dict = None,
**kwargs):
super(PagerView, self).__init__(**kwargs) super(PagerView, self).__init__(**kwargs)
self._container = container self._container = container
self.kbctls = kbctls or {} self.kbctls = kbctls or {}
@ -119,7 +164,11 @@ class PagerView(ScrollView):
('d',): partial(self.halfpage_y, 'd'), ('d',): partial(self.halfpage_y, 'd'),
# ('?',): # ('?',):
}) })
self.search = SearchBar(self.kbctls, container) # add contained child widget (can only be one)
self._contained = contained
self.add_widget(contained)
self.search = SearchBar(
self.kbctls, container, self, searcher=contained)
# spawn kb handler task # spawn kb handler task
nursery.start_soon(handle_input, self, self.kbctls) nursery.start_soon(handle_input, self, self.kbctls)

View File

@ -21,7 +21,7 @@ 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 from .pager import PagerView
log = get_logger('watchlist') log = get_logger('watchlist')
@ -50,7 +50,7 @@ _kv = (f'''
<Cell> <Cell>
font_size: 18 font_size: 18
text_size: self.size # text_size: self.size
size: self.texture_size size: self.texture_size
color: {colorcode('gray')} color: {colorcode('gray')}
font_color: {colorcode('gray')} font_color: {colorcode('gray')}
@ -59,7 +59,7 @@ _kv = (f'''
background_normal: '' background_normal: ''
valign: 'middle' valign: 'middle'
halign: 'center' halign: 'center'
outline_color: [0.1]*4 # outline_color: [0.1]*4
<HeaderCell> <HeaderCell>
font_size: 20 font_size: 20
@ -87,9 +87,10 @@ _kv = (f'''
# minimum_width: 200 # minimum_width: 200
# 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> <SearchBar>
# part of the `PagerView`
size_hint: 1, 0.03 size_hint: 1, 0.03
font_size: 25 font_size: 25
background_color: [0.13]*3 + [1] background_color: [0.13]*3 + [1]
@ -125,10 +126,12 @@ _qt_keys = {
def qtconvert( def qtconvert(
quote: dict, keymap: dict = _qt_keys, symbol_data: dict = None quote: dict, symbol_data: dict,
keymap: dict = _qt_keys,
) -> (dict, dict): ) -> (dict, dict):
"""Remap a list of quote dicts ``quotes`` using the mapping of old keys """Remap a list of quote dicts ``quotes`` using the mapping of old keys
-> new keys ``keymap``. -> new keys ``keymap`` returning 2 dicts: one with raw data and the other
for display.
Returns 2 dicts: first is the original values mapped by new keys, Returns 2 dicts: first is the original values mapped by new keys,
and the second is the same but with all values converted to a and the second is the same but with all values converted to a
@ -136,13 +139,10 @@ def qtconvert(
""" """
last = quote['lastTradePrice'] last = quote['lastTradePrice']
symbol = quote['symbol'] symbol = quote['symbol']
if symbol_data: # we can only compute % change from symbols data previous = symbol_data[symbol]['prevDayClosePrice']
previous = symbol_data[symbol]['prevDayClosePrice'] change = percent_change(previous, last)
change = percent_change(previous, last) share_count = symbol_data[symbol].get('outstandingShares', None)
share_count = symbol_data[symbol].get('outstandingShares', None) mktcap = share_count * last if share_count else 'NA'
mktcap = share_count * last if share_count else 'NA'
else:
change = 0
computed = { computed = {
'symbol': quote['symbol'], 'symbol': quote['symbol'],
'%': round(change, 3), '%': round(change, 3),
@ -157,7 +157,7 @@ def qtconvert(
# API servers can return `None` vals when markets are closed (weekend) # API servers can return `None` vals when markets are closed (weekend)
value = 0 if value is None else value value = 0 if value is None else value
# convert values to a displayble format # convert values to a displayble format using available formatting func
if isinstance(new_key, tuple): if isinstance(new_key, tuple):
new_key, func = new_key new_key, func = new_key
display_value = func(value) display_value = func(value)
@ -172,8 +172,9 @@ class HeaderCell(Button):
"""Column header cell label. """Column header cell label.
""" """
def on_press(self, value=None): def on_press(self, value=None):
# clicking on a col header indicates to rows by this column """Clicking on a col header indicates to sort rows by this column
# in `update_quotes()` in `update_quotes()`.
"""
table = self.row.table table = self.row.table
if self.row.is_header: if self.row.is_header:
table.sort_key = self.key table.sort_key = self.key
@ -215,7 +216,7 @@ class BidAskLayout(StackLayout):
cell_type = HeaderCell if header else Cell cell_type = HeaderCell if header else Cell
top_size = cell_type().font_size top_size = cell_type().font_size
small_size = top_size - 5 small_size = top_size - 5
top_prop = 0.7 top_prop = 0.7 # proportion of size used by top cell
bottom_prop = 1 - top_prop bottom_prop = 1 - top_prop
for (key, size_hint, font_size), value in zip( for (key, size_hint, font_size), value in zip(
[('last', (1, top_prop), top_size), [('last', (1, top_prop), top_size),
@ -375,22 +376,18 @@ class TickerTable(GridLayout):
): ):
self.add_widget(row) # row append self.add_widget(row) # row append
def ticker_search(self, patt):
"""Return sequence of matches when pattern ``patt`` is in a
symbol name. Most naive algo possible for the moment.
"""
for symbol, row in self.symbols2rows.items():
if patt in symbol:
yield symbol, row
def header_row(headers, **kwargs): def search(self, patt):
"""Create a single "header" row from a sequence of keys. """Search bar api compat.
""" """
headers_dict = {key: key for key in headers} return dict(self.ticker_search(patt)) or {}
row = Row(headers_dict, headers=headers, is_header_row=True, **kwargs)
return row
def ticker_table(quotes, **kwargs):
"""Create a new ticker table from a list of quote dicts.
"""
table = TickerTable(cols=1, **kwargs)
for ticker_record in quotes:
table.append_row(ticker_record)
return table
async def update_quotes( async def update_quotes(
@ -484,21 +481,29 @@ async def _async_main(name, watchlists, brokermod):
# build out UI # build out UI
Window.set_title(f"watchlist: {name}\t(press ? for help)") Window.set_title(f"watchlist: {name}\t(press ? for help)")
Builder.load_string(_kv) Builder.load_string(_kv)
# anchor = AnchorLayout(anchor_x='right', anchor_y='bottom')
box = BoxLayout(orientation='vertical', padding=5, spacing=5) box = BoxLayout(orientation='vertical', padding=5, spacing=5)
# anchor.add_widget(box)
header = header_row( # add header row
first_quotes[0].keys(), headers = first_quotes[0].keys()
header = Row(
{key: key for key in headers},
headers=headers,
is_header_row=True,
size_hint=(1, None), size_hint=(1, None),
) )
box.add_widget(header) box.add_widget(header)
grid = ticker_table(
first_quotes, # build grid
grid = TickerTable(
cols=1,
size_hint=(1, None), size_hint=(1, None),
) )
for ticker_record in first_quotes:
grid.append_row(ticker_record)
# associate the col headers row with the ticker table even though # associate the col headers row with the ticker table even though
# they're technically wrapped separately in containing BoxLayout # they're technically wrapped separately in containing BoxLayout
header.table = grid header.table = grid
# mark the initial sorted column header as bold and underlined # mark the initial sorted column header as bold and underlined
sort_cell = header.get_cell(grid.sort_key) sort_cell = header.get_cell(grid.sort_key)
sort_cell.bold = sort_cell.underline = True sort_cell.bold = sort_cell.underline = True
@ -506,8 +511,7 @@ async def _async_main(name, watchlists, brokermod):
# set up a pager 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'))
pager = PagerView(box, nursery) pager = PagerView(box, grid, nursery)
pager.add_widget(grid)
box.add_widget(pager) box.add_widget(pager)
widgets = { widgets = {