diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 1b6bc8ae..82690b7c 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -33,6 +33,7 @@ from PyQt5.QtWidgets import ( ) import numpy as np import pyqtgraph as pg +from pydantic import BaseModel import tractor import trio @@ -75,8 +76,9 @@ from .. import fsp from ..data import feed from ._forms import ( FieldsForm, - open_form, + mk_form, mk_order_pane_layout, + open_form_input_handling, ) @@ -228,9 +230,9 @@ class GodWidget(QWidget): # change the order config form over to the new chart # XXX: since the pp config is a singleton widget we have to # also switch it over to the new chart's interal-layout - self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_config) + self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane) linkedsplits.chart.qframe.hbox.addWidget( - self.pp_config, + self.pp_pane, alignment=Qt.AlignTop ) @@ -377,7 +379,7 @@ class LinkedSplits(QWidget): style=style, _is_main=True, - sidepane=self.godwidget.pp_config, + sidepane=self.godwidget.pp_pane, ) # add crosshair graphic self.chart.addItem(self.cursor) @@ -438,7 +440,7 @@ class LinkedSplits(QWidget): self.xaxis = xaxis # TODO: probably should formalize and call this something else? - class LambdaQFrame(QFrame): + class ChartnPane(QFrame): '''One-off ``QFrame`` composite which pairs a chart + sidepane ``FieldsForm`` (if provided). @@ -465,7 +467,7 @@ class LinkedSplits(QWidget): hbox.setContentsMargins(0, 0, 0, 0) hbox.setSpacing(3) - qframe = LambdaQFrame(self.splitter) + qframe = ChartnPane(self.splitter) cpw = ChartPlotWidget( @@ -517,7 +519,9 @@ class LinkedSplits(QWidget): # XXX: gives us outline on backside of y-axis cpw.getPlotItem().setContentsMargins(*CHART_MARGINS) - # link chart x-axis to main quotes chart + # link chart x-axis to main chart + # this is 1/2 of where the `Link` in ``LinkedSplit`` + # comes from ;) cpw.setXLink(self.chart) # add to cross-hair's known plots @@ -1373,6 +1377,36 @@ async def run_fsp( group_key=group_status_key, ) + class FspConfig(BaseModel): + + class Config: + validate_assignment = True + + name: str + period: int + + sidepane: FieldsForm = mk_form( + model=FspConfig( + name=display_name, + period=14, + ), + parent=linkedsplits.godwidget, + fields_schema={ + 'name': { + 'label': '**fsp**:', + 'type': 'select', + 'default_value': [ + f'{display_name}' + ], + }, + 'period': { + 'label': '**period**:', + 'type': 'edit', + 'default_value': 14, + }, + }, + ) + async with ( portal.open_stream_from( @@ -1388,23 +1422,8 @@ async def run_fsp( ) as stream, - open_form( - parent=linkedsplits.godwidget, - fields_schema={ - 'name': { - 'label': '**fsp**:', - 'type': 'select', - 'default_value': [ - f'{display_name}' - ], - }, - 'period': { - 'label': '**period**:', - 'type': 'edit', - 'default_value': 14, - }, - }, - ) as sidepane, + # TODO: + # open_form_input_handling(sidepane), ): @@ -1840,56 +1859,14 @@ async def _async_main( starting_done = sbar.open_status('starting ze sexy chartz') # generate order mode side-pane UI - + # A ``FieldsForm`` form to configure order entry + pp_pane: FieldsForm = mk_order_pane_layout(godwidget) + # add as next-to-y-axis singleton pane + godwidget.pp_pane = pp_pane async with ( trio.open_nursery() as root_n, - - # fields form to configure order entry - open_form( - parent=godwidget, - fields_schema={ - 'account': { - 'type': 'select', - 'default_value': [ - 'paper', - # 'ib.margin', - # 'ib.paper', - ], - }, - 'size_unit': { - 'label': '**allocate**:', - 'type': 'select', - 'default_value': [ - '$ size', - '% of port', - '# shares' - ], - }, - 'disti_weight': { - 'label': '**weight**:', - 'type': 'select', - 'default_value': ['uniform'], - }, - 'size': { - 'label': '**size**:', - 'type': 'edit', - 'default_value': 5000, - }, - 'slots': { - 'type': 'edit', - 'default_value': 4, - }, - }, - ) as pp_config, ): - pp_config: FieldsForm - mk_order_pane_layout(pp_config) - pp_config.show() - - # add as next-to-y-axis pane - godwidget.pp_config = pp_config - # set root nursery and task stack for spawning other charts/feeds # that run cached in the bg godwidget._root_n = root_n @@ -1932,9 +1909,10 @@ async def _async_main( await order_mode_ready.wait() - # start handling search bar kb inputs + # start handling peripherals input for top level widgets async with ( + # search bar kb inputs _event.open_handlers( [search.bar], event_types={QEvent.KeyPress}, @@ -1942,6 +1920,9 @@ async def _async_main( # let key repeats pass through for search filter_auto_repeats=False, ), + + # pp pane kb inputs + open_form_input_handling(pp_pane), ): # remove startup status text starting_done() diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index 5249d220..9f9c0267 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -41,6 +41,7 @@ from PyQt5.QtWidgets import ( QStyledItemDelegate, QStyleOptionViewItem, ) +import pydantic from ._event import open_handlers from ._style import hcolor, _font, _font_small, DpiAwareFont @@ -330,10 +331,9 @@ class FieldsForm(QWidget): async def handle_field_input( widget: QWidget, - # last_widget: QWidget, # had focus prior recv_chan: trio.abc.ReceiveChannel, - fields: FieldsForm, - allocator: Allocator, # noqa + form: FieldsForm, + model: pydantic.BaseModel, # noqa ) -> None: @@ -356,39 +356,38 @@ async def handle_field_input( }: widget.clearFocus() - fields.godwidget.focus() + form.godwidget.focus() continue # process field input if key in (Qt.Key_Enter, Qt.Key_Return): value = widget.text() key = widget._key - setattr(allocator, key, value) - print(allocator.dict()) + setattr(model, key, value) + print(model.dict()) -@asynccontextmanager -async def open_form( +def mk_form( + model: pydantic.BaseModel, parent: QWidget, fields_schema: dict, - # alloc: Allocator, - # orientation: str = 'horizontal', ) -> FieldsForm: - fields = FieldsForm(parent=parent) - from ._position import mk_pp_alloc - alloc = mk_pp_alloc() - fields.model = alloc + form = FieldsForm(parent=parent) + # TODO: generate components from model + # instead of schema dict (aka use an ORM) + form.model = model + # generate sub-components from schema dict for name, config in fields_schema.items(): wtype = config['type'] label = str(config.get('label', name)) # plain (line) edit field if wtype == 'edit': - w = fields.add_edit_field( + w = form.add_edit_field( label, config['default_value'] ) @@ -396,36 +395,48 @@ async def open_form( # drop-down selection elif wtype == 'select': values = list(config['default_value']) - w = fields.add_select_field( + w = form.add_select_field( label, values ) def write_model(text: str): print(f'{text}') - setattr(alloc, name, text) + setattr(form.model, name, text) w.currentTextChanged.connect(write_model) w._key = name + return form + + +@asynccontextmanager +async def open_form_input_handling( + + form: FieldsForm + +) -> FieldsForm: + + assert form.model, f'{form} must define a `.model`' + async with open_handlers( - list(fields.fields.values()), + list(form.fields.values()), event_types={ QEvent.KeyPress, }, async_handler=partial( handle_field_input, - fields=fields, - allocator=alloc, + form=form, + model=form.model, ), # block key repeats? filter_auto_repeats=True, ): - yield fields + yield form def mk_fill_status_bar( @@ -568,30 +579,73 @@ def mk_fill_status_bar( def mk_order_pane_layout( - fields: FieldsForm, + parent: QWidget, font_size: int = _font_small.px_size - 2 ) -> FieldsForm: + from ._position import mk_pp_alloc + # TODO: some kinda pydantic sub-type + # that enforces a composite widget attr er sumthin.. + # as part of our ORM thingers. + allocator = mk_pp_alloc() + # TODO: maybe just allocate the whole fields form here # and expect an async ctx entry? - - fields._font_size = font_size + form = mk_form( + parent=parent, + model=allocator, + fields_schema={ + 'account': { + 'type': 'select', + 'default_value': [ + 'paper', + # 'ib.margin', + # 'ib.paper', + ], + }, + 'size_unit': { + 'label': '**allocate**:', + 'type': 'select', + 'default_value': [ + '$ size', + '% of port', + '# shares' + ], + }, + 'disti_weight': { + 'label': '**weight**:', + 'type': 'select', + 'default_value': ['uniform'], + }, + 'size': { + 'label': '**size**:', + 'type': 'edit', + 'default_value': 5000, + }, + 'slots': { + 'type': 'edit', + 'default_value': 4, + }, + }, + ) + form._font_size = font_size + allocator._widget = form # top level pane layout # XXX: see ``FieldsForm.__init__()`` for why we can't do basic # config of the vbox here - vbox = fields.vbox + vbox = form.vbox - # _, h = fields.width(), fields.height() + # _, h = form.width(), form.height() # print(f'w, h: {w, h}') - hbox, bar = mk_fill_status_bar(fields, pane_vbox=vbox) + hbox, bar = mk_fill_status_bar(form, pane_vbox=vbox) # add pp fill bar + spacing vbox.addLayout(hbox, stretch=1/3) - feed_label = fields.add_field_label( + feed_label = form.add_field_label( dedent(""" brokerd.ib\n |_@localhost:8509\n @@ -613,4 +667,6 @@ def mk_order_pane_layout( # https://doc.qt.io/qt-5/layout.html#adding-widgets-to-a-layout vbox.setSpacing(36) - return fields + form.show() + + return form diff --git a/piker/ui/_position.py b/piker/ui/_position.py index e8b030df..0a4d6080 100644 --- a/piker/ui/_position.py +++ b/piker/ui/_position.py @@ -105,12 +105,15 @@ def mk_pp_alloc( slots: int _position: Position = None + _widget: QWidget = None def get_order_info( self, price: float, ) -> dict: + size = self.size / self.slots + units, r = divmod( round((self.size / self.slots)), price,