diff --git a/docs/source/upcoming_release_notes/611-enh_positioner_resizing.rst b/docs/source/upcoming_release_notes/611-enh_positioner_resizing.rst new file mode 100644 index 00000000..7190ca19 --- /dev/null +++ b/docs/source/upcoming_release_notes/611-enh_positioner_resizing.rst @@ -0,0 +1,25 @@ +611 enh_positioner_resizing +################# + +API Breaks +---------- +- N/A + +Features +-------- +- Rework the design, sizing, and font scaling of the positioner row widget to address + concerns about readability and poor use of space for positioners that don't need + all of the widget components. +- Implement dynamic resizing in all directions for positioner row widgets. + +Bugfixes +-------- +- Fix various issues that cause font clipping for specific motors using the positioner row widget. + +Maintenance +----------- +- N/A + +Contributors +------------ +- zllentz diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index 91352722..a084c590 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -3,10 +3,16 @@ Dynamically set widget font size based on its current size. """ +from __future__ import annotations -from qtpy import QtCore, QtGui, QtWidgets +import functools +import logging + +from qtpy import QtGui, QtWidgets from qtpy.QtCore import QRectF, Qt +logger = logging.getLogger(__name__) + def get_widget_maximum_font_size( widget: QtWidgets.QWidget, @@ -106,10 +112,21 @@ def patch_widget( widget: QtWidgets.QWidget, *, pad_percent: float = 0.0, + max_size: float | None = None, + min_size: float | None = None, ) -> None: """ Patch the widget to dynamically change its font. + This picks between patch_text_widget and patch_combo_widget as appropriate. + + Depending on which is chosen, different methods may be patched to ensure + that the widget will always have the maximum size font that fits within + the bounding box. + + Regardless of which method is chosen, the font will be dynamically + resized for the first time before this function returns. + Parameters ---------- widget : QtWidgets.QWidget @@ -118,26 +135,170 @@ def patch_widget( The normalized padding percentage (0.0 - 1.0) to use in determining the maximum font size. Content margin settings determine the content rectangle, and this padding is applied as a percentage on top of that. + max_size : float or None, optional + The maximum font point size we're allowed to apply to the widget. + min_size : float or None, optional + The minimum font point size we're allowed to apply to the widget. """ - def resizeEvent(event: QtGui.QResizeEvent) -> None: - font = widget.font() - font_size = get_widget_maximum_font_size( - widget, widget.text(), - pad_width=widget.width() * pad_percent, - pad_height=widget.height() * pad_percent, + if isinstance(widget, QtWidgets.QComboBox): + return patch_combo_widget( + widget=widget, + pad_percent=pad_percent, + max_size=max_size, + min_size=min_size, + ) + elif hasattr(widget, "setText") and hasattr(widget, "text"): + return patch_text_widget( + widget=widget, + pad_percent=pad_percent, + max_size=max_size, + min_size=min_size, + ) + else: + raise TypeError(f"Dynamic font not supported for {widget}") + + +def set_font_common( + widget: QtWidgets.QWidget, + font_size: int, + min_size: int, + max_size: int, +) -> None: + # 0.1 = avoid meaningless resizes + # 0.00001 = snap to min/max + delta = 0.1 + if max_size is not None and font_size > max_size: + font_size = max_size + delta = 0.00001 + if min_size is not None and font_size < min_size: + font_size = min_size + delta = 0.00001 + font = widget.font() + if abs(font.pointSizeF() - font_size) > delta: + font.setPointSizeF(font_size) + # Set the font directly + widget.setFont(font) + # Also set the font in the stylesheet + # In case some code resets the style + patch_style_font_size(widget=widget, font_size=font_size) + + +def patch_text_widget( + widget: QtWidgets.QLabel | QtWidgets.QLineEdit, + *, + pad_percent: float = 0.0, + max_size: float | None = None, + min_size: float | None = None, +): + """ + Specific patching for widgets with text() and setText() methods. + + This replaces resizeEvent and setText methods with versions that will + set the font size to the maximum fitting value when the widget is + resized or the text is updated. + + The text is immediately resized for the first time during this function call. + """ + def set_font_size() -> None: + font_size = get_max_font_size_cached( + widget.text(), + widget.width(), + widget.height(), + ) + set_font_common( + widget=widget, + font_size=font_size, + max_size=max_size, + min_size=min_size, + ) + + # Cache and reuse results per widget + # Low effort and not exhaustive, but reduces the usage of the big loop + @functools.lru_cache + def get_max_font_size_cached(text: str, width: int, height: int) -> float: + return get_widget_maximum_font_size( + widget, + text, + pad_width=width * pad_percent, + pad_height=height * pad_percent, ) - if abs(font.pointSizeF() - font_size) > 0.1: - font.setPointSizeF(font_size) - widget.setFont(font) + + def resizeEvent(event: QtGui.QResizeEvent) -> None: + set_font_size() return orig_resize_event(event) - def minimumSizeHint() -> QtCore.QSize: - # Do not give any size hint as it it changes during resizeEvent - return QtWidgets.QWidget.minimumSizeHint(widget) + def setText(*args, **kwargs) -> None: + # Re-evaluate the text size when the text changes too + rval = orig_set_text(*args, **kwargs) + set_font_size() + return rval - def sizeHint() -> QtCore.QSize: - # Do not give any size hint as it it changes during resizeEvent - return QtWidgets.QWidget.sizeHint(widget) + if hasattr(widget.resizeEvent, "_patched_methods_"): + return + + orig_resize_event = widget.resizeEvent + orig_set_text = widget.setText + + resizeEvent._patched_methods_ = ( + widget.resizeEvent, + widget.setText, + ) + widget.resizeEvent = resizeEvent + widget.setText = setText + set_font_size() + + +def patch_combo_widget( + widget: QtWidgets.QComboBox, + *, + pad_percent: float = 0.0, + max_size: float | None = None, + min_size: float | None = None, +): + """ + Specific patching for combobox widgets. + + This replaces resizeEvent with a version that will + set the font size to the maximum fitting value + when the widget is resized. + + The text is immediately resized for the first time during this function call. + """ + def set_font_size() -> None: + combo_options = [ + widget.itemText(index) for index in range(widget.count()) + ] + font_sizes = [ + get_max_font_size_cached( + text, + widget.width(), + widget.height(), + ) + for text in combo_options + ] + set_font_common( + widget=widget, + font_size=min(font_sizes), + max_size=max_size, + min_size=min_size, + ) + + # Cache and reuse results per widget + # Low effort and not exhaustive, but reduces the usage of the big loop + @functools.lru_cache + def get_max_font_size_cached(text: str, width: int, height: int) -> float: + return get_widget_maximum_font_size( + widget, + text, + pad_width=width * pad_percent, + pad_height=height * pad_percent, + ) + + def resizeEvent(event: QtGui.QResizeEvent) -> None: + set_font_size() + return orig_resize_event(event) + + # TODO: figure out best way to resize font when combo options change if hasattr(widget.resizeEvent, "_patched_methods_"): return @@ -146,12 +307,9 @@ def sizeHint() -> QtCore.QSize: resizeEvent._patched_methods_ = ( widget.resizeEvent, - widget.sizeHint, - widget.minimumSizeHint, ) widget.resizeEvent = resizeEvent - widget.sizeHint = sizeHint - widget.minimumSizeHint = minimumSizeHint + set_font_size() def unpatch_widget(widget: QtWidgets.QWidget) -> None: @@ -163,13 +321,27 @@ def unpatch_widget(widget: QtWidgets.QWidget) -> None: widget : QtWidgets.QWidget The widget to unpatch. """ + unpatch_style_font_size(widget=widget) if not hasattr(widget.resizeEvent, "_patched_methods_"): return + if isinstance(widget, QtWidgets.QComboBox): + return unpatch_combo_widget(widget) + elif hasattr(widget, "setText") and hasattr(widget, "text"): + return unpatch_text_widget(widget) + else: + raise TypeError("Somehow, we have a patched widget that is unpatchable.") + +def unpatch_text_widget(widget: QtWidgets.QLabel | QtWidgets.QLineEdit): + ( + widget.resizeEvent, + widget.setText, + ) = widget.resizeEvent._patched_methods_ + + +def unpatch_combo_widget(widget: QtWidgets.QComboBox): ( widget.resizeEvent, - widget.sizeHint, - widget.minimumSizeHint, ) = widget.resizeEvent._patched_methods_ @@ -188,3 +360,50 @@ def is_patched(widget: QtWidgets.QWidget) -> bool: True if the widget has been patched. """ return hasattr(widget.resizeEvent, "_patched_methods_") + + +standard_comment = "/* Auto patch from typhos.dynamic_font */" + + +def patch_style_font_size(widget: QtWidgets.QWidget, font_size: int) -> None: + """ + Update a widget's stylesheet to force the font size. + + Requires the widget stylesheet to either be empty or filled with + existing rules that have class specifiers, e.g. it's ok to have: + + QPushButton { padding: 2px; margin: 0px; background-color: red } + + But not to just have: + + padding: 2px; margin: 0px; background-color: red + + This will not be applied until next style reload, which can be + done via unpolish and polish + (see https://wiki.qt.io/Dynamic_Properties_and_Stylesheets) + + In this module, this is used to guard against stylesheet reloads + undoing the dynamic font size. We will not call unpolish or polish + ourselves, but some other code might. + """ + starting_stylesheet = widget.styleSheet() + if standard_comment in starting_stylesheet: + unpatch_style_font_size(widget=widget) + widget.setStyleSheet( + f"{widget.styleSheet()}\n" + f"{standard_comment}\n" + f"{widget.__class__.__name__} {{ font-size: {font_size} pt }}" + ) + + +def unpatch_style_font_size(widget: QtWidgets.QWidget) -> None: + """ + Undo the effects of patch_style_font_size. + + Assumes that the last two lines of the stylesheet are the comment + and the rule that we added. + """ + if standard_comment in widget.styleSheet(): + widget.setStyleSheet( + "\n".join(widget.styleSheet().split("\n")[:-2]) + ) diff --git a/typhos/panel.py b/typhos/panel.py index 849d0628..92240ff5 100644 --- a/typhos/panel.py +++ b/typhos/panel.py @@ -20,7 +20,7 @@ import ophyd from ophyd import Kind from ophyd.signal import EpicsSignal, EpicsSignalRO -from qtpy import QtCore, QtWidgets +from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtCore import Q_ENUMS, Property from . import display, utils @@ -724,6 +724,8 @@ def __init__(self, parent=None, init_channel=None): self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) self.contextMenuEvent = self.open_context_menu + self.nested_panel = False + def _get_kind(self, kind: str) -> ophyd.Kind: """Property getter for show[kind].""" return self._kinds[kind] @@ -822,10 +824,17 @@ def sortBy(self, value): def add_device(self, device): """Typhos hook for adding a new device.""" self.devices.clear() + self.nested_panel = False super().add_device(device) # Configure the layout for the new device self._panel_layout.add_device(device) self._update_panel() + parent = self.parent() + while parent is not None: + if isinstance(parent, TyphosSignalPanel): + self.nested_panel = True + break + parent = parent.parent() def set_device_display(self, display): """Typhos hook for when the TyphosDeviceDisplay is associated.""" @@ -856,6 +865,31 @@ def open_context_menu(self, ev): menu = self.generate_context_menu() menu.exec_(self.mapToGlobal(ev.pos())) + def maybe_fix_parent_size(self): + if self.nested_panel: + # force this widget's containers to give it enough space! + self.parent().setMinimumHeight(self.parent().minimumSizeHint().height()) + + def resizeEvent(self, event: QtGui.QResizeEvent): + """ + Fix the parent container's size whenever our size changes. + + This also runs when we add or filter rows. + """ + self.maybe_fix_parent_size() + return super().resizeEvent(event) + + def setVisible(self, visible: bool): + """ + Fix the parent container's size whenever we switch visibility. + + This also runs when we toggle a row visibility using the title + and when all signal rows get filtered all at once. + """ + rval = super().setVisible(visible) + self.maybe_fix_parent_size() + return rval + class CompositeSignalPanel(SignalPanel): """ diff --git a/typhos/positioner.py b/typhos/positioner.py index 5fe44143..b10f405a 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -10,7 +10,7 @@ import ophyd from pydm.widgets.channel import PyDMChannel -from qtpy import QtCore, QtWidgets, uic +from qtpy import QtCore, QtGui, QtWidgets, uic from typhos.display import TyphosDisplaySwitcher @@ -171,6 +171,11 @@ def __init__(self, parent=None): self._initialized = False self._moving_channel = None + self._show_lowlim = True + self._show_lowtrav = True + self._show_highlim = True + self._show_hightrav = True + super().__init__(parent=parent) self.ui = typing.cast(_TyphosPositionerUI, uic.loadUi(self.ui_template, self)) @@ -185,7 +190,7 @@ def __init__(self, parent=None): self.show_expert_button = False self._after_set_moving(False) - dynamic_font.patch_widget(self.ui.user_readback, pad_percent=0.01) + dynamic_font.patch_widget(self.ui.user_readback, pad_percent=0.05, min_size=4) def _clear_status_thread(self): """Clear a previous status thread.""" @@ -395,7 +400,6 @@ def clear_error(self): # False = Red, True = Green, None = no box (in motion is yellow) if not self._last_move: self._last_move = None - utils.reload_widget_stylesheet(self, cascade=True) def _get_position(self): if not self._readback: @@ -422,6 +426,7 @@ def _link_low_limit_switch(self, signal, widget): """Link the positioner lower limit switch with the ui element.""" if signal is None: widget.hide() + self._show_lowlim = False @utils.linked_attribute('high_limit_switch_attribute', 'ui.high_limit_switch', True) @@ -429,6 +434,7 @@ def _link_high_limit_switch(self, signal, widget): """Link the positioner high limit switch with the ui element.""" if signal is None: widget.hide() + self._show_highlim = False @utils.linked_attribute('low_limit_travel_attribute', 'ui.low_limit', True) def _link_low_travel(self, signal, widget): @@ -461,6 +467,8 @@ def _link_limits_by_limits_attr(self): # If not found or invalid, hide them: self.ui.low_limit.hide() self.ui.high_limit.hide() + self._show_lowtrav = False + self._show_hightrav = False @utils.linked_attribute('moving_attribute', 'ui.moving_indicator', True) def _link_moving(self, signal, widget): @@ -513,10 +521,7 @@ def _define_setpoint_widget(self): self.ui.set_value.setAlignment(QtCore.Qt.AlignCenter) self.ui.set_value.returnPressed.connect(self.set) - self.ui.set_value.setSizePolicy( - QtWidgets.QSizePolicy.Fixed, - QtWidgets.QSizePolicy.Fixed, - ) + self.ui.set_value.setSizePolicy(self.ui.user_setpoint.sizePolicy()) self.ui.set_value.setMinimumWidth( self.ui.user_setpoint.minimumWidth() ) @@ -907,11 +912,13 @@ class _TyphosPositionerRowUI(_TyphosPositionerUI): """Annotations helper for positioner_row.ui; not to be instantiated.""" notes_edit: TyphosNotesEdit - status_container_widget: QtWidgets.QFrame - extended_signal_panel: Optional[TyphosSignalPanel] - status_error_prefix: QtWidgets.QLabel - error_prefix: QtWidgets.QLabel + status_container_widget: QtWidgets.QWidget + extended_signal_panel: Optional[RowDetails] switcher: TyphosDisplaySwitcher + status_text_layout: None # Row UI doesn't use status_text_layout + low_limit_widget: QtWidgets.QWidget + high_limit_widget: QtWidgets.QWidget + setpoint_widget: QtWidgets.QWidget class TyphosPositionerRowWidget(TyphosPositionerWidget): @@ -932,12 +939,15 @@ def __init__(self, *args, **kwargs): self._alarm_level = AlarmLevel.DISCONNECTED super().__init__(*args, **kwargs) - - for idx in range(self.layout().count()): - item = self.layout().itemAt(idx) - if item is self.ui.status_text_layout: - self.layout().takeAt(idx) - break + dynamic_font.patch_widget(self.ui.low_limit, pad_percent=0.01, max_size=12, min_size=4) + dynamic_font.patch_widget(self.ui.high_limit, pad_percent=0.01, max_size=12, min_size=4) + dynamic_font.patch_widget(self.ui.device_name_label, pad_percent=0.01, min_size=4) + dynamic_font.patch_widget(self.ui.notes_edit, pad_percent=0.01, min_size=4) + dynamic_font.patch_widget(self.ui.alarm_label, pad_percent=0.01, min_size=4) + dynamic_font.patch_widget(self.ui.moving_indicator_label, pad_percent=0.01, min_size=4) + dynamic_font.patch_widget(self.ui.stop_button, pad_percent=0.4, min_size=4) + dynamic_font.patch_widget(self.ui.expert_button, pad_percent=0.3, min_size=4) + dynamic_font.patch_widget(self.ui.clear_error_button, pad_percent=0.3, min_size=4) # TODO move these out self._omit_names = [ @@ -986,7 +996,12 @@ def get_names_to_omit(self) -> list[str]: omit_signals = self.all_linked_signals to_keep_signals = [ getattr(device, attr, None) - for attr in (self.velocity_attribute, self.acceleration_attribute) + for attr in ( + self.velocity_attribute, + self.acceleration_attribute, + self.high_limit_travel_attribute, + self.low_limit_travel_attribute, + ) ] for sig in to_keep_signals: if sig in omit_signals: @@ -1009,13 +1024,7 @@ def _create_signal_panel(self) -> Optional[TyphosSignalPanel]: if self.device is None: return None - panel = TyphosSignalPanel() - panel.omitNames = self.get_names_to_omit() - panel.sortBy = SignalOrder.byName - panel.add_device(self.device) - - self.ui.layout().addWidget(panel) - return panel + return RowDetails(row=self, parent=self, flags=QtCore.Qt.Window) def _expand_layout(self) -> None: """Toggle the expansion of the signal panel.""" @@ -1023,18 +1032,12 @@ def _expand_layout(self) -> None: self.ui.extended_signal_panel = self._create_signal_panel() if self.ui.extended_signal_panel is None: return - to_show = True else: to_show = not self.ui.extended_signal_panel.isVisible() self.ui.extended_signal_panel.setVisible(to_show) - if to_show: - self.ui.expand_button.setText('v') - else: - self.ui.expand_button.setText('>') - def add_device(self, device: ophyd.Device) -> None: """Add (or rather set) the ophyd device for this positioner.""" super().add_device(device) @@ -1051,6 +1054,16 @@ def add_device(self, device: ophyd.Device) -> None: self.ui.switcher.help_toggle_button.setToolTip(self._get_tooltip()) self.ui.switcher.help_toggle_button.setEnabled(False) + if not any(( + self._show_lowlim, + self._show_highlim, + self._show_lowtrav, + self._show_hightrav, + )): + # Hide the limit sections + self.ui.low_limit_widget.hide() + self.ui.high_limit_widget.hide() + def _get_tooltip(self): """Update the tooltip based on device information.""" # Lifted from TyphosHelpFrame @@ -1081,13 +1094,25 @@ def _link_error_message(self, signal, widget): """Link the IOC error message with the ui element.""" if signal is None: widget.hide() - self.ui.error_prefix.hide() else: signal.subscribe(self.new_error_message) def new_error_message(self, value, *args, **kwargs): self.update_status_visibility(error_message=value) + def _define_setpoint_widget(self): + super()._define_setpoint_widget() + if isinstance(self.ui.set_value, QtWidgets.QComboBox): + # Pad extra to avoid intersecting drop-down arrow + dynamic_font.patch_widget(self.ui.set_value, pad_percent=0.18, min_size=4) + # Consume the vertical space left by the missing tweak widgets + self.ui.set_value.setMinimumHeight( + self.ui.user_setpoint.minimumHeight() + + self.ui.tweak_value.minimumHeight() + ) + else: + dynamic_font.patch_widget(self.ui.set_value, pad_percent=0.1, min_size=4) + def _set_status_text( self, message: TyphosStatusMessage | str, @@ -1137,16 +1162,12 @@ def update_status_visibility( else: self.ui.status_label.setText('Check alarm') has_status = True - has_status_error = False if has_status and has_error: # We want to avoid having duplicate information (low effort try) if error_message in status_text: has_error = False - has_status_error = True self.ui.status_label.setVisible(has_status) - self.ui.status_error_prefix.setVisible(has_status_error) self.ui.error_label.setVisible(has_error) - self.ui.error_prefix.setVisible(has_error) def clear_error_in_background(device): @@ -1162,3 +1183,106 @@ def inner(): td = threading.Thread(target=inner, daemon=True) td.start() + + +class RowDetails(QtWidgets.QWidget): + """ + Container class for floating window with positioner row's basic config info. + """ + row: TyphosPositionerRowWidget + resize_timer: QtCore.QTimer + + def __init__(self, row: TyphosPositionerRowWidget, parent: QtWidgets.QWidget | None = None, **kwargs): + super().__init__(parent=parent, **kwargs) + self.row = row + + self.label = QtWidgets.QLabel() + self.label.setText(row.ui.device_name_label.text()) + font = self.label.font() + font.setPointSize(font.pointSize() + 4) + self.label.setFont(font) + self.label.setMaximumHeight( + QtGui.QFontMetrics(font).boundingRect(self.label.text()).height() + ) + + self.panel = TyphosSignalPanel() + self.panel.omitNames = row.get_names_to_omit() + self.panel.sortBy = SignalOrder.byName + self.panel.add_device(row.device) + + self.scroll_area = QtWidgets.QScrollArea() + self.scroll_area.setFrameStyle(QtWidgets.QFrame.NoFrame) + self.scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setWidget(self.panel) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) + layout.addWidget(self.scroll_area) + + self.setLayout(layout) + self.resize_timer = QtCore.QTimer(parent=self) + self.resize_timer.timeout.connect(self.fix_scroll_size) + self.resize_timer.setInterval(1) + self.resize_timer.setSingleShot(True) + + self.original_panel_min_width = self.panel.minimumWidth() + self.last_resize_width = 0 + self.resize_done = False + + def hideEvent(self, event: QtGui.QHideEvent): + """ + After hide, update button text, even if we were hidden via clicking the "x". + """ + self.row.ui.expand_button.setText('>') + return super().hideEvent(event) + + def showEvent(self, event: QtGui.QShowEvent): + """ + Before show, update button text and move window to just under button. + """ + button = self.row.ui.expand_button + button.setText('v') + self.move( + button.mapToGlobal( + QtCore.QPoint( + button.pos().x(), + button.pos().y() + button.height() + + self.style().pixelMetric(QtWidgets.QStyle.PM_TitleBarHeight), + ) + ) + ) + if not self.resize_done: + self.resize_timer.start() + return super().showEvent(event) + + def fix_scroll_size(self): + """ + Slot that ensures the panel gets enough space in the scroll area. + + The panel, when created, has smaller sizing information than it does + a few moments after being shown for the first time. This might + update several times before settling down. + + We want to watch for this resize and set the scroll area width such + that there's enough room to see the widget at its minimum size. + """ + if self.panel.minimumWidth() <= self.original_panel_min_width: + # No change + self.resize_timer.start() + return + elif self.last_resize_width != self.panel.minimumWidth(): + # We are not stable yet + self.last_resize_width = self.panel.minimumWidth() + self.resize_timer.start() + return + + # Make sure the panel has enough space to exist! + self.scroll_area.setMinimumWidth( + self.panel.minimumWidth() + + self.style().pixelMetric(QtWidgets.QStyle.PM_ScrollBarExtent) + + 2 * self.style().pixelMetric(QtWidgets.QStyle.PM_ScrollView_ScrollBarOverlap) + + 2 * self.style().pixelMetric(QtWidgets.QStyle.PM_ScrollView_ScrollBarSpacing) + ) + self.resize_done = True diff --git a/typhos/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index 3b1e07e6..c4c253f8 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -1,12 +1,18 @@ from __future__ import annotations +import logging + import pytest import pytestqt.qtbot +from pydm.widgets.label import PyDMLabel from qtpy import QtCore, QtGui, QtWidgets from typhos.tests import conftest -from ..dynamic_font import is_patched, patch_widget, unpatch_widget +from ..dynamic_font import (is_patched, patch_style_font_size, patch_widget, + unpatch_style_font_size, unpatch_widget) + +logger = logging.getLogger(__name__) @pytest.mark.parametrize( @@ -14,6 +20,8 @@ [ QtWidgets.QLabel, QtWidgets.QPushButton, + QtWidgets.QComboBox, + PyDMLabel, ] ) def test_patching( @@ -22,7 +30,10 @@ def test_patching( qtbot: pytestqt.qtbot.QtBot, ) -> None: widget = cls() - widget.setText("Test\ntext") + if isinstance(widget, QtWidgets.QComboBox): + widget.addItems(["test", "text"]) + else: + widget.setText("Test\ntext") widget.setFixedSize(500, 500) qtbot.add_widget(widget) @@ -43,10 +54,18 @@ def test_patching( patch_widget(widget) assert is_patched(widget) + # Font size case 1: widget is resized widget.resizeEvent(event) - new_font_size = widget.font().pointSizeF() - print("Patched font size", new_font_size) - assert original_font_size != new_font_size + resized_font_size = widget.font().pointSizeF() + logger.debug(f"ResizeEvent patched font size is {resized_font_size}") + assert original_font_size != resized_font_size + + # Font size case 2: text is updated (not supported in combobox yet) + if not isinstance(widget, QtWidgets.QComboBox): + widget.setText(widget.text()*100) + new_text_font_size = widget.font().pointSizeF() + logger.debug(f"setText patched font size is {new_text_font_size}") + assert resized_font_size != new_text_font_size assert is_patched(widget) unpatch_widget(widget) @@ -56,3 +75,27 @@ def test_patching( widget, f"{request.node.name}_{cls.__name__}_dynamic_font_size", ) + + +@pytest.mark.parametrize( + "stylesheet", + [ + "", + "background: red", + "QLabel { background: blue }", + "QWidget { font-size: 12 pt }", + ] +) +def test_style_patching(stylesheet: str, qtbot: pytestqt.qtbot.QtBot): + widget = QtWidgets.QWidget() + qtbot.add_widget(widget) + widget.setStyleSheet(stylesheet) + + expected_text = "font-size: 16 pt" + assert expected_text not in widget.styleSheet() + patch_style_font_size(widget=widget, font_size=16) + assert expected_text in widget.styleSheet() + unpatch_style_font_size(widget=widget) + assert expected_text not in widget.styleSheet() + + assert widget.styleSheet() == stylesheet diff --git a/typhos/ui/devices/PositionerBase.embedded.ui b/typhos/ui/devices/PositionerBase.embedded.ui index 95c560fe..ab0ec048 100644 --- a/typhos/ui/devices/PositionerBase.embedded.ui +++ b/typhos/ui/devices/PositionerBase.embedded.ui @@ -6,8 +6,8 @@ 0 0 - 872 - 58 + 681 + 125 @@ -46,6 +46,18 @@ + + + 0 + 0 + + + + + 681 + 125 + + diff --git a/typhos/ui/widgets/positioner.ui b/typhos/ui/widgets/positioner.ui index 42ba14f5..1a081cfb 100644 --- a/typhos/ui/widgets/positioner.ui +++ b/typhos/ui/widgets/positioner.ui @@ -165,7 +165,7 @@ - font-size: 8pt; background-color: None; color: None; + background-color: None; color: None; low_limit @@ -173,6 +173,9 @@ Qt::AlignCenter + + false + @@ -221,6 +224,9 @@ false + + false + 20 @@ -354,6 +360,9 @@ Screen true + + false + @@ -382,6 +391,9 @@ Screen Qt::AlignCenter + + false + @@ -511,6 +523,9 @@ Screen true + + false + PyDMLabel::String @@ -638,7 +653,7 @@ Screen - font-size: 8pt; background-color: None; color: None; + background-color: None; color: None; high_limit @@ -646,6 +661,9 @@ Screen Qt::AlignCenter + + false + diff --git a/typhos/ui/widgets/positioner_row.ui b/typhos/ui/widgets/positioner_row.ui index 24e92f7e..2747c2ac 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -6,20 +6,20 @@ 0 0 - 720 - 160 + 681 + 125 - + 0 0 - 655 - 0 + 0 + 125 @@ -45,428 +45,306 @@ 0 - - - 0 + + + + 0 + 1 + + + + + 0 + 26 + - - - - - 0 - 0 - - - - - 60 - 35 - - - - - 16777215 - 35 - - - - - 12 - 75 - true - - - - ${name} - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - 0 - - - 0 - - - - - - - - 0 - 0 - - - - - 250 - 0 - - - - - - - - - - false - - - - - - - - 0 - 0 - - - - - - - - + + + 1 + + + + + + 1 + 0 + + + + + 60 + 24 + + + + + 16777215 + 16777215 + + + + + 12 + 75 + true + + + + ${name} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 0 + + + 0 + + + + + + + + 1 + 0 + + + + + 250 + 24 + + + + + + + + + + false + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + + + + + + - + - + 0 - 0 + 2 0 - 64 + 52 16777215 - 64 + 16777215 - + 3 - 0 + 1 - 0 + 1 - 0 + 1 - 0 + 1 - - - 0 - - - 0 + + + + 1 + 0 + - - - - - 30 - 30 - - - - - 30 - 30 - - - - - - - 1.000000000000000 - - - - - - - - 0 - 0 - - - - - 50 - 25 - - - - - 50 - 25 - - - - alarm - - - Qt::AlignCenter - - - true - - - - - - - - - 0 + + + 42 + 0 + - - 0 + + + 16777215 + 16777215 + - - - - - 0 - 0 - - - - - 30 - 30 - - - - - 30 - 30 - - - - - - - - Widget for graphical representation of bits from an integer number - with support for Channels and more from PyDM - - Parameters - ---------- - parent : QWidget - The parent widget for the Label - init_channel : str, optional - The channel to be used by the widget. - - - - false - - - false - - - - - - - - - - 20 - 100 - 239 - - - - - 60 - 60 - 60 - - - - false - - - false - - - false - - - 1 - - - 0 - - - - Bit 0 - - - - - - - - - 0 - 0 - - - - - 50 - 25 - - - - - 50 - 25 - - - - motion - - - Qt::AlignCenter - - - true - - - - - - - - 0 - 0 - - - - - 50 - 0 - - - - - 50 - 0 - - - - - + + + 1 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 0 + 2 + + + + + 30 + 30 + + + + + + + 1.000000000000000 + + + + + + + + 0 + 1 + + + + + 36 + 15 + + + + + 16777215 + 16777215 + + + + alarm + + + Qt::AlignCenter + + + false + + + + + - - - 0 + + + + 1 + 0 + - - - - - 0 - 0 - - - - - 150 - 35 - - - - - 16777215 - 35 - - - - - 16 - 50 - false - false - - - - - - - - - - font: 16pt - - - user_readback - - - Qt::AlignCenter - - - true - - - false - - - - - - - - - 0 + + + 42 + 0 + - - 0 + + + 16777215 + 16777215 + - - - - - 0 - 0 - - - - - 25 - 25 - - - - - 25 - 25 - - - - - - - + + + 1 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 0 + 2 + + + + + 30 + 30 + + + + + 16777215 + 16777215 + + + + + + + Widget for graphical representation of bits from an integer number with support for Channels and more from PyDM @@ -477,127 +355,157 @@ init_channel : str, optional The channel to be used by the widget. - - - false - - - - 255 - 150 - 0 - - - - - 50 - 50 - 50 - - - - false - - - true - - - - - - - - 0 - 0 - - - - - 40 - 25 - - - - - 40 - 25 - - - - - 8 - - - - - - - font-size: 8pt; background-color: None; color: None; - - - - - - Qt::AlignCenter - - - 1 - - - false - - - false - - - - - - - - 0 - 0 - - - - - 40 - 0 - - - - - 40 - 0 - - - - - + + + false + + + false + + + + + + + + + + 20 + 100 + 239 + + + + + 60 + 60 + 60 + + + + false + + + false + + + true + + + 1 + + + 0 + + + + Bit 0 + + + + + + + + + 0 + 1 + + + + + 36 + 15 + + + + + 16777215 + 16777215 + + + + motion + + + Qt::AlignCenter + + + false + + + + + - + - - 0 + + 7 0 150 - 0 + 50 - 150 + 16777215 16777215 - + + + 16 + 50 + false + false + + + + + + + + + + + + + user_readback + + + Qt::AlignCenter + + + true + + + false + + + + + + + + 5 + 0 + + + + + 248 + 0 + + + - 0 + 3 0 @@ -612,154 +520,54 @@ 0 - - - 0 - - - 0 - - - 0 - - - 0 + + + + 1 + 0 + - - - - - 0 - 0 - - - - - 100 - 25 - - - - - 100 - 25 - - - - - - - Qt::AlignCenter - - - false - - - - - - - - + 0 - - QLayout::SetDefaultConstraint - - 0 + 3 - 1 + 3 - 0 + 3 - 1 + 3 - - - - - 25 - 25 - - - - - - - - - + + + + 0 + 1 + + - 100 - 25 + 25 + 23 - 100 - 25 - - - - Qt::AlignCenter - - - - - - - - 25 - 25 + 16777215 + 16777215 - - + + + - - - - - - - - - - - - 0 - - - - - - 0 - 0 - - - - - 25 - 25 - - - - - 25 - 25 - - - - - - - + + Widget for graphical representation of bits from an integer number with support for Channels and more from PyDM @@ -770,366 +578,183 @@ init_channel : str, optional The channel to be used by the widget. - - - margin: 0px; - - - false - - - - 255 - 150 - 0 - - - - - 50 - 50 - 50 - - - - Qt::Vertical - - - false - - - true - - - 0 - - - - - - - - 0 - 0 - - - - - 40 - 25 - - - - - 40 - 25 - - - - - 8 - - - - - - - font-size: 8pt; background-color: None; color: None; - - - - - - Qt::AlignCenter - - - 1 - - - false - - - false - - - - - - - - 0 - 0 - - - - - 40 - 0 - - - - - 40 - 0 - - - - - - - - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 70 - 50 - - - - - 70 - 50 - - - - - 75 - true - - - - padding: 2px; margin: 0px; background-color: red - - - Stop - - - false - - - false - - - false - - - - - - - - 0 - 0 - - - - - 70 - 0 - - - - - 70 - 0 - - - - - - - - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 70 - 50 - - - - - 70 - 50 - - - - - 75 - true - - - - - - - - - - Expert -Screen - - - - ../../../../../.designer/backup../../../../../.designer/backup - - - - - - - - 0 - 0 - - - - - 70 - 0 - - - - - 70 - 16777215 - - - - - - - - - - - - - - 16777215 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - 0 - 0 - - - - - 25 - 25 - - - - - 25 - 25 - - - - Expand Details - - - > - - - false - - - false - - - - - - - 3 - - - - - 0 - + + + false + + + + 255 + 150 + 0 + + + + + 50 + 50 + 50 + + + + false + + + true + + + - + - + 0 - 0 + 1 - 0 - 25 + 40 + 23 16777215 - 25 + 16777215 + + + 8 + + + + + - color: red + background-color: None; color: None; - Error: + + + + Qt::AlignCenter + + + 1 + + + false + + + false + + + + + + + + 1 + 0 + + + + + 150 + 0 + + + + + 16777215 + 16777215 + + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + - + + + + 0 + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 100 + 24 + + + + + 16777215 + 16777215 + + + + + + + Qt::AlignCenter + + + false + + + + + + + + - + 0 - 0 + 1 @@ -1138,146 +763,579 @@ Screen 25 - - - 16777215 - 25 - - - - QLabel { color: black; background-color: none; border-radius: 0px; } - - - (Status label) - - - false - - - 0 - + + + 0 + + + QLayout::SetDefaultConstraint + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 1 + 0 + + + + + 25 + 24 + + + + - + + + + + + + + 5 + 0 + + + + + 100 + 24 + + + + + 16777215 + 16777215 + + + + Qt::AlignCenter + + + + + + + + 1 + 0 + + + + + 25 + 24 + + + + + + + + + - - - + + + + + + + 1 + 0 + + + 0 + + 3 + - 0 + 3 + + + 3 + + + 3 - + - + 0 - 0 + 1 - 0 - 25 + 25 + 23 16777215 - 25 + 16777215 + + + + + + Widget for graphical representation of bits from an integer number + with support for Channels and more from PyDM + + Parameters + ---------- + parent : QWidget + The parent widget for the Label + init_channel : str, optional + The channel to be used by the widget. + + - color: red + margin: 0px; - - Error: + + false + + + + 255 + 150 + 0 + + + + + 50 + 50 + 50 + + + + Qt::Vertical + + + false + + + true + + + 0 - + - + 0 - 0 + 1 - 0 - 25 + 40 + 23 16777215 - 25 + 16777215 + + + 8 + + - color: black; background-color: none; border-radius: 0px; + background-color: None; color: None; - - true + + Qt::AlignCenter - - 0 + + 1 + + + false - - PyDMLabel::String + + false - - - - - - - - 0 - 0 - - - - - 143 - 42 - - - - - 143 - 42 - - - - - 75 - true - - - - - - - - - - Clear Error - - - - + + + + + + + + + + 2 + 0 + + + + + 164 + 0 + + + + + 9 + + + + + + 0 + 0 + + + + + 70 + 34 + + + + + 16777215 + 16777215 + + + + + 75 + true + + + + QPushButton { padding: 2px; margin: 0px; background-color: red } + + + Stop + + + false + + + false + + + false + + + + + + + + 0 + 0 + + + + + 70 + 34 + + + + + 16777215 + 16777215 + + + + + 75 + true + + + + + + + + + + Expert +Screen + + + + ../../../../../.designer/backup../../../../../.designer/backup + + + + + + + + + + + + + + 0 + 1 + + + + + 0 + 26 + + + + + 16777215 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 25 + 25 + + + + + 25 + 25 + + + + Expand Details + + + > + + + false + + + false + + + + + + + + 14 + 0 + + + + + 0 + 26 + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 1 + 0 + + + + + 0 + 26 + + + + + 16777215 + 25 + + + + QLabel { color: black; background-color: none; border-radius: 0px; } + + + (Status label) + + + false + + + 0 + + + + + + + + 1 + 0 + + + + + 0 + 26 + + + + + 16777215 + 25 + + + + + + + PyDMLabel { color: black; background-color: none; border-radius: 0px; } + + + + + + true + + + 0 + + + PyDMLabel::String + + + + + + + + + + + 2 + 0 + + + + + 0 + + + 0 + + + + + + 2 + 0 + + + + + 146 + 25 + + + + + 16777215 + 16777215 + + + + + 75 + true + + + + + + + + + + Clear Error + + + + + @@ -1301,7 +1359,7 @@ Screen
pydm.widgets.line_edit
- TyphosAlarmRectangle + TyphosAlarmCircle QWidget
typhos.alarm