From 754a5cce4fb79939454bc5fa07b274ab13070aca Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 28 May 2021 09:40:19 -0400 Subject: [PATCH 01/21] WIP toying with a simple status bar --- piker/ui/_chart.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index a55202f3..e0e39beb 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -508,6 +508,8 @@ class ChartPlotWidget(pg.PlotWidget): def focus(self) -> None: # self.setFocus() self._vb.setFocus() + app = QtGui.QApplication.instance() + app.activeWindow().statusBar().showMessage("mode: view") def last_bar_in_view(self) -> int: self._ohlc[-1]['index'] @@ -1661,6 +1663,8 @@ async def _async_main( chart_app = widgets['main'] + + # attempt to configure DPI aware font size screen = current_screen() @@ -1675,6 +1679,10 @@ async def _async_main( # configure global DPI aware font size _font.configure_to_dpi(screen) + sbar = chart_app.window.statusBar() + sbar.setStyleSheet(f"font: {_font.px_size - 3}px") + sbar.showMessage('starting ze chartz...') + async with trio.open_nursery() as root_n: # set root nursery for spawning other charts/feeds @@ -1700,6 +1708,7 @@ async def _async_main( symbol, _, provider = sym.rpartition('.') # this internally starts a ``chart_symbol()`` task above + sbar.showMessage(f'loading {provider}.{symbol}...') chart_app.load_symbol(provider, symbol, loglevel) root_n.start_soon(load_providers, brokernames, loglevel) From 0564bbd9c3a9991b064f4167e42333ce56badb67 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 30 May 2021 08:44:02 -0400 Subject: [PATCH 02/21] Use focus switch signal to update status bar --- piker/ui/_exec.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py index 85488df8..8e15b3a8 100644 --- a/piker/ui/_exec.py +++ b/piker/ui/_exec.py @@ -124,6 +124,26 @@ class MainWindow(QtGui.QMainWindow): # 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': + return self.statusBar() + + def on_focus_change( + self, + old: QtGui.QWidget, + new: QtGui.QWidget, + ) -> None: + + log.debug(f'widget focus changed from {old} -> {new}') + + if new is None: + # cursor left window? + self.statusBar().showMessage('mode: none') + + else: + name = getattr(new, 'mode_name', '') + self.statusBar().showMessage(name) + def run_qtractor( func: Callable, @@ -192,6 +212,10 @@ def run_qtractor( # make window and exec window = window_type() + + # hook into app focus change events + app.focusChanged.connect(window.on_focus_change) + instance = main_widget() instance.window = window From 78e04e66a5a09867a6c6e6768f7a559f0575cd24 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 30 May 2021 08:45:40 -0400 Subject: [PATCH 03/21] Add mode names to search, repair LIFO ordering on select --- piker/ui/_search.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/piker/ui/_search.py b/piker/ui/_search.py index 3adb16a1..92e05e83 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,7 +155,9 @@ 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() @@ -424,6 +428,8 @@ class CompleterView(QTreeView): class SearchBar(QtWidgets.QLineEdit): + mode_name: str = 'mode: search' + def __init__( self, @@ -487,6 +493,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 +507,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 +626,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)), From 7da7dee02bf727cae4090ed477efd313d8c24484 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 30 May 2021 08:45:55 -0400 Subject: [PATCH 04/21] Add mode name to viewbox --- piker/ui/_interaction.py | 3 +++ 1 file changed, 3 insertions(+) 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, From cc60ad46fc8abcb3a011ef779fb6cb6bc6ff6686 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 30 May 2021 08:47:21 -0400 Subject: [PATCH 05/21] Skip chart updates if parent widget is hidden to save cpus --- piker/ui/_chart.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index e0e39beb..a06268e1 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -79,10 +79,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 +432,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? @@ -508,8 +511,6 @@ class ChartPlotWidget(pg.PlotWidget): def focus(self) -> None: # self.setFocus() self._vb.setFocus() - app = QtGui.QApplication.instance() - app.activeWindow().statusBar().showMessage("mode: view") def last_bar_in_view(self) -> int: self._ohlc[-1]['index'] @@ -1080,7 +1081,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() @@ -1389,10 +1397,15 @@ async def run_fsp( # 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 @@ -1679,7 +1692,7 @@ async def _async_main( # configure global DPI aware font size _font.configure_to_dpi(screen) - sbar = chart_app.window.statusBar() + sbar = chart_app.window.status_bar sbar.setStyleSheet(f"font: {_font.px_size - 3}px") sbar.showMessage('starting ze chartz...') From 74b63b9868448096fc904ab9d4d07ad4b2ff7005 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 31 May 2021 12:24:46 -0400 Subject: [PATCH 06/21] Add mode label to right side --- piker/ui/_exec.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py index 8e15b3a8..dec8a46d 100644 --- a/piker/ui/_exec.py +++ b/piker/ui/_exec.py @@ -40,7 +40,6 @@ from PyQt5.QtCore import ( import qdarkstyle # import qdarkgraystyle import trio -import tractor from outcome import Error from .._daemon import maybe_open_pikerd, _tractor_kwargs @@ -114,6 +113,29 @@ class MainWindow(QtGui.QMainWindow): self.setMinimumSize(*self.size) self.setWindowTitle(self.title) + self._status_label: QLabel = None + + @property + def status(self) -> QtGui.QLabel: + if not self._status_label: + # init mode label + from ._style import _font + self._status_label = label = QtGui.QLabel() #parent=self.status_bar) + label.setTextFormat(3) # markdown + label.setFont(_font.font) + label.setMargin(4) + label.setText("yo") + # label.show() + label.setAlignment( + QtCore.Qt.AlignVCenter + | QtCore.Qt.AlignRight + ) + self.status_bar.addPermanentWidget(label) + label.show() + + return self._status_label + + def closeEvent( self, event: QtGui.QCloseEvent, @@ -136,13 +158,12 @@ class MainWindow(QtGui.QMainWindow): log.debug(f'widget focus changed from {old} -> {new}') - if new is None: - # cursor left window? - self.statusBar().showMessage('mode: none') - - else: + if new is not None: + # # cursor left window? + # self.statusBar().showMessage('mode: none') name = getattr(new, 'mode_name', '') - self.statusBar().showMessage(name) + self.status.setText(name) + # self.statusBar().showMessage(name) def run_qtractor( From a9cdb94ff34c8b0127490efafa2e6d7135498173 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 31 May 2021 19:45:30 -0400 Subject: [PATCH 07/21] Port styling to latest `qtdarkstyle` version --- piker/ui/_style.py | 15 +++++++++------ setup.py | 11 +++-------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index 322247ec..b8b40686 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -22,7 +22,7 @@ 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 @@ -134,8 +134,6 @@ _font = DpiAwareFont() # 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 +173,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 +184,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/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``... From df50700aa5c235c4fe3f0fc0d09b6b7226f1103a Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 31 May 2021 19:47:39 -0400 Subject: [PATCH 08/21] Style a basic status bar and add a mode label to it --- piker/ui/_exec.py | 61 +++++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py index dec8a46d..203ff221 100644 --- a/piker/ui/_exec.py +++ b/piker/ui/_exec.py @@ -32,12 +32,14 @@ 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 from outcome import Error @@ -69,12 +71,9 @@ def current_screen() -> QtGui.QScreen: """ 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....') + print('trying to access QScreen...') if screen is None: time.sleep(0.5) continue @@ -113,19 +112,23 @@ class MainWindow(QtGui.QMainWindow): self.setMinimumSize(*self.size) self.setWindowTitle(self.title) + self._status_bar: QStatusbar = None self._status_label: QLabel = None @property - def status(self) -> QtGui.QLabel: + def mode_label(self) -> QtGui.QLabel: + + # init mode label if not self._status_label: - # init mode label - from ._style import _font - self._status_label = label = QtGui.QLabel() #parent=self.status_bar) + # TODO: i guess refactor stuff to avoid having to import here? + from ._style import _font, hcolor + self._status_label = label = QtGui.QLabel() + label.setStyleSheet( + f"QLabel {{ color : {hcolor('gunmetal')}; }}" + ) label.setTextFormat(3) # markdown label.setFont(_font.font) - label.setMargin(4) - label.setText("yo") - # label.show() + label.setMargin(2) label.setAlignment( QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight @@ -133,8 +136,8 @@ class MainWindow(QtGui.QMainWindow): self.status_bar.addPermanentWidget(label) label.show() - return self._status_label + return self._status_label def closeEvent( self, @@ -147,8 +150,25 @@ class MainWindow(QtGui.QMainWindow): os.kill(os.getpid(), signal.SIGINT) @property - def status_bar(self) -> 'QStatusBar': - return self.statusBar() + 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, hcolor + sb = self.statusBar() + sb.setStyleSheet(( + f"color : {hcolor('gunmetal')};" + f"background : {hcolor('default_dark')};" + # "min-height : 19px;" + f"font-size : {_font.px_size - 4}px;" + "padding : 0px;" + # "qproperty-alignment: AlignVCenter;" + )) + self.setStatusBar(sb) + self._status_bar = sb + + return self._status_bar def on_focus_change( self, @@ -159,11 +179,9 @@ class MainWindow(QtGui.QMainWindow): log.debug(f'widget focus changed from {old} -> {new}') if new is not None: - # # cursor left window? - # self.statusBar().showMessage('mode: none') + # cursor left window? name = getattr(new, 'mode_name', '') - self.status.setText(name) - # self.statusBar().showMessage(name) + self.mode_label.setText(name) def run_qtractor( @@ -229,7 +247,11 @@ 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 window = window_type() @@ -245,7 +267,6 @@ def run_qtractor( 'main': instance, } - # override tractor's defaults tractor_kwargs.update(_tractor_kwargs) From bb1c549d9412dec3e89a4e23ca125bc257b068d9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 31 May 2021 19:48:11 -0400 Subject: [PATCH 09/21] Add some initial status updates during startup --- piker/ui/_chart.py | 20 +++++++++++++++++--- piker/ui/order_mode.py | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index a06268e1..0d118b58 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -51,6 +51,7 @@ from ._graphics._lines import ( from ._l1 import L1Labels from ._graphics._ohlc import BarItems from ._graphics._curve import FastAppendCurve +from . import _style from ._style import ( _font, hcolor, @@ -192,6 +193,8 @@ class ChartSpace(QtGui.QWidget): symbol_key, loglevel, ) + self.window.status_bar.showMessage( + f'Loading {symbol_key}.{providername}...') self.set_chart_symbol(fqsn, linkedcharts) @@ -1241,6 +1244,9 @@ async def spawn_fsps( display_name = f'fsp.{fsp_func_name}' + linked_charts.window().status_bar.showMessage( + f'Loading FSP: {display_name}...') + # TODO: load function here and introspect # return stream type(s) @@ -1281,6 +1287,10 @@ async def spawn_fsps( conf, ) + status = linked_charts.window().status_bar + if display_name in status.currentMessage(): + status.clearMessage() + # blocks here until all fsp actors complete @@ -1541,6 +1551,9 @@ async def chart_symbol( add_label=False, ) + chart_app.window.status_bar.showMessage( + f'Finished loading {sym}.{brokername}') + # size view to data once at outset chart._set_yrange() @@ -1619,6 +1632,7 @@ async def chart_symbol( # linked_charts, # ) + linked_charts.window().status_bar.showMessage('Starting order mode...') await start_order_mode(chart, symbol, brokername) @@ -1676,8 +1690,6 @@ async def _async_main( chart_app = widgets['main'] - - # attempt to configure DPI aware font size screen = current_screen() @@ -1692,8 +1704,10 @@ async def _async_main( # 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 - sbar.setStyleSheet(f"font: {_font.px_size - 3}px") sbar.showMessage('starting ze chartz...') async with trio.open_nursery() as root_n: diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 1535f458..39181078 100644 --- a/piker/ui/order_mode.py +++ b/piker/ui/order_mode.py @@ -336,6 +336,8 @@ async def start_order_mode( # Begin order-response streaming + chart.window().status_bar.showMessage('Ready for trading') + # this is where we receive **back** messages # about executions **from** the EMS actor async for msg in trades_stream: From 193f4f945c52e1036f66e47e967e17d04b00decb Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 3 Jun 2021 08:53:12 -0400 Subject: [PATCH 10/21] Flip to using scaled hi DPI detected by Qt --- piker/ui/_style.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index b8b40686..704dd862 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -32,8 +32,10 @@ log = get_logger(__name__) # chart-wide fonts specified in inches _font_sizes: Dict[str, Dict[str, float]] = { 'hi': { - 'default': 0.0616, - 'small': 0.055, + # 'default': 0.0616, + 'default': 0.0616 * 2, + # 'small': 0.055, + 'small': 6/66 * (1 + 6/16), }, 'lo': { 'default': 6.5 / 64, @@ -95,7 +97,7 @@ class DpiAwareFont: # take the max since scaling can make things ugly in some cases pdpi = screen.physicalDotsPerInch() ldpi = screen.logicalDotsPerInch() - dpi = max(pdpi, ldpi) + dpi = min(pdpi, ldpi) # for low dpi scale everything down if dpi <= 97: @@ -105,8 +107,8 @@ class DpiAwareFont: 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"\nbest font size is {font_size} for min dpi {dpi}\n" ) self._set_qfont_px_size(font_size) From 62c456f8eb7257c5336ff12cac39b24b09661dc9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 4 Jun 2021 11:29:12 -0400 Subject: [PATCH 11/21] Downscale fonts on scaled-down hidpi displays --- piker/ui/_style.py | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index 704dd862..eb5d5d47 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -29,13 +29,15 @@ 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, - 'default': 0.0616 * 2, + 'default': _magic_inches, # 'small': 0.055, - 'small': 6/66 * (1 + 6/16), + 'small': 0.9 * _magic_inches, }, 'lo': { 'default': 6.5 / 64, @@ -45,6 +47,7 @@ _font_sizes: Dict[str, Dict[str, float]] = { class DpiAwareFont: + def __init__( self, # TODO: move to config @@ -54,10 +57,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: @@ -97,22 +100,30 @@ class DpiAwareFont: # take the max since scaling can make things ugly in some cases pdpi = screen.physicalDotsPerInch() ldpi = screen.logicalDotsPerInch() - dpi = min(pdpi, ldpi) + mx_dpi = max(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] - font_size = math.floor(inches * 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 *= scale / 2.5 + + self._font_inches = inches + + font_size = math.floor(inches * mx_dpi) log.info( f"\nscreen:{screen.name()} with pDPI: {pdpi}, lDPI: {ldpi}" - f"\nbest font size is {font_size} for min dpi {dpi}\n" + 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: @@ -132,6 +143,14 @@ class DpiAwareFont: # use inches size to be cross-resolution compatible? _font = DpiAwareFont() +_font_small = DpiAwareFont(font_size='small') + + +def _config_fonts_to_screen() -> None: + 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 From e924cbeb43f64107903ddffc0b3b2f192bbd405d Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 4 Jun 2021 11:29:44 -0400 Subject: [PATCH 12/21] Min debounce period for faster cached completions --- piker/ui/_search.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/piker/ui/_search.py b/piker/ui/_search.py index 92e05e83..cecfd7ef 100644 --- a/piker/ui/_search.py +++ b/piker/ui/_search.py @@ -163,7 +163,6 @@ class CompleterView(QTreeView): search.focus() def set_font_size(self, size: int = 18): - # dpi_px_size = _font.px_size # print(size) if size < 0: size = 16 @@ -702,8 +701,8 @@ 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_time: float = 6/16, # max pause required before slow relay ) -> None: """Task to search through providers and fill in possible @@ -752,7 +751,7 @@ async def fill_results( _search_active = trio.Event() break - if repeats > 2 and period >= max_pause_time: + if repeats > 2 and period > max_pause_time: _search_active = trio.Event() repeats = 0 break @@ -775,13 +774,17 @@ async def fill_results( for provider, (search, pause) in ( _searcher_cache.copy().items() ): - + # 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..') - # 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 ): @@ -796,6 +799,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, @@ -962,7 +973,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: From 112b3f0f073c9296572badf5f2ea71a7786cc4e0 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 4 Jun 2021 11:30:15 -0400 Subject: [PATCH 13/21] Drop hidpi font size hacking from contents labels --- piker/ui/_graphics/_cursor.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) 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) From f68295653dd8b8e4108f4f860ec0cddee2cd0c0a Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 4 Jun 2021 11:31:00 -0400 Subject: [PATCH 14/21] Load cache search before other providers --- piker/ui/_chart.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 0d118b58..5f56659a 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -53,7 +53,7 @@ from ._graphics._ohlc import BarItems from ._graphics._curve import FastAppendCurve from . import _style from ._style import ( - _font, + _config_fonts_to_screen, hcolor, CHART_MARGINS, _xaxis_at, @@ -1006,7 +1006,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( @@ -1702,7 +1702,7 @@ 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) + _config_fonts_to_screen() # TODO: do styling / themeing setup # _style.style_ze_sheets(chart_app) @@ -1738,8 +1738,6 @@ async def _async_main( sbar.showMessage(f'loading {provider}.{symbol}...') 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( @@ -1748,8 +1746,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, From 9d31f8ca6a8b133394ce135396e74c00ef8fb7dd Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 4 Jun 2021 11:31:49 -0400 Subject: [PATCH 15/21] Add default "small" dpi aware font --- piker/ui/_exec.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py index 203ff221..b5f65ce2 100644 --- a/piker/ui/_exec.py +++ b/piker/ui/_exec.py @@ -112,7 +112,7 @@ class MainWindow(QtGui.QMainWindow): self.setMinimumSize(*self.size) self.setWindowTitle(self.title) - self._status_bar: QStatusbar = None + self._status_bar: QStatusBar = None self._status_label: QLabel = None @property @@ -121,13 +121,13 @@ class MainWindow(QtGui.QMainWindow): # init mode label if not self._status_label: # TODO: i guess refactor stuff to avoid having to import here? - from ._style import _font, hcolor + 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.font) + label.setFont(_font_small.font) label.setMargin(2) label.setAlignment( QtCore.Qt.AlignVCenter @@ -136,7 +136,6 @@ class MainWindow(QtGui.QMainWindow): self.status_bar.addPermanentWidget(label) label.show() - return self._status_label def closeEvent( @@ -155,14 +154,14 @@ class MainWindow(QtGui.QMainWindow): # 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, hcolor + from ._style import _font_small, hcolor sb = self.statusBar() sb.setStyleSheet(( f"color : {hcolor('gunmetal')};" f"background : {hcolor('default_dark')};" - # "min-height : 19px;" - f"font-size : {_font.px_size - 4}px;" + f"font-size : {_font_small.px_size}px;" "padding : 0px;" + # "min-height : 19px;" # "qproperty-alignment: AlignVCenter;" )) self.setStatusBar(sb) From ce67022e2e561e06a65811442f7691f56cb52bf8 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 8 Jun 2021 14:20:43 -0400 Subject: [PATCH 16/21] Only add font size scaled down when dpi scaling > 2 --- piker/ui/_style.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index eb5d5d47..87d9031b 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -101,6 +101,7 @@ class DpiAwareFont: pdpi = screen.physicalDotsPerInch() ldpi = screen.logicalDotsPerInch() mx_dpi = max(pdpi, ldpi) + mn_dpi = min(pdpi, ldpi) scale = round(ldpi/pdpi) if mx_dpi <= 97: # for low dpi use larger font sizes @@ -109,15 +110,18 @@ class DpiAwareFont: 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 *= scale / 2.5 + inches = inches * (1 / scale) * (1 + 6/16) + dpi = mx_dpi self._font_inches = inches - font_size = math.floor(inches * mx_dpi) + font_size = math.floor(inches * dpi) log.info( f"\nscreen:{screen.name()} with pDPI: {pdpi}, lDPI: {ldpi}" f"\nOur best guess font size is {font_size}\n" From 90b06730528e3433298b58c3a5f90bc54b079e13 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 10 Jun 2021 11:30:57 -0400 Subject: [PATCH 17/21] Add multi-status support to window --- piker/ui/_exec.py | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py index b5f65ce2..03874f49 100644 --- a/piker/ui/_exec.py +++ b/piker/ui/_exec.py @@ -102,6 +102,39 @@ 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) @@ -133,7 +166,7 @@ class MainWindow(QtGui.QMainWindow): QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight ) - self.status_bar.addPermanentWidget(label) + self.statusBar().addPermanentWidget(label) label.show() return self._status_label @@ -165,7 +198,7 @@ class MainWindow(QtGui.QMainWindow): # "qproperty-alignment: AlignVCenter;" )) self.setStatusBar(sb) - self._status_bar = sb + self._status_bar = MultiStatus(sb, []) return self._status_bar @@ -261,11 +294,6 @@ def run_qtractor( instance = main_widget() instance.window = window - widgets = { - 'window': window, - 'main': instance, - } - # override tractor's defaults tractor_kwargs.update(_tractor_kwargs) @@ -275,7 +303,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( From 0dcadec11a66de84fad2fd080314644f770d09b4 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 10 Jun 2021 11:31:40 -0400 Subject: [PATCH 18/21] Add multi-status updates throughout chart init --- piker/ui/_chart.py | 40 ++++++++++++++++++---------------------- piker/ui/order_mode.py | 11 +++++++++-- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 5f56659a..7d2eff79 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -51,7 +51,6 @@ from ._graphics._lines import ( from ._l1 import L1Labels from ._graphics._ohlc import BarItems from ._graphics._curve import FastAppendCurve -from . import _style from ._style import ( _config_fonts_to_screen, hcolor, @@ -193,8 +192,6 @@ class ChartSpace(QtGui.QWidget): symbol_key, loglevel, ) - self.window.status_bar.showMessage( - f'Loading {symbol_key}.{providername}...') self.set_chart_symbol(fqsn, linkedcharts) @@ -1244,9 +1241,6 @@ async def spawn_fsps( display_name = f'fsp.{fsp_func_name}' - linked_charts.window().status_bar.showMessage( - f'Loading FSP: {display_name}...') - # TODO: load function here and introspect # return stream type(s) @@ -1287,10 +1281,6 @@ async def spawn_fsps( conf, ) - status = linked_charts.window().status_bar - if display_name in status.currentMessage(): - status.clearMessage() - # blocks here until all fsp actors complete @@ -1311,6 +1301,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 @@ -1404,6 +1397,8 @@ async def run_fsp( last = time.time() + done() + # update chart graphics async for value in stream: @@ -1504,7 +1499,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: @@ -1514,11 +1509,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: @@ -1551,8 +1549,7 @@ async def chart_symbol( add_label=False, ) - chart_app.window.status_bar.showMessage( - f'Finished loading {sym}.{brokername}') + loading_sym_done() # size view to data once at outset chart._set_yrange() @@ -1632,8 +1629,7 @@ async def chart_symbol( # linked_charts, # ) - linked_charts.window().status_bar.showMessage('Starting order mode...') - await start_order_mode(chart, symbol, brokername) + await start_order_mode(chart, symbol, provider) async def load_providers( @@ -1650,7 +1646,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( @@ -1674,7 +1670,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, @@ -1688,7 +1684,7 @@ async def _async_main( """ - chart_app = widgets['main'] + chart_app = main_widget # attempt to configure DPI aware font size screen = current_screen() @@ -1708,7 +1704,7 @@ async def _async_main( # _style.style_ze_sheets(chart_app) sbar = chart_app.window.status_bar - sbar.showMessage('starting ze chartz...') + starting_done = sbar.open_status('starting ze chartz...') async with trio.open_nursery() as root_n: @@ -1735,7 +1731,6 @@ async def _async_main( symbol, _, provider = sym.rpartition('.') # this internally starts a ``chart_symbol()`` task above - sbar.showMessage(f'loading {provider}.{symbol}...') chart_app.load_symbol(provider, symbol, loglevel) # spin up a search engine for the local cached symbol set @@ -1767,6 +1762,7 @@ async def _async_main( key_stream, ) + starting_done() await trio.sleep_forever() diff --git a/piker/ui/order_mode.py b/piker/ui/order_mode.py index 39181078..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,8 +343,7 @@ async def start_order_mode( return ohlc['index'][-1] # Begin order-response streaming - - chart.window().status_bar.showMessage('Ready for trading') + done() # this is where we receive **back** messages # about executions **from** the EMS actor From 84f61c9a92cec5ad4311214925601340aafc54b1 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 10 Jun 2021 11:56:41 -0400 Subject: [PATCH 19/21] Avoid clearing cached results; detect repeats later --- piker/ui/_search.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/piker/ui/_search.py b/piker/ui/_search.py index cecfd7ef..4360fd5b 100644 --- a/piker/ui/_search.py +++ b/piker/ui/_search.py @@ -702,7 +702,9 @@ async def fill_results( # kb debouncing pauses (bracket defaults) min_pause_time: float = 0.01, # absolute min typing throttle - max_pause_time: float = 6/16, # max pause required before slow relay + + # max pause required before slow relay + max_pause_time: float = 6/16 + 0.001, ) -> None: """Task to search through providers and fill in possible @@ -751,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 @@ -763,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 @@ -774,20 +770,22 @@ async def fill_results( for provider, (search, pause) in ( _searcher_cache.copy().items() ): - # 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..') - # 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). + # 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, @@ -815,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() From d269edc0b37deb3a0c61217acaee2e64785b8a21 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 13 Jun 2021 23:47:52 -0400 Subject: [PATCH 20/21] 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. --- piker/ui/_chart.py | 8 +- piker/ui/_exec.py | 171 ++++------------------------------------- piker/ui/_style.py | 9 ++- piker/ui/_window.py | 180 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 165 deletions(-) create mode 100644 piker/ui/_window.py diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 7d2eff79..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 ( - _config_fonts_to_screen, 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 @@ -1687,7 +1686,7 @@ async def _async_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 @@ -1697,9 +1696,6 @@ 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 03874f49..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,7 +29,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, @@ -47,6 +44,7 @@ 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__) @@ -60,34 +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 - - 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. @@ -102,126 +72,12 @@ 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 = MainWindow, + window_type: QtGui.QMainWindow = None, ) -> None: # avoids annoying message when entering debugger from qt loop pyqtRemoveInputHook() @@ -239,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 @@ -286,8 +138,20 @@ 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) @@ -316,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/_style.py b/piker/ui/_style.py index 87d9031b..83daaa79 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -25,7 +25,6 @@ from PyQt5 import QtCore, QtGui from qdarkstyle import DarkPalette from ..log import get_logger -from ._exec import current_screen log = get_logger(__name__) @@ -69,13 +68,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 @@ -151,6 +152,8 @@ _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 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 From f320f952880b2ecc63c284aaa372b92a008ecc9b Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 13 Jun 2021 23:59:02 -0400 Subject: [PATCH 21/21] Drop old hi-dpi font inches sizes --- piker/ui/_style.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index 83daaa79..6e89d8b6 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -33,9 +33,7 @@ _magic_inches = 0.0666 * (1 + 6/16) # chart-wide fonts specified in inches _font_sizes: Dict[str, Dict[str, float]] = { 'hi': { - # 'default': 0.0616, 'default': _magic_inches, - # 'small': 0.055, 'small': 0.9 * _magic_inches, }, 'lo': {