From 9b5f92f818975d34223a28c8551ae68cf85bea72 Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 11 Mar 2026 19:07:38 -0400 Subject: [PATCH] Improve styling and logging for UI font-size zoom Refine zoom methods in `MainWindow` and font helpers in `_style` to return `px_size` up the call chain and log detailed zoom state on each change. Deats, - make `_set_qfont_px_size()` return `self.px_size`. - make `configure_to_dpi()` and `_config_fonts_to_screen()` return the new `px_size` up through the call chain. - add `font_size` to `log.info()` in `zoom_in()`, `zoom_out()`, and `reset_zoom()` alongside `zoom_step` and `zoom_level(%)`. - reformat `has_ctrl`/`_has_shift` bitwise checks and key-match tuples to multiline style. - comment out `Shift` modifier requirement for zoom hotkeys (now `Ctrl`-only). - comment out unused `mn_dpi` and `dpi` locals. Also, - convert all single-line docstrings to `'''` multiline style across zoom and font methods. - rewrap `configure_to_dpi()` docstring to 67 chars. - move `from . import _style` to module-level import in `_window.py`. - drop unused `screen` binding in `boundingRect()`. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- piker/ui/_style.py | 41 +++++--- piker/ui/_window.py | 238 +++++++++++++++++++++++++++++--------------- 2 files changed, 186 insertions(+), 93 deletions(-) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index f310dc34..e1da8b7d 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -79,9 +79,13 @@ class DpiAwareFont: self._font_inches: float = None self._screen = None - def _set_qfont_px_size(self, px_size: int) -> None: + def _set_qfont_px_size( + self, + px_size: int, + ) -> int: self._qfont.setPixelSize(int(px_size)) self._qfm = QtGui.QFontMetrics(self._qfont) + return self.px_size @property def screen(self) -> QtGui.QScreen: @@ -128,17 +132,18 @@ class DpiAwareFont: self, screen: QtGui.QScreen | None = None, zoom_level: float = 1.0, - ): + ) -> int: ''' - Set an appropriately sized font size depending on the screen DPI. + Set an appropriately sized font size depending on the screen DPI + or scale the size according to `zoom_level`. - If we end up needing to generalize this more here there are resources - listed in the script in ``snippets/qt_screen_info.py``. + If we end up needing to generalize this more here there are + resources listed in the script in + ``snippets/qt_screen_info.py``. ''' if self._font_size is not None: - self._set_qfont_px_size(self._font_size * zoom_level) - return + return self._set_qfont_px_size(self._font_size * zoom_level) # NOTE: if no font size set either in the [ui] section of the # config or not yet computed from our magic scaling calcs, @@ -157,7 +162,7 @@ class DpiAwareFont: ldpi = pdpi mx_dpi = max(pdpi, ldpi) - mn_dpi = min(pdpi, ldpi) + # mn_dpi = min(pdpi, ldpi) scale = round(ldpi/pdpi, ndigits=2) if mx_dpi <= 97: # for low dpi use larger font sizes @@ -166,7 +171,7 @@ class DpiAwareFont: else: # hidpi use smaller font sizes inches = _font_sizes['hi'][self._font_size_calc_key] - dpi = mn_dpi + # dpi = mn_dpi mult = 1.0 @@ -213,10 +218,10 @@ class DpiAwareFont: f"\nOur best guess font size is {font_size}\n" ) # apply the size - self._set_qfont_px_size(font_size) + return self._set_qfont_px_size(font_size) def boundingRect(self, value: str) -> QtCore.QRectF: - if (screen := self.screen) is None: + if self.screen is None: raise RuntimeError("You must call .configure_to_dpi() first!") unscaled_br: QtCore.QRectF = self._qfm.boundingRect(value) @@ -233,12 +238,22 @@ _font = DpiAwareFont() _font_small = DpiAwareFont(_font_size_key='small') -def _config_fonts_to_screen(zoom_level: float = 1.0) -> None: - 'configure global DPI aware font sizes' +def _config_fonts_to_screen( + zoom_level: float = 1.0 +) -> int: + ''' + Configure global DPI aware font size(s). + If `zoom_level` is provided we apply it to auto-calculated + DPI-aware font. + + Return the new `DpiAwareFont.px_size`. + + ''' global _font, _font_small _font.configure_to_dpi(zoom_level=zoom_level) _font_small.configure_to_dpi(zoom_level=zoom_level) + return _font.px_size def get_fonts() -> tuple[ diff --git a/piker/ui/_window.py b/piker/ui/_window.py index 6253a655..d25f6d4d 100644 --- a/piker/ui/_window.py +++ b/piker/ui/_window.py @@ -18,7 +18,6 @@ Qt main window singletons and stuff. """ - from __future__ import annotations import os import signal @@ -44,7 +43,11 @@ from piker.ui.qt import ( QObject, ) from ..log import get_logger -from ._style import _font_small, hcolor +from . import _style +from ._style import ( + _font_small, + hcolor, +) from ._widget import GodWidget @@ -52,7 +55,7 @@ log = get_logger(__name__) class GlobalZoomEventFilter(QObject): - """ + ''' Application-level event filter for global UI zoom shortcuts. This filter intercepts keyboard events BEFORE they reach widgets, @@ -64,18 +67,18 @@ class GlobalZoomEventFilter(QObject): - Ctrl+Shift+Minus: Zoom out - Ctrl+Shift+0: Reset zoom - """ - + ''' def __init__(self, main_window: MainWindow): super().__init__() self.main_window = main_window def eventFilter(self, obj: QObject, event: QEvent) -> bool: - """ + ''' Filter keyboard events for global zoom shortcuts. Returns True to filter out (consume) the event, False to pass through. - """ + + ''' if event.type() == QEvent.Type.KeyPress: key = event.key() mods = event.modifiers() @@ -84,28 +87,49 @@ class GlobalZoomEventFilter(QObject): mods = mods & ~Qt.KeyboardModifier.KeypadModifier # Check if we have Ctrl+Shift (both required) - has_ctrl = bool(mods & Qt.KeyboardModifier.ControlModifier) - has_shift = bool(mods & Qt.KeyboardModifier.ShiftModifier) + has_ctrl = bool( + mods + & + Qt.KeyboardModifier.ControlModifier + ) + _has_shift = bool( + mods + & + Qt.KeyboardModifier.ShiftModifier + ) # Only handle UI zoom if BOTH Ctrl and Shift are pressed # For Plus key: user presses Cmd+Shift+Equal (which makes Plus) # For Minus key: user presses Cmd+Shift+Minus - if has_ctrl and has_shift: + if ( + has_ctrl + # and + # has_shift + ): # Zoom in: Ctrl+Shift+Plus # Note: Plus key usually comes as Key_Equal with Shift modifier - if key in (Qt.Key.Key_Plus, Qt.Key.Key_Equal): + if key in ( + Qt.Key.Key_Plus, + Qt.Key.Key_Equal, + ): self.main_window.zoom_in() return True # consume event # Zoom out: Ctrl+Shift+Minus # Note: On some keyboards Shift+Minus produces '_' (Underscore) - elif key in (Qt.Key.Key_Minus, Qt.Key.Key_Underscore): + elif key in ( + Qt.Key.Key_Minus, + Qt.Key.Key_Underscore, + ): self.main_window.zoom_out() return True # consume event # Reset zoom: Ctrl+Shift+0 # Note: On some keyboards Shift+0 produces ')' (ParenRight) - elif key in (Qt.Key.Key_0, Qt.Key.Key_ParenRight): + elif key in ( + Qt.Key.Key_0, + Qt.Key.Key_ParenRight, + ): self.main_window.reset_zoom() return True # consume event @@ -117,6 +141,7 @@ class GlobalZoomEventFilter(QObject): class MultiStatus: + bar: QStatusBar statuses: list[str] @@ -127,17 +152,19 @@ class MultiStatus: self._status_groups: dict[str, (set, Callable)] = {} def open_status( + self, msg: str, - final_msg: str | None = None, + final_msg: str|None = None, clear_on_next: bool = False, - group_key: Union[bool, str] | None = False, + group_key: Union[bool, str]|None = False, + ) -> Union[Callable[..., None], str]: - """ + ''' Add a status to the status bar and return a close callback which when called will remove the status ``msg``. - """ + ''' for old_msg in self._to_clear: try: self.statuses.remove(old_msg) @@ -216,10 +243,10 @@ class MultiStatus: return ret def render(self) -> None: - """ + ''' Display all open statuses to bar. - """ + ''' if self.statuses: self.bar.showMessage(f'{" ".join(self.statuses)}') else: @@ -227,6 +254,7 @@ class MultiStatus: class MainWindow(QMainWindow): + # XXX: for tiling wms this should scale # with the alloted window size. # TODO: detect for tiling and if untrue set some size? @@ -241,11 +269,11 @@ class MainWindow(QMainWindow): self.setWindowTitle(self.title) # set by runtime after `trio` is engaged. - self.godwidget: GodWidget | None = None + self.godwidget: GodWidget|None = None self._status_bar: QStatusBar = None self._status_label: QLabel = None - self._size: tuple[int, int] | None = None + self._size: tuple[int, int]|None = None # restore window geometry from previous session settings = QSettings('pikers', 'piker') @@ -265,7 +293,7 @@ class MainWindow(QMainWindow): self._zoom_filter: GlobalZoomEventFilter | None = None def install_global_zoom_filter(self) -> None: - """Install application-level event filter for global UI zoom shortcuts.""" + '''Install application-level event filter for global UI zoom shortcuts.''' if self._zoom_filter is None: self._zoom_filter = GlobalZoomEventFilter(self) app = QApplication.instance() @@ -277,6 +305,7 @@ class MainWindow(QMainWindow): # init mode label if not self._status_label: + self._status_label = label = QLabel() label.setStyleSheet( f"""QLabel {{ @@ -285,10 +314,15 @@ class MainWindow(QMainWindow): """ # font-size : {font_size}px; ) - label.setTextFormat(Qt.TextFormat.MarkdownText) + label.setTextFormat( + Qt.TextFormat.MarkdownText + ) label.setFont(_font_small.font) label.setMargin(2) - label.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight) + label.setAlignment( + QtCore.Qt.AlignVCenter + |QtCore.Qt.AlignRight + ) self.statusBar().addPermanentWidget(label) label.show() @@ -297,8 +331,11 @@ class MainWindow(QMainWindow): def closeEvent( self, event: QCloseEvent, + ) -> None: - """Cancel the root actor asap.""" + '''Cancel the root actor asap. + + ''' # save window geometry for next session settings = QSettings('pikers', 'piker') settings.setValue('windowGeometry', self.saveGeometry()) @@ -312,17 +349,16 @@ class MainWindow(QMainWindow): # 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;" - ) - ) + 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, []) @@ -331,22 +367,28 @@ class MainWindow(QMainWindow): def set_mode_name( self, name: str, + ) -> None: self.mode_label.setText(f'mode:{name}') def on_focus_change( self, + last: QWidget, current: QWidget, + ) -> None: - """ + ''' Focus handler. For now updates the "current mode" name. - """ - log.debug(f'widget focus changed from,\n{last} -> {current}') + ''' + log.debug( + f'widget focus changed from,\n' + f'{last} -> {current}' + ) if current is not None: # cursor left window? @@ -354,10 +396,10 @@ class MainWindow(QMainWindow): self.set_mode_name(name) def current_screen(self) -> QScreen: - """ + ''' Get a frickin screen (if we can, gawd). - """ + ''' app = QApplication.instance() for _ in range(3): @@ -373,28 +415,29 @@ class MainWindow(QMainWindow): # try for the first one we can find screen = app.screens()[0] - assert screen, 'Wow Qt is dumb as shit and has no screen...' + assert screen, "Wow Qt is dumb as shit and has no screen..." return screen def configure_to_desktop( self, - size: tuple[int, int] | None = None, + size: tuple[int, int]|None = None, + ) -> None: - """ + ''' Explicitly size the window dimensions (for stacked window managers). For tina systems (like windoze) try to do a sane window size on startup. - """ + ''' # https://stackoverflow.com/a/18975846 if not size and not self._size: # app = QApplication.instance() geo = self.current_screen().geometry() h, w = geo.height(), geo.width() # use approx 1/3 of the area of the screen by default - self._size = round(w * 0.666), round(h * 0.666) + self._size = round(w * .666), round(h * .666) self.resize(*size or self._size) @@ -426,46 +469,79 @@ class MainWindow(QMainWindow): event.accept() def zoom_in(self) -> None: - """Increase UI zoom level.""" - new_zoom = min(self._zoom_level + self._zoom_step, self._max_zoom) - if new_zoom != self._zoom_level: - self._zoom_level = new_zoom - self._apply_zoom() - log.info(f'Zoomed in to {self._zoom_level:.1%}') + ''' + Increase overall UI-widgets zoom level by scaling it the + global font sizes. - def zoom_out(self) -> None: - """Decrease UI zoom level.""" - new_zoom = max(self._zoom_level - self._zoom_step, self._min_zoom) + ''' + new_zoom: float = min( + self._zoom_level + self._zoom_step, + self._max_zoom, + ) if new_zoom != self._zoom_level: self._zoom_level = new_zoom - self._apply_zoom() - log.info(f'Zoomed out to {self._zoom_level:.1%}') + font_size: int = self._apply_zoom() + log.info( + f'Zoomed in UI\n' + f'zoom_step: {self._zoom_step!r}\n' + f'zoom_level(%): {self._zoom_level:.1%}\n' + f'font_size: {font_size!r}' + ) + + def zoom_out(self) -> float: + ''' + Decrease UI zoom level. + + ''' + new_zoom: float = max(self._zoom_level - self._zoom_step, self._min_zoom) + if new_zoom != self._zoom_level: + self._zoom_level = new_zoom + font_size: int = self._apply_zoom() + log.info( + f'Zoomed out UI\n' + f'zoom_step: {self._zoom_step!r}\n' + f'zoom_level(%): {self._zoom_level:.1%}\n' + f'font_size: {font_size!r}' + ) + + return new_zoom def reset_zoom(self) -> None: - """Reset UI zoom to 100%.""" + ''' + Reset UI zoom to 100%. + + ''' if self._zoom_level != 1.0: self._zoom_level = 1.0 - self._apply_zoom() - log.info('Reset zoom to 100%') + font_size: int = self._apply_zoom() + log.info( + f'Reset zoom level\n' + f'zoom_step: {self._zoom_step!r}\n' + f'zoom_level(%): {self._zoom_level:.1%}\n' + f'font_size: {font_size!r}' + ) - def _apply_zoom(self) -> None: - """Apply current zoom level to all UI elements.""" - from . import _style + return self._zoom_level + def _apply_zoom(self) -> int: + ''' + Apply current zoom level to all UI elements. + + ''' # reconfigure fonts with zoom multiplier - _style._config_fonts_to_screen(zoom_level=self._zoom_level) + font_size: int = _style._config_fonts_to_screen( + zoom_level=self._zoom_level + ) # update status bar styling with new font size if self._status_bar: sb = self.statusBar() - sb.setStyleSheet( - ( - f'color : {hcolor("gunmetal")};' - f'background : {hcolor("default_dark")};' - f'font-size : {_style._font_small.px_size}px;' - 'padding : 0px;' - ) - ) + sb.setStyleSheet(( + f"color : {hcolor('gunmetal')};" + f"background : {hcolor('default_dark')};" + f"font-size : {_style._font_small.px_size}px;" + "padding : 0px;" + )) # force update of mode label if it exists if self._status_label: @@ -484,8 +560,13 @@ class MainWindow(QMainWindow): self._refresh_widget_fonts(self.godwidget) self.godwidget.update() + return font_size + def _update_chart_order_panes(self) -> None: - """Update order entry panels in all charts.""" + ''' + Update order entry panels in all charts. + + ''' if not self.godwidget: return @@ -520,10 +601,7 @@ class MainWindow(QMainWindow): subplot_view = subplot_chart.view if hasattr(subplot_view, 'order_mode') and subplot_view.order_mode: subplot_order_mode = subplot_view.order_mode - if ( - hasattr(subplot_order_mode, 'pane') - and subplot_order_mode.pane - ): + if hasattr(subplot_order_mode, 'pane') and subplot_order_mode.pane: subplot_order_mode.pane.update_fonts() # resize all sidepanes to match main chart's sidepane width @@ -532,7 +610,7 @@ class MainWindow(QMainWindow): splits.resize_sidepanes() def _update_chart_axes(self, chart) -> None: - """Update axis fonts and sizing for a chart.""" + '''Update axis fonts and sizing for a chart.''' from . import _style # update price axis (right side) @@ -556,11 +634,11 @@ class MainWindow(QMainWindow): chart.update() def _refresh_widget_fonts(self, widget: QWidget) -> None: - """ + ''' Recursively update font sizes in all child widgets. This handles widgets that have font-size hardcoded in their stylesheets. - """ + ''' from . import _style # recursively process all children @@ -591,7 +669,7 @@ _qt_win: QMainWindow = None def main_window() -> MainWindow: - "Return the actor-global Qt window." + 'Return the actor-global Qt window.' global _qt_win assert _qt_win