Compare commits
11 Commits
416a121de1
...
cf97a70b2c
| Author | SHA1 | Date |
|---|---|---|
|
|
cf97a70b2c | |
|
|
a603f321b1 | |
|
|
da2cefa0a9 | |
|
|
e1a81fa433 | |
|
|
073f496f87 | |
|
|
5b08ee1317 | |
|
|
28d0b4ce36 | |
|
|
d4d4628680 | |
|
|
78e5c799f0 | |
|
|
ee45b46638 | |
|
|
4b6bd55df4 |
|
|
@ -102,12 +102,12 @@ class AggTrade(Struct, frozen=True):
|
|||
a: int # Aggregate trade ID
|
||||
p: float # Price
|
||||
q: float # Quantity with all the market trades
|
||||
nq: float # Normal quantity without the trades involving RPI orders
|
||||
f: int # First trade ID
|
||||
l: int # noqa Last trade ID
|
||||
T: int # Trade time
|
||||
m: bool # Is the buyer the market maker?
|
||||
M: bool|None = None # Ignore
|
||||
nq: float|None = None # Normal quantity without the trades involving RPI orders
|
||||
|
||||
|
||||
async def stream_messages(
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ class Axis(pg.AxisItem):
|
|||
self.pi = plotitem
|
||||
self._dpi_font = _font
|
||||
|
||||
# store for later recalculation on zoom
|
||||
self._typical_max_str = typical_max_str
|
||||
|
||||
self.setTickFont(_font.font)
|
||||
font_size = self._dpi_font.font.pixelSize()
|
||||
|
||||
|
|
@ -156,6 +159,41 @@ class Axis(pg.AxisItem):
|
|||
def size_to_values(self) -> None:
|
||||
pass
|
||||
|
||||
def update_fonts(self, font: DpiAwareFont) -> None:
|
||||
'''Update font and recalculate axis sizing after zoom change.'''
|
||||
# IMPORTANT: tell Qt we're about to change geometry
|
||||
self.prepareGeometryChange()
|
||||
|
||||
self._dpi_font = font
|
||||
self.setTickFont(font.font)
|
||||
font_size = font.font.pixelSize()
|
||||
|
||||
# recalculate text offset based on new font size
|
||||
text_offset = None
|
||||
if self.orientation in ('bottom',):
|
||||
text_offset = floor(0.25 * font_size)
|
||||
elif self.orientation in ('left', 'right'):
|
||||
text_offset = floor(font_size / 2)
|
||||
|
||||
if text_offset:
|
||||
self.setStyle(tickTextOffset=text_offset)
|
||||
|
||||
# recalculate bounding rect with new font
|
||||
# Note: typical_max_str should be stored from init
|
||||
if not hasattr(self, '_typical_max_str'):
|
||||
self._typical_max_str = '100 000.000 ' # fallback default
|
||||
self.typical_br = font._qfm.boundingRect(self._typical_max_str)
|
||||
|
||||
# Update PyQtGraph's internal text size tracking
|
||||
# This is critical - PyQtGraph uses these internally for auto-expand
|
||||
if self.orientation in ['left', 'right']:
|
||||
self.textWidth = self.typical_br.width()
|
||||
else:
|
||||
self.textHeight = self.typical_br.height()
|
||||
|
||||
# resize axis to fit new font - this triggers PyQtGraph's auto-expand
|
||||
self.size_to_values()
|
||||
|
||||
def txt_offsets(self) -> tuple[int, int]:
|
||||
return tuple(self.style['tickTextOffset'])
|
||||
|
||||
|
|
@ -256,7 +294,14 @@ class PriceAxis(Axis):
|
|||
self._min_tick = size
|
||||
|
||||
def size_to_values(self) -> None:
|
||||
self.setWidth(self.typical_br.width())
|
||||
# Call PyQtGraph's internal width update mechanism
|
||||
# This respects autoExpandTextSpace and updates min/max constraints
|
||||
self._updateWidth()
|
||||
# tell Qt our preferred size changed so layout recalculates
|
||||
self.updateGeometry()
|
||||
# force parent plot item to recalculate its layout
|
||||
if self.pi and hasattr(self.pi, 'updateGeometry'):
|
||||
self.pi.updateGeometry()
|
||||
|
||||
# XXX: drop for now since it just eats up h space
|
||||
|
||||
|
|
@ -300,7 +345,14 @@ class DynamicDateAxis(Axis):
|
|||
}
|
||||
|
||||
def size_to_values(self) -> None:
|
||||
self.setHeight(self.typical_br.height() + 1)
|
||||
# Call PyQtGraph's internal height update mechanism
|
||||
# This respects autoExpandTextSpace and updates min/max constraints
|
||||
self._updateHeight()
|
||||
# tell Qt our preferred size changed so layout recalculates
|
||||
self.updateGeometry()
|
||||
# force parent plot item to recalculate its layout
|
||||
if self.pi and hasattr(self.pi, 'updateGeometry'):
|
||||
self.pi.updateGeometry()
|
||||
|
||||
def _indexes_to_timestrs(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -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,13 @@ 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)
|
||||
self.updateGeometry()
|
||||
|
||||
def resize_to_results(
|
||||
self,
|
||||
w: float | None = 0,
|
||||
|
|
@ -630,6 +637,29 @@ 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()
|
||||
|
||||
self.updateGeometry()
|
||||
|
||||
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
|
||||
|
|
@ -182,24 +186,25 @@ class DpiAwareFont:
|
|||
# always going to hit that error in range mapping from inches:
|
||||
# float to px size: int.
|
||||
self._font_inches = inches
|
||||
font_size = math.floor(inches * dpi)
|
||||
font_size = math.floor(inches * pdpi)
|
||||
|
||||
# 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
|
||||
self._set_qfont_px_size(font_size)
|
||||
|
||||
def boundingRect(self, value: str) -> QtCore.QRectF:
|
||||
|
||||
screen = self.screen
|
||||
if screen is None:
|
||||
if (screen := self.screen) is None:
|
||||
raise RuntimeError("You must call .configure_to_dpi() first!")
|
||||
|
||||
unscaled_br = self._qfm.boundingRect(value)
|
||||
|
||||
unscaled_br: QtCore.QRectF = self._qfm.boundingRect(value)
|
||||
return QtCore.QRectF(
|
||||
0,
|
||||
0,
|
||||
|
|
@ -213,12 +218,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,70 @@ 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()
|
||||
|
||||
# Mask out the KeypadModifier which Qt sometimes adds
|
||||
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)
|
||||
|
||||
# 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:
|
||||
# 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):
|
||||
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):
|
||||
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):
|
||||
self.main_window.reset_zoom()
|
||||
return True # consume event
|
||||
|
||||
# Pass through if only Ctrl (no Shift) - this goes to chart zoom
|
||||
# Pass through all other events too
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class MultiStatus:
|
||||
|
||||
bar: QStatusBar
|
||||
|
|
@ -181,6 +248,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 = 1.0 # Start at 100% (normal)
|
||||
self._min_zoom: float = 0.5
|
||||
self._max_zoom: float = 3.0 # Reduced from 10.0 to prevent extreme cropping
|
||||
self._zoom_step: float = 0.2 # 20% per keypress
|
||||
|
||||
# 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 +421,161 @@ 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:
|
||||
# update axes
|
||||
self._update_chart_axes(chart)
|
||||
|
||||
# update order pane
|
||||
if 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():
|
||||
# update subplot axes
|
||||
self._update_chart_axes(subplot_chart)
|
||||
|
||||
# update subplot order pane
|
||||
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()
|
||||
|
||||
# resize all sidepanes to match main chart's sidepane width
|
||||
# this ensures volume/subplot sidepanes match the main chart
|
||||
if splits and hasattr(splits, 'resize_sidepanes'):
|
||||
splits.resize_sidepanes()
|
||||
|
||||
def _update_chart_axes(self, chart) -> None:
|
||||
'''Update axis fonts and sizing for a chart.'''
|
||||
from . import _style
|
||||
|
||||
# update price axis (right side)
|
||||
if hasattr(chart, 'pi') and chart.pi:
|
||||
plot_item = chart.pi
|
||||
# get all axes from plot item
|
||||
for axis_name in ['left', 'right', 'bottom', 'top']:
|
||||
axis = plot_item.getAxis(axis_name)
|
||||
if axis and hasattr(axis, 'update_fonts'):
|
||||
axis.update_fonts(_style._font)
|
||||
|
||||
# force plot item to recalculate its entire layout
|
||||
plot_item.updateGeometry()
|
||||
|
||||
# force chart widget to update
|
||||
if hasattr(chart, 'updateGeometry'):
|
||||
chart.updateGeometry()
|
||||
|
||||
# trigger a full scene update
|
||||
if hasattr(chart, 'update'):
|
||||
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
|
||||
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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
#!env xonsh
|
||||
'''
|
||||
Compute the pxs-per-inch (PPI) naively for the local DE.
|
||||
|
||||
NOTE, currently this only supports the `sway`-TWM on wayland.
|
||||
|
||||
!TODO!
|
||||
- [ ] support Xorg (and possibly other OSs as well?
|
||||
- [ ] conver this to pure py code, dropping the `.xsh` specifics
|
||||
instead for `subprocess` API calls?
|
||||
- [ ] possibly unify all this with `./qt_screen_info.py` as part of
|
||||
a "PPI config wizard" or something, but more then likely we'll
|
||||
have lib-ified version inside modden/piker by then?
|
||||
|
||||
'''
|
||||
|
||||
import math
|
||||
import json
|
||||
|
||||
# XXX, xonsh part using "subprocess mode"
|
||||
disp_infos: list[dict] = json.loads($(wlr-randr --json))
|
||||
lappy: dict = disp_infos[0]
|
||||
|
||||
dims: dict[str, int] = lappy['physical_size']
|
||||
w_cm: int = dims['width']
|
||||
h_cm: int = dims['height']
|
||||
|
||||
# cm per inch
|
||||
cpi: float = 25.4
|
||||
|
||||
# compute "diagonal" size (aka hypot)
|
||||
diag_inches: float = math.sqrt((h_cm/cpi)**2 + (w_cm/cpi)**2)
|
||||
|
||||
# compute reso-hypot / inches-hypot
|
||||
hi_res: dict[str, float|bool] = lappy['modes'][0]
|
||||
w_px: int = hi_res['width']
|
||||
h_px: int = hi_res['height']
|
||||
|
||||
diag_pxs: float = math.sqrt(h_px**2 + w_px**2)
|
||||
unscaled_ppi: float = diag_pxs/diag_inches
|
||||
|
||||
# retrieve TWM info on the display (including scaling info)
|
||||
sway_disp_info: dict = json.loads($(swaymsg -r -t get_outputs))[0]
|
||||
scale: float = sway_disp_info['scale']
|
||||
|
||||
print(
|
||||
f'output: {sway_disp_info["name"]!r}\n'
|
||||
f'--- DIMENSIONS ---\n'
|
||||
f'w_cm: {w_cm!r}\n'
|
||||
f'h_cm: {h_cm!r}\n'
|
||||
f'w_px: {w_px!r}\n'
|
||||
f'h_cm: {h_px!r}\n'
|
||||
f'\n'
|
||||
f'--- DIAGONALS ---\n'
|
||||
f'diag_inches: {diag_inches!r}\n'
|
||||
f'diag_pxs: {diag_pxs!r}\n'
|
||||
f'\n'
|
||||
f'--- PPI-related-info ---\n'
|
||||
f'(DE reported) scale: {scale!r}\n'
|
||||
f'unscaled PPI: {unscaled_ppi!r}\n'
|
||||
f'|_ =sqrt(h_px**2 + w_px**2) / sqrt(h_in**2 + w_in**2)\n'
|
||||
f'scaled PPI: {unscaled_ppi/scale!r}\n'
|
||||
f'|_ =unscaled_ppi/scale\n'
|
||||
)
|
||||
|
|
@ -31,8 +31,8 @@ Resource list for mucking with DPIs on multiple screens:
|
|||
- https://doc.qt.io/qt-5/qguiapplication.html#screenAt
|
||||
|
||||
'''
|
||||
import os
|
||||
|
||||
from pyqtgraph import QtGui
|
||||
from PyQt6 import (
|
||||
QtCore,
|
||||
QtWidgets,
|
||||
|
|
@ -43,6 +43,11 @@ from PyQt6.QtCore import (
|
|||
QSize,
|
||||
QRect,
|
||||
)
|
||||
from pyqtgraph import QtGui
|
||||
|
||||
|
||||
# https://doc.qt.io/qt-6/highdpi.html#environment-variable-reference
|
||||
os.environ['QT_USE_PHYSICAL_DPI'] = '1'
|
||||
|
||||
# Proper high DPI scaling is available in Qt >= 5.6.0. This attibute
|
||||
# must be set before creating the application
|
||||
|
|
@ -58,13 +63,22 @@ if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
|
|||
True,
|
||||
)
|
||||
|
||||
# NOTE, inherits `QGuiApplication`
|
||||
# https://doc.qt.io/qt-6/qapplication.html
|
||||
# https://doc.qt.io/qt-6/qguiapplication.html
|
||||
app = QtWidgets.QApplication([])
|
||||
#
|
||||
# ^TODO? various global DPI settings?
|
||||
# [ ] DPI rounding policy,
|
||||
# - https://doc.qt.io/qt-6/qt.html#HighDpiScaleFactorRoundingPolicy-enum
|
||||
# - https://doc.qt.io/qt-6/qguiapplication.html#setHighDpiScaleFactorRoundingPolicy
|
||||
|
||||
window = QtWidgets.QMainWindow()
|
||||
main_widget = QtWidgets.QWidget()
|
||||
window.setCentralWidget(main_widget)
|
||||
window.show()
|
||||
|
||||
pxr: float = main_widget.devicePixelRatioF()
|
||||
_main_pxr: float = main_widget.devicePixelRatioF()
|
||||
|
||||
# explicitly get main widget and primary displays
|
||||
current_screen: QtGui.QScreen = app.screenAt(
|
||||
|
|
@ -77,7 +91,13 @@ for screen in app.screens():
|
|||
name: str = screen.name()
|
||||
model: str = screen.model().rstrip()
|
||||
size: QSize = screen.size()
|
||||
geo: QRect = screen.availableGeometry()
|
||||
geo: QRect = screen.geometry()
|
||||
|
||||
# device-pixel-ratio
|
||||
# https://doc.qt.io/qt-6/highdpi.html
|
||||
pxr: float = screen.devicePixelRatio()
|
||||
|
||||
unscaled_size: QSize = pxr * size
|
||||
phydpi: float = screen.physicalDotsPerInch()
|
||||
logdpi: float = screen.logicalDotsPerInch()
|
||||
is_primary: bool = screen is primary_screen
|
||||
|
|
@ -88,11 +108,12 @@ for screen in app.screens():
|
|||
f'|_primary: {is_primary}\n'
|
||||
f' _current: {is_current}\n'
|
||||
f' _model: {model}\n'
|
||||
f' _screen size: {size}\n'
|
||||
f' _screen geometry: {geo}\n'
|
||||
f' _devicePixelRationF(): {pxr}\n'
|
||||
f' _physical dpi: {phydpi}\n'
|
||||
f' _logical dpi: {logdpi}\n'
|
||||
f' _size: {size}\n'
|
||||
f' _geometry: {geo}\n'
|
||||
f' _devicePixelRatio(): {pxr}\n'
|
||||
f' _unscaled-size: {unscaled_size!r}\n'
|
||||
f' _physical-dpi: {phydpi}\n'
|
||||
f' _logical-dpi: {logdpi}\n'
|
||||
)
|
||||
|
||||
# app-wide font info
|
||||
|
|
@ -110,8 +131,8 @@ str_w: int = str_br.width()
|
|||
|
||||
print(
|
||||
f'------ global font settings ------\n'
|
||||
f'font dpi: {fontdpi}\n'
|
||||
f'font height: {font_h}\n'
|
||||
f'string bounding rect: {str_br}\n'
|
||||
f'string width : {str_w}\n'
|
||||
f'font dpi: {fontdpi!r}\n'
|
||||
f'font height: {font_h!r}\n'
|
||||
f'string bounding rect: {str_br!r}\n'
|
||||
f'string width : {str_w!r}\n'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import pytest
|
||||
from piker.ui._style import DpiAwareFont
|
||||
|
||||
|
||||
class MockScreen:
|
||||
def __init__(self, pdpi, ldpi, name="MockScreen"):
|
||||
self._pdpi = pdpi
|
||||
self._ldpi = ldpi
|
||||
self._name = name
|
||||
|
||||
def physicalDotsPerInch(self):
|
||||
return self._pdpi
|
||||
|
||||
def logicalDotsPerInch(self):
|
||||
return self._ldpi
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pdpi, ldpi, expected_px",
|
||||
[
|
||||
(96, 96, 9), # normal DPI
|
||||
(169, 96, 15), # HiDPI
|
||||
(120, 96, 10), # mid-DPI
|
||||
]
|
||||
)
|
||||
def test_font_px_size(pdpi, ldpi, expected_px):
|
||||
font = DpiAwareFont()
|
||||
font.configure_to_dpi(screen=MockScreen(pdpi, ldpi))
|
||||
|
||||
px = font.px_size
|
||||
print(f"{pdpi}x{ldpi} DPI -> Computed pixel size: {px}")
|
||||
|
||||
assert px == expected_px
|
||||
Loading…
Reference in New Issue