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
dpi-font-auto-calc_goodboy_test
Gud Boi 2026-03-11 19:07:38 -04:00
parent 88a9ae4144
commit 9b5f92f818
2 changed files with 186 additions and 93 deletions

View File

@ -79,9 +79,13 @@ class DpiAwareFont:
self._font_inches: float = None self._font_inches: float = None
self._screen = 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._qfont.setPixelSize(int(px_size))
self._qfm = QtGui.QFontMetrics(self._qfont) self._qfm = QtGui.QFontMetrics(self._qfont)
return self.px_size
@property @property
def screen(self) -> QtGui.QScreen: def screen(self) -> QtGui.QScreen:
@ -128,17 +132,18 @@ class DpiAwareFont:
self, self,
screen: QtGui.QScreen | None = None, screen: QtGui.QScreen | None = None,
zoom_level: float = 1.0, 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 If we end up needing to generalize this more here there are
listed in the script in ``snippets/qt_screen_info.py``. resources listed in the script in
``snippets/qt_screen_info.py``.
''' '''
if self._font_size is not None: if self._font_size is not None:
self._set_qfont_px_size(self._font_size * zoom_level) return self._set_qfont_px_size(self._font_size * zoom_level)
return
# NOTE: if no font size set either in the [ui] section of the # NOTE: if no font size set either in the [ui] section of the
# config or not yet computed from our magic scaling calcs, # config or not yet computed from our magic scaling calcs,
@ -157,7 +162,7 @@ class DpiAwareFont:
ldpi = pdpi ldpi = pdpi
mx_dpi = max(pdpi, ldpi) mx_dpi = max(pdpi, ldpi)
mn_dpi = min(pdpi, ldpi) # mn_dpi = min(pdpi, ldpi)
scale = round(ldpi/pdpi, ndigits=2) scale = round(ldpi/pdpi, ndigits=2)
if mx_dpi <= 97: # for low dpi use larger font sizes if mx_dpi <= 97: # for low dpi use larger font sizes
@ -166,7 +171,7 @@ class DpiAwareFont:
else: # hidpi use smaller font sizes else: # hidpi use smaller font sizes
inches = _font_sizes['hi'][self._font_size_calc_key] inches = _font_sizes['hi'][self._font_size_calc_key]
dpi = mn_dpi # dpi = mn_dpi
mult = 1.0 mult = 1.0
@ -213,10 +218,10 @@ class DpiAwareFont:
f"\nOur best guess font size is {font_size}\n" f"\nOur best guess font size is {font_size}\n"
) )
# apply the size # 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: 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!") raise RuntimeError("You must call .configure_to_dpi() first!")
unscaled_br: QtCore.QRectF = self._qfm.boundingRect(value) unscaled_br: QtCore.QRectF = self._qfm.boundingRect(value)
@ -233,12 +238,22 @@ _font = DpiAwareFont()
_font_small = DpiAwareFont(_font_size_key='small') _font_small = DpiAwareFont(_font_size_key='small')
def _config_fonts_to_screen(zoom_level: float = 1.0) -> None: def _config_fonts_to_screen(
'configure global DPI aware font sizes' 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 global _font, _font_small
_font.configure_to_dpi(zoom_level=zoom_level) _font.configure_to_dpi(zoom_level=zoom_level)
_font_small.configure_to_dpi(zoom_level=zoom_level) _font_small.configure_to_dpi(zoom_level=zoom_level)
return _font.px_size
def get_fonts() -> tuple[ def get_fonts() -> tuple[

View File

@ -18,7 +18,6 @@
Qt main window singletons and stuff. Qt main window singletons and stuff.
""" """
from __future__ import annotations from __future__ import annotations
import os import os
import signal import signal
@ -44,7 +43,11 @@ from piker.ui.qt import (
QObject, QObject,
) )
from ..log import get_logger from ..log import get_logger
from ._style import _font_small, hcolor from . import _style
from ._style import (
_font_small,
hcolor,
)
from ._widget import GodWidget from ._widget import GodWidget
@ -52,7 +55,7 @@ log = get_logger(__name__)
class GlobalZoomEventFilter(QObject): class GlobalZoomEventFilter(QObject):
""" '''
Application-level event filter for global UI zoom shortcuts. Application-level event filter for global UI zoom shortcuts.
This filter intercepts keyboard events BEFORE they reach widgets, This filter intercepts keyboard events BEFORE they reach widgets,
@ -64,18 +67,18 @@ class GlobalZoomEventFilter(QObject):
- Ctrl+Shift+Minus: Zoom out - Ctrl+Shift+Minus: Zoom out
- Ctrl+Shift+0: Reset zoom - Ctrl+Shift+0: Reset zoom
""" '''
def __init__(self, main_window: MainWindow): def __init__(self, main_window: MainWindow):
super().__init__() super().__init__()
self.main_window = main_window self.main_window = main_window
def eventFilter(self, obj: QObject, event: QEvent) -> bool: def eventFilter(self, obj: QObject, event: QEvent) -> bool:
""" '''
Filter keyboard events for global zoom shortcuts. Filter keyboard events for global zoom shortcuts.
Returns True to filter out (consume) the event, False to pass through. Returns True to filter out (consume) the event, False to pass through.
"""
'''
if event.type() == QEvent.Type.KeyPress: if event.type() == QEvent.Type.KeyPress:
key = event.key() key = event.key()
mods = event.modifiers() mods = event.modifiers()
@ -84,28 +87,49 @@ class GlobalZoomEventFilter(QObject):
mods = mods & ~Qt.KeyboardModifier.KeypadModifier mods = mods & ~Qt.KeyboardModifier.KeypadModifier
# Check if we have Ctrl+Shift (both required) # Check if we have Ctrl+Shift (both required)
has_ctrl = bool(mods & Qt.KeyboardModifier.ControlModifier) has_ctrl = bool(
has_shift = bool(mods & Qt.KeyboardModifier.ShiftModifier) mods
&
Qt.KeyboardModifier.ControlModifier
)
_has_shift = bool(
mods
&
Qt.KeyboardModifier.ShiftModifier
)
# Only handle UI zoom if BOTH Ctrl and Shift are pressed # Only handle UI zoom if BOTH Ctrl and Shift are pressed
# For Plus key: user presses Cmd+Shift+Equal (which makes Plus) # For Plus key: user presses Cmd+Shift+Equal (which makes Plus)
# For Minus key: user presses Cmd+Shift+Minus # 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 # Zoom in: Ctrl+Shift+Plus
# Note: Plus key usually comes as Key_Equal with Shift modifier # 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() self.main_window.zoom_in()
return True # consume event return True # consume event
# Zoom out: Ctrl+Shift+Minus # Zoom out: Ctrl+Shift+Minus
# Note: On some keyboards Shift+Minus produces '_' (Underscore) # 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() self.main_window.zoom_out()
return True # consume event return True # consume event
# Reset zoom: Ctrl+Shift+0 # Reset zoom: Ctrl+Shift+0
# Note: On some keyboards Shift+0 produces ')' (ParenRight) # 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() self.main_window.reset_zoom()
return True # consume event return True # consume event
@ -117,6 +141,7 @@ class GlobalZoomEventFilter(QObject):
class MultiStatus: class MultiStatus:
bar: QStatusBar bar: QStatusBar
statuses: list[str] statuses: list[str]
@ -127,17 +152,19 @@ class MultiStatus:
self._status_groups: dict[str, (set, Callable)] = {} self._status_groups: dict[str, (set, Callable)] = {}
def open_status( def open_status(
self, self,
msg: str, msg: str,
final_msg: str | None = None, final_msg: str|None = None,
clear_on_next: bool = False, clear_on_next: bool = False,
group_key: Union[bool, str] | None = False, group_key: Union[bool, str]|None = False,
) -> Union[Callable[..., None], str]: ) -> Union[Callable[..., None], str]:
""" '''
Add a status to the status bar and return a close callback which Add a status to the status bar and return a close callback which
when called will remove the status ``msg``. when called will remove the status ``msg``.
""" '''
for old_msg in self._to_clear: for old_msg in self._to_clear:
try: try:
self.statuses.remove(old_msg) self.statuses.remove(old_msg)
@ -216,10 +243,10 @@ class MultiStatus:
return ret return ret
def render(self) -> None: def render(self) -> None:
""" '''
Display all open statuses to bar. Display all open statuses to bar.
""" '''
if self.statuses: if self.statuses:
self.bar.showMessage(f'{" ".join(self.statuses)}') self.bar.showMessage(f'{" ".join(self.statuses)}')
else: else:
@ -227,6 +254,7 @@ class MultiStatus:
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
# XXX: for tiling wms this should scale # XXX: for tiling wms this should scale
# with the alloted window size. # with the alloted window size.
# TODO: detect for tiling and if untrue set some size? # TODO: detect for tiling and if untrue set some size?
@ -241,11 +269,11 @@ class MainWindow(QMainWindow):
self.setWindowTitle(self.title) self.setWindowTitle(self.title)
# set by runtime after `trio` is engaged. # set by runtime after `trio` is engaged.
self.godwidget: GodWidget | None = None self.godwidget: GodWidget|None = None
self._status_bar: QStatusBar = None self._status_bar: QStatusBar = None
self._status_label: QLabel = 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 # restore window geometry from previous session
settings = QSettings('pikers', 'piker') settings = QSettings('pikers', 'piker')
@ -265,7 +293,7 @@ class MainWindow(QMainWindow):
self._zoom_filter: GlobalZoomEventFilter | None = None self._zoom_filter: GlobalZoomEventFilter | None = None
def install_global_zoom_filter(self) -> 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: if self._zoom_filter is None:
self._zoom_filter = GlobalZoomEventFilter(self) self._zoom_filter = GlobalZoomEventFilter(self)
app = QApplication.instance() app = QApplication.instance()
@ -277,6 +305,7 @@ class MainWindow(QMainWindow):
# init mode label # init mode label
if not self._status_label: if not self._status_label:
self._status_label = label = QLabel() self._status_label = label = QLabel()
label.setStyleSheet( label.setStyleSheet(
f"""QLabel {{ f"""QLabel {{
@ -285,10 +314,15 @@ class MainWindow(QMainWindow):
""" """
# font-size : {font_size}px; # font-size : {font_size}px;
) )
label.setTextFormat(Qt.TextFormat.MarkdownText) label.setTextFormat(
Qt.TextFormat.MarkdownText
)
label.setFont(_font_small.font) label.setFont(_font_small.font)
label.setMargin(2) label.setMargin(2)
label.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight) label.setAlignment(
QtCore.Qt.AlignVCenter
|QtCore.Qt.AlignRight
)
self.statusBar().addPermanentWidget(label) self.statusBar().addPermanentWidget(label)
label.show() label.show()
@ -297,8 +331,11 @@ class MainWindow(QMainWindow):
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent, event: QCloseEvent,
) -> None: ) -> None:
"""Cancel the root actor asap.""" '''Cancel the root actor asap.
'''
# save window geometry for next session # save window geometry for next session
settings = QSettings('pikers', 'piker') settings = QSettings('pikers', 'piker')
settings.setValue('windowGeometry', self.saveGeometry()) settings.setValue('windowGeometry', self.saveGeometry())
@ -312,17 +349,16 @@ class MainWindow(QMainWindow):
# style and cached the status bar on first access # style and cached the status bar on first access
if not self._status_bar: if not self._status_bar:
sb = self.statusBar() sb = self.statusBar()
sb.setStyleSheet( sb.setStyleSheet((
( f"color : {hcolor('gunmetal')};"
f'color : {hcolor("gunmetal")};' f"background : {hcolor('default_dark')};"
f'background : {hcolor("default_dark")};' f"font-size : {_font_small.px_size}px;"
f'font-size : {_font_small.px_size}px;' "padding : 0px;"
'padding : 0px;'
# "min-height : 19px;" # "min-height : 19px;"
# "qproperty-alignment: AlignVCenter;" # "qproperty-alignment: AlignVCenter;"
) ))
)
self.setStatusBar(sb) self.setStatusBar(sb)
self._status_bar = MultiStatus(sb, []) self._status_bar = MultiStatus(sb, [])
@ -331,22 +367,28 @@ class MainWindow(QMainWindow):
def set_mode_name( def set_mode_name(
self, self,
name: str, name: str,
) -> None: ) -> None:
self.mode_label.setText(f'mode:{name}') self.mode_label.setText(f'mode:{name}')
def on_focus_change( def on_focus_change(
self, self,
last: QWidget, last: QWidget,
current: QWidget, current: QWidget,
) -> None: ) -> None:
""" '''
Focus handler. Focus handler.
For now updates the "current mode" name. 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: if current is not None:
# cursor left window? # cursor left window?
@ -354,10 +396,10 @@ class MainWindow(QMainWindow):
self.set_mode_name(name) self.set_mode_name(name)
def current_screen(self) -> QScreen: def current_screen(self) -> QScreen:
""" '''
Get a frickin screen (if we can, gawd). Get a frickin screen (if we can, gawd).
""" '''
app = QApplication.instance() app = QApplication.instance()
for _ in range(3): for _ in range(3):
@ -373,28 +415,29 @@ class MainWindow(QMainWindow):
# try for the first one we can find # try for the first one we can find
screen = app.screens()[0] 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 return screen
def configure_to_desktop( def configure_to_desktop(
self, self,
size: tuple[int, int] | None = None, size: tuple[int, int]|None = None,
) -> None: ) -> None:
""" '''
Explicitly size the window dimensions (for stacked window Explicitly size the window dimensions (for stacked window
managers). managers).
For tina systems (like windoze) try to do a sane window size on For tina systems (like windoze) try to do a sane window size on
startup. startup.
""" '''
# https://stackoverflow.com/a/18975846 # https://stackoverflow.com/a/18975846
if not size and not self._size: if not size and not self._size:
# app = QApplication.instance() # app = QApplication.instance()
geo = self.current_screen().geometry() geo = self.current_screen().geometry()
h, w = geo.height(), geo.width() h, w = geo.height(), geo.width()
# use approx 1/3 of the area of the screen by default # 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) self.resize(*size or self._size)
@ -426,46 +469,79 @@ class MainWindow(QMainWindow):
event.accept() event.accept()
def zoom_in(self) -> None: def zoom_in(self) -> None:
"""Increase UI zoom level.""" '''
new_zoom = min(self._zoom_level + self._zoom_step, self._max_zoom) Increase overall UI-widgets zoom level by scaling it the
if new_zoom != self._zoom_level: global font sizes.
self._zoom_level = new_zoom
self._apply_zoom()
log.info(f'Zoomed in to {self._zoom_level:.1%}')
def zoom_out(self) -> None: '''
"""Decrease UI zoom level.""" new_zoom: float = min(
new_zoom = max(self._zoom_level - self._zoom_step, self._min_zoom) self._zoom_level + self._zoom_step,
self._max_zoom,
)
if new_zoom != self._zoom_level: if new_zoom != self._zoom_level:
self._zoom_level = new_zoom self._zoom_level = new_zoom
self._apply_zoom() font_size: int = self._apply_zoom()
log.info(f'Zoomed out to {self._zoom_level:.1%}') 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: def reset_zoom(self) -> None:
"""Reset UI zoom to 100%.""" '''
Reset UI zoom to 100%.
'''
if self._zoom_level != 1.0: if self._zoom_level != 1.0:
self._zoom_level = 1.0 self._zoom_level = 1.0
self._apply_zoom() font_size: int = self._apply_zoom()
log.info('Reset zoom to 100%') 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: return self._zoom_level
"""Apply current zoom level to all UI elements."""
from . import _style
def _apply_zoom(self) -> int:
'''
Apply current zoom level to all UI elements.
'''
# reconfigure fonts with zoom multiplier # 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 # update status bar styling with new font size
if self._status_bar: if self._status_bar:
sb = self.statusBar() sb = self.statusBar()
sb.setStyleSheet( sb.setStyleSheet((
( f"color : {hcolor('gunmetal')};"
f'color : {hcolor("gunmetal")};' f"background : {hcolor('default_dark')};"
f'background : {hcolor("default_dark")};' f"font-size : {_style._font_small.px_size}px;"
f'font-size : {_style._font_small.px_size}px;' "padding : 0px;"
'padding : 0px;' ))
)
)
# force update of mode label if it exists # force update of mode label if it exists
if self._status_label: if self._status_label:
@ -484,8 +560,13 @@ class MainWindow(QMainWindow):
self._refresh_widget_fonts(self.godwidget) self._refresh_widget_fonts(self.godwidget)
self.godwidget.update() self.godwidget.update()
return font_size
def _update_chart_order_panes(self) -> None: 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: if not self.godwidget:
return return
@ -520,10 +601,7 @@ class MainWindow(QMainWindow):
subplot_view = subplot_chart.view subplot_view = subplot_chart.view
if hasattr(subplot_view, 'order_mode') and subplot_view.order_mode: if hasattr(subplot_view, 'order_mode') and subplot_view.order_mode:
subplot_order_mode = subplot_view.order_mode subplot_order_mode = subplot_view.order_mode
if ( if hasattr(subplot_order_mode, 'pane') and subplot_order_mode.pane:
hasattr(subplot_order_mode, 'pane')
and subplot_order_mode.pane
):
subplot_order_mode.pane.update_fonts() subplot_order_mode.pane.update_fonts()
# resize all sidepanes to match main chart's sidepane width # resize all sidepanes to match main chart's sidepane width
@ -532,7 +610,7 @@ class MainWindow(QMainWindow):
splits.resize_sidepanes() splits.resize_sidepanes()
def _update_chart_axes(self, chart) -> None: 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 from . import _style
# update price axis (right side) # update price axis (right side)
@ -556,11 +634,11 @@ class MainWindow(QMainWindow):
chart.update() chart.update()
def _refresh_widget_fonts(self, widget: QWidget) -> None: def _refresh_widget_fonts(self, widget: QWidget) -> None:
""" '''
Recursively update font sizes in all child widgets. Recursively update font sizes in all child widgets.
This handles widgets that have font-size hardcoded in their stylesheets. This handles widgets that have font-size hardcoded in their stylesheets.
""" '''
from . import _style from . import _style
# recursively process all children # recursively process all children
@ -591,7 +669,7 @@ _qt_win: QMainWindow = None
def main_window() -> MainWindow: def main_window() -> MainWindow:
"Return the actor-global Qt window." 'Return the actor-global Qt window.'
global _qt_win global _qt_win
assert _qt_win assert _qt_win