tractor_struct_and_godw_mod #72
228
piker/types.py
228
piker/types.py
|
|
@ -21,230 +21,6 @@ Extensions to built-in or (heavily used but 3rd party) friend-lib
|
|||
types.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from collections import UserList
|
||||
from pprint import (
|
||||
saferepr,
|
||||
from tractor.msg.pretty_struct import (
|
||||
Struct as Struct,
|
||||
)
|
||||
from typing import Any
|
||||
|
||||
from msgspec import (
|
||||
msgpack,
|
||||
Struct as _Struct,
|
||||
structs,
|
||||
)
|
||||
|
||||
|
||||
class DiffDump(UserList):
|
||||
'''
|
||||
Very simple list delegator that repr() dumps (presumed) tuple
|
||||
elements of the form `tuple[str, Any, Any]` in a nice
|
||||
multi-line readable form for analyzing `Struct` diffs.
|
||||
|
||||
'''
|
||||
def __repr__(self) -> str:
|
||||
if not len(self):
|
||||
return super().__repr__()
|
||||
|
||||
# format by displaying item pair's ``repr()`` on multiple,
|
||||
# indented lines such that they are more easily visually
|
||||
# comparable when printed to console when printed to
|
||||
# console.
|
||||
repstr: str = '[\n'
|
||||
for k, left, right in self:
|
||||
repstr += (
|
||||
f'({k},\n'
|
||||
f'\t{repr(left)},\n'
|
||||
f'\t{repr(right)},\n'
|
||||
')\n'
|
||||
)
|
||||
repstr += ']\n'
|
||||
return repstr
|
||||
|
||||
|
||||
class Struct(
|
||||
_Struct,
|
||||
|
||||
# https://jcristharif.com/msgspec/structs.html#tagged-unions
|
||||
# tag='pikerstruct',
|
||||
# tag=True,
|
||||
):
|
||||
'''
|
||||
A "human friendlier" (aka repl buddy) struct subtype.
|
||||
|
||||
'''
|
||||
def _sin_props(self) -> Iterator[
|
||||
tuple[
|
||||
structs.FieldIinfo,
|
||||
str,
|
||||
Any,
|
||||
]
|
||||
]:
|
||||
'''
|
||||
Iterate over all non-@property fields of this struct.
|
||||
|
||||
'''
|
||||
fi: structs.FieldInfo
|
||||
for fi in structs.fields(self):
|
||||
key: str = fi.name
|
||||
val: Any = getattr(self, key)
|
||||
yield fi, key, val
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
include_non_members: bool = True,
|
||||
|
||||
) -> dict:
|
||||
'''
|
||||
Like it sounds.. direct delegation to:
|
||||
https://jcristharif.com/msgspec/api.html#msgspec.structs.asdict
|
||||
|
||||
BUT, by default we pop all non-member (aka not defined as
|
||||
struct fields) fields by default.
|
||||
|
||||
'''
|
||||
asdict: dict = structs.asdict(self)
|
||||
if include_non_members:
|
||||
return asdict
|
||||
|
||||
# only return a dict of the struct members
|
||||
# which were provided as input, NOT anything
|
||||
# added as type-defined `@property` methods!
|
||||
sin_props: dict = {}
|
||||
fi: structs.FieldInfo
|
||||
for fi, k, v in self._sin_props():
|
||||
sin_props[k] = asdict[k]
|
||||
|
||||
return sin_props
|
||||
|
||||
def pformat(
|
||||
self,
|
||||
field_indent: int = 2,
|
||||
indent: int = 0,
|
||||
|
||||
) -> str:
|
||||
'''
|
||||
Recursion-safe `pprint.pformat()` style formatting of
|
||||
a `msgspec.Struct` for sane reading by a human using a REPL.
|
||||
|
||||
'''
|
||||
# global whitespace indent
|
||||
ws: str = ' '*indent
|
||||
|
||||
# field whitespace indent
|
||||
field_ws: str = ' '*(field_indent + indent)
|
||||
|
||||
# qtn: str = ws + self.__class__.__qualname__
|
||||
qtn: str = self.__class__.__qualname__
|
||||
|
||||
obj_str: str = '' # accumulator
|
||||
fi: structs.FieldInfo
|
||||
k: str
|
||||
v: Any
|
||||
for fi, k, v in self._sin_props():
|
||||
|
||||
# TODO: how can we prefer `Literal['option1', 'option2,
|
||||
# ..]` over .__name__ == `Literal` but still get only the
|
||||
# latter for simple types like `str | int | None` etc..?
|
||||
ft: type = fi.type
|
||||
typ_name: str = getattr(ft, '__name__', str(ft))
|
||||
|
||||
# recurse to get sub-struct's `.pformat()` output Bo
|
||||
if isinstance(v, Struct):
|
||||
val_str: str = v.pformat(
|
||||
indent=field_indent + indent,
|
||||
field_indent=indent + field_indent,
|
||||
)
|
||||
|
||||
else: # the `pprint` recursion-safe format:
|
||||
# https://docs.python.org/3.11/library/pprint.html#pprint.saferepr
|
||||
val_str: str = saferepr(v)
|
||||
|
||||
obj_str += (field_ws + f'{k}: {typ_name} = {val_str},\n')
|
||||
|
||||
return (
|
||||
f'{qtn}(\n'
|
||||
f'{obj_str}'
|
||||
f'{ws})'
|
||||
)
|
||||
|
||||
# TODO: use a pprint.PrettyPrinter instance around ONLY rendering
|
||||
# inside a known tty?
|
||||
# def __repr__(self) -> str:
|
||||
# ...
|
||||
|
||||
# __str__ = __repr__ = pformat
|
||||
__repr__ = pformat
|
||||
|
||||
def copy(
|
||||
self,
|
||||
update: dict | None = None,
|
||||
|
||||
) -> Struct:
|
||||
'''
|
||||
Validate-typecast all self defined fields, return a copy of
|
||||
us with all such fields.
|
||||
|
||||
NOTE: This is kinda like the default behaviour in
|
||||
`pydantic.BaseModel` except a copy of the object is
|
||||
returned making it compat with `frozen=True`.
|
||||
|
||||
'''
|
||||
if update:
|
||||
for k, v in update.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
# NOTE: roundtrip serialize to validate
|
||||
# - enode to msgpack binary format,
|
||||
# - decode that back to a struct.
|
||||
return msgpack.Decoder(type=type(self)).decode(
|
||||
msgpack.Encoder().encode(self)
|
||||
)
|
||||
|
||||
def typecast(
|
||||
self,
|
||||
|
||||
# TODO: allow only casting a named subset?
|
||||
# fields: set[str] | None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Cast all fields using their declared type annotations
|
||||
(kinda like what `pydantic` does by default).
|
||||
|
||||
NOTE: this of course won't work on frozen types, use
|
||||
``.copy()`` above in such cases.
|
||||
|
||||
'''
|
||||
# https://jcristharif.com/msgspec/api.html#msgspec.structs.fields
|
||||
fi: structs.FieldInfo
|
||||
for fi in structs.fields(self):
|
||||
setattr(
|
||||
self,
|
||||
fi.name,
|
||||
fi.type(getattr(self, fi.name)),
|
||||
)
|
||||
|
||||
def __sub__(
|
||||
self,
|
||||
other: Struct,
|
||||
|
||||
) -> DiffDump[tuple[str, Any, Any]]:
|
||||
'''
|
||||
Compare fields/items key-wise and return a ``DiffDump``
|
||||
for easy visual REPL comparison B)
|
||||
|
||||
'''
|
||||
diffs: DiffDump[tuple[str, Any, Any]] = DiffDump()
|
||||
for fi in structs.fields(self):
|
||||
attr_name: str = fi.name
|
||||
ours: Any = getattr(self, attr_name)
|
||||
theirs: Any = getattr(other, attr_name)
|
||||
if ours != theirs:
|
||||
diffs.append((
|
||||
attr_name,
|
||||
ours,
|
||||
theirs,
|
||||
))
|
||||
|
||||
return diffs
|
||||
|
|
|
|||
|
|
@ -27,15 +27,15 @@ import trio
|
|||
from piker.ui.qt import (
|
||||
QEvent,
|
||||
)
|
||||
from ..service import maybe_spawn_brokerd
|
||||
from . import _chart
|
||||
from . import _event
|
||||
from ._exec import run_qtractor
|
||||
from ..data.feed import install_brokerd_search
|
||||
from ..data._symcache import open_symcache
|
||||
from ..accounting import unpack_fqme
|
||||
from . import _search
|
||||
from ._chart import GodWidget
|
||||
from ..accounting import unpack_fqme
|
||||
from ..data._symcache import open_symcache
|
||||
from ..data.feed import install_brokerd_search
|
||||
from ..log import get_logger
|
||||
from ..service import maybe_spawn_brokerd
|
||||
from ._exec import run_qtractor
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
|
@ -73,8 +73,8 @@ async def load_provider_search(
|
|||
|
||||
async def _async_main(
|
||||
|
||||
# implicit required argument provided by ``qtractor_run()``
|
||||
main_widget: GodWidget,
|
||||
# implicit required argument provided by `qtractor_run()`
|
||||
main_widget: _chart.GodWidget,
|
||||
|
||||
syms: list[str],
|
||||
brokers: dict[str, ModuleType],
|
||||
|
|
@ -87,6 +87,9 @@ async def _async_main(
|
|||
Provision the "main" widget with initial symbol data and root nursery.
|
||||
|
||||
"""
|
||||
# set as singleton
|
||||
_chart._godw = main_widget
|
||||
|
||||
from . import _display
|
||||
from ._pg_overrides import _do_overrides
|
||||
_do_overrides()
|
||||
|
|
@ -201,6 +204,6 @@ def _main(
|
|||
brokermods,
|
||||
piker_loglevel,
|
||||
),
|
||||
main_widget_type=GodWidget,
|
||||
main_widget_type=_chart.GodWidget,
|
||||
tractor_kwargs=tractor_kwargs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ from typing import (
|
|||
)
|
||||
|
||||
import pyqtgraph as pg
|
||||
import trio
|
||||
|
||||
from piker.ui.qt import (
|
||||
QtCore,
|
||||
|
|
@ -41,6 +40,7 @@ from piker.ui.qt import (
|
|||
QVBoxLayout,
|
||||
QSplitter,
|
||||
)
|
||||
from ._widget import GodWidget
|
||||
from ._axes import (
|
||||
DynamicDateAxis,
|
||||
PriceAxis,
|
||||
|
|
@ -61,10 +61,6 @@ from ._style import (
|
|||
_xaxis_at,
|
||||
# _min_points_to_show,
|
||||
)
|
||||
from ..data.feed import (
|
||||
Feed,
|
||||
Flume,
|
||||
)
|
||||
from ..accounting import (
|
||||
MktPair,
|
||||
)
|
||||
|
|
@ -78,286 +74,12 @@ from . import _pg_overrides as pgo
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from ._display import DisplayState
|
||||
from ..data.flows import Flume
|
||||
from ..data.feed import Feed
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class GodWidget(QWidget):
|
||||
'''
|
||||
"Our lord and savior, the holy child of window-shua, there is no
|
||||
widget above thee." - 6|6
|
||||
|
||||
The highest level composed widget which contains layouts for
|
||||
organizing charts as well as other sub-widgets used to control or
|
||||
modify them.
|
||||
|
||||
'''
|
||||
search: SearchWidget
|
||||
mode_name: str = 'god'
|
||||
|
||||
def __init__(
|
||||
|
||||
self,
|
||||
parent=None,
|
||||
|
||||
) -> None:
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.search: SearchWidget | None = None
|
||||
|
||||
self.hbox = QHBoxLayout(self)
|
||||
self.hbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.hbox.setSpacing(6)
|
||||
self.hbox.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.vbox = QVBoxLayout()
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.vbox.setSpacing(2)
|
||||
self.vbox.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.hbox.addLayout(self.vbox)
|
||||
|
||||
self._chart_cache: dict[
|
||||
str,
|
||||
tuple[LinkedSplits, LinkedSplits],
|
||||
] = {}
|
||||
|
||||
self.hist_linked: LinkedSplits | None = None
|
||||
self.rt_linked: LinkedSplits | None = None
|
||||
self._active_cursor: Cursor | None = None
|
||||
|
||||
# assigned in the startup func `_async_main()`
|
||||
self._root_n: trio.Nursery = None
|
||||
|
||||
self._widgets: dict[str, QWidget] = {}
|
||||
self._resizing: bool = False
|
||||
|
||||
# TODO: do we need this, when would god get resized
|
||||
# and the window does not? Never right?!
|
||||
# self.reg_for_resize(self)
|
||||
|
||||
# TODO: strat loader/saver that we don't need yet.
|
||||
# def init_strategy_ui(self):
|
||||
# self.toolbar_layout = QHBoxLayout()
|
||||
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# self.vbox.addLayout(self.toolbar_layout)
|
||||
# self.strategy_box = StrategyBoxWidget(self)
|
||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||
|
||||
@property
|
||||
def linkedsplits(self) -> LinkedSplits:
|
||||
return self.rt_linked
|
||||
|
||||
def set_chart_symbols(
|
||||
self,
|
||||
group_key: tuple[str], # of form <fqme>.<providername>
|
||||
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
|
||||
|
||||
) -> None:
|
||||
# re-sort org cache symbol list in LIFO order
|
||||
cache = self._chart_cache
|
||||
cache.pop(group_key, None)
|
||||
cache[group_key] = all_linked
|
||||
|
||||
def get_chart_symbols(
|
||||
self,
|
||||
symbol_key: str,
|
||||
|
||||
) -> tuple[LinkedSplits, LinkedSplits]: # type: ignore
|
||||
return self._chart_cache.get(symbol_key)
|
||||
|
||||
async def load_symbols(
|
||||
self,
|
||||
fqmes: list[str],
|
||||
loglevel: str,
|
||||
reset: bool = False,
|
||||
|
||||
) -> trio.Event:
|
||||
'''
|
||||
Load a new contract into the charting app.
|
||||
|
||||
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
||||
|
||||
'''
|
||||
# NOTE: for now we use the first symbol in the set as the "key"
|
||||
# for the overlay of feeds on the chart.
|
||||
group_key: tuple[str] = tuple(fqmes)
|
||||
|
||||
all_linked = self.get_chart_symbols(group_key)
|
||||
order_mode_started = trio.Event()
|
||||
|
||||
if not self.vbox.isEmpty():
|
||||
|
||||
# XXX: seems to make switching slower?
|
||||
# qframe = self.hist_linked.chart.qframe
|
||||
# if qframe.sidepane is self.search:
|
||||
# qframe.hbox.removeWidget(self.search)
|
||||
|
||||
for linked in [self.rt_linked, self.hist_linked]:
|
||||
# XXX: this is CRITICAL especially with pixel buffer caching
|
||||
linked.hide()
|
||||
linked.unfocus()
|
||||
|
||||
# XXX: pretty sure we don't need this
|
||||
# remove any existing plots?
|
||||
# XXX: ahh we might want to support cache unloading..
|
||||
# self.vbox.removeWidget(linked)
|
||||
|
||||
# switching to a new viewable chart
|
||||
if all_linked is None or reset:
|
||||
from ._display import display_symbol_data
|
||||
|
||||
# we must load a fresh linked charts set
|
||||
self.rt_linked = rt_charts = LinkedSplits(self)
|
||||
self.hist_linked = hist_charts = LinkedSplits(self)
|
||||
|
||||
# spawn new task to start up and update new sub-chart instances
|
||||
self._root_n.start_soon(
|
||||
display_symbol_data,
|
||||
self,
|
||||
fqmes,
|
||||
loglevel,
|
||||
order_mode_started,
|
||||
)
|
||||
|
||||
# self.vbox.addWidget(hist_charts)
|
||||
self.vbox.addWidget(rt_charts)
|
||||
self.set_chart_symbols(
|
||||
group_key,
|
||||
(hist_charts, rt_charts),
|
||||
)
|
||||
|
||||
for linked in [hist_charts, rt_charts]:
|
||||
linked.show()
|
||||
linked.focus()
|
||||
|
||||
await trio.sleep(0)
|
||||
|
||||
else:
|
||||
# symbol is already loaded and ems ready
|
||||
order_mode_started.set()
|
||||
|
||||
self.hist_linked, self.rt_linked = all_linked
|
||||
|
||||
for linked in all_linked:
|
||||
# TODO:
|
||||
# - we'll probably want per-instrument/provider state here?
|
||||
# change the order config form over to the new chart
|
||||
|
||||
# chart is already in memory so just focus it
|
||||
linked.show()
|
||||
linked.focus()
|
||||
linked.graphics_cycle()
|
||||
await trio.sleep(0)
|
||||
|
||||
# resume feeds *after* rendering chart view asap
|
||||
chart = linked.chart
|
||||
if chart:
|
||||
chart.resume_all_feeds()
|
||||
|
||||
# TODO: we need a check to see if the chart
|
||||
# last had the xlast in view, if so then shift so it's
|
||||
# still in view, if the user was viewing history then
|
||||
# do nothing yah?
|
||||
self.rt_linked.chart.main_viz.default_view(
|
||||
do_min_bars=True,
|
||||
)
|
||||
|
||||
# if a history chart instance is already up then
|
||||
# set the search widget as its sidepane.
|
||||
hist_chart = self.hist_linked.chart
|
||||
if hist_chart:
|
||||
hist_chart.qframe.set_sidepane(self.search)
|
||||
|
||||
# NOTE: this is really stupid/hard to follow.
|
||||
# we have to reposition the active position nav
|
||||
# **AFTER** applying the search bar as a sidepane
|
||||
# to the newly switched to symbol.
|
||||
await trio.sleep(0)
|
||||
|
||||
# TODO: probably stick this in some kinda `LooknFeel` API?
|
||||
for tracker in self.rt_linked.mode.trackers.values():
|
||||
pp_nav = tracker.nav
|
||||
if tracker.live_pp.cumsize:
|
||||
pp_nav.show()
|
||||
pp_nav.hide_info()
|
||||
else:
|
||||
pp_nav.hide()
|
||||
|
||||
# set window titlebar info
|
||||
symbol = self.rt_linked.mkt
|
||||
if symbol is not None:
|
||||
self.window.setWindowTitle(
|
||||
f'{symbol.fqme} '
|
||||
f'tick:{symbol.size_tick}'
|
||||
)
|
||||
|
||||
return order_mode_started
|
||||
|
||||
def focus(self) -> None:
|
||||
'''
|
||||
Focus the top level widget which in turn focusses the chart
|
||||
ala "view mode".
|
||||
|
||||
'''
|
||||
# go back to view-mode focus (aka chart focus)
|
||||
self.clearFocus()
|
||||
chart = self.rt_linked.chart
|
||||
if chart:
|
||||
chart.setFocus()
|
||||
|
||||
def reg_for_resize(
|
||||
self,
|
||||
widget: QWidget,
|
||||
) -> None:
|
||||
getattr(widget, 'on_resize')
|
||||
self._widgets[widget.mode_name] = widget
|
||||
|
||||
def on_win_resize(self, event: QtCore.QEvent) -> None:
|
||||
'''
|
||||
Top level god widget handler from window (the real yaweh) resize
|
||||
events such that any registered widgets which wish to be
|
||||
notified are invoked using our pythonic `.on_resize()` method
|
||||
api.
|
||||
|
||||
Where we do UX magic to make things not suck B)
|
||||
|
||||
'''
|
||||
if self._resizing:
|
||||
return
|
||||
|
||||
self._resizing = True
|
||||
|
||||
log.info('God widget resize')
|
||||
for name, widget in self._widgets.items():
|
||||
widget.on_resize()
|
||||
|
||||
self._resizing = False
|
||||
|
||||
# on_resize = on_win_resize
|
||||
|
||||
def get_cursor(self) -> Cursor:
|
||||
return self._active_cursor
|
||||
|
||||
def iter_linked(self) -> Iterator[LinkedSplits]:
|
||||
for linked in [self.hist_linked, self.rt_linked]:
|
||||
yield linked
|
||||
|
||||
def resize_all(self) -> None:
|
||||
'''
|
||||
Dynamic resize sequence: adjusts all sub-widgets/charts to
|
||||
sensible default ratios of what space is detected as available
|
||||
on the display / window.
|
||||
|
||||
'''
|
||||
rt_linked = self.rt_linked
|
||||
rt_linked.set_split_sizes()
|
||||
self.rt_linked.resize_sidepanes()
|
||||
self.hist_linked.resize_sidepanes(from_linked=rt_linked)
|
||||
self.search.on_resize()
|
||||
|
||||
|
||||
class ChartnPane(QFrame):
|
||||
'''
|
||||
One-off ``QFrame`` composite which pairs a chart
|
||||
|
|
@ -369,9 +91,9 @@ class ChartnPane(QFrame):
|
|||
https://doc.qt.io/qt-5/qwidget.html#composite-widgets
|
||||
|
||||
'''
|
||||
sidepane: FieldsForm | SearchWidget
|
||||
sidepane: FieldsForm|SearchWidget
|
||||
hbox: QHBoxLayout
|
||||
chart: ChartPlotWidget | None = None
|
||||
chart: ChartPlotWidget|None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -387,13 +109,13 @@ class ChartnPane(QFrame):
|
|||
self.chart = None
|
||||
|
||||
hbox = self.hbox = QHBoxLayout(self)
|
||||
hbox.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||
hbox.setAlignment(Qt.AlignTop|Qt.AlignLeft)
|
||||
hbox.setContentsMargins(0, 0, 0, 0)
|
||||
hbox.setSpacing(3)
|
||||
|
||||
def set_sidepane(
|
||||
self,
|
||||
sidepane: FieldsForm | SearchWidget,
|
||||
sidepane: FieldsForm|SearchWidget,
|
||||
) -> None:
|
||||
|
||||
# add sidepane **after** chart; place it on axis side
|
||||
|
|
@ -404,7 +126,7 @@ class ChartnPane(QFrame):
|
|||
self._sidepane = sidepane
|
||||
|
||||
@property
|
||||
def sidepane(self) -> FieldsForm | SearchWidget:
|
||||
def sidepane(self) -> FieldsForm|SearchWidget:
|
||||
return self._sidepane
|
||||
|
||||
|
||||
|
|
@ -419,7 +141,6 @@ class LinkedSplits(QWidget):
|
|||
|
||||
'''
|
||||
def __init__(
|
||||
|
||||
self,
|
||||
godwidget: GodWidget,
|
||||
|
||||
|
|
@ -450,7 +171,7 @@ class LinkedSplits(QWidget):
|
|||
# chart-local graphics state that can be passed to
|
||||
# a ``graphic_update_cycle()`` call by any task wishing to
|
||||
# update the UI for a given "chart instance".
|
||||
self.display_state: DisplayState | None = None
|
||||
self.display_state: DisplayState|None = None
|
||||
|
||||
self._mkt: MktPair = None
|
||||
|
||||
|
|
@ -486,7 +207,7 @@ class LinkedSplits(QWidget):
|
|||
|
||||
def set_split_sizes(
|
||||
self,
|
||||
prop: float | None = None,
|
||||
prop: float|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -567,8 +288,8 @@ class LinkedSplits(QWidget):
|
|||
|
||||
# style?
|
||||
self.chart.setFrameStyle(
|
||||
QFrame.Shape.StyledPanel |
|
||||
QFrame.Shadow.Plain
|
||||
QFrame.Shape.StyledPanel
|
||||
|QFrame.Shadow.Plain
|
||||
)
|
||||
|
||||
return self.chart
|
||||
|
|
@ -580,11 +301,11 @@ class LinkedSplits(QWidget):
|
|||
shm: ShmArray,
|
||||
flume: Flume,
|
||||
|
||||
array_key: str | None = None,
|
||||
array_key: str|None = None,
|
||||
style: str = 'line',
|
||||
_is_main: bool = False,
|
||||
|
||||
sidepane: QWidget | None = None,
|
||||
sidepane: QWidget|None = None,
|
||||
draw_kwargs: dict = {},
|
||||
|
||||
**cpw_kwargs,
|
||||
|
|
@ -687,7 +408,7 @@ class LinkedSplits(QWidget):
|
|||
cpw.plotItem.vb.linked = self
|
||||
cpw.setFrameStyle(
|
||||
QFrame.Shape.StyledPanel
|
||||
# | QFrame.Shadow.Plain
|
||||
# |QFrame.Shadow.Plain
|
||||
)
|
||||
|
||||
# don't show the little "autoscale" A label.
|
||||
|
|
@ -800,7 +521,7 @@ class LinkedSplits(QWidget):
|
|||
|
||||
def resize_sidepanes(
|
||||
self,
|
||||
from_linked: LinkedSplits | None = None,
|
||||
from_linked: LinkedSplits|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
@ -874,7 +595,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# TODO: load from config
|
||||
use_open_gl: bool = False,
|
||||
|
||||
static_yrange: tuple[float, float] | None = None,
|
||||
static_yrange: tuple[float, float]|None = None,
|
||||
|
||||
parent=None,
|
||||
**kwargs,
|
||||
|
|
@ -889,7 +610,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
# NOTE: must be set bfore calling ``.mk_vb()``
|
||||
self.linked = linkedsplits
|
||||
self.sidepane: FieldsForm | None = None
|
||||
self.sidepane: FieldsForm|None = None
|
||||
|
||||
# source of our custom interactions
|
||||
self.cv = self.mk_vb(name)
|
||||
|
|
@ -923,7 +644,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self.useOpenGL(use_open_gl)
|
||||
self.name = name
|
||||
self.data_key = data_key or name
|
||||
self.qframe: ChartnPane | None = None
|
||||
self.qframe: ChartnPane|None = None
|
||||
|
||||
# scene-local placeholder for book graphics
|
||||
# sizing to avoid overlap with data contents
|
||||
|
|
@ -934,7 +655,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# registry of overlay curve names
|
||||
self._vizs: dict[str, Viz] = {}
|
||||
|
||||
self.feed: Feed | None = None
|
||||
self.feed: Feed|None = None
|
||||
|
||||
self._labels = {} # registry of underlying graphics
|
||||
self._ysticks = {} # registry of underlying graphics
|
||||
|
|
@ -1027,11 +748,11 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
def increment_view(
|
||||
self,
|
||||
datums: int = 1,
|
||||
vb: ChartView | None = None,
|
||||
vb: ChartView|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Increment the data view ``datums``` steps toward y-axis thus
|
||||
Increment the data view `datums`` steps toward y-axis thus
|
||||
"following" the current time slot/step/bar.
|
||||
|
||||
'''
|
||||
|
|
@ -1041,7 +762,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
x_shift = viz.index_step() * datums
|
||||
|
||||
if datums >= 300:
|
||||
print("FUCKING FIX THE GLOBAL STEP BULLSHIT")
|
||||
log.warning('FUCKING FIX THE GLOBAL STEP BULLSHIT')
|
||||
# breakpoint()
|
||||
return
|
||||
|
||||
|
|
@ -1058,8 +779,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
def overlay_plotitem(
|
||||
self,
|
||||
name: str,
|
||||
index: int | None = None,
|
||||
axis_title: str | None = None,
|
||||
index: int|None = None,
|
||||
axis_title: str|None = None,
|
||||
axis_side: str = 'right',
|
||||
axis_kwargs: dict = {},
|
||||
|
||||
|
|
@ -1147,14 +868,14 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
shm: ShmArray,
|
||||
flume: Flume,
|
||||
|
||||
array_key: str | None = None,
|
||||
array_key: str|None = None,
|
||||
overlay: bool = False,
|
||||
color: str | None = None,
|
||||
color: str|None = None,
|
||||
add_label: bool = True,
|
||||
pi: pg.PlotItem | None = None,
|
||||
pi: pg.PlotItem|None = None,
|
||||
step_mode: bool = False,
|
||||
is_ohlc: bool = False,
|
||||
add_sticky: None | str = 'right',
|
||||
add_sticky: None|str = 'right',
|
||||
|
||||
**graphics_kwargs,
|
||||
|
||||
|
|
@ -1252,7 +973,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# use the tick size precision for display
|
||||
name = name or pi.name
|
||||
mkt: MktPair = self.linked.mkt
|
||||
digits: int | None = None
|
||||
digits: int|None = None
|
||||
if name in mkt.fqme:
|
||||
digits = mkt.price_tick_digits
|
||||
|
||||
|
|
@ -1286,7 +1007,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
shm: ShmArray,
|
||||
flume: Flume,
|
||||
|
||||
array_key: str | None = None,
|
||||
array_key: str|None = None,
|
||||
**draw_curve_kwargs,
|
||||
|
||||
) -> Viz:
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import pyqtgraph as pg
|
|||
|
||||
from piker.ui.qt import (
|
||||
QtWidgets,
|
||||
QGraphicsItem,
|
||||
Qt,
|
||||
QLineF,
|
||||
QRectF,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ from ._style import (
|
|||
from ._lines import LevelLine
|
||||
from ..log import get_logger
|
||||
|
||||
# TODO, rm the cycle here!
|
||||
from ._widget import (
|
||||
GodWidget,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._chart import (
|
||||
GodWidget,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ from . import _style
|
|||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._chart import GodWidget
|
||||
from ._widget import GodWidget
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
|
@ -91,6 +91,10 @@ def run_qtractor(
|
|||
window_type: QMainWindow = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Run the Qt event loop and embed `trio` via guest mode on it.
|
||||
|
||||
'''
|
||||
# avoids annoying message when entering debugger from qt loop
|
||||
pyqtRemoveInputHook()
|
||||
|
||||
|
|
@ -170,7 +174,7 @@ def run_qtractor(
|
|||
# hook into app focus change events
|
||||
app.focusChanged.connect(window.on_focus_change)
|
||||
|
||||
instance = main_widget_type()
|
||||
instance: GodWidget = main_widget_type()
|
||||
instance.window = window
|
||||
|
||||
# override tractor's defaults
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ log = get_logger(__name__)
|
|||
def update_fsp_chart(
|
||||
viz,
|
||||
graphics_name: str,
|
||||
array_key: str | None,
|
||||
array_key: str|None,
|
||||
**kwargs,
|
||||
|
||||
) -> None:
|
||||
|
|
@ -87,7 +87,11 @@ def update_fsp_chart(
|
|||
|
||||
# guard against unreadable case
|
||||
if not last_row:
|
||||
log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}')
|
||||
log.warning(
|
||||
f'Read-race on shm array,\n'
|
||||
f'graphics_name: {graphics_name!r}\n'
|
||||
f'shm.token: {shm.token}\n'
|
||||
)
|
||||
return
|
||||
|
||||
# update graphics
|
||||
|
|
@ -203,7 +207,6 @@ async def open_fsp_actor_cluster(
|
|||
|
||||
|
||||
async def run_fsp_ui(
|
||||
|
||||
linkedsplits: LinkedSplits,
|
||||
flume: Flume,
|
||||
started: trio.Event,
|
||||
|
|
@ -471,7 +474,7 @@ class FspAdmin:
|
|||
target: Fsp,
|
||||
conf: dict[str, dict[str, Any]],
|
||||
|
||||
worker_name: str | None = None,
|
||||
worker_name: str|None = None,
|
||||
loglevel: str = 'info',
|
||||
|
||||
) -> (Flume, trio.Event):
|
||||
|
|
@ -623,8 +626,10 @@ async def open_fsp_admin(
|
|||
event.set()
|
||||
|
||||
|
||||
# TODO, passing in `pikerd` related settings here!
|
||||
# [ ] read in the `tractor` setting for `enable_transports: list`
|
||||
# from the root `conf.toml`!
|
||||
async def open_vlm_displays(
|
||||
|
||||
linked: LinkedSplits,
|
||||
flume: Flume,
|
||||
dvlm: bool = True,
|
||||
|
|
@ -634,12 +639,12 @@ async def open_vlm_displays(
|
|||
|
||||
) -> None:
|
||||
'''
|
||||
Volume subchart displays.
|
||||
Vlm (volume) subchart displays.
|
||||
|
||||
Since "volume" is often included directly alongside OHLCV price
|
||||
data, we don't really need a separate FSP-actor + shm array for it
|
||||
since it's likely already directly adjacent to OHLC samples from the
|
||||
data provider.
|
||||
data, we don't really need a separate FSP-actor + shm array for
|
||||
it since it's likely already directly adjacent to OHLC samples
|
||||
from the data provider.
|
||||
|
||||
Further only if volume data is detected (it sometimes isn't provided
|
||||
eg. forex, certain commodities markets) will volume dependent FSPs
|
||||
|
|
|
|||
|
|
@ -237,8 +237,8 @@ class LevelLabel(YAxisLabel):
|
|||
class L1Label(LevelLabel):
|
||||
|
||||
text_flags = (
|
||||
QtCore.Qt.TextDontClip
|
||||
| QtCore.Qt.AlignLeft
|
||||
QtCore.Qt.TextFlag.TextDontClip
|
||||
| QtCore.Qt.AlignmentFlag.AlignLeft
|
||||
)
|
||||
|
||||
def set_label_str(
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ def hcolor(name: str) -> str:
|
|||
'cool_green': '#33b864',
|
||||
'dull_green': '#74a662',
|
||||
'hedge_green': '#518360',
|
||||
'lilypad_green': '#839c84',
|
||||
|
||||
# orders and alerts
|
||||
'alert_yellow': '#e2d083',
|
||||
|
|
@ -335,6 +336,7 @@ def hcolor(name: str) -> str:
|
|||
'sell_red': '#b6003f',
|
||||
# 'sell_red': '#d00048',
|
||||
'sell_red_light': '#f85462',
|
||||
'wine': '#69212d',
|
||||
|
||||
# 'sell_red': '#f85462',
|
||||
# 'sell_red_light': '#ff4d5c',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,346 @@
|
|||
# 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/>.
|
||||
|
||||
'''
|
||||
Root-most (what they call a "central widget") of every Qt-UI-app's
|
||||
window.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Iterator,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import trio
|
||||
|
||||
from piker.ui.qt import (
|
||||
QtCore,
|
||||
Qt,
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from ..log import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._search import SearchWidget
|
||||
from ._chart import (
|
||||
LinkedSplits,
|
||||
)
|
||||
from ._cursor import (
|
||||
Cursor,
|
||||
)
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
_godw: GodWidget|None = None
|
||||
|
||||
def get_godw() -> GodWidget:
|
||||
'''
|
||||
Get the top level "god widget", the root/central-most Qt
|
||||
widget-object set as `QMainWindow.setCentralWidget(_godw)`.
|
||||
|
||||
See `piker.ui._exec` for the runtime init details and all the
|
||||
machinery for running `trio` on the Qt event loop in guest mode.
|
||||
|
||||
'''
|
||||
if _godw is None:
|
||||
raise RuntimeError(
|
||||
'No god-widget initialized ??\n'
|
||||
'Have you called `run_qtractor()` yet?\n'
|
||||
)
|
||||
return _godw
|
||||
|
||||
|
||||
class GodWidget(QWidget):
|
||||
'''
|
||||
"Our lord and savior, the holy child of window-shua, there is no
|
||||
widget above thee." - 6|6
|
||||
|
||||
The highest level composed widget which contains layouts for
|
||||
organizing charts as well as other sub-widgets used to control or
|
||||
modify them.
|
||||
|
||||
'''
|
||||
search: SearchWidget
|
||||
mode_name: str = 'god'
|
||||
|
||||
def __init__(
|
||||
|
||||
self,
|
||||
parent=None,
|
||||
|
||||
) -> None:
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.search: SearchWidget|None = None
|
||||
|
||||
self.hbox = QHBoxLayout(self)
|
||||
self.hbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.hbox.setSpacing(6)
|
||||
self.hbox.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.vbox = QVBoxLayout()
|
||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.vbox.setSpacing(2)
|
||||
self.vbox.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.hbox.addLayout(self.vbox)
|
||||
|
||||
self._chart_cache: dict[
|
||||
str,
|
||||
tuple[LinkedSplits, LinkedSplits],
|
||||
] = {}
|
||||
|
||||
self.hist_linked: LinkedSplits|None = None
|
||||
self.rt_linked: LinkedSplits|None = None
|
||||
self._active_cursor: Cursor|None = None
|
||||
|
||||
# assigned in the startup func `_async_main()`
|
||||
self._root_n: trio.Nursery = None
|
||||
|
||||
self._widgets: dict[str, QWidget] = {}
|
||||
self._resizing: bool = False
|
||||
|
||||
# TODO: do we need this, when would god get resized
|
||||
# and the window does not? Never right?!
|
||||
# self.reg_for_resize(self)
|
||||
|
||||
# TODO: strat loader/saver that we don't need yet.
|
||||
# def init_strategy_ui(self):
|
||||
# self.toolbar_layout = QHBoxLayout()
|
||||
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# self.vbox.addLayout(self.toolbar_layout)
|
||||
# self.strategy_box = StrategyBoxWidget(self)
|
||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||
|
||||
@property
|
||||
def linkedsplits(self) -> LinkedSplits:
|
||||
return self.rt_linked
|
||||
|
||||
def set_chart_symbols(
|
||||
self,
|
||||
group_key: tuple[str], # of form <fqme>.<providername>
|
||||
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
|
||||
|
||||
) -> None:
|
||||
# re-sort org cache symbol list in LIFO order
|
||||
cache = self._chart_cache
|
||||
cache.pop(group_key, None)
|
||||
cache[group_key] = all_linked
|
||||
|
||||
def get_chart_symbols(
|
||||
self,
|
||||
symbol_key: str,
|
||||
|
||||
) -> tuple[LinkedSplits, LinkedSplits]: # type: ignore
|
||||
return self._chart_cache.get(symbol_key)
|
||||
|
||||
async def load_symbols(
|
||||
self,
|
||||
fqmes: list[str],
|
||||
loglevel: str,
|
||||
reset: bool = False,
|
||||
|
||||
) -> trio.Event:
|
||||
'''
|
||||
Load a new contract into the charting app.
|
||||
|
||||
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
||||
|
||||
'''
|
||||
# NOTE: for now we use the first symbol in the set as the "key"
|
||||
# for the overlay of feeds on the chart.
|
||||
group_key: tuple[str] = tuple(fqmes)
|
||||
|
||||
all_linked = self.get_chart_symbols(group_key)
|
||||
order_mode_started = trio.Event()
|
||||
|
||||
if not self.vbox.isEmpty():
|
||||
|
||||
# XXX: seems to make switching slower?
|
||||
# qframe = self.hist_linked.chart.qframe
|
||||
# if qframe.sidepane is self.search:
|
||||
# qframe.hbox.removeWidget(self.search)
|
||||
|
||||
for linked in [self.rt_linked, self.hist_linked]:
|
||||
# XXX: this is CRITICAL especially with pixel buffer caching
|
||||
linked.hide()
|
||||
linked.unfocus()
|
||||
|
||||
# XXX: pretty sure we don't need this
|
||||
# remove any existing plots?
|
||||
# XXX: ahh we might want to support cache unloading..
|
||||
# self.vbox.removeWidget(linked)
|
||||
|
||||
# switching to a new viewable chart
|
||||
if all_linked is None or reset:
|
||||
from ._display import display_symbol_data
|
||||
|
||||
# we must load a fresh linked charts set
|
||||
from ._chart import LinkedSplits
|
||||
self.rt_linked = rt_charts = LinkedSplits(self)
|
||||
self.hist_linked = hist_charts = LinkedSplits(self)
|
||||
|
||||
# spawn new task to start up and update new sub-chart instances
|
||||
self._root_n.start_soon(
|
||||
display_symbol_data,
|
||||
self,
|
||||
fqmes,
|
||||
loglevel,
|
||||
order_mode_started,
|
||||
)
|
||||
|
||||
# self.vbox.addWidget(hist_charts)
|
||||
self.vbox.addWidget(rt_charts)
|
||||
self.set_chart_symbols(
|
||||
group_key,
|
||||
(hist_charts, rt_charts),
|
||||
)
|
||||
|
||||
for linked in [hist_charts, rt_charts]:
|
||||
linked.show()
|
||||
linked.focus()
|
||||
|
||||
await trio.sleep(0)
|
||||
|
||||
else:
|
||||
# symbol is already loaded and ems ready
|
||||
order_mode_started.set()
|
||||
|
||||
self.hist_linked, self.rt_linked = all_linked
|
||||
|
||||
for linked in all_linked:
|
||||
# TODO:
|
||||
# - we'll probably want per-instrument/provider state here?
|
||||
# change the order config form over to the new chart
|
||||
|
||||
# chart is already in memory so just focus it
|
||||
linked.show()
|
||||
linked.focus()
|
||||
linked.graphics_cycle()
|
||||
await trio.sleep(0)
|
||||
|
||||
# resume feeds *after* rendering chart view asap
|
||||
chart = linked.chart
|
||||
if chart:
|
||||
chart.resume_all_feeds()
|
||||
|
||||
# TODO: we need a check to see if the chart
|
||||
# last had the xlast in view, if so then shift so it's
|
||||
# still in view, if the user was viewing history then
|
||||
# do nothing yah?
|
||||
self.rt_linked.chart.main_viz.default_view(
|
||||
do_min_bars=True,
|
||||
)
|
||||
|
||||
# if a history chart instance is already up then
|
||||
# set the search widget as its sidepane.
|
||||
hist_chart = self.hist_linked.chart
|
||||
if hist_chart:
|
||||
hist_chart.qframe.set_sidepane(self.search)
|
||||
|
||||
# NOTE: this is really stupid/hard to follow.
|
||||
# we have to reposition the active position nav
|
||||
# **AFTER** applying the search bar as a sidepane
|
||||
# to the newly switched to symbol.
|
||||
await trio.sleep(0)
|
||||
|
||||
# TODO: probably stick this in some kinda `LooknFeel` API?
|
||||
for tracker in self.rt_linked.mode.trackers.values():
|
||||
pp_nav = tracker.nav
|
||||
if tracker.live_pp.cumsize:
|
||||
pp_nav.show()
|
||||
pp_nav.hide_info()
|
||||
else:
|
||||
pp_nav.hide()
|
||||
|
||||
# set window titlebar info
|
||||
symbol = self.rt_linked.mkt
|
||||
if symbol is not None:
|
||||
self.window.setWindowTitle(
|
||||
f'{symbol.fqme} '
|
||||
f'tick:{symbol.size_tick}'
|
||||
)
|
||||
|
||||
return order_mode_started
|
||||
|
||||
def focus(self) -> None:
|
||||
'''
|
||||
Focus the top level widget which in turn focusses the chart
|
||||
ala "view mode".
|
||||
|
||||
'''
|
||||
# go back to view-mode focus (aka chart focus)
|
||||
self.clearFocus()
|
||||
chart = self.rt_linked.chart
|
||||
if chart:
|
||||
chart.setFocus()
|
||||
|
||||
def reg_for_resize(
|
||||
self,
|
||||
widget: QWidget,
|
||||
) -> None:
|
||||
getattr(widget, 'on_resize')
|
||||
self._widgets[widget.mode_name] = widget
|
||||
|
||||
def on_win_resize(self, event: QtCore.QEvent) -> None:
|
||||
'''
|
||||
Top level god widget handler from window (the real yaweh) resize
|
||||
events such that any registered widgets which wish to be
|
||||
notified are invoked using our pythonic `.on_resize()` method
|
||||
api.
|
||||
|
||||
Where we do UX magic to make things not suck B)
|
||||
|
||||
'''
|
||||
if self._resizing:
|
||||
return
|
||||
|
||||
self._resizing = True
|
||||
|
||||
log.info('God widget resize')
|
||||
for name, widget in self._widgets.items():
|
||||
widget.on_resize()
|
||||
|
||||
self._resizing = False
|
||||
|
||||
# on_resize = on_win_resize
|
||||
|
||||
def get_cursor(self) -> Cursor:
|
||||
return self._active_cursor
|
||||
|
||||
def iter_linked(self) -> Iterator[LinkedSplits]:
|
||||
for linked in [self.hist_linked, self.rt_linked]:
|
||||
yield linked
|
||||
|
||||
def resize_all(self) -> None:
|
||||
'''
|
||||
Dynamic resize sequence: adjusts all sub-widgets/charts to
|
||||
sensible default ratios of what space is detected as available
|
||||
on the display / window.
|
||||
|
||||
'''
|
||||
rt_linked = self.rt_linked
|
||||
rt_linked.set_split_sizes()
|
||||
self.rt_linked.resize_sidepanes()
|
||||
self.hist_linked.resize_sidepanes(from_linked=rt_linked)
|
||||
self.search.on_resize()
|
||||
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ from piker.ui.qt import (
|
|||
)
|
||||
from ..log import get_logger
|
||||
from ._style import _font_small, hcolor
|
||||
from ._chart import GodWidget
|
||||
from ._widget import GodWidget
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
|
@ -61,9 +61,9 @@ class MultiStatus:
|
|||
|
||||
self,
|
||||
msg: str,
|
||||
final_msg: str | None = None,
|
||||
final_msg: str|None = None,
|
||||
clear_on_next: bool = False,
|
||||
group_key: Union[bool, str] | None = False,
|
||||
group_key: Union[bool, str]|None = False,
|
||||
|
||||
) -> Union[Callable[..., None], str]:
|
||||
'''
|
||||
|
|
@ -175,11 +175,11 @@ class MainWindow(QMainWindow):
|
|||
self.setWindowTitle(self.title)
|
||||
|
||||
# set by runtime after `trio` is engaged.
|
||||
self.godwidget: GodWidget | None = None
|
||||
self.godwidget: GodWidget|None = None
|
||||
|
||||
self._status_bar: QStatusBar = None
|
||||
self._status_label: QLabel = None
|
||||
self._size: tuple[int, int] | None = None
|
||||
self._size: tuple[int, int]|None = None
|
||||
|
||||
@property
|
||||
def mode_label(self) -> QLabel:
|
||||
|
|
@ -202,7 +202,7 @@ class MainWindow(QMainWindow):
|
|||
label.setMargin(2)
|
||||
label.setAlignment(
|
||||
QtCore.Qt.AlignVCenter
|
||||
| QtCore.Qt.AlignRight
|
||||
|QtCore.Qt.AlignRight
|
||||
)
|
||||
self.statusBar().addPermanentWidget(label)
|
||||
label.show()
|
||||
|
|
@ -288,7 +288,7 @@ class MainWindow(QMainWindow):
|
|||
|
||||
def configure_to_desktop(
|
||||
self,
|
||||
size: tuple[int, int] | None = None,
|
||||
size: tuple[int, int]|None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
|
|||
|
|
@ -59,8 +59,14 @@ from piker.data import (
|
|||
from piker.types import Struct
|
||||
from piker.log import get_logger
|
||||
from piker.ui.qt import Qt
|
||||
from ._editors import LineEditor, ArrowEditor
|
||||
from ._lines import order_line, LevelLine
|
||||
from ._editors import (
|
||||
LineEditor,
|
||||
ArrowEditor,
|
||||
)
|
||||
from ._lines import (
|
||||
order_line,
|
||||
LevelLine,
|
||||
)
|
||||
from ._position import (
|
||||
PositionTracker,
|
||||
SettingsPane,
|
||||
|
|
|
|||
Loading…
Reference in New Issue