From 0f3faec35daf0d4ecc75967bbaadb26f66ea4917 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 22 Nov 2018 19:39:40 -0500 Subject: [PATCH 1/5] Reduce the scroll boundary bounce --- piker/ui/pager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piker/ui/pager.py b/piker/ui/pager.py index 7ab5bb6e..5b38aa76 100644 --- a/piker/ui/pager.py +++ b/piker/ui/pager.py @@ -184,5 +184,5 @@ class PagerView(ScrollView): _, 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)) + limited = max(-0.01, min(new, 1.01)) self.scroll_y = limited From a7f3008d3482c0c0764dbb99832db5138f7826a9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 23 Nov 2018 10:50:40 -0500 Subject: [PATCH 2/5] Match the author's general apparel It's still a bit of a shit show, and I've left a lot of commented tweaks that need to be further played with, but I think this is a much better look for what I'm considering to be one of the main "entry point" apps for `piker`. To get any more serious fine tuning the way I want I may have to talk to some kivy experts as I'm having some headaches with button borders, padding, and the header row height.. Some of the new changes include: - port to the new `brokers.data` module - much darker theme with a stronger terminal vibe - last trade price and volume amount flash on each trade - fixed the symbol search bar to be a static height; before it was getting squashed oddly when using stacked windows - make all the cells transparent (for now) such that I can just use a row color (relates to cell padding/spacing - can't seem to ditch it) - start adding type annotations --- piker/ui/monitor.py | 247 ++++++++++++++++++++++++++++++-------------- 1 file changed, 170 insertions(+), 77 deletions(-) diff --git a/piker/ui/monitor.py b/piker/ui/monitor.py index cc1fc2a8..9d7eee9e 100644 --- a/piker/ui/monitor.py +++ b/piker/ui/monitor.py @@ -7,8 +7,10 @@ Launch with ``piker monitor ``. """ from itertools import chain from types import ModuleType, AsyncGeneratorType +from typing import List import trio +import tractor from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.stacklayout import StackLayout @@ -41,41 +43,60 @@ def colorcode(name): return _colors[name if name else 'gray'] -_bs = 4 # border size -_color = [0.13]*3 # nice shade of gray +_bs = 0.75 # border size +# medium shade of gray that seems to match the +# default i3 window borders +_i3_rgba = [0.14]*3 + [1] +# slightly off black like the jellybean bg from +# vim colorscheme +_cell_rgba = [0.07]*3 + [1] +_black_rgba = [0]*4 _kv = (f''' #:kivy 1.10.0 - font_size: 20 - # text_size: self.size - size: self.texture_size - color: {colorcode('gray')} - font_color: {colorcode('gray')} - font_name: 'Roboto-Regular' - background_color: [0.13]*3 + [1] - background_normal: '' - valign: 'middle' + font_size: 21 + # make text wrap to botom + text_size: self.size halign: 'center' - # outline_color: [0.1]*4 + valign: 'middle' + size: self.texture_size + # color: {colorcode('gray')} + # font_color: {colorcode('gray')} + font_name: 'Roboto-Regular' + background_color: [0]*4 # by default transparent; use row color + # background_color: {_cell_rgba} + # spacing: 0, 0 + # padding: [0]*4 - font_size: 20 - background_color: [0]*4 - canvas.before: - Color: - rgb: {_color} - BorderImage: # use a fixed size border - pos: self.pos - size: [self.size[0] - {_bs}, self.size[1]] - # 0s are because the containing TickerTable already has spacing - border: [0, {_bs} , 0, {_bs}] + font_size: 21 + background_color: [0]*4 # by default transparent; use row color + # background_color: {_cell_rgba} + # canvas.before: + # Color: + # rgba: [0.13]*4 + # BorderImage: # use a fixed size border + # pos: self.pos + # size: [self.size[0] - {_bs}, self.size[1]] + # # 0s are because the containing TickerTable already has spacing + # # border: [0, {_bs} , 0, {_bs}] + # border: [0, {_bs} , 0, 0] - spacing: '{_bs}dp' - row_force_default: True - row_default_height: 63 + spacing: [{_bs}] + # row_force_default: True + row_default_height: 62 cols: 1 + canvas.before: + Color: + # i3 style gray as background + rgba: {_i3_rgba} + # rgba: {_cell_rgba} + Rectangle: + # scale with container self here refers to the widget i.e BoxLayout + pos: self.pos + size: self.size spacing: [{_bs}, 0] @@ -84,14 +105,26 @@ _kv = (f''' # minimum_height: 200 # should be pulled from Cell text size # minimum_width: 200 # row_force_default: True - # row_default_height: 75 - # outline_color: [.7]*4 + # row_default_height: 61 # determines the header row size + padding: [0]*4 + spacing: [0] + canvas.before: + Color: + # rgba: [0]*4 + rgba: {_cell_rgba} + Rectangle: + # self here refers to the widget i.e Row(GridLayout) + pos: self.pos + size: self.size + +# part of the `PagerView` - # part of the `PagerView` - size_hint: 1, 0.03 + size_hint: 1, None + # static size of 51 px + height: 51 font_size: 25 - background_color: [0.13]*3 + [1] + background_color: {_i3_rgba} ''') @@ -103,6 +136,7 @@ class HeaderCell(Button): in `update_quotes()`. """ table = self.row.table + # if this is a row header cell then sort by the clicked field if self.row.is_header: table.sort_key = self.key @@ -114,7 +148,7 @@ class HeaderCell(Button): # outline the header text to indicate it's been the last clicked self.underline = True self.bold = True - # mark this cell as the last + # mark this cell as the last selected table.last_clicked_col_cell = self # sort and render the rows immediately self.row.table.render_rows(table.quote_cache) @@ -122,7 +156,7 @@ class HeaderCell(Button): # allow highlighting of row headers for tracking elif self.is_header: if self.background_color == self.color: - self.background_color = [0]*4 + self.background_color = _black_rgba else: self.background_color = self.color @@ -137,24 +171,30 @@ class BidAskLayout(StackLayout): single unit oriented with the last 2 under the first. """ def __init__(self, values, header=False, **kwargs): + # uncomment to get vertical stacked bid-ask + # super(BidAskLayout, self).__init__(orientation='bt-lr', **kwargs) super(BidAskLayout, self).__init__(orientation='lr-tb', **kwargs) assert len(values) == 3, "You can only provide 3 values: last,bid,ask" self._keys2cells = {} cell_type = HeaderCell if header else Cell top_size = cell_type().font_size - small_size = top_size - 2 - top_prop = 0.55 # proportion of size used by top cell + small_size = top_size - 4 + top_prop = 0.5 # proportion of size used by top cell bottom_prop = 1 - top_prop for (key, size_hint, font_size), value in zip( [('last', (1, top_prop), top_size), ('bid', (0.5, bottom_prop), small_size), ('ask', (0.5, bottom_prop), small_size)], + # uncomment to get vertical stacked bid-ask + # [('last', (top_prop, 1), top_size), + # ('bid', (bottom_prop, 0.5), small_size), + # ('ask', (bottom_prop, 0.5), small_size)], values ): cell = cell_type( text=str(value), size_hint=size_hint, - width=self.width/2 - 3, + # width=self.width/2 - 3, font_size=font_size ) self._keys2cells[key] = cell @@ -249,21 +289,32 @@ class Row(GridLayout): return cell def update(self, record, displayable): + """Update this row's cells with new values from a quote ``record``. + + Return all cells that changed in a ``dict``. + """ # color changed field values + cells = {} + gray = colorcode('gray') + fgreen = colorcode('forestgreen') + red = colorcode('red2') for key, val in record.items(): # logic for cell text coloring: up-green, down-red if self._last_record[key] < val: - color = colorcode('forestgreen') + color = fgreen elif self._last_record[key] > val: - color = colorcode('red2') + color = red else: - color = colorcode('gray') + color = gray cell = self.get_cell(key) cell.text = str(displayable[key]) cell.color = color + if color != gray: + cells[key] = cell self._last_record = record + return cells class TickerTable(GridLayout): @@ -277,6 +328,7 @@ class TickerTable(GridLayout): self.row_filter = lambda item: item # for tracking last clicked column header cell self.last_clicked_col_cell = None + self._last_row_toggle = 0 def append_row(self, record, bidasks=None): """Append a `Row` of `Cell` objects to this table. @@ -288,7 +340,8 @@ class TickerTable(GridLayout): return row def render_rows( - self, pairs: {str: (dict, Row)}, sort_key: str = None, row_filter=None, + self, pairs: {str: (dict, Row)}, sort_key: str = None, + row_filter=None, ): """Sort and render all rows on the ticker grid from ``pairs``. """ @@ -317,7 +370,7 @@ class TickerTable(GridLayout): async def update_quotes( - nursery: 'Nursery', + nursery: trio._core._run.Nursery, brokermod: ModuleType, widgets: dict, agen: AsyncGeneratorType, @@ -326,11 +379,19 @@ async def update_quotes( ): """Process live quotes by updating ticker rows. """ - grid = widgets['grid'] + table = widgets['table'] + flash_keys = {'low', 'high'} - def color_row(row, data): + async def revert_cells_color(cells): + await trio.sleep(0.3) + for cell in cells: + cell.background_color = _black_rgba + + def color_row(row, data, cells): hdrcell = row.get_cell('symbol') chngcell = row.get_cell('%') + + # determine daily change color daychange = float(data['%']) if daychange < 0.: color = colorcode('red2') @@ -339,49 +400,86 @@ async def update_quotes( else: color = colorcode('gray') - # row header and % cell color + # update row header and '%' cell text color chngcell.color = hdrcell.color = color - # bgcolor = color.copy() - # bgcolor[-1] = 0.25 - # chngcell.background_color = bgcolor - # if the cell has been "highlighted" make sure to change its color if hdrcell.background_color != [0]*4: hdrcell.background_color = color + # briefly highlight bg of certain cells on each trade execution + unflash = set() + tick_color = None + last = cells.get('last') + if not last: + vol = cells.get('vol') + if not vol: + return # no trade exec took place + + # flash gray on volume tick + # (means trade exec @ current price) + last = row.get_cell('last') + tick_color = colorcode('gray') + else: + tick_color = last.color + + last.background_color = tick_color + unflash.add(last) + # flash the size cell + size = row.get_cell('size') + size.background_color = tick_color + unflash.add(size) + + # flash all other cells + for key in flash_keys: + cell = cells.get(key) + if cell: + cell.background_color = cell.color + unflash.add(cell) + + # revert flash state momentarily + nursery.start_soon(revert_cells_color, unflash) + cache = {} - grid.quote_cache = cache + table.quote_cache = cache # initial coloring for sym, quote in first_quotes.items(): - row = grid.symbols2rows[sym] + row = table.symbols2rows[sym] record, displayable = brokermod.format_quote( quote, symbol_data=symbol_data) row.update(record, displayable) - color_row(row, record) + color_row(row, record, {}) cache[sym] = (record, row) # render all rows once up front - grid.render_rows(cache) + table.render_rows(cache) # real-time cell update loop async for quotes in agen: # new quotes data only for symbol, quote in quotes.items(): record, displayable = brokermod.format_quote( quote, symbol_data=symbol_data) - row = grid.symbols2rows[symbol] + row = table.symbols2rows[symbol] cache[symbol] = (record, row) - row.update(record, displayable) - color_row(row, record) + cells = row.update(record, displayable) + color_row(row, record, cells) - grid.render_rows(cache) + table.render_rows(cache) log.debug("Waiting on quotes") - log.warn("`brokerd` connection dropped") + log.warn("Data feed connection dropped") nursery.cancel_scope.cancel() -async def _async_main(name, portal, tickers, brokermod, rate): +async def _async_main( + name: str, + portal: tractor._portal.Portal, + tickers: List[str], + brokermod: ModuleType, + rate: int, + # an async generator instance which yields quotes dict packets + quote_gen: AsyncGeneratorType, +) -> None: '''Launch kivy app + all other related tasks. This is started with cli cmd `piker monitor`. @@ -389,18 +487,13 @@ async def _async_main(name, portal, tickers, brokermod, rate): # subscribe for tickers (this performs a possible filtering # where invalid symbols are discarded) sd = await portal.run( - "piker.brokers.core", 'symbol_data', - broker=brokermod.name, tickers=tickers) - - # an async generator instance - agen = await portal.run( - "piker.brokers.core", 'start_quote_stream', + "piker.brokers.data", 'symbol_data', broker=brokermod.name, tickers=tickers) async with trio.open_nursery() as nursery: # get first quotes response log.debug("Waiting on first quote...") - quotes = await agen.__anext__() + quotes = await quote_gen.__anext__() first_quotes = [ brokermod.format_quote(quote, symbol_data=sd)[0] for quote in quotes.values()] @@ -413,7 +506,7 @@ async def _async_main(name, portal, tickers, brokermod, rate): # build out UI Window.set_title(f"monitor: {name}\t(press ? for help)") Builder.load_string(_kv) - box = BoxLayout(orientation='vertical', padding=5, spacing=5) + box = BoxLayout(orientation='vertical', spacing=0) # define bid-ask "stacked" cells # (TODO: needs some rethinking and renaming for sure) @@ -430,48 +523,48 @@ async def _async_main(name, portal, tickers, brokermod, rate): ) box.add_widget(header) - # build grid - grid = TickerTable( + # build table + table = TickerTable( cols=1, size_hint=(1, None), ) for ticker_record in first_quotes: - grid.append_row(ticker_record, bidasks=bidasks) + table.append_row(ticker_record, bidasks=bidasks) # associate the col headers row with the ticker table even though # they're technically wrapped separately in containing BoxLayout - header.table = grid + header.table = table # mark the initial sorted column header as bold and underlined - sort_cell = header.get_cell(grid.sort_key) + sort_cell = header.get_cell(table.sort_key) sort_cell.bold = sort_cell.underline = True - grid.last_clicked_col_cell = sort_cell + table.last_clicked_col_cell = sort_cell # set up a pager view for large ticker lists - grid.bind(minimum_height=grid.setter('height')) - pager = PagerView(box, grid, nursery) + table.bind(minimum_height=table.setter('height')) + pager = PagerView(box, table, nursery) box.add_widget(pager) widgets = { # 'anchor': anchor, 'root': box, - 'grid': grid, + 'table': table, 'box': box, 'header': header, 'pager': pager, } nursery.start_soon( - update_quotes, nursery, brokermod, widgets, agen, sd, quotes) + update_quotes, nursery, brokermod, widgets, quote_gen, sd, quotes) try: # Trio-kivy entry point. await async_runTouchApp(widgets['root']) # run kivy - await agen.aclose() # cancel aysnc gen call + await quote_gen.aclose() # cancel aysnc gen call finally: # un-subscribe from symbols stream (cancel if brokerd # was already torn down - say by SIGINT) with trio.move_on_after(0.2): await portal.run( - "piker.brokers.core", 'modify_quote_stream', + "piker.brokers.data", 'modify_quote_stream', broker=brokermod.name, tickers=[]) # cancel GUI update task From 3ea28f04a471f4c4ffd5029adabe2925400dcd7c Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 23 Nov 2018 11:04:21 -0500 Subject: [PATCH 3/5] Mention 3.7 in Readme --- README.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 3f602363..f75b67b2 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,12 @@ piker ----- -Anti-fragile_ trading gear for hackers, scientists, stay-at-home quants and underpants warriors. +Trading gear for hackers. |pypi| |travis| |versions| |license| |docs| .. |travis| image:: https://img.shields.io/travis/pikers/piker/master.svg :target: https://travis-ci.org/pikers/piker -.. _Anti-fragile: https://www.sciencedirect.com/science/article/pii/S1877050916302290 - Install ******* ``piker`` is currently under heavy alpha development and as such should @@ -25,7 +23,7 @@ For a development install:: pipenv install --dev -e . pipenv shell -To start the real-time index ETF watchlist:: +To start the real-time index ETF watchlist with the `robinhood` backend:: piker watch indexes -l info @@ -50,7 +48,7 @@ Then start the client app as normal:: Laggy distros ============= -For those running pop-culture distros that don't yet ship ``python3.6`` +For those running pop-culture distros that don't yet ship ``python3.7`` you'll need to install it as well as `kivy source build`_ dependencies since currently there's reliance on an async development branch. @@ -60,5 +58,10 @@ since currently there's reliance on an async development branch. Tech **** ``piker`` is an attempt at a pro-grade, next-gen open source toolset -for trading and financial analysis. As such, it tries to use as much -cutting edge tech as possible including Python 3.6+ and ``trio``. +for real-time trading and financial analysis. + +It tries to use as much cutting edge tech as possible including (but not limited to): + +- Python 3.7+ +- ``trio`` +- ``tractor`` From 488bdb34be322719522674e56686d062b5978171 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 23 Nov 2018 22:21:53 -0500 Subject: [PATCH 4/5] Add mouse-over row highlighting --- piker/ui/kivy/__init__.py | 0 piker/ui/kivy/hoverable.py | 60 ++++++++++++++++++++++++++++++++++++ piker/ui/monitor.py | 62 +++++++++++++++++++++++++++++++++++--- 3 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 piker/ui/kivy/__init__.py create mode 100644 piker/ui/kivy/hoverable.py diff --git a/piker/ui/kivy/__init__.py b/piker/ui/kivy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/piker/ui/kivy/hoverable.py b/piker/ui/kivy/hoverable.py new file mode 100644 index 00000000..58e00008 --- /dev/null +++ b/piker/ui/kivy/hoverable.py @@ -0,0 +1,60 @@ +"""Hoverable Behaviour (changing when the mouse is on the widget by O. Poyen. +License: LGPL +""" +__author__ = 'Olivier Poyen' + + +from kivy.properties import BooleanProperty, ObjectProperty +from kivy.factory import Factory +from kivy.core.window import Window + + +class HoverBehavior(object): + """Hover behavior. + + :Events: + `on_enter` + Fired when mouse enter the bbox of the widget. + `on_leave` + Fired when the mouse exit the widget. + """ + hovered = BooleanProperty(False) + # Contains the last relevant point received by the Hoverable. This can + # be used in `on_enter` or `on_leave` in order to know where was dispatched + # the event. + border_point = ObjectProperty(None) + + def __init__(self, **kwargs): + self.register_event_type('on_enter') + self.register_event_type('on_leave') + Window.bind(mouse_pos=self.on_mouse_pos) + super(HoverBehavior, self).__init__(**kwargs) + + def on_mouse_pos(self, *args): + # do proceed if I'm not displayed <=> If have no parent + if not self.get_root_window(): + return + pos = args[1] + # Next line to_widget allow to compensate for relative layout + inside = self.collide_point(*self.to_widget(*pos)) + if self.hovered == inside: + # We have already done what was needed + return + self.border_point = pos + self.hovered = inside + if inside: + self.dispatch('on_enter') + else: + self.dispatch('on_leave') + + # implement these in the widget impl + + def on_enter(self): + pass + + def on_leave(self): + pass + + +# register for global use via kivy.factory.Factory +Factory.register('HoverBehavior', HoverBehavior) diff --git a/piker/ui/monitor.py b/piker/ui/monitor.py index 9d7eee9e..1afeb5ca 100644 --- a/piker/ui/monitor.py +++ b/piker/ui/monitor.py @@ -19,9 +19,11 @@ from kivy.lang import Builder from kivy import utils from kivy.app import async_runTouchApp from kivy.core.window import Window +from kivy.graphics import Color, Rectangle, RoundedRectangle from ..log import get_logger from .pager import PagerView +from .kivy.hoverable import HoverBehavior log = get_logger('monitor') @@ -44,13 +46,16 @@ def colorcode(name): _bs = 0.75 # border size + # medium shade of gray that seems to match the # default i3 window borders _i3_rgba = [0.14]*3 + [1] + # slightly off black like the jellybean bg from # vim colorscheme _cell_rgba = [0.07]*3 + [1] _black_rgba = [0]*4 + _kv = (f''' #:kivy 1.10.0 @@ -128,6 +133,50 @@ _kv = (f''' ''') +class HighlightRowHoverable(HoverBehavior): + def on_enter(self): + """Highlight row on enter. + """ + log.debug( + f"Entered cell {self} through {self.border_point}") + row = self + if row.mouse_over or row.is_header: + return + + # add draw instructions + color = row._color = Color(*_i3_rgba) + rect = row._rect = RoundedRectangle( + size=row.size, + pos=row.pos, + radius=(10,) + ) + # add to canvas + canvas = row.canvas + if row._rect not in canvas.before.children: + canvas.before.add(color) + canvas.before.add(rect) + + # mark row as being "selected" + row.mouse_over = True + + def on_leave(self): + """Un-highlight row on exit. + """ + log.debug( + f"Left cell {self} through {self.border_point}") + row = self + if not row.mouse_over or row.is_header: + return + canvas = row.canvas + # remove instructions from canvas + if row._color in canvas.before.children: + canvas.before.remove(row._color) + canvas.before.remove(row._rect) + + # mark row as being "un-selected" + row.mouse_over = False + + class HeaderCell(Button): """Column header cell label. """ @@ -224,7 +273,7 @@ class BidAskLayout(StackLayout): return [self.last, self.bid, self.ask] -class Row(GridLayout): +class Row(GridLayout, HighlightRowHoverable): """A grid for displaying a row of ticker quote data. The row fields can be updated using the ``fields`` property which will in @@ -232,14 +281,17 @@ class Row(GridLayout): """ def __init__( self, record, headers=(), bidasks=None, table=None, - is_header_row=False, + is_header=False, **kwargs ): super(Row, self).__init__(cols=len(record), **kwargs) self._cell_widgets = {} self._last_record = record self.table = table - self.is_header = is_header_row + self.is_header = is_header + + # selection state + self.mouse_over = False # create `BidAskCells` first layouts = {} @@ -248,7 +300,7 @@ class Row(GridLayout): for key, children in bidasks.items(): layout = BidAskLayout( [record[key]] + [record[child] for child in children], - header=is_header_row + header=is_header ) layout.row = self layouts[key] = layout @@ -518,7 +570,7 @@ async def _async_main( {key: key for key in headers}, headers=headers, bidasks=bidasks, - is_header_row=True, + is_header=True, size_hint=(1, None), ) box.add_widget(header) From a8a5e836b9d1b171851e3c921550a06f9bd7bcef Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 24 Nov 2018 15:41:53 -0500 Subject: [PATCH 5/5] Use static instruction for highlighted row Instead of all this adding/removing of canvas instructions nonsense simple add a static "highlighted" rectangle to each row and make its size very small when there's no mouse over. Mad props to @tshirtman for showing me the light :D --- piker/ui/kivy/hoverable.py | 2 +- piker/ui/monitor.py | 88 ++++++++++++++++---------------------- 2 files changed, 37 insertions(+), 53 deletions(-) diff --git a/piker/ui/kivy/hoverable.py b/piker/ui/kivy/hoverable.py index 58e00008..4aa70c24 100644 --- a/piker/ui/kivy/hoverable.py +++ b/piker/ui/kivy/hoverable.py @@ -31,7 +31,7 @@ class HoverBehavior(object): super(HoverBehavior, self).__init__(**kwargs) def on_mouse_pos(self, *args): - # do proceed if I'm not displayed <=> If have no parent + # don't proceed if I'm not displayed <=> If have no parent if not self.get_root_window(): return pos = args[1] diff --git a/piker/ui/monitor.py b/piker/ui/monitor.py index 1afeb5ca..5f159ace 100644 --- a/piker/ui/monitor.py +++ b/piker/ui/monitor.py @@ -19,7 +19,6 @@ from kivy.lang import Builder from kivy import utils from kivy.app import async_runTouchApp from kivy.core.window import Window -from kivy.graphics import Color, Rectangle, RoundedRectangle from ..log import get_logger from .pager import PagerView @@ -121,6 +120,14 @@ _kv = (f''' # self here refers to the widget i.e Row(GridLayout) pos: self.pos size: self.size + # row higlighting on mouse over + Color: + rgba: {_i3_rgba} + RoundedRectangle: + size: self.width, self.height if self.hovered else 1 + pos: self.pos + radius: (10,) + # part of the `PagerView` @@ -133,51 +140,17 @@ _kv = (f''' ''') -class HighlightRowHoverable(HoverBehavior): - def on_enter(self): - """Highlight row on enter. - """ - log.debug( - f"Entered cell {self} through {self.border_point}") - row = self - if row.mouse_over or row.is_header: - return +class Cell(Button): + """Data cell: the fundemental widget. - # add draw instructions - color = row._color = Color(*_i3_rgba) - rect = row._rect = RoundedRectangle( - size=row.size, - pos=row.pos, - radius=(10,) - ) - # add to canvas - canvas = row.canvas - if row._rect not in canvas.before.children: - canvas.before.add(color) - canvas.before.add(rect) - - # mark row as being "selected" - row.mouse_over = True - - def on_leave(self): - """Un-highlight row on exit. - """ - log.debug( - f"Left cell {self} through {self.border_point}") - row = self - if not row.mouse_over or row.is_header: - return - canvas = row.canvas - # remove instructions from canvas - if row._color in canvas.before.children: - canvas.before.remove(row._color) - canvas.before.remove(row._rect) - - # mark row as being "un-selected" - row.mouse_over = False + ``key`` is the column name index value. + """ + def __init__(self, key=None, **kwargs): + super(Cell, self).__init__(**kwargs) + self.key = key -class HeaderCell(Button): +class HeaderCell(Cell): """Column header cell label. """ def on_press(self, value=None): @@ -210,11 +183,6 @@ class HeaderCell(Button): self.background_color = self.color -class Cell(Button): - """Data cell. - """ - - class BidAskLayout(StackLayout): """Cell which houses three buttons containing a last, bid, and ask in a single unit oriented with the last 2 under the first. @@ -273,7 +241,7 @@ class BidAskLayout(StackLayout): return [self.last, self.bid, self.ask] -class Row(GridLayout, HighlightRowHoverable): +class Row(GridLayout, HoverBehavior): """A grid for displaying a row of ticker quote data. The row fields can be updated using the ``fields`` property which will in @@ -321,20 +289,20 @@ class Row(GridLayout, HighlightRowHoverable): # these cells have already been added to the `BidAskLayout` continue else: - cell = self._append_cell(val, header=header) + cell = self._append_cell(val, key, header=header) cell.key = key self._cell_widgets[key] = cell def get_cell(self, key): return self._cell_widgets[key] - def _append_cell(self, text, header=False): + def _append_cell(self, text, key, header=False): if not len(self._cell_widgets) < self.cols: raise ValueError(f"Can not append more then {self.cols} cells") # header cells just have a different colour celltype = HeaderCell if header else Cell - cell = celltype(text=str(text)) + cell = celltype(text=str(text), key=key) cell.is_header = header cell.row = self self.add_widget(cell) @@ -368,6 +336,22 @@ class Row(GridLayout, HighlightRowHoverable): self._last_record = record return cells + # mouse over handlers + def on_enter(self): + """Highlight layout on enter. + """ + log.debug( + f"Entered row {type(self)} through {self.border_point}") + # don't highlight header row + if getattr(self, 'is_header', None): + self.hovered = False + + def on_leave(self): + """Un-highlight layout on exit. + """ + log.debug( + f"Left row {type(self)} through {self.border_point}") + class TickerTable(GridLayout): """A grid for displaying ticker quote records as a table.