diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 0b7caf63..5cb89f54 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -210,9 +210,9 @@ async def increment_history_view( ): hist_chart: ChartPlotWidget = ds.hist_chart hist_viz: Viz = ds.hist_viz - viz: Viz = ds.viz + # viz: Viz = ds.viz assert 'hist' in hist_viz.shm.token['shm_name'] - name: str = hist_viz.name + # name: str = hist_viz.name # TODO: seems this is more reliable at keeping the slow # chart incremented in view more correctly? @@ -225,7 +225,8 @@ async def increment_history_view( # draw everything from scratch on first entry! for curve_name, hist_viz in hist_chart._vizs.items(): log.info(f'Forcing hard redraw -> {curve_name}') - hist_viz.update_graphics(force_redraw=True) + hist_viz.reset_graphics() + # hist_viz.update_graphics(force_redraw=True) async with open_sample_stream(1.) as min_istream: async for msg in min_istream: @@ -248,27 +249,27 @@ async def increment_history_view( # - samplerd could emit the actual update range via # tuple and then we only enter the below block if that # range is detected as in-view? - match msg: - case { - 'backfilling': (viz_name, timeframe), - } if ( - viz_name == name - ): - log.warning( - f'Forcing HARD REDRAW:\n' - f'name: {name}\n' - f'timeframe: {timeframe}\n' - ) - # TODO: only allow this when the data is IN VIEW! - # also, we probably can do this more efficiently - # / smarter by only redrawing the portion of the - # path necessary? - { - 60: hist_viz, - 1: viz, - }[timeframe].update_graphics( - force_redraw=True - ) + # match msg: + # case { + # 'backfilling': (viz_name, timeframe), + # } if ( + # viz_name == name + # ): + # log.warning( + # f'Forcing HARD REDRAW:\n' + # f'name: {name}\n' + # f'timeframe: {timeframe}\n' + # ) + # # TODO: only allow this when the data is IN VIEW! + # # also, we probably can do this more efficiently + # # / smarter by only redrawing the portion of the + # # path necessary? + # { + # 60: hist_viz, + # 1: viz, + # }[timeframe].update_graphics( + # force_redraw=True + # ) # check if slow chart needs an x-domain shift and/or # y-range resize. @@ -309,6 +310,7 @@ async def increment_history_view( async def graphics_update_loop( + dss: dict[str, DisplayState], nurse: trio.Nursery, godwidget: GodWidget, feed: Feed, @@ -350,8 +352,6 @@ async def graphics_update_loop( 'i_last_slow_t': 0, # multiview-global slow (1m) step index } - dss: dict[str, DisplayState] = {} - for fqme, flume in feed.flumes.items(): ohlcv = flume.rt_shm hist_ohlcv = flume.hist_shm @@ -470,67 +470,68 @@ async def graphics_update_loop( if ds.hist_vars['i_last'] < ds.hist_vars['i_last_append']: await tractor.pause() - try: - # XXX TODO: we need to do _dss UPDATE here so that when - # a feed-view is switched you can still remote annotate the - # prior view.. - from . import _remote_ctl - _remote_ctl._dss = dss + # try: - # main real-time quotes update loop - stream: tractor.MsgStream - async with feed.open_multi_stream() as stream: - assert stream - async for quotes in stream: - quote_period = time.time() - last_quote_s - quote_rate = round( - 1/quote_period, 1) if quote_period > 0 else float('inf') + # XXX TODO: we need to do _dss UPDATE here so that when + # a feed-view is switched you can still remote annotate the + # prior view.. + from . import _remote_ctl + _remote_ctl._dss.update(dss) + + # main real-time quotes update loop + stream: tractor.MsgStream + async with feed.open_multi_stream() as stream: + # assert stream + async for quotes in stream: + quote_period = time.time() - last_quote_s + quote_rate = round( + 1/quote_period, 1) if quote_period > 0 else float('inf') + if ( + quote_period <= 1/_quote_throttle_rate + + # in the absolute worst case we shouldn't see more then + # twice the expected throttle rate right!? + # and quote_rate >= _quote_throttle_rate * 2 + and quote_rate >= display_rate + ): + pass + # log.warning(f'High quote rate {mkt.fqme}: {quote_rate}') + + last_quote_s: float = time.time() + + for fqme, quote in quotes.items(): + ds = dss[fqme] + ds.quotes = quote + rt_pi, hist_pi = pis[fqme] + + # chart isn't active/shown so skip render cycle and + # pause feed(s) if ( - quote_period <= 1/_quote_throttle_rate - - # in the absolute worst case we shouldn't see more then - # twice the expected throttle rate right!? - # and quote_rate >= _quote_throttle_rate * 2 - and quote_rate >= display_rate + fast_chart.linked.isHidden() + or not rt_pi.isVisible() ): - pass - # log.warning(f'High quote rate {mkt.fqme}: {quote_rate}') + print(f'{fqme} skipping update for HIDDEN CHART') + fast_chart.pause_all_feeds() + continue - last_quote_s = time.time() + ic = fast_chart.view._in_interact + if ic: + fast_chart.pause_all_feeds() + print(f'{fqme} PAUSING DURING INTERACTION') + await ic.wait() + fast_chart.resume_all_feeds() - for fqme, quote in quotes.items(): - ds = dss[fqme] - ds.quotes = quote - rt_pi, hist_pi = pis[fqme] + # sync call to update all graphics/UX components. + graphics_update_cycle( + ds, + quote, + ) - # chart isn't active/shown so skip render cycle and - # pause feed(s) - if ( - fast_chart.linked.isHidden() - or not rt_pi.isVisible() - ): - print(f'{fqme} skipping update for HIDDEN CHART') - fast_chart.pause_all_feeds() - continue - - ic = fast_chart.view._in_interact - if ic: - fast_chart.pause_all_feeds() - print(f'{fqme} PAUSING DURING INTERACTION') - await ic.wait() - fast_chart.resume_all_feeds() - - # sync call to update all graphics/UX components. - graphics_update_cycle( - ds, - quote, - ) - - finally: - # XXX: cancel any remote annotation control ctxs - _remote_ctl._dss = None - for cid, (ctx, aids) in _remote_ctl._ctxs.items(): - await ctx.cancel() + # finally: + # # XXX: cancel any remote annotation control ctxs + # _remote_ctl._dss = None + # for cid, (ctx, aids) in _remote_ctl._ctxs.items(): + # await ctx.cancel() def graphics_update_cycle( @@ -1554,8 +1555,10 @@ async def display_symbol_data( ) # start update loop task + dss: dict[str, DisplayState] = {} ln.start_soon( graphics_update_loop, + dss, ln, godwidget, feed, @@ -1569,15 +1572,31 @@ async def display_symbol_data( order_ctl_fqme: str = fqmes[0] mode: OrderMode async with ( + open_order_mode( feed, godwidget, order_ctl_fqme, order_mode_started, loglevel=loglevel - ) as mode - ): + ) as mode, + # TODO: maybe have these startup sooner before + # order mode fully boots? but we gotta, + # -[ ] decouple the order mode bindings until + # the mode has fully booted.. + # -[ ] maybe do an Event to sync? + + # start input handling for ``ChartView`` input + # (i.e. kb + mouse handling loops) + rt_chart.view.open_async_input_handler( + dss=dss, + ), + hist_chart.view.open_async_input_handler( + dss=dss, + ), + + ): rt_linked.mode = mode rt_viz = rt_chart.get_viz(order_ctl_fqme) diff --git a/piker/ui/_event.py b/piker/ui/_event.py index b83dd578..27b98c97 100644 --- a/piker/ui/_event.py +++ b/piker/ui/_event.py @@ -201,8 +201,8 @@ async def open_signal_handler( async for args in recv: await async_handler(*args) - async with trio.open_nursery() as n: - n.start_soon(proxy_to_handler) + async with trio.open_nursery() as tn: + tn.start_soon(proxy_to_handler) async with send: yield @@ -212,18 +212,48 @@ async def open_handlers( source_widgets: list[QWidget], event_types: set[QEvent], - async_handler: Callable[[QWidget, trio.abc.ReceiveChannel], None], - **kwargs, + + # NOTE: if you want to bind in additional kwargs to the handler + # pass in a `partial()` instead! + async_handler: Callable[ + [QWidget, trio.abc.ReceiveChannel], # required handler args + None + ], + + # XXX: these are ONLY inputs available to the + # `open_event_stream()` event-relay to mem-chan factor above! + **open_ev_stream_kwargs, ) -> None: + ''' + Connect and schedule an async handler function to receive an + arbitrary `QWidget`'s events with kb/mouse msgs repacked into + structs (see above) and shuttled over a mem-chan to the input + `async_handler` to allow interaction-IO processing from + a `trio` func-as-task. + + ''' + widget: QWidget + streams: list[trio.abc.ReceiveChannel] async with ( - trio.open_nursery() as n, + trio.open_nursery() as tn, gather_contexts([ - open_event_stream(widget, event_types, **kwargs) + open_event_stream( + widget, + event_types, + **open_ev_stream_kwargs, + ) for widget in source_widgets ]) as streams, ): - for widget, event_recv_stream in zip(source_widgets, streams): - n.start_soon(async_handler, widget, event_recv_stream) + for widget, event_recv_stream in zip( + source_widgets, + streams, + ): + tn.start_soon( + async_handler, + widget, + event_recv_stream, + ) yield diff --git a/piker/ui/_interaction.py b/piker/ui/_interaction.py index 4856a600..7c710506 100644 --- a/piker/ui/_interaction.py +++ b/piker/ui/_interaction.py @@ -23,6 +23,7 @@ from contextlib import ( asynccontextmanager, ExitStack, ) +from functools import partial import time from typing import ( Callable, @@ -74,6 +75,7 @@ if TYPE_CHECKING: ) from ._dataviz import Viz from .order_mode import OrderMode + from ._display import DisplayState log = get_logger(__name__) @@ -102,6 +104,7 @@ async def handle_viewmode_kb_inputs( view: ChartView, recv_chan: trio.abc.ReceiveChannel, + dss: dict[str, DisplayState], ) -> None: @@ -177,17 +180,42 @@ async def handle_viewmode_kb_inputs( Qt.Key_P, } ): - import tractor feed = order_mode.feed # noqa chart = order_mode.chart # noqa viz = chart.main_viz # noqa vlm_chart = chart.linked.subplots['volume'] # noqa vlm_viz = vlm_chart.main_viz # noqa dvlm_pi = vlm_chart._vizs['dolla_vlm'].plot # noqa + import tractor await tractor.pause() view.interact_graphics_cycle() - # SEARCH MODE # + # FORCE graphics reset-and-render of all currently + # shown data `Viz`s for the current chart app. + if ( + ctrl + and key in { + Qt.Key_R, + } + ): + fqme: str + ds: DisplayState + for fqme, ds in dss.items(): + + viz: Viz + for tf, viz in { + 60: ds.hist_viz, + 1: ds.viz, + }.items(): + # TODO: only allow this when the data is IN VIEW! + # also, we probably can do this more efficiently + # / smarter by only redrawing the portion of the + # path necessary? + viz.reset_graphics() + + # ------ - ------ + # SEARCH MODE + # ------ - ------ # ctlr-/ for "lookup", "search" -> open search tree if ( ctrl @@ -247,8 +275,10 @@ async def handle_viewmode_kb_inputs( delta=-view.def_delta, ) - elif key == Qt.Key_R: - + elif ( + not ctrl + and key == Qt.Key_R + ): # NOTE: seems that if we don't yield a Qt render # cycle then the m4 downsampled curves will show here # without another reset.. @@ -431,6 +461,7 @@ async def handle_viewmode_mouse( view: ChartView, recv_chan: trio.abc.ReceiveChannel, + dss: dict[str, DisplayState], ) -> None: @@ -567,6 +598,7 @@ class ChartView(ViewBox): @asynccontextmanager async def open_async_input_handler( self, + **handler_kwargs, ) -> ChartView: @@ -577,14 +609,20 @@ class ChartView(ViewBox): QEvent.KeyPress, QEvent.KeyRelease, }, - async_handler=handle_viewmode_kb_inputs, + async_handler=partial( + handle_viewmode_kb_inputs, + **handler_kwargs, + ), ), _event.open_handlers( [self], event_types={ gs_mouse.GraphicsSceneMousePress, }, - async_handler=handle_viewmode_mouse, + async_handler=partial( + handle_viewmode_mouse, + **handler_kwargs, + ), ), ): yield self diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index e51cf909..dd02cde5 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -930,13 +930,8 @@ async def open_order_mode( msg, ) - # start async input handling for chart's view async with ( - # ``ChartView`` input async handler startup - chart.view.open_async_input_handler(), - hist_chart.view.open_async_input_handler(), - # pp pane kb inputs open_form_input_handling( form,