Merge pull request #189 from pikers/status_bar

Basic status bar
msgspec_fixes
goodboy 2021-06-14 00:01:01 -04:00 committed by GitHub
commit e52ecbe589
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 375 additions and 158 deletions

View File

@ -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()

View File

@ -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_()

View File

@ -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)

View File

@ -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,

View File

@ -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:

View File

@ -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

180
piker/ui/_window.py 100644
View File

@ -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 <https://www.gnu.org/licenses/>.
"""
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

View File

@ -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

View File

@ -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``...