Re-org main window singleton into a new module

Avoids some cyclical and confusing import time stuff that we needed to get
DPI aware fonts configured from the active display. Move the main window
singleton into its own module and add a `main_window()` getter for it.
Make `current_screen()` a ``MainWindow` method to avoid so many module
variables.
status_bar
Tyler Goodlet 2021-06-13 23:47:52 -04:00
parent 84f61c9a92
commit d269edc0b3
4 changed files with 203 additions and 165 deletions

View File

@ -52,7 +52,6 @@ from ._l1 import L1Labels
from ._graphics._ohlc import BarItems from ._graphics._ohlc import BarItems
from ._graphics._curve import FastAppendCurve from ._graphics._curve import FastAppendCurve
from ._style import ( from ._style import (
_config_fonts_to_screen,
hcolor, hcolor,
CHART_MARGINS, CHART_MARGINS,
_xaxis_at, _xaxis_at,
@ -68,7 +67,7 @@ from ..data import maybe_open_shm_array
from .. import brokers from .. import brokers
from .. import data from .. import data
from ..log import get_logger from ..log import get_logger
from ._exec import run_qtractor, current_screen from ._exec import run_qtractor
from ._interaction import ChartView from ._interaction import ChartView
from .order_mode import start_order_mode from .order_mode import start_order_mode
from .. import fsp from .. import fsp
@ -1687,7 +1686,7 @@ async def _async_main(
chart_app = main_widget chart_app = main_widget
# attempt to configure DPI aware font size # 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 # configure graphics update throttling based on display refresh rate
global _clear_throttle_rate global _clear_throttle_rate
@ -1697,9 +1696,6 @@ async def _async_main(
) )
log.info(f'Set graphics update rate to {_clear_throttle_rate} Hz') 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 # TODO: do styling / themeing setup
# _style.style_ze_sheets(chart_app) # _style.style_ze_sheets(chart_app)

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. All global Qt runtime settings are mostly defined here.
""" """
from typing import Tuple, Callable, Dict, Any from typing import Tuple, Callable, Dict, Any
import os
import platform import platform
import signal
import time
import traceback import traceback
# Qt specific # Qt specific
@ -32,7 +29,7 @@ import PyQt5 # noqa
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph import QtGui from pyqtgraph import QtGui
from PyQt5 import QtCore from PyQt5 import QtCore
from PyQt5.QtGui import QLabel, QStatusBar # from PyQt5.QtGui import QLabel, QStatusBar
from PyQt5.QtCore import ( from PyQt5.QtCore import (
pyqtRemoveInputHook, pyqtRemoveInputHook,
Qt, Qt,
@ -47,6 +44,7 @@ from outcome import Error
from .._daemon import maybe_open_pikerd, _tractor_kwargs from .._daemon import maybe_open_pikerd, _tractor_kwargs
from ..log import get_logger from ..log import get_logger
from ._pg_overrides import _do_overrides from ._pg_overrides import _do_overrides
from . import _style
log = get_logger(__name__) log = get_logger(__name__)
@ -60,34 +58,6 @@ pg.enableExperimental = True
_do_overrides() _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: # XXX: pretty sure none of this shit works on linux as per:
# https://bugreports.qt.io/browse/QTBUG-53022 # https://bugreports.qt.io/browse/QTBUG-53022
# it seems to work on windows.. no idea wtf is up. # it seems to work on windows.. no idea wtf is up.
@ -102,126 +72,12 @@ if platform.system() == "Windows":
QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) 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( def run_qtractor(
func: Callable, func: Callable,
args: Tuple, args: Tuple,
main_widget: QtGui.QWidget, main_widget: QtGui.QWidget,
tractor_kwargs: Dict[str, Any] = {}, tractor_kwargs: Dict[str, Any] = {},
window_type: QtGui.QMainWindow = MainWindow, window_type: QtGui.QMainWindow = None,
) -> None: ) -> None:
# avoids annoying message when entering debugger from qt loop # avoids annoying message when entering debugger from qt loop
pyqtRemoveInputHook() pyqtRemoveInputHook()
@ -239,10 +95,6 @@ def run_qtractor(
# XXX: lmfao, this is how you disable text edit cursor blinking..smh # XXX: lmfao, this is how you disable text edit cursor blinking..smh
app.setCursorFlashTime(0) app.setCursorFlashTime(0)
# set global app singleton
global _qt_app
_qt_app = app
# This code is from Nathaniel, and I quote: # This code is from Nathaniel, and I quote:
# "This is substantially faster than using a signal... for some # "This is substantially faster than using a signal... for some
# reason Qt signal dispatch is really slow (and relies on events # reason Qt signal dispatch is really slow (and relies on events
@ -286,8 +138,20 @@ def run_qtractor(
app.setStyleSheet(stylesheet) app.setStyleSheet(stylesheet)
# make window and exec # make window and exec
from . import _window
if window_type is None:
window_type = _window.MainWindow
window = window_type() 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 # hook into app focus change events
app.focusChanged.connect(window.on_focus_change) app.focusChanged.connect(window.on_focus_change)
@ -316,11 +180,6 @@ def run_qtractor(
window.main_widget = main_widget window.main_widget = main_widget
window.setCentralWidget(instance) window.setCentralWidget(instance)
# store global ref
# set global app singleton
global _qt_win
_qt_win = window
# actually render to screen # actually render to screen
window.show() window.show()
app.exec_() app.exec_()

View File

@ -25,7 +25,6 @@ from PyQt5 import QtCore, QtGui
from qdarkstyle import DarkPalette from qdarkstyle import DarkPalette
from ..log import get_logger from ..log import get_logger
from ._exec import current_screen
log = get_logger(__name__) log = get_logger(__name__)
@ -69,13 +68,15 @@ class DpiAwareFont:
@property @property
def screen(self) -> QtGui.QScreen: def screen(self) -> QtGui.QScreen:
from ._window import main_window
if self._screen is not None: if self._screen is not None:
try: try:
self._screen.refreshRate() self._screen.refreshRate()
except RuntimeError: except RuntimeError:
self._screen = current_screen() self._screen = main_window().current_screen()
else: else:
self._screen = current_screen() self._screen = main_window().current_screen()
return self._screen return self._screen
@ -151,6 +152,8 @@ _font_small = DpiAwareFont(font_size='small')
def _config_fonts_to_screen() -> None: def _config_fonts_to_screen() -> None:
'configure global DPI aware font sizes'
global _font, _font_small global _font, _font_small
_font.configure_to_dpi() _font.configure_to_dpi()
_font_small.configure_to_dpi() _font_small.configure_to_dpi()

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