diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 838da304..7d2eff79 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -52,6 +52,7 @@ from ._l1 import L1Labels from ._graphics._ohlc import BarItems from ._graphics._curve import FastAppendCurve from ._style import ( + _config_fonts_to_screen, hcolor, CHART_MARGINS, _xaxis_at, @@ -67,7 +68,7 @@ from ..data import maybe_open_shm_array from .. import brokers from .. import data from ..log import get_logger -from ._exec import run_qtractor +from ._exec import run_qtractor, current_screen from ._interaction import ChartView from .order_mode import start_order_mode from .. import fsp @@ -1686,7 +1687,7 @@ async def _async_main( chart_app = main_widget # attempt to configure DPI aware font size - screen = chart_app.window.current_screen() + screen = current_screen() # configure graphics update throttling based on display refresh rate global _clear_throttle_rate @@ -1696,6 +1697,9 @@ async def _async_main( ) log.info(f'Set graphics update rate to {_clear_throttle_rate} Hz') + # configure global DPI aware font size + _config_fonts_to_screen() + # TODO: do styling / themeing setup # _style.style_ze_sheets(chart_app) diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py index 3b72e8b0..03874f49 100644 --- a/piker/ui/_exec.py +++ b/piker/ui/_exec.py @@ -21,7 +21,10 @@ 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 @@ -29,7 +32,7 @@ import PyQt5 # noqa import pyqtgraph as pg from pyqtgraph import QtGui from PyQt5 import QtCore -# from PyQt5.QtGui import QLabel, QStatusBar +from PyQt5.QtGui import QLabel, QStatusBar from PyQt5.QtCore import ( pyqtRemoveInputHook, Qt, @@ -44,7 +47,6 @@ 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__) @@ -58,6 +60,34 @@ 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 + + for _ in range(3): + screen = _qt_app.screenAt(_qt_win.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 = _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. @@ -72,12 +102,126 @@ if platform.system() == "Windows": QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) +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: + # TODO: i guess refactor stuff to avoid having to import here? + from ._style import _font_small, hcolor + 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: + # TODO: i guess refactor stuff to avoid having to import here? + from ._style import _font_small, hcolor + 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 run_qtractor( func: Callable, args: Tuple, main_widget: QtGui.QWidget, tractor_kwargs: Dict[str, Any] = {}, - window_type: QtGui.QMainWindow = None, + window_type: QtGui.QMainWindow = MainWindow, ) -> None: # avoids annoying message when entering debugger from qt loop pyqtRemoveInputHook() @@ -95,6 +239,10 @@ 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 @@ -138,20 +286,8 @@ def run_qtractor( 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) @@ -180,6 +316,11 @@ 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/_style.py b/piker/ui/_style.py index 6e89d8b6..e5e2bab9 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -25,6 +25,7 @@ from PyQt5 import QtCore, QtGui from qdarkstyle import DarkPalette from ..log import get_logger +from ._exec import current_screen log = get_logger(__name__) @@ -66,15 +67,13 @@ 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 = main_window().current_screen() + self._screen = current_screen() else: - self._screen = main_window().current_screen() + self._screen = current_screen() return self._screen @@ -150,8 +149,6 @@ _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() diff --git a/piker/ui/_window.py b/piker/ui/_window.py deleted file mode 100644 index 60210160..00000000 --- a/piker/ui/_window.py +++ /dev/null @@ -1,180 +0,0 @@ -# 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