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
parent
84f61c9a92
commit
d269edc0b3
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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_()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue