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
- TyphosAlarmRectangle
+ TyphosAlarmCircle
QWidget