First (untested) draft remote annotation ctl API
Since we can and want to eventually allow remote control of pretty much all UIs, this drafts out a new `.ui._remote_ctl` module with a new `@tractor.context` called `remote_annotate()` which simply starts a msg loop which allows for (eventual) initial control of a `SelectRect` through IPC msgs. Remote controller impl deats: - make `._display.graphics_update_loop()` set a `._remote_ctl._dss: dict` for all chart actor-global `DisplayState` instances which can then be controlled from the `remote_annotate()` handler task. - also stash any remote client controller `tractor.Context` handles in a module var for broadband IPC cancellation on any display loop shutdown. - draft a further global map to track graphics object instances since likely we'll want to support remote mutation where the client can use the `id(obj): int` key as an IPC handle/uuid. - just draft out a client-side `@acm` for now: `open_annots_client()` to be filled out in up coming commits. UI component tweaks in support of the above: - change/add `SelectRect.set_view_pos()` and `.set_scene_pos()` to allow specifying the rect coords in either of the scene or viewbox domains. - use these new apis in the interaction loop. - add a `SelectRect.add_to_view()` to avoid having annotation client code knowing "how" a graphics obj needs to be added and can instead just pass only the target `ChartView` during init. - drop all the status label updates from the display loop since they don't really work all the time, and probably it's not a feature we want to keep in the longer term (over just console output and/or using the status bar for simpler "current state / mkt" infos). - allows a bit of simplification of `.ui._fsp` method APIs to not pass around status (bar) callbacks as well!distribute_dis
parent
ab84303da7
commit
8d324acf91
|
@ -56,8 +56,8 @@ _line_styles: dict[str, int] = {
|
||||||
|
|
||||||
class FlowGraphic(pg.GraphicsObject):
|
class FlowGraphic(pg.GraphicsObject):
|
||||||
'''
|
'''
|
||||||
Base class with minimal interface for `QPainterPath` implemented,
|
Base class with minimal interface for `QPainterPath`
|
||||||
real-time updated "data flow" graphics.
|
implemented, real-time updated "data flow" graphics.
|
||||||
|
|
||||||
See subtypes below.
|
See subtypes below.
|
||||||
|
|
||||||
|
@ -167,11 +167,12 @@ class FlowGraphic(pg.GraphicsObject):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# XXX: due to a variety of weird jitter bugs and "smearing"
|
# XXX: due to a variety of weird jitter bugs and "smearing"
|
||||||
# artifacts when click-drag panning and viewing history time series,
|
# artifacts when click-drag panning and viewing history time
|
||||||
# we offer this ctx-mngr interface to allow temporarily disabling
|
# series, we offer this ctx-mngr interface to allow temporarily
|
||||||
# Qt's graphics caching mode; this is now currently used from
|
# disabling Qt's graphics caching mode; this is now currently
|
||||||
# ``ChartView.start/signal_ic()`` methods which also disable the
|
# used from ``ChartView.start/signal_ic()`` methods which also
|
||||||
# rt-display loop when the user is moving around a view.
|
# disable the rt-display loop when the user is moving around
|
||||||
|
# a view.
|
||||||
@cm
|
@cm
|
||||||
def reset_cache(self) -> None:
|
def reset_cache(self) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -563,7 +563,8 @@ class Viz(Struct):
|
||||||
|
|
||||||
def view_range(self) -> tuple[int, int]:
|
def view_range(self) -> tuple[int, int]:
|
||||||
'''
|
'''
|
||||||
Return the start and stop x-indexes for the managed ``ViewBox``.
|
Return the start and stop x-indexes for the managed
|
||||||
|
``ViewBox``.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
vr = self.plot.viewRect()
|
vr = self.plot.viewRect()
|
||||||
|
|
|
@ -470,6 +470,10 @@ async def graphics_update_loop(
|
||||||
if ds.hist_vars['i_last'] < ds.hist_vars['i_last_append']:
|
if ds.hist_vars['i_last'] < ds.hist_vars['i_last_append']:
|
||||||
await tractor.pause()
|
await tractor.pause()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import _remote_ctl
|
||||||
|
_remote_ctl._dss = dss
|
||||||
|
|
||||||
# main real-time quotes update loop
|
# main real-time quotes update loop
|
||||||
stream: tractor.MsgStream
|
stream: tractor.MsgStream
|
||||||
async with feed.open_multi_stream() as stream:
|
async with feed.open_multi_stream() as stream:
|
||||||
|
@ -519,6 +523,12 @@ async def graphics_update_loop(
|
||||||
quote,
|
quote,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# XXX: cancel any remote annotation control ctxs
|
||||||
|
_remote_ctl._dss = None
|
||||||
|
for ctx in _remote_ctl._ctxs:
|
||||||
|
await ctx.cancel()
|
||||||
|
|
||||||
|
|
||||||
def graphics_update_cycle(
|
def graphics_update_cycle(
|
||||||
ds: DisplayState,
|
ds: DisplayState,
|
||||||
|
@ -1235,7 +1245,7 @@ async def display_symbol_data(
|
||||||
fast from a cached watch-list.
|
fast from a cached watch-list.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
sbar = godwidget.window.status_bar
|
# sbar = godwidget.window.status_bar
|
||||||
# historical data fetch
|
# historical data fetch
|
||||||
# brokermod = brokers.get_brokermod(provider)
|
# brokermod = brokers.get_brokermod(provider)
|
||||||
|
|
||||||
|
@ -1245,11 +1255,11 @@ async def display_symbol_data(
|
||||||
# group_key=loading_sym_key,
|
# group_key=loading_sym_key,
|
||||||
# )
|
# )
|
||||||
|
|
||||||
for fqme in fqmes:
|
# for fqme in fqmes:
|
||||||
loading_sym_key = sbar.open_status(
|
# loading_sym_key = sbar.open_status(
|
||||||
f'loading {fqme} ->',
|
# f'loading {fqme} ->',
|
||||||
group_key=True
|
# group_key=True
|
||||||
)
|
# )
|
||||||
|
|
||||||
# (TODO: make this not so shit XD)
|
# (TODO: make this not so shit XD)
|
||||||
# close group status once a symbol feed fully loads to view.
|
# close group status once a symbol feed fully loads to view.
|
||||||
|
@ -1422,7 +1432,7 @@ async def display_symbol_data(
|
||||||
start_fsp_displays,
|
start_fsp_displays,
|
||||||
rt_linked,
|
rt_linked,
|
||||||
flume,
|
flume,
|
||||||
loading_sym_key,
|
# loading_sym_key,
|
||||||
loglevel,
|
loglevel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ from ..log import get_logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._chart import GodWidget
|
from ._chart import GodWidget
|
||||||
|
from ._interaction import ChartView
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
@ -261,7 +262,7 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
super().__init__(0, 0, 1, 1)
|
super().__init__(0, 0, 1, 1)
|
||||||
|
|
||||||
# self.rbScaleBox = QGraphicsRectItem(0, 0, 1, 1)
|
# self.rbScaleBox = QGraphicsRectItem(0, 0, 1, 1)
|
||||||
self.vb = viewbox
|
self.vb: ViewBox = viewbox
|
||||||
self._chart: 'ChartPlotWidget' = None # noqa
|
self._chart: 'ChartPlotWidget' = None # noqa
|
||||||
|
|
||||||
# override selection box color
|
# override selection box color
|
||||||
|
@ -297,6 +298,19 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
'sigma: {std:.2f}',
|
'sigma: {std:.2f}',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def add_to_view(
|
||||||
|
self,
|
||||||
|
view: ChartView,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Self-defined view hookup impl.
|
||||||
|
|
||||||
|
'''
|
||||||
|
view.addItem(
|
||||||
|
self,
|
||||||
|
ignoreBounds=True,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chart(self) -> 'ChartPlotWidget': # noqa
|
def chart(self) -> 'ChartPlotWidget': # noqa
|
||||||
return self._chart
|
return self._chart
|
||||||
|
@ -324,21 +338,40 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
self.vb.mapFromView(self._abs_top_right)
|
self.vb.mapFromView(self._abs_top_right)
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_pos(
|
def set_scen_pos(
|
||||||
self,
|
self,
|
||||||
p1: QPointF,
|
p1: QPointF,
|
||||||
p2: QPointF
|
p2: QPointF
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set position of selection rect and accompanying label, move
|
'''
|
||||||
label to match.
|
Set position from scene coords of selection rect (normally
|
||||||
|
from mouse position) and accompanying label, move label to
|
||||||
|
match.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
if self._label_proxy is None:
|
# map to view coords
|
||||||
|
self.set_view_pos(
|
||||||
|
self.vb.mapToView(p1),
|
||||||
|
self.vb.mapToView(p2),
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_view_pos(
|
||||||
|
self,
|
||||||
|
start_pos: QPointF,
|
||||||
|
end_pos: QPointF,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Set position from `ViewBox` coords (i.e. from the actual
|
||||||
|
data domain) of rect (and any accompanying label which is
|
||||||
|
moved to match).
|
||||||
|
|
||||||
|
'''
|
||||||
# https://doc.qt.io/qt-5/qgraphicsproxywidget.html
|
# https://doc.qt.io/qt-5/qgraphicsproxywidget.html
|
||||||
self._label_proxy = self.vb.scene().addWidget(self._label)
|
if self._label_proxy is None:
|
||||||
|
self._label_proxy = self.vb.scene(
|
||||||
start_pos = self.vb.mapToView(p1)
|
).addWidget(self._label)
|
||||||
end_pos = self.vb.mapToView(p2)
|
|
||||||
|
|
||||||
# map to view coords and update area
|
# map to view coords and update area
|
||||||
r = QtCore.QRectF(start_pos, end_pos)
|
r = QtCore.QRectF(start_pos, end_pos)
|
||||||
|
@ -398,8 +431,9 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
# self._label.show()
|
# self._label.show()
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Clear the selection box from view.
|
'''
|
||||||
|
Clear the selection box from view.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
self._label.hide()
|
self._label.hide()
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
|
@ -560,7 +560,7 @@ class FspAdmin:
|
||||||
conf: dict, # yeah probably dumb..
|
conf: dict, # yeah probably dumb..
|
||||||
loglevel: str = 'error',
|
loglevel: str = 'error',
|
||||||
|
|
||||||
) -> (trio.Event, ChartPlotWidget):
|
) -> trio.Event:
|
||||||
|
|
||||||
flume, started = await self.start_engine_task(
|
flume, started = await self.start_engine_task(
|
||||||
target,
|
target,
|
||||||
|
@ -927,7 +927,7 @@ async def start_fsp_displays(
|
||||||
|
|
||||||
linked: LinkedSplits,
|
linked: LinkedSplits,
|
||||||
flume: Flume,
|
flume: Flume,
|
||||||
group_status_key: str,
|
# group_status_key: str,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -974,21 +974,23 @@ async def start_fsp_displays(
|
||||||
flume,
|
flume,
|
||||||
) as admin,
|
) as admin,
|
||||||
):
|
):
|
||||||
statuses = []
|
statuses: list[trio.Event] = []
|
||||||
for target, conf in fsp_conf.items():
|
for target, conf in fsp_conf.items():
|
||||||
started = await admin.open_fsp_chart(
|
started: trio.Event = await admin.open_fsp_chart(
|
||||||
target,
|
target,
|
||||||
conf,
|
conf,
|
||||||
)
|
)
|
||||||
done = linked.window().status_bar.open_status(
|
# done = linked.window().status_bar.open_status(
|
||||||
f'loading fsp, {target}..',
|
# f'loading fsp, {target}..',
|
||||||
group_key=group_status_key,
|
# group_key=group_status_key,
|
||||||
)
|
# )
|
||||||
statuses.append((started, done))
|
# statuses.append((started, done))
|
||||||
|
statuses.append(started)
|
||||||
|
|
||||||
for fsp_loaded, status_cb in statuses:
|
# for fsp_loaded, status_cb in statuses:
|
||||||
|
for fsp_loaded in statuses:
|
||||||
await fsp_loaded.wait()
|
await fsp_loaded.wait()
|
||||||
profiler(f'attached to fsp portal: {target}')
|
profiler(f'attached to fsp portal: {target}')
|
||||||
status_cb()
|
# status_cb()
|
||||||
|
|
||||||
# blocks on nursery until all fsp actors complete
|
# blocks on nursery until all fsp actors complete
|
||||||
|
|
|
@ -500,7 +500,11 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
# add our selection box annotator
|
# add our selection box annotator
|
||||||
self.select_box = SelectRect(self)
|
self.select_box = SelectRect(self)
|
||||||
self.addItem(self.select_box, ignoreBounds=True)
|
self.select_box.add_to_view(self)
|
||||||
|
# self.addItem(
|
||||||
|
# self.select_box,
|
||||||
|
# ignoreBounds=True,
|
||||||
|
# )
|
||||||
|
|
||||||
self.mode = None
|
self.mode = None
|
||||||
self.order_mode: bool = False
|
self.order_mode: bool = False
|
||||||
|
@ -761,14 +765,17 @@ class ChartView(ViewBox):
|
||||||
# This is the final position in the drag
|
# This is the final position in the drag
|
||||||
if ev.isFinish():
|
if ev.isFinish():
|
||||||
|
|
||||||
self.select_box.mouse_drag_released(down_pos, pos)
|
self.select_box.mouse_drag_released(
|
||||||
|
down_pos,
|
||||||
|
pos,
|
||||||
|
)
|
||||||
|
|
||||||
# NOTE: think of this as a `.mouse_drag_release()`
|
# NOTE: think of this as a `.mouse_drag_release()`
|
||||||
# (bc HINT that's what i called the shit ass
|
# (bc HINT that's what i called the shit ass
|
||||||
# method that wrapped this call [yes, as a single
|
# method that wrapped this call [yes, as a single
|
||||||
# fucking call] originally.. you bish, guille)
|
# fucking call] originally.. you bish, guille)
|
||||||
# Bo.. oraleeee
|
# Bo.. oraleeee
|
||||||
self.select_box.set_pos(
|
self.select_box.set_scen_pos(
|
||||||
down_pos,
|
down_pos,
|
||||||
pos,
|
pos,
|
||||||
)
|
)
|
||||||
|
@ -786,7 +793,10 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print('drag finish?')
|
print('drag finish?')
|
||||||
self.select_box.set_pos(down_pos, pos)
|
self.select_box.set_scen_pos(
|
||||||
|
down_pos,
|
||||||
|
pos,
|
||||||
|
)
|
||||||
|
|
||||||
# update shape of scale box
|
# update shape of scale box
|
||||||
# self.updateScaleBox(ev.buttonDownPos(), ev.pos())
|
# self.updateScaleBox(ev.buttonDownPos(), ev.pos())
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
# piker: trading gear for hackers
|
||||||
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
'''
|
||||||
|
Remote control tasks for sending annotations (and maybe more cmds)
|
||||||
|
to a chart from some other actor.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
from contextlib import asynccontextmanager as acm
|
||||||
|
|
||||||
|
import tractor
|
||||||
|
# import trio
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QGraphicsItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
from piker.log import get_logger
|
||||||
|
from ._display import DisplayState
|
||||||
|
from ._editors import SelectRect
|
||||||
|
from ._chart import ChartPlotWidget
|
||||||
|
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
# NOTE: this is set by the `._display.graphics_update_loop()` once
|
||||||
|
# all chart widgets / Viz per flume have been initialized allowing
|
||||||
|
# for remote annotation (control) of any chart-actor's mkt feed by
|
||||||
|
# fqme lookup Bo
|
||||||
|
_dss: dict[str, DisplayState] | None = None
|
||||||
|
|
||||||
|
# stash each and every client connection so that they can all
|
||||||
|
# be cancelled on shutdown/error.
|
||||||
|
_ctxs: set[tractor.Context] = set()
|
||||||
|
|
||||||
|
# global map of all uniquely created annotation-graphics
|
||||||
|
# so that they can be mutated (eventually) by a client.
|
||||||
|
_annots: dict[int, QGraphicsItem] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@tractor.context
|
||||||
|
async def remote_annotate(
|
||||||
|
ctx: tractor.Context,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
global _dss, _ctxs
|
||||||
|
assert _dss
|
||||||
|
|
||||||
|
_ctxs.add(ctx)
|
||||||
|
|
||||||
|
# send back full fqme symbology to caller
|
||||||
|
await ctx.started(list(_dss))
|
||||||
|
|
||||||
|
async with ctx.open_stream() as annot_req_stream:
|
||||||
|
async for msg in annot_req_stream:
|
||||||
|
match msg:
|
||||||
|
case {
|
||||||
|
'fqme': fqme,
|
||||||
|
'cmd': 'SelectRect',
|
||||||
|
'color': color,
|
||||||
|
'timeframe': timeframe,
|
||||||
|
# 'meth': str(meth),
|
||||||
|
'meth': 'set_view_pos',
|
||||||
|
'kwargs': {
|
||||||
|
'start_pos': tuple(start_pos),
|
||||||
|
'end_pos': tuple(end_pos),
|
||||||
|
},
|
||||||
|
}:
|
||||||
|
ds: DisplayState = _dss[fqme]
|
||||||
|
chart: ChartPlotWidget = {
|
||||||
|
60: ds.hist_chart,
|
||||||
|
1: ds.chart,
|
||||||
|
}[timeframe]
|
||||||
|
|
||||||
|
# annot type lookup from cmd
|
||||||
|
rect = SelectRect(
|
||||||
|
chart.cv,
|
||||||
|
|
||||||
|
# TODO: pull from conf.toml?
|
||||||
|
color=color or 'dad_blue',
|
||||||
|
)
|
||||||
|
rect.set_view_pos(
|
||||||
|
start_pos=start_pos,
|
||||||
|
end_pos=end_pos,
|
||||||
|
)
|
||||||
|
await annot_req_stream.send(id(rect))
|
||||||
|
|
||||||
|
case _:
|
||||||
|
log.error(
|
||||||
|
'Unknown remote annotation cmd:\n'
|
||||||
|
f'{msg}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@acm
|
||||||
|
async def open_annots_client(
|
||||||
|
uid: tuple[str, str],
|
||||||
|
|
||||||
|
) -> 'AnnotClient':
|
||||||
|
# TODO: load connetion to a specific chart actor
|
||||||
|
# -[ ] pull from either service scan or config
|
||||||
|
# -[ ] return some kinda client/proxy thinger?
|
||||||
|
# -[ ] maybe we should finally just provide this as
|
||||||
|
# a `tractor.hilevel.CallableProxy` or wtv?
|
||||||
|
# -[ ] use this from the storage.cli stuff to mark up gaps!
|
||||||
|
...
|
Loading…
Reference in New Issue