Move `GodWidget` to new `._widget` mod
Extract root-most widget to resolve (various) `.ui` import cycles when the type is declared on `Struct`s.. Deats, - flip to `from ._widget import GodWidget`. - move `Feed` + `Flume` imports to TYPE_CHECKING in `._chart` - drop unused `trio` import from `._chart` - fix docstring typo: "datums```" -> "`datums``" - change `print()` to `log.warning()` for global step msg (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-codeorder_line_cancel_nowork_debugging
parent
5853bc2404
commit
5af7a82340
|
|
@ -29,7 +29,6 @@ from typing import (
|
||||||
)
|
)
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import trio
|
|
||||||
|
|
||||||
from piker.ui.qt import (
|
from piker.ui.qt import (
|
||||||
QtCore,
|
QtCore,
|
||||||
|
|
@ -41,6 +40,7 @@ from piker.ui.qt import (
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
)
|
)
|
||||||
|
from ._widget import GodWidget
|
||||||
from ._axes import (
|
from ._axes import (
|
||||||
DynamicDateAxis,
|
DynamicDateAxis,
|
||||||
PriceAxis,
|
PriceAxis,
|
||||||
|
|
@ -61,10 +61,6 @@ from ._style import (
|
||||||
_xaxis_at,
|
_xaxis_at,
|
||||||
# _min_points_to_show,
|
# _min_points_to_show,
|
||||||
)
|
)
|
||||||
from ..data.feed import (
|
|
||||||
Feed,
|
|
||||||
Flume,
|
|
||||||
)
|
|
||||||
from ..accounting import (
|
from ..accounting import (
|
||||||
MktPair,
|
MktPair,
|
||||||
)
|
)
|
||||||
|
|
@ -78,305 +74,12 @@ from . import _pg_overrides as pgo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._display import DisplayState
|
from ._display import DisplayState
|
||||||
|
from ..data.flows import Flume
|
||||||
|
from ..data.feed import Feed
|
||||||
|
|
||||||
log = get_logger(__name__)
|
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
|
|
||||||
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):
|
class ChartnPane(QFrame):
|
||||||
'''
|
'''
|
||||||
One-off ``QFrame`` composite which pairs a chart
|
One-off ``QFrame`` composite which pairs a chart
|
||||||
|
|
@ -438,7 +141,6 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
godwidget: GodWidget,
|
godwidget: GodWidget,
|
||||||
|
|
||||||
|
|
@ -1050,7 +752,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
) -> 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.
|
"following" the current time slot/step/bar.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
@ -1060,7 +762,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
x_shift = viz.index_step() * datums
|
x_shift = viz.index_step() * datums
|
||||||
|
|
||||||
if datums >= 300:
|
if datums >= 300:
|
||||||
print("FUCKING FIX THE GLOBAL STEP BULLSHIT")
|
log.warning('FUCKING FIX THE GLOBAL STEP BULLSHIT')
|
||||||
# breakpoint()
|
# breakpoint()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,11 @@ from ._style import (
|
||||||
from ._lines import LevelLine
|
from ._lines import LevelLine
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
|
# TODO, rm the cycle here!
|
||||||
|
from ._widget import (
|
||||||
|
GodWidget,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._chart import (
|
from ._chart import (
|
||||||
GodWidget,
|
GodWidget,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ from . import _style
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._chart import GodWidget
|
from ._widget import GodWidget
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -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 ..log import get_logger
|
||||||
from ._style import _font_small, hcolor
|
from ._style import _font_small, hcolor
|
||||||
from ._chart import GodWidget
|
from ._widget import GodWidget
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue