diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index a55202f3..838da304 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -52,7 +52,6 @@ from ._l1 import L1Labels from ._graphics._ohlc import BarItems from ._graphics._curve import FastAppendCurve from ._style import ( - _font, hcolor, CHART_MARGINS, _xaxis_at, @@ -68,7 +67,7 @@ from ..data import maybe_open_shm_array from .. import brokers from .. import data from ..log import get_logger -from ._exec import run_qtractor, current_screen +from ._exec import run_qtractor from ._interaction import ChartView from .order_mode import start_order_mode from .. import fsp @@ -79,10 +78,11 @@ log = get_logger(__name__) class ChartSpace(QtGui.QWidget): - """High level widget which contains layouts for organizing - lower level charts as well as other widgets used to control - or modify them. - """ + '''Highest level composed widget which contains layouts for + organizing lower level charts as well as other widgets used to + control or modify them. + + ''' def __init__(self, parent=None): super().__init__(parent) @@ -431,6 +431,8 @@ class ChartPlotWidget(pg.PlotWidget): _l1_labels: L1Labels = None + mode_name: str = 'mode: view' + # TODO: can take a ``background`` color setting - maybe there's # a better one? @@ -1000,7 +1002,7 @@ async def test_bed( _clear_throttle_rate: int = 60 # Hz -_book_throttle_rate: int = 20 # Hz +_book_throttle_rate: int = 16 # Hz async def chart_from_quotes( @@ -1078,7 +1080,14 @@ async def chart_from_quotes( tick_margin = 2 * tick_size last_ask = last_bid = last_clear = time.time() + chart.show() + async for quotes in stream: + + # chart isn't actively shown so just skip render cycle + if chart._lc.isHidden(): + continue + for sym, quote in quotes.items(): now = time.time() @@ -1291,6 +1300,9 @@ async def run_fsp( This is called once for each entry in the fsp config map. """ + done = linked_charts.window().status_bar.open_status( + f'loading FSP: {display_name}..') + async with portal.open_stream_from( # subactor entrypoint @@ -1384,13 +1396,20 @@ async def run_fsp( last = time.time() + done() + # update chart graphics async for value in stream: + # chart isn't actively shown so just skip render cycle + if chart._lc.isHidden(): + continue + now = time.time() period = now - last + # if period <= 1/30: - if period <= 1/_clear_throttle_rate - 0.001: + if period <= 1/_clear_throttle_rate: # faster then display refresh rate # print(f'quote too fast: {1/period}') continue @@ -1479,7 +1498,7 @@ async def check_for_new_bars(feed, ohlcv, linked_charts): async def chart_symbol( chart_app: ChartSpace, - brokername: str, + provider: str, sym: str, loglevel: str, ) -> None: @@ -1489,11 +1508,14 @@ async def chart_symbol( can be viewed and switched between extremely fast. """ + sbar = chart_app.window.status_bar + loading_sym_done = sbar.open_status(f'loading {sym}.{provider}..') + # historical data fetch - brokermod = brokers.get_brokermod(brokername) + brokermod = brokers.get_brokermod(provider) async with data.open_feed( - brokername, + provider, [sym], loglevel=loglevel, ) as feed: @@ -1526,6 +1548,8 @@ async def chart_symbol( add_label=False, ) + loading_sym_done() + # size view to data once at outset chart._set_yrange() @@ -1604,7 +1628,7 @@ async def chart_symbol( # linked_charts, # ) - await start_order_mode(chart, symbol, brokername) + await start_order_mode(chart, symbol, provider) async def load_providers( @@ -1621,7 +1645,7 @@ async def load_providers( # search engines. for broker in brokernames: - log.info(f'Loading brokerd for {broker}') + log.info(f'loading brokerd for {broker}..') # spin up broker daemons for each provider portal = await stack.enter_async_context( maybe_spawn_brokerd( @@ -1645,7 +1669,7 @@ async def load_providers( async def _async_main( # implicit required argument provided by ``qtractor_run()`` - widgets: Dict[str, Any], + main_widget: ChartSpace, sym: str, brokernames: str, @@ -1659,10 +1683,10 @@ async def _async_main( """ - chart_app = widgets['main'] + chart_app = main_widget # attempt to configure DPI aware font size - screen = current_screen() + screen = chart_app.window.current_screen() # configure graphics update throttling based on display refresh rate global _clear_throttle_rate @@ -1672,8 +1696,11 @@ async def _async_main( ) log.info(f'Set graphics update rate to {_clear_throttle_rate} Hz') - # configure global DPI aware font size - _font.configure_to_dpi(screen) + # TODO: do styling / themeing setup + # _style.style_ze_sheets(chart_app) + + sbar = chart_app.window.status_bar + starting_done = sbar.open_status('starting ze chartz...') async with trio.open_nursery() as root_n: @@ -1702,8 +1729,6 @@ async def _async_main( # this internally starts a ``chart_symbol()`` task above chart_app.load_symbol(provider, symbol, loglevel) - root_n.start_soon(load_providers, brokernames, loglevel) - # spin up a search engine for the local cached symbol set async with _search.register_symbol_search( @@ -1712,8 +1737,14 @@ async def _async_main( _search.search_simple_dict, source=chart_app._chart_cache, ), + # cache is super fast so debounce on super short period + pause_period=0.01, ): + # load other providers into search **after** + # the chart's select cache + root_n.start_soon(load_providers, brokernames, loglevel) + # start handling search bar kb inputs async with open_key_stream( search.bar, @@ -1727,6 +1758,7 @@ async def _async_main( key_stream, ) + starting_done() await trio.sleep_forever() diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py index 85488df8..3b72e8b0 100644 --- a/piker/ui/_exec.py +++ b/piker/ui/_exec.py @@ -21,10 +21,7 @@ Run ``trio`` in guest mode on top of the Qt event loop. All global Qt runtime settings are mostly defined here. """ from typing import Tuple, Callable, Dict, Any -import os import platform -import signal -import time import traceback # Qt specific @@ -32,20 +29,22 @@ import PyQt5 # noqa import pyqtgraph as pg from pyqtgraph import QtGui from PyQt5 import QtCore +# from PyQt5.QtGui import QLabel, QStatusBar from PyQt5.QtCore import ( pyqtRemoveInputHook, Qt, QCoreApplication, ) import qdarkstyle +from qdarkstyle import DarkPalette # import qdarkgraystyle import trio -import tractor from outcome import Error from .._daemon import maybe_open_pikerd, _tractor_kwargs from ..log import get_logger from ._pg_overrides import _do_overrides +from . import _style log = get_logger(__name__) @@ -59,37 +58,6 @@ pg.enableExperimental = True _do_overrides() -# singleton app per actor -_qt_app: QtGui.QApplication = None -_qt_win: QtGui.QMainWindow = None - - -def current_screen() -> QtGui.QScreen: - """Get a frickin screen (if we can, gawd). - - """ - global _qt_win, _qt_app - - start = time.time() - - tries = 3 - for _ in range(3): - screen = _qt_app.screenAt(_qt_win.pos()) - print(f'trying to get screen....') - if screen is None: - time.sleep(0.5) - continue - - break - else: - if screen is None: - # try for the first one we can find - screen = _qt_app.screens()[0] - - assert screen, "Wow Qt is dumb as shit and has no screen..." - return screen - - # XXX: pretty sure none of this shit works on linux as per: # https://bugreports.qt.io/browse/QTBUG-53022 # it seems to work on windows.. no idea wtf is up. @@ -104,33 +72,12 @@ if platform.system() == "Windows": QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) -class MainWindow(QtGui.QMainWindow): - - size = (800, 500) - title = 'piker chart (ur symbol is loading bby)' - - def __init__(self, parent=None): - super().__init__(parent) - self.setMinimumSize(*self.size) - self.setWindowTitle(self.title) - - def closeEvent( - self, - event: QtGui.QCloseEvent, - ) -> None: - """Cancel the root actor asap. - - """ - # raising KBI seems to get intercepted by by Qt so just use the system. - os.kill(os.getpid(), signal.SIGINT) - - def run_qtractor( func: Callable, args: Tuple, main_widget: QtGui.QWidget, tractor_kwargs: Dict[str, Any] = {}, - window_type: QtGui.QMainWindow = MainWindow, + window_type: QtGui.QMainWindow = None, ) -> None: # avoids annoying message when entering debugger from qt loop pyqtRemoveInputHook() @@ -148,10 +95,6 @@ def run_qtractor( # XXX: lmfao, this is how you disable text edit cursor blinking..smh app.setCursorFlashTime(0) - # set global app singleton - global _qt_app - _qt_app = app - # This code is from Nathaniel, and I quote: # "This is substantially faster than using a signal... for some # reason Qt signal dispatch is really slow (and relies on events @@ -188,19 +131,33 @@ def run_qtractor( app.quit() # load dark theme - app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='pyqt5')) + stylesheet = qdarkstyle.load_stylesheet( + qt_api='pyqt5', + palette=DarkPalette, + ) + app.setStyleSheet(stylesheet) # make window and exec + from . import _window + + if window_type is None: + window_type = _window.MainWindow + window = window_type() + + # set global app's main window singleton + _window._qt_win = window + + # configure global DPI aware font sizes now that a screen + # should be active from which we can read a DPI. + _style._config_fonts_to_screen() + + # hook into app focus change events + app.focusChanged.connect(window.on_focus_change) + instance = main_widget() instance.window = window - widgets = { - 'window': window, - 'main': instance, - } - - # override tractor's defaults tractor_kwargs.update(_tractor_kwargs) @@ -210,7 +167,7 @@ def run_qtractor( async with maybe_open_pikerd( **tractor_kwargs, ): - await func(*((widgets,) + args)) + await func(*((instance,) + args)) # guest mode entry trio.lowlevel.start_guest_run( @@ -223,11 +180,6 @@ def run_qtractor( window.main_widget = main_widget window.setCentralWidget(instance) - # store global ref - # set global app singleton - global _qt_win - _qt_win = window - # actually render to screen window.show() app.exec_() diff --git a/piker/ui/_graphics/_cursor.py b/piker/ui/_graphics/_cursor.py index 0919f6f9..a32a3870 100644 --- a/piker/ui/_graphics/_cursor.py +++ b/piker/ui/_graphics/_cursor.py @@ -31,6 +31,7 @@ from .._style import ( _xaxis_at, hcolor, _font, + _font_small, ) from .._axes import YAxisLabel, XAxisLabel from ...log import get_logger @@ -109,7 +110,7 @@ class LineDot(pg.CurvePoint): return False -# TODO: change this into our own ``Label`` +# TODO: change this into our own ``_label.Label`` class ContentsLabel(pg.LabelItem): """Label anchored to a ``ViewBox`` typically for displaying datum-wise points from the "viewed" contents. @@ -138,22 +139,14 @@ class ContentsLabel(pg.LabelItem): justify_text: str = 'left', font_size: Optional[int] = None, ) -> None: - font_size = font_size or _font.font.pixelSize() + + font_size = font_size or _font_small.px_size super().__init__( justify=justify_text, size=f'{str(font_size)}px' ) - if _font._physical_dpi >= 97: - # ad-hoc scale it based on boundingRect - # TODO: need proper fix for this? - typical_br = _font._qfm.boundingRect('Qyp') - anchor_font_size = math.ceil(typical_br.height() * 1.25) - - else: - anchor_font_size = font_size - # anchor to viewbox self.setParentItem(chart._vb) chart.scene().addItem(self) @@ -165,7 +158,7 @@ class ContentsLabel(pg.LabelItem): ydim = margins[1] if inspect.isfunction(margins[1]): - margins = margins[0], ydim(anchor_font_size) + margins = margins[0], ydim(font_size) self.anchor(itemPos=index, parentPos=index, offset=margins) diff --git a/piker/ui/_interaction.py b/piker/ui/_interaction.py index 9c407f3c..ac87e799 100644 --- a/piker/ui/_interaction.py +++ b/piker/ui/_interaction.py @@ -467,6 +467,9 @@ class ChartView(ViewBox): - zoom on right-click-n-drag to cursor position """ + + mode_name: str = 'mode: view' + def __init__( self, parent: pg.PlotItem = None, diff --git a/piker/ui/_search.py b/piker/ui/_search.py index 3adb16a1..4360fd5b 100644 --- a/piker/ui/_search.py +++ b/piker/ui/_search.py @@ -96,6 +96,8 @@ class SimpleDelegate(QStyledItemDelegate): class CompleterView(QTreeView): + mode_name: str = 'mode: search-nav' + # XXX: relevant docs links: # - simple widget version of this: # https://doc.qt.io/qt-5/qtreewidget.html#details @@ -153,13 +155,14 @@ class CompleterView(QTreeView): self._font_size: int = 0 # pixels def on_pressed(self, idx: QModelIndex) -> None: + '''Mouse pressed on view handler. + ''' search = self.parent() search.chart_current_item(clear_to_cache=False) search.focus() def set_font_size(self, size: int = 18): - # dpi_px_size = _font.px_size # print(size) if size < 0: size = 16 @@ -424,6 +427,8 @@ class CompleterView(QTreeView): class SearchBar(QtWidgets.QLineEdit): + mode_name: str = 'mode: search' + def __init__( self, @@ -487,6 +492,8 @@ class SearchWidget(QtGui.QWidget): Includes helper methods for item management in the sub-widgets. ''' + mode_name: str = 'mode: search' + def __init__( self, chart_space: 'ChartSpace', # type: ignore # noqa @@ -499,7 +506,7 @@ class SearchWidget(QtGui.QWidget): # size it as we specify self.setSizePolicy( QtWidgets.QSizePolicy.Fixed, - QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Fixed, ) self.chart_app = chart_space @@ -618,13 +625,15 @@ class SearchWidget(QtGui.QWidget): # making?) fqsn = '.'.join([symbol, provider]).lower() - # Re-order the symbol cache on the chart to display in - # LIFO order. this is normally only done internally by - # the chart on new symbols being loaded into memory - chart.set_chart_symbol(fqsn, chart.linkedcharts) - if clear_to_cache: + self.bar.clear() + + # Re-order the symbol cache on the chart to display in + # LIFO order. this is normally only done internally by + # the chart on new symbols being loaded into memory + chart.set_chart_symbol(fqsn, chart.linkedcharts) + self.view.set_section_entries( 'cache', values=list(reversed(chart._chart_cache)), @@ -692,8 +701,10 @@ async def fill_results( recv_chan: trio.abc.ReceiveChannel, # kb debouncing pauses (bracket defaults) - min_pause_time: float = 0.1, - max_pause_time: float = 6/16, + min_pause_time: float = 0.01, # absolute min typing throttle + + # max pause required before slow relay + max_pause_time: float = 6/16 + 0.001, ) -> None: """Task to search through providers and fill in possible @@ -742,11 +753,6 @@ async def fill_results( _search_active = trio.Event() break - if repeats > 2 and period >= max_pause_time: - _search_active = trio.Event() - repeats = 0 - break - if text == last_text: repeats += 1 @@ -754,9 +760,8 @@ async def fill_results( # print('search currently disabled') break - log.debug(f'Search req for {text}') - already_has_results = has_results[text] + log.debug(f'Search req for {text}') # issue multi-provider fan-out search request and place # "searching.." statuses on outstanding results providers @@ -765,16 +770,22 @@ async def fill_results( for provider, (search, pause) in ( _searcher_cache.copy().items() ): - - if provider != 'cache': - view.clear_section( - provider, status_field='-> searchin..') - - # only conduct search on this backend if it's - # registered for the corresponding pause period. + # XXX: only conduct search on this backend if it's + # registered for the corresponding pause period AND + # it hasn't already been searched with the current + # input pattern (in which case just look up the old + # results). if (period >= pause) and ( provider not in already_has_results ): + + # TODO: it may make more sense TO NOT search the + # cache in a bg task since we know it's fully + # cpu-bound. + if provider != 'cache': + view.clear_section( + provider, status_field='-> searchin..') + await n.start( pack_matches, view, @@ -786,6 +797,14 @@ async def fill_results( ) else: # already has results for this input text results = matches[(provider, text)] + + # TODO really for the cache we need an + # invalidation signal so that we only re-search + # the cache once it's been mutated by the chart + # switcher.. right now we're just always + # re-searching it's ``dict`` since it's easier + # but it also causes it to be slower then cached + # results from other providers on occasion. if results and provider != 'cache': view.set_section_entries( section=provider, @@ -794,6 +813,11 @@ async def fill_results( else: view.clear_section(provider) + if repeats > 2 and period > max_pause_time: + _search_active = trio.Event() + repeats = 0 + break + bar.show() @@ -952,7 +976,7 @@ async def register_symbol_search( global _searcher_cache - pause_period = pause_period or 0.125 + pause_period = pause_period or 0.1 # deliver search func to consumer try: diff --git a/piker/ui/_style.py b/piker/ui/_style.py index 322247ec..6e89d8b6 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -22,18 +22,19 @@ import math import pyqtgraph as pg from PyQt5 import QtCore, QtGui -from qdarkstyle.palette import DarkPalette +from qdarkstyle import DarkPalette from ..log import get_logger -from ._exec import current_screen log = get_logger(__name__) +_magic_inches = 0.0666 * (1 + 6/16) + # chart-wide fonts specified in inches _font_sizes: Dict[str, Dict[str, float]] = { 'hi': { - 'default': 0.0616, - 'small': 0.055, + 'default': _magic_inches, + 'small': 0.9 * _magic_inches, }, 'lo': { 'default': 6.5 / 64, @@ -43,6 +44,7 @@ _font_sizes: Dict[str, Dict[str, float]] = { class DpiAwareFont: + def __init__( self, # TODO: move to config @@ -52,10 +54,10 @@ class DpiAwareFont: ) -> None: self.name = name self._qfont = QtGui.QFont(name) - # self._iwl = size_in_inches or _default_font_inches_we_like self._font_size: str = font_size self._qfm = QtGui.QFontMetrics(self._qfont) self._physical_dpi = None + self._font_inches: float = None self._screen = None def _set_qfont_px_size(self, px_size: int) -> None: @@ -64,13 +66,15 @@ class DpiAwareFont: @property def screen(self) -> QtGui.QScreen: + from ._window import main_window + if self._screen is not None: try: self._screen.refreshRate() except RuntimeError: - self._screen = current_screen() + self._screen = main_window().current_screen() else: - self._screen = current_screen() + self._screen = main_window().current_screen() return self._screen @@ -95,22 +99,34 @@ class DpiAwareFont: # take the max since scaling can make things ugly in some cases pdpi = screen.physicalDotsPerInch() ldpi = screen.logicalDotsPerInch() - dpi = max(pdpi, ldpi) + mx_dpi = max(pdpi, ldpi) + mn_dpi = min(pdpi, ldpi) + scale = round(ldpi/pdpi) - # for low dpi scale everything down - if dpi <= 97: + if mx_dpi <= 97: # for low dpi use larger font sizes inches = _font_sizes['lo'][self._font_size] - else: + + else: # hidpi use smaller font sizes inches = _font_sizes['hi'][self._font_size] + dpi = mn_dpi + + # dpi is likely somewhat scaled down so use slightly larger font size + if scale > 1 and self._font_size: + # TODO: this denominator should probably be determined from + # relative aspect rations or something? + inches = inches * (1 / scale) * (1 + 6/16) + dpi = mx_dpi + + self._font_inches = inches + font_size = math.floor(inches * dpi) log.info( - f"\nscreen:{screen.name()} with DPI: {dpi}" - f"\nbest font size is {font_size}\n" + f"\nscreen:{screen.name()} with pDPI: {pdpi}, lDPI: {ldpi}" + f"\nOur best guess font size is {font_size}\n" ) - + # apply the size self._set_qfont_px_size(font_size) - self._physical_dpi = dpi def boundingRect(self, value: str) -> QtCore.QRectF: @@ -130,12 +146,20 @@ class DpiAwareFont: # use inches size to be cross-resolution compatible? _font = DpiAwareFont() +_font_small = DpiAwareFont(font_size='small') + + +def _config_fonts_to_screen() -> None: + 'configure global DPI aware font sizes' + + global _font, _font_small + _font.configure_to_dpi() + _font_small.configure_to_dpi() + # TODO: re-compute font size when main widget switches screens? # https://forum.qt.io/topic/54136/how-do-i-get-the-qscreen-my-widget-is-on-qapplication-desktop-screen-returns-a-qwidget-and-qobject_cast-qscreen-returns-null/3 -# _i3_rgba = QtGui.QColor.fromRgbF(*[0.14]*3 + [1]) - # splitter widget config _xaxis_at = 'bottom' @@ -175,6 +199,7 @@ def hcolor(name: str) -> str: 'gray': '#808080', # like the kick 'grayer': '#4c4c4c', 'grayest': '#3f3f3f', + 'i3': '#494D4F', 'jet': '#343434', 'cadet': '#91A3B0', 'marengo': '#91A3B0', @@ -185,9 +210,13 @@ def hcolor(name: str) -> str: 'bracket': '#666666', # like the logo 'original': '#a9a9a9', - # palette - 'default': DarkPalette.COLOR_BACKGROUND_NORMAL, - 'default_light': DarkPalette.COLOR_BACKGROUND_LIGHT, + # from ``qdarkstyle`` palette + 'default_darkest': DarkPalette.COLOR_BACKGROUND_1, + 'default_dark': DarkPalette.COLOR_BACKGROUND_2, + 'default': DarkPalette.COLOR_BACKGROUND_3, + 'default_light': DarkPalette.COLOR_BACKGROUND_4, + 'default_lightest': DarkPalette.COLOR_BACKGROUND_5, + 'default_spotlight': DarkPalette.COLOR_BACKGROUND_6, 'white': '#ffffff', # for tinas and sunbathers diff --git a/piker/ui/_window.py b/piker/ui/_window.py new file mode 100644 index 00000000..60210160 --- /dev/null +++ b/piker/ui/_window.py @@ -0,0 +1,180 @@ +# piker: trading gear for hackers +# Copyright (C) Tyler Goodlet (in stewardship for piker0) + +# 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 . + +""" +Qt main window singletons and stuff. + +""" +import os +import signal +import time +from typing import Callable + +from pyqtgraph import QtGui +from PyQt5 import QtCore +from PyQt5.QtGui import QLabel, QStatusBar + +from ..log import get_logger +from ._style import _font_small, hcolor + + +log = get_logger(__name__) + + +class MultiStatus: + + bar: QStatusBar + statuses: list[str] + + def __init__(self, bar, statuses) -> None: + self.bar = bar + self.statuses = statuses + + def open_status( + self, + msg: str, + ) -> Callable[..., None]: + '''Add a status to the status bar and return a close callback which + when called will remove the status ``msg``. + + ''' + self.statuses.append(msg) + + def remove_msg() -> None: + self.statuses.remove(msg) + self.render() + + self.render() + return remove_msg + + def render(self) -> None: + if self.statuses: + self.bar.showMessage(f'{" ".join(self.statuses)}') + else: + self.bar.clearMessage() + + +class MainWindow(QtGui.QMainWindow): + + size = (800, 500) + title = 'piker chart (ur symbol is loading bby)' + + def __init__(self, parent=None): + super().__init__(parent) + self.setMinimumSize(*self.size) + self.setWindowTitle(self.title) + + self._status_bar: QStatusBar = None + self._status_label: QLabel = None + + @property + def mode_label(self) -> QtGui.QLabel: + + # init mode label + if not self._status_label: + + self._status_label = label = QtGui.QLabel() + label.setStyleSheet( + f"QLabel {{ color : {hcolor('gunmetal')}; }}" + ) + label.setTextFormat(3) # markdown + label.setFont(_font_small.font) + label.setMargin(2) + label.setAlignment( + QtCore.Qt.AlignVCenter + | QtCore.Qt.AlignRight + ) + self.statusBar().addPermanentWidget(label) + label.show() + + return self._status_label + + def closeEvent( + self, + event: QtGui.QCloseEvent, + ) -> None: + """Cancel the root actor asap. + + """ + # raising KBI seems to get intercepted by by Qt so just use the system. + os.kill(os.getpid(), signal.SIGINT) + + @property + def status_bar(self) -> QStatusBar: + + # style and cached the status bar on first access + if not self._status_bar: + + sb = self.statusBar() + sb.setStyleSheet(( + f"color : {hcolor('gunmetal')};" + f"background : {hcolor('default_dark')};" + f"font-size : {_font_small.px_size}px;" + "padding : 0px;" + # "min-height : 19px;" + # "qproperty-alignment: AlignVCenter;" + )) + self.setStatusBar(sb) + self._status_bar = MultiStatus(sb, []) + + return self._status_bar + + def on_focus_change( + self, + old: QtGui.QWidget, + new: QtGui.QWidget, + ) -> None: + + log.debug(f'widget focus changed from {old} -> {new}') + + if new is not None: + # cursor left window? + name = getattr(new, 'mode_name', '') + self.mode_label.setText(name) + + def current_screen(self) -> QtGui.QScreen: + """Get a frickin screen (if we can, gawd). + + """ + app = QtGui.QApplication.instance() + + for _ in range(3): + screen = app.screenAt(self.pos()) + print('trying to access QScreen...') + if screen is None: + time.sleep(0.5) + continue + + break + else: + if screen is None: + # try for the first one we can find + screen = app.screens()[0] + + assert screen, "Wow Qt is dumb as shit and has no screen..." + return screen + + +# singleton app per actor +_qt_win: QtGui.QMainWindow = None + + +def main_window() -> MainWindow: + 'Return the actor-global Qt window.' + + global _qt_win + assert _qt_win + return _qt_win diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 1535f458..9cb7d4d2 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -309,7 +309,15 @@ async def start_order_mode( chart: 'ChartPlotWidget', # noqa symbol: Symbol, brokername: str, + ) -> None: + '''Activate chart-trader order mode loop: + - connect to emsd + - load existing positions + - begin order handling loop + + ''' + done = chart.window().status_bar.open_status('Starting order mode...') # spawn EMS actor-service async with ( @@ -335,6 +343,7 @@ async def start_order_mode( return ohlc['index'][-1] # Begin order-response streaming + done() # this is where we receive **back** messages # about executions **from** the EMS actor diff --git a/setup.py b/setup.py index 4b8d8b1d..ebd034fa 100755 --- a/setup.py +++ b/setup.py @@ -70,17 +70,12 @@ setup( # UI 'PyQt5', 'pyqtgraph', - 'qdarkstyle==2.8.1', - #'kivy', see requirement.txt; using a custom branch atm - - # tsdbs - 'pymarketstore', - - #'kivy', see requirement.txt; using a custom branch atm - + 'qdarkstyle >= 3.0.2', # fuzzy search 'fuzzywuzzy[speedup]', + # tsdbs + 'pymarketstore', ], tests_require=['pytest'], python_requires=">=3.9", # literally for ``datetime.datetime.fromisoformat``...