🔴 No files deleted.
🟢 a/piker/ui/_exec.py for adding global keyboard shortcuts for UI zoom. 🛠️ a/piker/ui/_forms.py -> Added methods to update fonts and sizes dynamically. 🛠️ a/piker/ui/_label.py -> Added method to update font and color after zoom. 🛠️ a/piker/ui/_position.py -> Included update_fonts method for labels and bars. 🛠️ a/piker/ui/_search.py -> Added update_fonts method for search view components. 🛠️ a/piker/ui/_style.py -> Enhanced configure_to_dpi() to support zoom level multipliers. 🛠️ a/piker/ui/_window.py -> Implemented application-wide zoom with event filter and zoom methods, including signals to update UI elements' font sizes and styles. 🔴 No files deleted.macos_fixes_2025
							parent
							
								
									61edb5cb19
								
							
						
					
					
						commit
						ed3a8d81b1
					
				|  | @ -199,6 +199,9 @@ def run_qtractor( | |||
|     if is_windows: | ||||
|         window.configure_to_desktop() | ||||
| 
 | ||||
|     # install global keyboard shortcuts for UI zoom | ||||
|     window.install_global_zoom_filter() | ||||
| 
 | ||||
|     # actually render to screen | ||||
|     window.show() | ||||
|     app.exec_() | ||||
|  |  | |||
|  | @ -124,6 +124,13 @@ class Edit(QLineEdit): | |||
|         self.sizeHint() | ||||
|         self.update() | ||||
| 
 | ||||
|     def update_fonts(self, font: DpiAwareFont) -> None: | ||||
|         '''Update font and recalculate widget size.''' | ||||
|         self.dpi_font = font | ||||
|         self.setFont(font.font) | ||||
|         # tell Qt our size hint changed so it recalculates layout | ||||
|         self.updateGeometry() | ||||
| 
 | ||||
|     def focus(self) -> None: | ||||
|         self.selectAll() | ||||
|         self.show() | ||||
|  | @ -241,6 +248,14 @@ class Selection(QComboBox): | |||
|         icon_size = round(h * 0.75) | ||||
|         self.setIconSize(QSize(icon_size, icon_size)) | ||||
| 
 | ||||
|     def update_fonts(self, font: DpiAwareFont) -> None: | ||||
|         '''Update font and recalculate widget size.''' | ||||
|         self.setFont(font.font) | ||||
|         # recalculate heights with new font | ||||
|         self.resize() | ||||
|         # tell Qt our size hint changed so it recalculates layout | ||||
|         self.updateGeometry() | ||||
| 
 | ||||
|     def set_items( | ||||
|         self, | ||||
|         keys: list[str], | ||||
|  | @ -431,6 +446,39 @@ class FieldsForm(QWidget): | |||
|         self.fields[key] = select | ||||
|         return select | ||||
| 
 | ||||
|     def update_fonts(self) -> None: | ||||
|         '''Update font sizes after zoom change.''' | ||||
|         from ._style import _font, _font_small | ||||
| 
 | ||||
|         # update stored font size | ||||
|         self._font_size = _font_small.px_size - 2 | ||||
| 
 | ||||
|         # update all labels | ||||
|         for name, label in self.labels.items(): | ||||
|             if hasattr(label, 'update_font'): | ||||
|                 label.update_font(_font.font, self._font_size - 1) | ||||
| 
 | ||||
|         # update all fields (edits, selects) | ||||
|         for key, field in self.fields.items(): | ||||
|             # first check for our custom update_fonts method (Edit, Selection) | ||||
|             if hasattr(field, 'update_fonts'): | ||||
|                 field.update_fonts(_font) | ||||
|             # then handle stylesheet updates for those without custom methods | ||||
|             elif hasattr(field, 'setStyleSheet'): | ||||
|                 # regenerate stylesheet with new font size | ||||
|                 field.setStyleSheet( | ||||
|                     f"""QLineEdit {{ | ||||
|                         color : {hcolor('gunmetal')}; | ||||
|                         font-size : {self._font_size}px; | ||||
|                     }} | ||||
|                     """ | ||||
|                 ) | ||||
|                 field.setFont(_font.font) | ||||
| 
 | ||||
|             # for Selection widgets that need style updates | ||||
|             if hasattr(field, 'set_style'): | ||||
|                 field.set_style(color='gunmetal', font_size=self._font_size) | ||||
| 
 | ||||
| 
 | ||||
| async def handle_field_input( | ||||
| 
 | ||||
|  | @ -633,6 +681,37 @@ class FillStatusBar(QProgressBar): | |||
|         self.setRange(0, int(slots)) | ||||
|         self.setValue(value) | ||||
| 
 | ||||
|     def update_fonts(self, font_size: int) -> None: | ||||
|         '''Update font size after zoom change.''' | ||||
|         from ._style import _font_small | ||||
| 
 | ||||
|         self.font_size = font_size | ||||
|         # regenerate stylesheet with new font size | ||||
|         self.setStyleSheet( | ||||
|             f""" | ||||
|             QProgressBar {{ | ||||
| 
 | ||||
|                 text-align: center; | ||||
| 
 | ||||
|                 font-size : {self.font_size - 2}px; | ||||
| 
 | ||||
|                 background-color: {hcolor('papas_special')}; | ||||
|                 color : {hcolor('papas_special')}; | ||||
| 
 | ||||
|                 border: {self.border_px}px solid {hcolor('default_light')}; | ||||
|                 border-radius: 2px; | ||||
|             }} | ||||
|             QProgressBar::chunk {{ | ||||
| 
 | ||||
|                 background-color: {hcolor('default_spotlight')}; | ||||
|                 color: {hcolor('bracket')}; | ||||
| 
 | ||||
|                 border-radius: 2px; | ||||
|             }} | ||||
|             """ | ||||
|         ) | ||||
|         self.setFont(_font_small.font) | ||||
| 
 | ||||
| 
 | ||||
| def mk_fill_status_bar( | ||||
| 
 | ||||
|  |  | |||
|  | @ -334,3 +334,19 @@ class FormatLabel(QLabel): | |||
|         out = self.fmt_str.format(**fields) | ||||
|         self.setText(out) | ||||
|         return out | ||||
| 
 | ||||
|     def update_font( | ||||
|         self, | ||||
|         font: QtGui.QFont, | ||||
|         font_size: int, | ||||
|         font_color: str = 'default_lightest', | ||||
|     ) -> None: | ||||
|         '''Update font after zoom change.''' | ||||
|         self.setStyleSheet( | ||||
|             f"""QLabel {{ | ||||
|                 color : {hcolor(font_color)}; | ||||
|                 font-size : {font_size}px; | ||||
|             }} | ||||
|             """ | ||||
|         ) | ||||
|         self.setFont(font) | ||||
|  |  | |||
|  | @ -178,6 +178,26 @@ class SettingsPane: | |||
|     # encompasing high level namespace | ||||
|     order_mode: OrderMode | None = None  # typing: ignore # noqa | ||||
| 
 | ||||
|     def update_fonts(self) -> None: | ||||
|         '''Update font sizes after zoom change.''' | ||||
|         from ._style import _font_small | ||||
| 
 | ||||
|         # update form fields | ||||
|         if self.form and hasattr(self.form, 'update_fonts'): | ||||
|             self.form.update_fonts() | ||||
| 
 | ||||
|         # update fill status bar | ||||
|         if self.fill_bar and hasattr(self.fill_bar, 'update_fonts'): | ||||
|             self.fill_bar.update_fonts(_font_small.px_size) | ||||
| 
 | ||||
|         # update labels with new fonts | ||||
|         if self.step_label: | ||||
|             self.step_label.setFont(_font_small.font) | ||||
|         if self.pnl_label: | ||||
|             self.pnl_label.setFont(_font_small.font) | ||||
|         if self.limit_label: | ||||
|             self.limit_label.setFont(_font_small.font) | ||||
| 
 | ||||
|     def set_accounts( | ||||
|         self, | ||||
|         names: list[str], | ||||
|  |  | |||
|  | @ -174,6 +174,12 @@ class CompleterView(QTreeView): | |||
| 
 | ||||
|         self.setStyleSheet(f"font: {size}px") | ||||
| 
 | ||||
|     def update_fonts(self) -> None: | ||||
|         '''Update font sizes after zoom change.''' | ||||
|         self.set_font_size(_font.px_size) | ||||
|         self.setIndentation(_font.px_size) | ||||
|         self.setFont(_font.font) | ||||
| 
 | ||||
|     def resize_to_results( | ||||
|         self, | ||||
|         w: float | None = 0, | ||||
|  | @ -630,6 +636,27 @@ class SearchWidget(QtWidgets.QWidget): | |||
|             | align_flag.AlignLeft, | ||||
|         ) | ||||
| 
 | ||||
|     def update_fonts(self) -> None: | ||||
|         '''Update font sizes after zoom change.''' | ||||
|         # regenerate label stylesheet with new font size | ||||
|         self.label.setStyleSheet( | ||||
|             f"""QLabel {{ | ||||
|                 color : {hcolor('default_lightest')}; | ||||
|                 font-size : {_font.px_size - 2}px; | ||||
|             }} | ||||
|             """ | ||||
|         ) | ||||
|         self.label.setFont(_font.font) | ||||
| 
 | ||||
|         # update search bar and view fonts | ||||
|         if hasattr(self.bar, 'update_fonts'): | ||||
|             self.bar.update_fonts(_font) | ||||
|         elif hasattr(self.bar, 'setFont'): | ||||
|             self.bar.setFont(_font.font) | ||||
| 
 | ||||
|         if hasattr(self.view, 'update_fonts'): | ||||
|             self.view.update_fonts() | ||||
| 
 | ||||
|     def focus(self) -> None: | ||||
|         self.show() | ||||
|         self.bar.focus() | ||||
|  |  | |||
|  | @ -80,7 +80,7 @@ class DpiAwareFont: | |||
|         self._screen = None | ||||
| 
 | ||||
|     def _set_qfont_px_size(self, px_size: int) -> None: | ||||
|         self._qfont.setPixelSize(px_size) | ||||
|         self._qfont.setPixelSize(int(px_size)) | ||||
|         self._qfm = QtGui.QFontMetrics(self._qfont) | ||||
| 
 | ||||
|     @property | ||||
|  | @ -109,7 +109,11 @@ class DpiAwareFont: | |||
|     def px_size(self) -> int: | ||||
|         return self._qfont.pixelSize() | ||||
| 
 | ||||
|     def configure_to_dpi(self, screen: QtGui.QScreen | None = None): | ||||
|     def configure_to_dpi( | ||||
|         self, | ||||
|         screen: QtGui.QScreen | None = None, | ||||
|         zoom_level: float = 1.0, | ||||
|     ): | ||||
|         ''' | ||||
|         Set an appropriately sized font size depending on the screen DPI. | ||||
| 
 | ||||
|  | @ -118,7 +122,7 @@ class DpiAwareFont: | |||
| 
 | ||||
|         ''' | ||||
|         if self._font_size is not None: | ||||
|             self._set_qfont_px_size(self._font_size) | ||||
|             self._set_qfont_px_size(self._font_size * zoom_level) | ||||
|             return | ||||
| 
 | ||||
|         # NOTE: if no font size set either in the [ui] section of the | ||||
|  | @ -184,9 +188,13 @@ class DpiAwareFont: | |||
|         self._font_inches = inches | ||||
|         font_size = math.floor(inches * dpi) | ||||
| 
 | ||||
|         # apply zoom level multiplier | ||||
|         font_size = int(font_size * zoom_level) | ||||
| 
 | ||||
|         log.debug( | ||||
|             f"screen:{screen.name()}\n" | ||||
|             f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n" | ||||
|             f"zoom_level: {zoom_level}\n" | ||||
|             f"\nOur best guess font size is {font_size}\n" | ||||
|         ) | ||||
|         # apply the size | ||||
|  | @ -213,12 +221,12 @@ _font = DpiAwareFont() | |||
| _font_small = DpiAwareFont(_font_size_key='small') | ||||
| 
 | ||||
| 
 | ||||
| def _config_fonts_to_screen() -> None: | ||||
| def _config_fonts_to_screen(zoom_level: float = 1.0) -> None: | ||||
|     'configure global DPI aware font sizes' | ||||
| 
 | ||||
|     global _font, _font_small | ||||
|     _font.configure_to_dpi() | ||||
|     _font_small.configure_to_dpi() | ||||
|     _font.configure_to_dpi(zoom_level=zoom_level) | ||||
|     _font_small.configure_to_dpi(zoom_level=zoom_level) | ||||
| 
 | ||||
| 
 | ||||
| # TODO: re-compute font size when main widget switches screens? | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
| Qt main window singletons and stuff. | ||||
| 
 | ||||
| """ | ||||
| from __future__ import annotations | ||||
| import os | ||||
| import signal | ||||
| import time | ||||
|  | @ -37,6 +38,8 @@ from piker.ui.qt import ( | |||
|     QStatusBar, | ||||
|     QScreen, | ||||
|     QCloseEvent, | ||||
|     QEvent, | ||||
|     QObject, | ||||
| ) | ||||
| from ..log import get_logger | ||||
| from ._style import _font_small, hcolor | ||||
|  | @ -46,6 +49,60 @@ from ._chart import GodWidget | |||
| 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, | ||||
|     allowing us to implement global UI zoom shortcuts that take precedence | ||||
|     over widget-specific shortcuts. | ||||
| 
 | ||||
|     Shortcuts: | ||||
|     - Ctrl+Shift+Plus/Equal: Zoom in | ||||
|     - 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() | ||||
| 
 | ||||
|             # Check for Ctrl+Shift modifier combination | ||||
|             has_ctrl_shift = ( | ||||
|                 (mods & Qt.KeyboardModifier.ControlModifier) and | ||||
|                 (mods & Qt.KeyboardModifier.ShiftModifier) | ||||
|             ) | ||||
| 
 | ||||
|             if has_ctrl_shift: | ||||
|                 # Zoom in: Ctrl+Shift+Plus or Ctrl+Shift+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 | ||||
|                 elif key == Qt.Key.Key_Minus: | ||||
|                     self.main_window.zoom_out() | ||||
|                     return True  # consume event | ||||
| 
 | ||||
|                 # Reset zoom: Ctrl+Shift+0 | ||||
|                 elif key == Qt.Key.Key_0: | ||||
|                     self.main_window.reset_zoom() | ||||
|                     return True  # consume event | ||||
| 
 | ||||
|         # Pass through all other events | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| class MultiStatus: | ||||
| 
 | ||||
|     bar: QStatusBar | ||||
|  | @ -181,6 +238,24 @@ class MainWindow(QMainWindow): | |||
|         self._status_label: QLabel = None | ||||
|         self._size: tuple[int, int] | None = None | ||||
| 
 | ||||
|         # zoom level for UI scaling (1.0 = 100%, 1.5 = 150%, etc) | ||||
|         # Change this value to set the default startup zoom level | ||||
|         self._zoom_level: float = 4.0  # Start at 200% zoom | ||||
|         self._min_zoom: float = 0.5 | ||||
|         self._max_zoom: float = 10.0 | ||||
|         self._zoom_step: float = 1.0 | ||||
| 
 | ||||
|         # event filter for global zoom shortcuts | ||||
|         self._zoom_filter: GlobalZoomEventFilter | None = None | ||||
| 
 | ||||
|     def install_global_zoom_filter(self) -> None: | ||||
|         '''Install application-level event filter for global UI zoom shortcuts.''' | ||||
|         if self._zoom_filter is None: | ||||
|             self._zoom_filter = GlobalZoomEventFilter(self) | ||||
|             app = QApplication.instance() | ||||
|             app.installEventFilter(self._zoom_filter) | ||||
|             log.info('Installed global zoom shortcuts: Ctrl+Shift+Plus/Minus/0') | ||||
| 
 | ||||
|     @property | ||||
|     def mode_label(self) -> QLabel: | ||||
| 
 | ||||
|  | @ -336,6 +411,123 @@ class MainWindow(QMainWindow): | |||
|         self.godwidget.on_win_resize(event) | ||||
|         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%}') | ||||
| 
 | ||||
|     def zoom_out(self) -> None: | ||||
|         '''Decrease UI zoom level.''' | ||||
|         new_zoom = max(self._zoom_level - self._zoom_step, self._min_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%}') | ||||
| 
 | ||||
|     def reset_zoom(self) -> None: | ||||
|         '''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%') | ||||
| 
 | ||||
|     def _apply_zoom(self) -> None: | ||||
|         '''Apply current zoom level to all UI elements.''' | ||||
|         from . import _style | ||||
| 
 | ||||
|         # reconfigure fonts with zoom multiplier | ||||
|         _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;" | ||||
|             )) | ||||
| 
 | ||||
|         # force update of mode label if it exists | ||||
|         if self._status_label: | ||||
|             self._status_label.setFont(_style._font_small.font) | ||||
| 
 | ||||
|         # update godwidget and its children | ||||
|         if self.godwidget: | ||||
|             # update search widget if it exists | ||||
|             if hasattr(self.godwidget, 'search') and self.godwidget.search: | ||||
|                 self.godwidget.search.update_fonts() | ||||
| 
 | ||||
|             # update order mode panes in all chart views | ||||
|             self._update_chart_order_panes() | ||||
| 
 | ||||
|             # recursively update all other widgets with stylesheets | ||||
|             self._refresh_widget_fonts(self.godwidget) | ||||
|             self.godwidget.update() | ||||
| 
 | ||||
|     def _update_chart_order_panes(self) -> None: | ||||
|         '''Update order entry panels in all charts.''' | ||||
|         if not self.godwidget: | ||||
|             return | ||||
| 
 | ||||
|         # iterate through all linked splits (hist and rt) | ||||
|         for splits_name in ['hist_linked', 'rt_linked']: | ||||
|             splits = getattr(self.godwidget, splits_name, None) | ||||
|             if not splits: | ||||
|                 continue | ||||
| 
 | ||||
|             # get main chart | ||||
|             chart = getattr(splits, 'chart', None) | ||||
|             if chart and hasattr(chart, 'view'): | ||||
|                 view = chart.view | ||||
|                 if hasattr(view, 'order_mode') and view.order_mode: | ||||
|                     order_mode = view.order_mode | ||||
|                     if hasattr(order_mode, 'pane') and order_mode.pane: | ||||
|                         order_mode.pane.update_fonts() | ||||
| 
 | ||||
|             # also check subplots | ||||
|             subplots = getattr(splits, 'subplots', {}) | ||||
|             for name, subplot_chart in subplots.items(): | ||||
|                 if hasattr(subplot_chart, 'view'): | ||||
|                     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: | ||||
|                             subplot_order_mode.pane.update_fonts() | ||||
| 
 | ||||
|     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 | ||||
|         for child in widget.findChildren(QWidget): | ||||
|             # skip widgets that have their own update_fonts method (handled separately) | ||||
|             if hasattr(child, 'update_fonts'): | ||||
|                 continue | ||||
| 
 | ||||
|             # update child's stylesheet if it has font-size | ||||
|             child_stylesheet = child.styleSheet() | ||||
|             if child_stylesheet and 'font-size' in child_stylesheet: | ||||
|                 # for labels and simple widgets, regenerate stylesheet | ||||
|                 # this is a heuristic - may need refinement | ||||
|                 try: | ||||
|                     child.setFont(_style._font.font) | ||||
|                 except (AttributeError, RuntimeError): | ||||
|                     pass | ||||
| 
 | ||||
|             # update child's font | ||||
|             try: | ||||
|                 child.setFont(_style._font.font) | ||||
|             except (AttributeError, RuntimeError): | ||||
|                 pass | ||||
| 
 | ||||
| 
 | ||||
| # singleton app per actor | ||||
| _qt_win: QMainWindow = None | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ from PyQt6.QtCore import ( | |||
|     QSize, | ||||
|     QModelIndex, | ||||
|     QItemSelectionModel, | ||||
|     QObject, | ||||
|     pyqtBoundSignal, | ||||
|     pyqtRemoveInputHook, | ||||
| ) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue