From f07e5019273227b1212d6e00fe69b29055dffedf Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 31 May 2024 21:19:46 +0000 Subject: [PATCH 01/27] TST: create a test that demonstrates the issue --- typhos/tests/test_dynamic_font.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/typhos/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index 3b1e07e6..08a73476 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -6,7 +6,8 @@ from typhos.tests import conftest -from ..dynamic_font import is_patched, patch_widget, unpatch_widget +from ..dynamic_font import (get_widget_maximum_font_size, is_patched, + patch_widget, unpatch_widget) @pytest.mark.parametrize( @@ -56,3 +57,25 @@ def test_patching( widget, f"{request.node.name}_{cls.__name__}_dynamic_font_size", ) + + +def test_wide_label( + qapp: QtWidgets.QApplication, + qtbot: pytestqt.qtbot.QtBot, +): + """ + Replicate the wide label text in RIXS that clips + """ + widget = QtWidgets.QLabel() + qtbot.add_widget(widget) + widget.setText("143252.468 urad") + font = widget.font() + font.setPointSizeF(16.0) + widget.setFont(font) + assert widget.font().pointSizeF() == 16.0 + + patch_widget(widget) + widget.setFixedSize(162.36, 34.65) + for _ in range(3): + qapp.processEvents() + assert widget.font().pointSizeF() == get_widget_maximum_font_size(widget, widget.text()) From d1b4e8d76bf6be1e3011728c1558ce1fea441af1 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 31 May 2024 21:50:40 +0000 Subject: [PATCH 02/27] ENH: also set the font size on patch in case no resize event occurs --- typhos/dynamic_font.py | 9 ++++++++- typhos/tests/test_dynamic_font.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index 91352722..46fa0c45 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -3,10 +3,13 @@ Dynamically set widget font size based on its current size. """ +import logging from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtCore import QRectF, Qt +logger = logging.getLogger(__name__) + def get_widget_maximum_font_size( widget: QtWidgets.QWidget, @@ -119,7 +122,7 @@ def patch_widget( maximum font size. Content margin settings determine the content rectangle, and this padding is applied as a percentage on top of that. """ - def resizeEvent(event: QtGui.QResizeEvent) -> None: + def set_font_size() -> None: font = widget.font() font_size = get_widget_maximum_font_size( widget, widget.text(), @@ -129,6 +132,9 @@ def resizeEvent(event: QtGui.QResizeEvent) -> None: 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: @@ -152,6 +158,7 @@ def sizeHint() -> QtCore.QSize: widget.resizeEvent = resizeEvent widget.sizeHint = sizeHint widget.minimumSizeHint = minimumSizeHint + set_font_size() def unpatch_widget(widget: QtWidgets.QWidget) -> None: diff --git a/typhos/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index 08a73476..f074650f 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -75,7 +75,7 @@ def test_wide_label( assert widget.font().pointSizeF() == 16.0 patch_widget(widget) - widget.setFixedSize(162.36, 34.65) + widget.setFixedSize(162, 34) for _ in range(3): qapp.processEvents() assert widget.font().pointSizeF() == get_widget_maximum_font_size(widget, widget.text()) From 1e5ad8d392af70fd3c8b7ed85c33f14fc0993e84 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 31 May 2024 21:53:12 +0000 Subject: [PATCH 03/27] TST: for some reason this works here but not in pos widget --- typhos/tests/test_dynamic_font.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/typhos/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index f074650f..c18bf93b 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -60,7 +60,6 @@ def test_patching( def test_wide_label( - qapp: QtWidgets.QApplication, qtbot: pytestqt.qtbot.QtBot, ): """ @@ -76,6 +75,9 @@ def test_wide_label( patch_widget(widget) widget.setFixedSize(162, 34) - for _ in range(3): - qapp.processEvents() + event = QtGui.QResizeEvent( + QtCore.QSize(162, 34), + widget.size(), + ) + widget.resizeEvent(event) assert widget.font().pointSizeF() == get_widget_maximum_font_size(widget, widget.text()) From b036759ed19dc0c5295c3369500964752e0ba4b5 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 4 Jun 2024 00:24:48 +0000 Subject: [PATCH 04/27] TST: finally a promising test failure --- typhos/tests/test_dynamic_font.py | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/typhos/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index c18bf93b..7e039d36 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -2,8 +2,11 @@ import pytest import pytestqt.qtbot +from ophyd.sim import SynAxis +from pydm.widgets.label import PyDMLabel from qtpy import QtCore, QtGui, QtWidgets +from typhos.positioner import TyphosPositionerRowWidget from typhos.tests import conftest from ..dynamic_font import (get_widget_maximum_font_size, is_patched, @@ -15,6 +18,7 @@ [ QtWidgets.QLabel, QtWidgets.QPushButton, + PyDMLabel, ] ) def test_patching( @@ -81,3 +85,35 @@ def test_wide_label( ) widget.resizeEvent(event) assert widget.font().pointSizeF() == get_widget_maximum_font_size(widget, widget.text()) + + +def test_positioner_label( + qapp: QtWidgets.QApplication, + qtbot: pytestqt.qtbot.QtBot, + motor: SynAxis, +): + """ + Literally try the positioner label that causes issues + """ + pos = 143252 + units = "urad" + + widget = TyphosPositionerRowWidget() + qtbot.add_widget(widget) + widget.readback_attribute = "readback" + widget.add_device(motor) + qapp.processEvents() + + motor.readback._metadata["units"] = units + motor.readback._metadata["precision"] = 3 + motor.readback._run_metadata_callbacks() + motor.velocity.put(10000000) + motor.set(pos).wait(timeout=1.0) + qapp.processEvents() + + expected_text = f"{pos:.3f} {units}" + expected_size = get_widget_maximum_font_size(widget.user_readback, expected_text) + actual_text = widget.user_readback.text() + actual_size = widget.user_readback.font().pointSizeF() + assert expected_text == actual_text + assert expected_size == actual_size From 86fff4ccf91cf649449cb7d59985696ae95196a9 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 5 Jun 2024 23:43:22 +0000 Subject: [PATCH 05/27] FIX: max/min sizes, remove unused size info patches, remove hard-coded fonts, only resize row widget limits --- typhos/dynamic_font.py | 38 +++++++++++++++++++---------- typhos/positioner.py | 4 ++- typhos/ui/widgets/positioner.ui | 4 +-- typhos/ui/widgets/positioner_row.ui | 8 +++--- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index 46fa0c45..754783f6 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -3,9 +3,11 @@ Dynamically set widget font size based on its current size. """ +from __future__ import annotations + import logging -from qtpy import QtCore, QtGui, QtWidgets +from qtpy import QtGui, QtWidgets from qtpy.QtCore import QRectF, Qt logger = logging.getLogger(__name__) @@ -109,6 +111,8 @@ 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. @@ -121,6 +125,10 @@ 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 set_font_size() -> None: font = widget.font() @@ -129,7 +137,14 @@ def set_font_size() -> None: pad_width=widget.width() * pad_percent, pad_height=widget.height() * pad_percent, ) - if abs(font.pointSizeF() - font_size) > 0.1: + 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 + if abs(font.pointSizeF() - font_size) > delta: font.setPointSizeF(font_size) widget.setFont(font) @@ -137,27 +152,24 @@ 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 sizeHint() -> QtCore.QSize: - # Do not give any size hint as it it changes during resizeEvent - return QtWidgets.QWidget.sizeHint(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 if hasattr(widget.resizeEvent, "_patched_methods_"): return orig_resize_event = widget.resizeEvent + orig_set_text = widget.setText resizeEvent._patched_methods_ = ( widget.resizeEvent, - widget.sizeHint, - widget.minimumSizeHint, + widget.setText ) widget.resizeEvent = resizeEvent - widget.sizeHint = sizeHint - widget.minimumSizeHint = minimumSizeHint + widget.setText = setText set_font_size() diff --git a/typhos/positioner.py b/typhos/positioner.py index 5fe44143..bc310146 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -185,7 +185,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=6) def _clear_status_thread(self): """Clear a previous status thread.""" @@ -932,6 +932,8 @@ def __init__(self, *args, **kwargs): self._alarm_level = AlarmLevel.DISCONNECTED super().__init__(*args, **kwargs) + 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) for idx in range(self.layout().count()): item = self.layout().itemAt(idx) diff --git a/typhos/ui/widgets/positioner.ui b/typhos/ui/widgets/positioner.ui index 42ba14f5..90a47ef6 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 @@ -638,7 +638,7 @@ Screen - font-size: 8pt; background-color: None; color: None; + background-color: None; color: None; high_limit diff --git a/typhos/ui/widgets/positioner_row.ui b/typhos/ui/widgets/positioner_row.ui index 24e92f7e..c9ffc1aa 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -7,7 +7,7 @@ 0 0 720 - 160 + 162 @@ -416,7 +416,7 @@ - font: 16pt + user_readback @@ -532,7 +532,7 @@ - font-size: 8pt; background-color: None; color: None; + background-color: None; color: None; @@ -834,7 +834,7 @@ - font-size: 8pt; background-color: None; color: None; + background-color: None; color: None; From 04fca11c065c6aa37c93eb9e664e8fff60c88a49 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 5 Jun 2024 23:53:27 +0000 Subject: [PATCH 06/27] ENH: low effort way to avoid redoing get_max_font_size loop --- typhos/dynamic_font.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index 754783f6..e6df0ac6 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +import functools import logging from qtpy import QtGui, QtWidgets @@ -132,11 +133,13 @@ def patch_widget( """ def set_font_size() -> 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, + font_size = get_max_font_size_cached( + widget.text(), + widget.width(), + widget.height(), ) + # 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 @@ -148,6 +151,17 @@ def set_font_size() -> None: font.setPointSizeF(font_size) widget.setFont(font) + # 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) From 522eccdf407bd8a12fa0a04206efcb51576cfcc8 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 5 Jun 2024 23:54:32 +0000 Subject: [PATCH 07/27] TST: use new mark on new test that has positioner widget --- typhos/tests/test_dynamic_font.py | 1 + 1 file changed, 1 insertion(+) diff --git a/typhos/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index 7e039d36..fd1b9773 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -87,6 +87,7 @@ def test_wide_label( assert widget.font().pointSizeF() == get_widget_maximum_font_size(widget, widget.text()) +@pytest.mark.no_gc def test_positioner_label( qapp: QtWidgets.QApplication, qtbot: pytestqt.qtbot.QtBot, From 61e51c199d4f2aa57ee7db946b25d81da3b3b541 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 6 Jun 2024 00:00:03 +0000 Subject: [PATCH 08/27] FIX: forgot to update dynamic font's unpatch method --- typhos/dynamic_font.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index e6df0ac6..e96640d0 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -201,8 +201,7 @@ def unpatch_widget(widget: QtWidgets.QWidget) -> None: ( widget.resizeEvent, - widget.sizeHint, - widget.minimumSizeHint, + widget.setText, ) = widget.resizeEvent._patched_methods_ From 72617d9d39dd9142d34d82a9f2f3855def34099b Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 6 Jun 2024 00:10:45 +0000 Subject: [PATCH 09/27] ENH: add a warning about stylesheet fonts to the dynamic font patcher. --- typhos/dynamic_font.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index e96640d0..8bc35f4b 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -131,6 +131,12 @@ def patch_widget( min_size : float or None, optional The minimum font point size we're allowed to apply to the widget. """ + if "font-size" in widget.styleSheet(): + logger.warning( + f"Widget named {widget.objectName()} has a fixed size from its " + "stylesheet, and cannot be resized dynamically." + ) + def set_font_size() -> None: font = widget.font() font_size = get_max_font_size_cached( From 1279786eb0ffb2953e068d9079d889a08552ccf3 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 6 Jun 2024 00:16:41 +0000 Subject: [PATCH 10/27] TST: remove unstable precise tests, replace with simple check in old test --- typhos/tests/test_dynamic_font.py | 79 ++++++------------------------- 1 file changed, 15 insertions(+), 64 deletions(-) diff --git a/typhos/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index fd1b9773..007c7ae0 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -1,16 +1,17 @@ from __future__ import annotations +import logging + import pytest import pytestqt.qtbot -from ophyd.sim import SynAxis from pydm.widgets.label import PyDMLabel from qtpy import QtCore, QtGui, QtWidgets -from typhos.positioner import TyphosPositionerRowWidget from typhos.tests import conftest -from ..dynamic_font import (get_widget_maximum_font_size, is_patched, - patch_widget, unpatch_widget) +from ..dynamic_font import is_patched, patch_widget, unpatch_widget + +logger = logging.getLogger(__name__) @pytest.mark.parametrize( @@ -48,10 +49,17 @@ 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 + 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) @@ -61,60 +69,3 @@ def test_patching( widget, f"{request.node.name}_{cls.__name__}_dynamic_font_size", ) - - -def test_wide_label( - qtbot: pytestqt.qtbot.QtBot, -): - """ - Replicate the wide label text in RIXS that clips - """ - widget = QtWidgets.QLabel() - qtbot.add_widget(widget) - widget.setText("143252.468 urad") - font = widget.font() - font.setPointSizeF(16.0) - widget.setFont(font) - assert widget.font().pointSizeF() == 16.0 - - patch_widget(widget) - widget.setFixedSize(162, 34) - event = QtGui.QResizeEvent( - QtCore.QSize(162, 34), - widget.size(), - ) - widget.resizeEvent(event) - assert widget.font().pointSizeF() == get_widget_maximum_font_size(widget, widget.text()) - - -@pytest.mark.no_gc -def test_positioner_label( - qapp: QtWidgets.QApplication, - qtbot: pytestqt.qtbot.QtBot, - motor: SynAxis, -): - """ - Literally try the positioner label that causes issues - """ - pos = 143252 - units = "urad" - - widget = TyphosPositionerRowWidget() - qtbot.add_widget(widget) - widget.readback_attribute = "readback" - widget.add_device(motor) - qapp.processEvents() - - motor.readback._metadata["units"] = units - motor.readback._metadata["precision"] = 3 - motor.readback._run_metadata_callbacks() - motor.velocity.put(10000000) - motor.set(pos).wait(timeout=1.0) - qapp.processEvents() - - expected_text = f"{pos:.3f} {units}" - expected_size = get_widget_maximum_font_size(widget.user_readback, expected_text) - actual_text = widget.user_readback.text() - actual_size = widget.user_readback.font().pointSizeF() - assert expected_text == actual_text - assert expected_size == actual_size From 3a308146a6242a8b8bb413427514136200540fc0 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 6 Jun 2024 01:09:50 +0000 Subject: [PATCH 11/27] ENH: redesign the horizontal resize behavior of positioner rows --- typhos/positioner.py | 5 +- typhos/ui/widgets/positioner_row.ui | 93 +++++++++-------------------- 2 files changed, 30 insertions(+), 68 deletions(-) diff --git a/typhos/positioner.py b/typhos/positioner.py index bc310146..74b4ca5c 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -513,10 +513,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() ) diff --git a/typhos/ui/widgets/positioner_row.ui b/typhos/ui/widgets/positioner_row.ui index c9ffc1aa..fce4e1dd 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -6,7 +6,7 @@ 0 0 - 720 + 824 162 @@ -150,7 +150,7 @@ 64 - + 3 @@ -392,13 +392,13 @@ 150 - 35 + 55 16777215 - 35 + 55 @@ -503,10 +503,10 @@ - + - + 0 0 @@ -519,7 +519,7 @@ - 40 + 16777215 25 @@ -551,34 +551,12 @@ - - - - - 0 - 0 - - - - - 40 - 0 - - - - - 40 - 0 - - - - - + 0 0 @@ -591,7 +569,7 @@ - 150 + 16777215 16777215 @@ -617,15 +595,15 @@ 0 - 0 + 25 0 - 0 + 25 - + @@ -636,13 +614,13 @@ 100 - 25 + 30 - 100 - 25 + 16777215 + 30 @@ -694,6 +672,12 @@ + + + 0 + 0 + + 100 @@ -702,7 +686,7 @@ - 100 + 16777215 25 @@ -732,6 +716,9 @@ + + 0 + 0 @@ -808,7 +795,7 @@ - + 0 0 @@ -821,7 +808,7 @@ - 40 + 16777215 25 @@ -853,28 +840,6 @@ - - - - - 0 - 0 - - - - - 40 - 0 - - - - - 40 - 0 - - - - @@ -1251,13 +1216,13 @@ Screen 143 - 42 + 25 143 - 42 + 25 From 222158e761d3416247d77208f6a89ccddaccc16f Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 6 Jun 2024 21:15:50 +0000 Subject: [PATCH 12/27] ENH: combo widget resizing, allow limit setting with row widget --- typhos/dynamic_font.py | 111 ++++++++++++++++++++++++++++ typhos/positioner.py | 24 +++++- typhos/ui/widgets/positioner_row.ui | 12 +-- 3 files changed, 140 insertions(+), 7 deletions(-) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index 8bc35f4b..c32a89f2 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -136,7 +136,34 @@ def patch_widget( f"Widget named {widget.objectName()} has a fixed size from its " "stylesheet, and cannot be resized dynamically." ) + 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 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 + """ def set_font_size() -> None: font = widget.font() font_size = get_max_font_size_cached( @@ -193,6 +220,72 @@ def setText(*args, **kwargs) -> None: 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 + """ + def set_font_size() -> None: + font = widget.font() + combo_options = [ + widget.itemText(index) for index in range(widget.count()) + ] + font_sizes = [ + get_max_font_size_cached( + text, + widget.height(), + widget.width(), + ) + for text in combo_options + ] + font_size = min(font_sizes) + # 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 + if abs(font.pointSizeF() - font_size) > delta: + font.setPointSizeF(font_size) + widget.setFont(font) + + # 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 + + orig_resize_event = widget.resizeEvent + + resizeEvent._patched_methods_ = ( + widget.resizeEvent, + ) + widget.resizeEvent = resizeEvent + set_font_size() + + def unpatch_widget(widget: QtWidgets.QWidget) -> None: """ Remove dynamic font size patch from the widget, if previously applied. @@ -204,13 +297,31 @@ def unpatch_widget(widget: QtWidgets.QWidget) -> None: """ if not hasattr(widget.resizeEvent, "_patched_methods_"): return + if isinstance(widget, QtWidgets.QComboBox): + return unpatch_combo_widget( + widget=widget, + ) + elif hasattr(widget, "setText") and hasattr(widget, "text"): + return unpatch_text_widget( + 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.resizeEvent._patched_methods_ + + def is_patched(widget: QtWidgets.QWidget) -> bool: """ Check if widget has been patched for dynamically-resizing fonts. diff --git a/typhos/positioner.py b/typhos/positioner.py index 74b4ca5c..d6d46676 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -508,6 +508,15 @@ def _define_setpoint_widget(self): self.ui.set_value.activated.connect(self.set) self.ui.set_value.setMinimumContentsLength(20) self.ui.tweak_widget.setVisible(False) + # Consume the space left by removing the tweak widget + self.ui.set_value.setMinimumHeight( + self.ui.user_setpoint.minimumHeight() + + self.ui.tweak_value.minimumHeight() + ) + self.ui.set_value.setMaximumHeight( + self.ui.user_setpoint.maximumHeight() + + self.ui.tweak_value.maximumHeight() + ) else: self.ui.set_value = QtWidgets.QLineEdit() self.ui.set_value.setAlignment(QtCore.Qt.AlignCenter) @@ -985,7 +994,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: @@ -1087,6 +1101,14 @@ def _link_error_message(self, signal, widget): 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) + else: + dynamic_font.patch_widget(self.ui.set_value, pad_percent=0.1, min_size=4) + def _set_status_text( self, message: TyphosStatusMessage | str, diff --git a/typhos/ui/widgets/positioner_row.ui b/typhos/ui/widgets/positioner_row.ui index fce4e1dd..6924a82c 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -392,13 +392,13 @@ 150 - 55 + 50 16777215 - 55 + 50 @@ -595,13 +595,13 @@ 0 - 25 + 0 0 - 25 + 0 @@ -1017,7 +1017,7 @@ Screen - + @@ -1205,7 +1205,7 @@ Screen - + From b41cf3c3f8c768eec0542873083462a1a192079c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 6 Jun 2024 21:50:13 +0000 Subject: [PATCH 13/27] FIX: w/h were swapped for combos, eliminate alarm-sensitive borders and stylesheet reloads --- typhos/dynamic_font.py | 12 ++++-------- typhos/positioner.py | 1 - typhos/ui/widgets/positioner.ui | 18 ++++++++++++++++++ typhos/ui/widgets/positioner_row.ui | 4 ++-- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index c32a89f2..09ed01f9 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -213,7 +213,7 @@ def setText(*args, **kwargs) -> None: resizeEvent._patched_methods_ = ( widget.resizeEvent, - widget.setText + widget.setText, ) widget.resizeEvent = resizeEvent widget.setText = setText @@ -238,8 +238,8 @@ def set_font_size() -> None: font_sizes = [ get_max_font_size_cached( text, - widget.height(), widget.width(), + widget.height(), ) for text in combo_options ] @@ -298,13 +298,9 @@ def unpatch_widget(widget: QtWidgets.QWidget) -> None: if not hasattr(widget.resizeEvent, "_patched_methods_"): return if isinstance(widget, QtWidgets.QComboBox): - return unpatch_combo_widget( - widget=widget, - ) + return unpatch_combo_widget(widget) elif hasattr(widget, "setText") and hasattr(widget, "text"): - return unpatch_text_widget( - widget=widget, - ) + return unpatch_text_widget(widget) else: raise TypeError("Somehow, we have a patched widget that is unpatchable.") diff --git a/typhos/positioner.py b/typhos/positioner.py index d6d46676..3963e3e4 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -395,7 +395,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: diff --git a/typhos/ui/widgets/positioner.ui b/typhos/ui/widgets/positioner.ui index 90a47ef6..1a081cfb 100644 --- a/typhos/ui/widgets/positioner.ui +++ b/typhos/ui/widgets/positioner.ui @@ -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 @@ -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 6924a82c..218f93a7 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -6,12 +6,12 @@ 0 0 - 824 + 655 162 - + 0 0 From 1b897f7d165545b4e40430f9d0a60225279b800c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 6 Jun 2024 22:46:05 +0000 Subject: [PATCH 14/27] ENH: allow combobox widget to take space left by missing limits --- typhos/positioner.py | 34 +- typhos/ui/widgets/positioner_row.ui | 600 +++++++++++++++------------- 2 files changed, 354 insertions(+), 280 deletions(-) diff --git a/typhos/positioner.py b/typhos/positioner.py index 3963e3e4..1ef4a5df 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -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.05, min_size=6) + 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.""" @@ -421,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) @@ -428,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): @@ -460,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): @@ -917,6 +926,9 @@ class _TyphosPositionerRowUI(_TyphosPositionerUI): status_error_prefix: QtWidgets.QLabel error_prefix: QtWidgets.QLabel switcher: TyphosDisplaySwitcher + high_limit_frame: QtWidgets.QFrame + low_limit_frame: QtWidgets.QFrame + setpoint_frame: QtWidgets.QFrame class TyphosPositionerRowWidget(TyphosPositionerWidget): @@ -1063,6 +1075,26 @@ 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 and expand the setpoint widget + # Typically a combobox + self.ui.low_limit_frame.hide() + self.ui.high_limit_frame.hide() + low_policy = self.ui.low_limit_frame.sizePolicy() + high_policy = self.ui.high_limit_frame.sizePolicy() + setpoint_policy = self.ui.setpoint_frame.sizePolicy() + setpoint_policy.setHorizontalStretch( + setpoint_policy.horizontalStretch() + + low_policy.horizontalStretch() + + high_policy.horizontalStretch() + ) + self.ui.setpoint_frame.setSizePolicy(setpoint_policy) + def _get_tooltip(self): """Update the tooltip based on device information.""" # Lifted from TyphosHelpFrame diff --git a/typhos/ui/widgets/positioner_row.ui b/typhos/ui/widgets/positioner_row.ui index 218f93a7..12c156df 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -11,7 +11,7 @@ - + 0 0 @@ -150,7 +150,7 @@ 64 - + 3 @@ -377,96 +377,120 @@ - - - 0 + + + + 4 + 0 + - - - - - 0 - 0 - - - - - 150 - 50 - - - - - 16777215 - 50 - - - - - 16 - 50 - false - false - - - - - - - - - - - - - user_readback - - - Qt::AlignCenter - - - true - - - false - - - - + + + 0 + + + + + + 0 + 0 + + + + + 150 + 50 + + + + + 16777215 + 50 + + + + + 16 + 50 + false + false + + + + + + + + + + + + + user_readback + + + Qt::AlignCenter + + + true + + + false + + + + + - - - 0 - - - 0 + + + + 1 + 0 + - - - - - 0 - 0 - - - - - 25 - 25 - - - - - 25 - 25 - - - - - - - + + + 0 + + + 0 + + + 0 + + + 0 + + + 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 @@ -477,87 +501,88 @@ 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 - - - - - 16777215 - 25 - - - - - 8 - - - - - - - background-color: None; color: None; - - - - - - Qt::AlignCenter - - - 1 - - - false - - - false - - - - + + + false + + + + 255 + 150 + 0 + + + + + 50 + 50 + 50 + + + + false + + + true + + + + + + + + 0 + 0 + + + + + 40 + 25 + + + + + 16777215 + 25 + + + + + 8 + + + + + + + background-color: None; color: None; + + + + + + Qt::AlignCenter + + + 1 + + + false + + + false + + + + + - 0 + 2 0 @@ -715,38 +740,54 @@ - - - 0 - - - 0 + + + + 1 + 0 + - - - - - 0 - 0 - - - - - 25 - 25 - - - - - 25 - 25 - - - - - - - + + + 0 + + + 0 + + + 0 + + + 0 + + + 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 @@ -757,90 +798,91 @@ 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 - - - - - 16777215 - 25 - - - - - 8 - - - - - - - background-color: None; color: None; - - - - - - Qt::AlignCenter - - - 1 - - - false - - - false - - - - + + + margin: 0px; + + + false + + + + 255 + 150 + 0 + + + + + 50 + 50 + 50 + + + + Qt::Vertical + + + false + + + true + + + 0 + + + + + + + + 0 + 0 + + + + + 40 + 25 + + + + + 16777215 + 25 + + + + + 8 + + + + + + + background-color: None; color: None; + + + + + + Qt::AlignCenter + + + 1 + + + false + + + false + + + + + From 46d63b8396d04c64276ed0b0600767af03712340 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 6 Jun 2024 22:56:57 +0000 Subject: [PATCH 15/27] ENH: attempts to normalize sizing between forms, stop making combobox big for square widget --- typhos/positioner.py | 27 +++++++++++++++++---------- typhos/ui/widgets/positioner_row.ui | 8 ++++---- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/typhos/positioner.py b/typhos/positioner.py index 1ef4a5df..b8bd0c96 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -516,15 +516,6 @@ def _define_setpoint_widget(self): self.ui.set_value.activated.connect(self.set) self.ui.set_value.setMinimumContentsLength(20) self.ui.tweak_widget.setVisible(False) - # Consume the space left by removing the tweak widget - self.ui.set_value.setMinimumHeight( - self.ui.user_setpoint.minimumHeight() - + self.ui.tweak_value.minimumHeight() - ) - self.ui.set_value.setMaximumHeight( - self.ui.user_setpoint.maximumHeight() - + self.ui.tweak_value.maximumHeight() - ) else: self.ui.set_value = QtWidgets.QLineEdit() self.ui.set_value.setAlignment(QtCore.Qt.AlignCenter) @@ -1081,7 +1072,8 @@ def add_device(self, device: ophyd.Device) -> None: self._show_lowtrav, self._show_hightrav, )): - # Hide the limit sections and expand the setpoint widget + # Hide the limit sections + # Expand the setpoint widget horizontally # Typically a combobox self.ui.low_limit_frame.hide() self.ui.high_limit_frame.hide() @@ -1094,6 +1086,12 @@ def add_device(self, device: ophyd.Device) -> None: + high_policy.horizontalStretch() ) self.ui.setpoint_frame.setSizePolicy(setpoint_policy) + self.ui.set_value.setMinimumWidth( + self.ui.set_value.minimumWidth() + + self.ui.low_limit.minimumWidth() + + self.ui.high_limit.minimumWidth() + + 2 * self.ui.row_frame.layout().spacing() + ) def _get_tooltip(self): """Update the tooltip based on device information.""" @@ -1137,6 +1135,15 @@ def _define_setpoint_widget(self): 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() + ) + self.ui.set_value.setMaximumHeight( + self.ui.user_setpoint.maximumHeight() + + self.ui.tweak_value.maximumHeight() + ) else: dynamic_font.patch_widget(self.ui.set_value, pad_percent=0.1, min_size=4) diff --git a/typhos/ui/widgets/positioner_row.ui b/typhos/ui/widgets/positioner_row.ui index 12c156df..0b801487 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -379,7 +379,7 @@ - + 4 0 @@ -445,7 +445,7 @@ - + 1 0 @@ -581,7 +581,7 @@ - + 2 0 @@ -742,7 +742,7 @@ - + 1 0 From 1036b83d72b9491cb779336b701e8a9612aa8ca0 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 6 Jun 2024 23:36:06 +0000 Subject: [PATCH 16/27] TST: add combobox test to dynamic text --- typhos/tests/test_dynamic_font.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/typhos/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index 007c7ae0..ac100d20 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -19,6 +19,7 @@ [ QtWidgets.QLabel, QtWidgets.QPushButton, + QtWidgets.QComboBox, PyDMLabel, ] ) @@ -28,7 +29,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) @@ -55,11 +59,12 @@ def test_patching( 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 - 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 + # 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) From ac1444ed437231eedd767df458c0546eaf9ba12f Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 7 Jun 2024 20:01:46 +0000 Subject: [PATCH 17/27] DOC: pre-release notes for 611 --- .../611-enh_positioner_resizing.rst | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/source/upcoming_release_notes/611-enh_positioner_resizing.rst 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..d5f31e03 --- /dev/null +++ b/docs/source/upcoming_release_notes/611-enh_positioner_resizing.rst @@ -0,0 +1,23 @@ +611 enh_positioner_resizing +################# + +API Breaks +---------- +- N/A + +Features +-------- +- Implement dynamic resizing in all directions for positioner widgets. +- Make positioner widgets more vertically compact. + +Bugfixes +-------- +- Fix various issues that cause font clipping for specific motors using the positioner row widget. + +Maintenance +----------- +- N/A + +Contributors +------------ +- zllentz From 5aea3db3eec2f31a3665fe5e23a148d408dc226c Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Sat, 8 Jun 2024 00:59:20 +0000 Subject: [PATCH 18/27] WIP: somewhat stable but incomplete resizing rework --- typhos/positioner.py | 88 +- typhos/ui/devices/PositionerBase.embedded.ui | 16 +- typhos/ui/widgets/positioner_row.ui | 1941 +++++++++--------- 3 files changed, 1057 insertions(+), 988 deletions(-) diff --git a/typhos/positioner.py b/typhos/positioner.py index b8bd0c96..69f7b157 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -912,14 +912,13 @@ class _TyphosPositionerRowUI(_TyphosPositionerUI): """Annotations helper for positioner_row.ui; not to be instantiated.""" notes_edit: TyphosNotesEdit - status_container_widget: QtWidgets.QFrame + status_container_widget: QtWidgets.QWidget extended_signal_panel: Optional[TyphosSignalPanel] - status_error_prefix: QtWidgets.QLabel - error_prefix: QtWidgets.QLabel switcher: TyphosDisplaySwitcher - high_limit_frame: QtWidgets.QFrame - low_limit_frame: QtWidgets.QFrame - setpoint_frame: QtWidgets.QFrame + 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): @@ -942,12 +941,19 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) 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) - - 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.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) + + # 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 # TODO move these out self._omit_names = [ @@ -964,6 +970,7 @@ def __init__(self, *args, **kwargs): self.ui.extended_signal_panel = None self.ui.expand_button.clicked.connect(self._expand_layout) self.ui.status_label.setText("") + self._pre_signal_panel_min_height = self.minimumHeight() # TODO: ${name} / macros don't expand here @@ -1036,9 +1043,9 @@ def _expand_layout(self) -> None: """Toggle the expansion of the signal panel.""" if self.ui.extended_signal_panel is None: self.ui.extended_signal_panel = self._create_signal_panel() + self._extended_signal_panel_height = self.ui.extended_signal_panel.height() if self.ui.extended_signal_panel is None: return - to_show = True else: to_show = not self.ui.extended_signal_panel.isVisible() @@ -1047,8 +1054,13 @@ def _expand_layout(self) -> None: if to_show: self.ui.expand_button.setText('v') + self.setMinimumHeight( + self._pre_signal_panel_min_height + + self._extended_signal_panel_height + ) else: self.ui.expand_button.setText('>') + self.setMinimumHeight(self._pre_signal_panel_min_height) def add_device(self, device: ophyd.Device) -> None: """Add (or rather set) the ophyd device for this positioner.""" @@ -1073,25 +1085,28 @@ def add_device(self, device: ophyd.Device) -> None: self._show_hightrav, )): # Hide the limit sections - # Expand the setpoint widget horizontally - # Typically a combobox - self.ui.low_limit_frame.hide() - self.ui.high_limit_frame.hide() - low_policy = self.ui.low_limit_frame.sizePolicy() - high_policy = self.ui.high_limit_frame.sizePolicy() - setpoint_policy = self.ui.setpoint_frame.sizePolicy() - setpoint_policy.setHorizontalStretch( - setpoint_policy.horizontalStretch() - + low_policy.horizontalStretch() - + high_policy.horizontalStretch() - ) - self.ui.setpoint_frame.setSizePolicy(setpoint_policy) - self.ui.set_value.setMinimumWidth( - self.ui.set_value.minimumWidth() - + self.ui.low_limit.minimumWidth() - + self.ui.high_limit.minimumWidth() - + 2 * self.ui.row_frame.layout().spacing() - ) + self.ui.low_limit_widget.hide() + self.ui.high_limit_widget.hide() + + """ + # Trying to get the tweak widget to resize... + if isinstance(self.ui.set_value, QtWidgets.QLineEdit): + # Keep font sizing consistent among set/tweak widgets + def setFont(font: QtGui.QFont): + self.ui.tweak_value.setFont(font) + self.ui.tweak_negative.setFont(font) + self.ui.tweak_positive.setFont(font) + return orig_set_font(font) + orig_set_font = self.ui.set_value.setFont + self.ui.set_value.setFont = setFont + # The tweak entry widget has trouble changing its size, do it manually + def resizeEvent(event: QtGui.QResizeEvent): + rval = orig_resize(event) + self.ui.tweak_value.setMinimumHeight(event.size().height() // 2 - 1) + return rval + orig_resize = self.ui.setpoint_widget.resizeEvent + self.ui.setpoint_widget.resizeEvent = resizeEvent + """ def _get_tooltip(self): """Update the tooltip based on device information.""" @@ -1123,7 +1138,6 @@ 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) @@ -1140,10 +1154,6 @@ def _define_setpoint_widget(self): self.ui.user_setpoint.minimumHeight() + self.ui.tweak_value.minimumHeight() ) - self.ui.set_value.setMaximumHeight( - self.ui.user_setpoint.maximumHeight() - + self.ui.tweak_value.maximumHeight() - ) else: dynamic_font.patch_widget(self.ui.set_value, pad_percent=0.1, min_size=4) @@ -1196,16 +1206,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): 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_row.ui b/typhos/ui/widgets/positioner_row.ui index 0b801487..a38148fa 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -6,20 +6,20 @@ 0 0 - 655 - 162 + 681 + 125 - + 0 0 - 655 - 0 + 0 + 125 @@ -45,396 +45,238 @@ 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 - - - - - - 30 - 30 - - - - - 30 - 30 - - - - - - - 1.000000000000000 - - - - - - - - 0 - 0 - - - - - 50 - 25 - - - - - 50 - 25 - - - - alarm - - - Qt::AlignCenter - - - true - - - - - - - - - 0 - - - 0 - - - - - - 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 - - - - - - - - + - - 4 + + 1 0 - + + + 42 + 0 + + + + + 16777215 + 16777215 + + + - 0 + 1 + + + 3 + + + 3 - - + + 3 + + + 3 + + + - + 0 - 0 + 2 - 150 - 50 - - - - - 16777215 - 50 + 30 + 30 - - - 16 - 50 - false - false - - - - + + 1.000000000000000 - - + + + + + + + 0 + 1 + + + + + 36 + 15 + + + + + 16777215 + 16777215 + - user_readback + alarm Qt::AlignCenter - - true - - + false @@ -443,47 +285,59 @@ - + - + 1 0 - + + + 42 + 0 + + + + + 16777215 + 16777215 + + + - 0 + 1 - 0 + 3 - 0 + 3 - 0 + 3 - 0 + 3 - - + + - + 0 - 0 + 2 - 25 - 25 + 30 + 30 - 25 - 25 + 16777215 + 16777215 @@ -502,75 +356,81 @@ The channel to be used by the widget. + + false + false + + + + + + - 255 - 150 - 0 + 20 + 100 + 239 - 50 - 50 - 50 + 60 + 60 + 60 false + + false + true + + 1 + + + 0 + + + + Bit 0 + + - - + + - + 0 - 0 + 1 - 40 - 25 + 36 + 15 16777215 - 25 + 16777215 - - - 8 - - - - - - - background-color: None; color: None; - - + motion Qt::AlignCenter - - 1 - - - false - - + false @@ -579,17 +439,17 @@ - + - - 2 + + 7 0 150 - 0 + 50 @@ -598,9 +458,54 @@ 16777215 - + + + 16 + 50 + false + false + + + + + + + + + + + + + user_readback + + + Qt::AlignCenter + + + true + + + false + + + + + + + + 5 + 0 + + + + + 248 + 0 + + + - 0 + 3 0 @@ -615,121 +520,476 @@ 0 - - - 0 - - - 0 - - - 0 - - - 0 + + + + 1 + 0 + - - - - - 0 - 0 - - - - - 100 - 30 - - - - - 16777215 - 30 - - - - - - - Qt::AlignCenter - - - false - - - - - - - - + 0 - - QLayout::SetDefaultConstraint - - 0 + 3 - 1 + 3 - 0 + 3 - 1 + 3 - - - - - 25 - 25 - - - - - - - - - + - + 0 - 0 + 1 - 100 - 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. + + + + false + + + + 255 + 150 + 0 + + + + + 50 + 50 + 50 + + + + false + + + true + + + + + + + + 0 + 1 + + + + + 40 + 23 + + + + + 16777215 + 16777215 + + + 8 + + + + + + + background-color: None; color: None; + + + + Qt::AlignCenter + + 1 + + + false + + + false + + + + + + + + + + + 4 + 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 + 1 + + + + + 0 + 25 + + + + + 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 + + + 3 + + + 3 + + + 3 + - + + + + 0 + 1 + + 25 - 25 + 23 + + + + + 16777215 + 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. + + + + margin: 0px; + + + false + + + + 255 + 150 + 0 + + + + + 50 + 50 + 50 + + + + Qt::Vertical + + + false + + + true + + + 0 + + + + + + + + 0 + 1 + + + + + 40 + 23 + + + + + 16777215 + 16777215 + + + + + 8 + + + + + + + background-color: None; color: None; + - + + + + + Qt::AlignCenter + + + 1 + + + false + + + false @@ -740,16 +1000,198 @@ - + + + + 2 + 0 + + + + + 164 + 0 + + + + + 9 + + + + + + 0 + 0 + + + + + 70 + 34 + + + + + 16777215 + 16777215 + + + + + 75 + true + + + + 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 + + + + + - - 1 + + 14 0 - + + + 0 + 26 + + + - 0 + 3 0 @@ -763,529 +1205,138 @@ 0 - - + + - - 0 + + 1 0 - 25 - 25 + 0 + 26 - 25 + 16777215 25 - - - - - - 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. - - - margin: 0px; + QLabel { color: black; background-color: none; border-radius: 0px; } - + + (Status label) + + false - - - 255 - 150 - 0 - + + 0 - - - 50 - 50 - 50 - + + + + + + + 1 + 0 + - - Qt::Vertical + + + 0 + 26 + - - false + + + 16777215 + 25 + - + + + + + color: black; background-color: none; border-radius: 0px; + + + + + true - + 0 + + PyDMLabel::String + - - + + + + + + + + 2 + 0 + + + + + 0 + + + 0 + + + - - 0 + + 2 0 - 40 + 146 25 16777215 - 25 + 16777215 - 8 + 75 + true - background-color: None; color: None; + - - - - Qt::AlignCenter - - - 1 - - - false - - - false + Clear Error - - - - 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 - - - - - - 0 - 0 - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - color: red - - - Error: - - - - - - - - 0 - 0 - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - QLabel { color: black; background-color: none; border-radius: 0px; } - - - (Status label) - - - false - - - 0 - - - - - - - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - color: red - - - Error: - - - - - - - - 0 - 0 - - - - - 0 - 25 - - - - - 16777215 - 25 - - - - - - - color: black; background-color: none; border-radius: 0px; - - - - - - true - - - 0 - - - PyDMLabel::String - - - - - - - - - - - - 0 - 0 - - - - - 143 - 25 - - - - - 143 - 25 - - - - - 75 - true - - - - - - - - - - Clear Error - - - - - @@ -1308,7 +1359,7 @@ Screen
pydm.widgets.line_edit
- TyphosAlarmRectangle + TyphosAlarmCircle QWidget
typhos.alarm
From 3cfef7a888e3eee48d34581d2c7bee384b7de7ed Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 11 Jun 2024 00:00:39 +0000 Subject: [PATCH 19/27] ENH: rework row config display as floating window to dodge resize/flashing issues. --- typhos/positioner.py | 106 ++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/typhos/positioner.py b/typhos/positioner.py index 69f7b157..95e38060 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 @@ -913,7 +913,7 @@ class _TyphosPositionerRowUI(_TyphosPositionerUI): notes_edit: TyphosNotesEdit status_container_widget: QtWidgets.QWidget - extended_signal_panel: Optional[TyphosSignalPanel] + extended_signal_panel: Optional[RowDetails] switcher: TyphosDisplaySwitcher status_text_layout: None # Row UI doesn't use status_text_layout low_limit_widget: QtWidgets.QWidget @@ -949,12 +949,6 @@ def __init__(self, *args, **kwargs): 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) - # 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 - # TODO move these out self._omit_names = [ "motor_egu", @@ -970,7 +964,6 @@ def __init__(self, *args, **kwargs): self.ui.extended_signal_panel = None self.ui.expand_button.clicked.connect(self._expand_layout) self.ui.status_label.setText("") - self._pre_signal_panel_min_height = self.minimumHeight() # TODO: ${name} / macros don't expand here @@ -1031,19 +1024,12 @@ 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) def _expand_layout(self) -> None: """Toggle the expansion of the signal panel.""" if self.ui.extended_signal_panel is None: self.ui.extended_signal_panel = self._create_signal_panel() - self._extended_signal_panel_height = self.ui.extended_signal_panel.height() if self.ui.extended_signal_panel is None: return to_show = True @@ -1052,16 +1038,6 @@ def _expand_layout(self) -> None: self.ui.extended_signal_panel.setVisible(to_show) - if to_show: - self.ui.expand_button.setText('v') - self.setMinimumHeight( - self._pre_signal_panel_min_height - + self._extended_signal_panel_height - ) - else: - self.ui.expand_button.setText('>') - self.setMinimumHeight(self._pre_signal_panel_min_height) - def add_device(self, device: ophyd.Device) -> None: """Add (or rather set) the ophyd device for this positioner.""" super().add_device(device) @@ -1088,26 +1064,6 @@ def add_device(self, device: ophyd.Device) -> None: self.ui.low_limit_widget.hide() self.ui.high_limit_widget.hide() - """ - # Trying to get the tweak widget to resize... - if isinstance(self.ui.set_value, QtWidgets.QLineEdit): - # Keep font sizing consistent among set/tweak widgets - def setFont(font: QtGui.QFont): - self.ui.tweak_value.setFont(font) - self.ui.tweak_negative.setFont(font) - self.ui.tweak_positive.setFont(font) - return orig_set_font(font) - orig_set_font = self.ui.set_value.setFont - self.ui.set_value.setFont = setFont - # The tweak entry widget has trouble changing its size, do it manually - def resizeEvent(event: QtGui.QResizeEvent): - rval = orig_resize(event) - self.ui.tweak_value.setMinimumHeight(event.size().height() // 2 - 1) - return rval - orig_resize = self.ui.setpoint_widget.resizeEvent - self.ui.setpoint_widget.resizeEvent = resizeEvent - """ - def _get_tooltip(self): """Update the tooltip based on device information.""" # Lifted from TyphosHelpFrame @@ -1227,3 +1183,59 @@ 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 + + def __init__(self, row: TyphosPositionerRowWidget, parent: QtWidgets.QWidget | None = None): + super().__init__(parent=parent) + self.row = row + + self.label = QtWidgets.QLabel(parent=self) + 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(parent=self) + self.panel.omitNames = row.get_names_to_omit() + self.panel.sortBy = SignalOrder.byName + self.panel.add_device(row.device) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) + layout.addWidget(self.panel) + + self.setLayout(layout) + + 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), + ) + ) + ) + + return super().showEvent(event) From 01129251dc99e7974cf51defb26e3061c21644b2 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Tue, 11 Jun 2024 00:05:49 +0000 Subject: [PATCH 20/27] ENH: allocate proportionally less expansion to set/tweak to make limits more readable for large positions --- typhos/ui/widgets/positioner_row.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typhos/ui/widgets/positioner_row.ui b/typhos/ui/widgets/positioner_row.ui index a38148fa..4af238b5 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -659,7 +659,7 @@ - 4 + 1 0 From 58b3bae03e31eeb901f05c96af5a8c95f738b4f3 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 12 Jun 2024 00:36:33 +0000 Subject: [PATCH 21/27] FIX: magic scroll area resizing --- typhos/positioner.py | 55 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/typhos/positioner.py b/typhos/positioner.py index 95e38060..41f31986 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -1190,12 +1190,13 @@ 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): super().__init__(parent=parent) self.row = row - self.label = QtWidgets.QLabel(parent=self) + self.label = QtWidgets.QLabel() self.label.setText(row.ui.device_name_label.text()) font = self.label.font() font.setPointSize(font.pointSize() + 4) @@ -1204,16 +1205,31 @@ def __init__(self, row: TyphosPositionerRowWidget, parent: QtWidgets.QWidget | N QtGui.QFontMetrics(font).boundingRect(self.label.text()).height() ) - self.panel = TyphosSignalPanel(parent=self) + 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.panel) + 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): """ @@ -1237,5 +1253,36 @@ def showEvent(self, event: QtGui.QShowEvent): ) ) ) - + 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 From bd0780de0e8367e6c5f69a2114f8c32e927adeaa Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 12 Jun 2024 17:27:20 +0000 Subject: [PATCH 22/27] DOC: revise pre-release docs --- .../upcoming_release_notes/611-enh_positioner_resizing.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/upcoming_release_notes/611-enh_positioner_resizing.rst b/docs/source/upcoming_release_notes/611-enh_positioner_resizing.rst index d5f31e03..7190ca19 100644 --- a/docs/source/upcoming_release_notes/611-enh_positioner_resizing.rst +++ b/docs/source/upcoming_release_notes/611-enh_positioner_resizing.rst @@ -7,8 +7,10 @@ API Breaks Features -------- -- Implement dynamic resizing in all directions for positioner widgets. -- Make positioner widgets more vertically compact. +- 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 -------- From 63bd144f45df1714017759f16afdabc09b028cd9 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 12 Jun 2024 20:51:50 +0000 Subject: [PATCH 23/27] ENH: ensure the details window gets closed when the spawning window closes --- typhos/positioner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typhos/positioner.py b/typhos/positioner.py index 41f31986..b10f405a 100644 --- a/typhos/positioner.py +++ b/typhos/positioner.py @@ -1024,7 +1024,7 @@ def _create_signal_panel(self) -> Optional[TyphosSignalPanel]: if self.device is None: return None - return RowDetails(row=self) + return RowDetails(row=self, parent=self, flags=QtCore.Qt.Window) def _expand_layout(self) -> None: """Toggle the expansion of the signal panel.""" @@ -1192,8 +1192,8 @@ class RowDetails(QtWidgets.QWidget): row: TyphosPositionerRowWidget resize_timer: QtCore.QTimer - def __init__(self, row: TyphosPositionerRowWidget, parent: QtWidgets.QWidget | None = None): - super().__init__(parent=parent) + def __init__(self, row: TyphosPositionerRowWidget, parent: QtWidgets.QWidget | None = None, **kwargs): + super().__init__(parent=parent, **kwargs) self.row = row self.label = QtWidgets.QLabel() From 68f28cbd52ac7a71af34c83a167aceab34afbc15 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 12 Jun 2024 20:55:41 +0000 Subject: [PATCH 24/27] DOC: update widget dynamic font resizing docs --- typhos/dynamic_font.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index 09ed01f9..7bf6e7a5 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -118,6 +118,15 @@ def patch_widget( """ 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 @@ -162,7 +171,13 @@ def patch_text_widget( min_size: float | None = None, ): """ - Specific patching for widgets with text() and setText() methods + 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 = widget.font() @@ -228,7 +243,13 @@ def patch_combo_widget( min_size: float | None = None, ): """ - Specific patching for combobox widgets + 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: font = widget.font() From 1deb854a1a6a0e40dc2b6431c9deaac4220548f9 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Wed, 12 Jun 2024 22:03:52 +0000 Subject: [PATCH 25/27] FIX: also patch the font size into the stylesheet, add test, remove defunt font-size warning --- typhos/dynamic_font.py | 117 ++++++++++++++++++++-------- typhos/tests/test_dynamic_font.py | 27 ++++++- typhos/ui/widgets/positioner_row.ui | 4 +- 3 files changed, 113 insertions(+), 35 deletions(-) diff --git a/typhos/dynamic_font.py b/typhos/dynamic_font.py index 7bf6e7a5..a084c590 100644 --- a/typhos/dynamic_font.py +++ b/typhos/dynamic_font.py @@ -140,11 +140,6 @@ def patch_widget( min_size : float or None, optional The minimum font point size we're allowed to apply to the widget. """ - if "font-size" in widget.styleSheet(): - logger.warning( - f"Widget named {widget.objectName()} has a fixed size from its " - "stylesheet, and cannot be resized dynamically." - ) if isinstance(widget, QtWidgets.QComboBox): return patch_combo_widget( widget=widget, @@ -163,6 +158,31 @@ def patch_widget( 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, *, @@ -180,24 +200,17 @@ def patch_text_widget( The text is immediately resized for the first time during this function call. """ def set_font_size() -> None: - font = widget.font() font_size = get_max_font_size_cached( widget.text(), widget.width(), widget.height(), ) - # 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 - if abs(font.pointSizeF() - font_size) > delta: - font.setPointSizeF(font_size) - widget.setFont(font) + 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 @@ -252,7 +265,6 @@ def patch_combo_widget( The text is immediately resized for the first time during this function call. """ def set_font_size() -> None: - font = widget.font() combo_options = [ widget.itemText(index) for index in range(widget.count()) ] @@ -264,19 +276,12 @@ def set_font_size() -> None: ) for text in combo_options ] - font_size = min(font_sizes) - # 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 - if abs(font.pointSizeF() - font_size) > delta: - font.setPointSizeF(font_size) - widget.setFont(font) + 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 @@ -316,6 +321,7 @@ 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): @@ -354,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/tests/test_dynamic_font.py b/typhos/tests/test_dynamic_font.py index ac100d20..c4c253f8 100644 --- a/typhos/tests/test_dynamic_font.py +++ b/typhos/tests/test_dynamic_font.py @@ -9,7 +9,8 @@ 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__) @@ -74,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/widgets/positioner_row.ui b/typhos/ui/widgets/positioner_row.ui index 4af238b5..2747c2ac 100644 --- a/typhos/ui/widgets/positioner_row.ui +++ b/typhos/ui/widgets/positioner_row.ui @@ -1044,7 +1044,7 @@
- padding: 2px; margin: 0px; background-color: red + QPushButton { padding: 2px; margin: 0px; background-color: red } Stop @@ -1263,7 +1263,7 @@ Screen - color: black; background-color: none; border-radius: 0px; + PyDMLabel { color: black; background-color: none; border-radius: 0px; } From 0afb0ef9008db0ec7056ea76deb5bdf848689877 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 13 Jun 2024 23:30:38 +0000 Subject: [PATCH 26/27] FIX: no more clipping! Dynamic panel resizing! Much rejoicing! --- typhos/panel.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/typhos/panel.py b/typhos/panel.py index 849d0628..dcfafd4b 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,12 @@ def open_context_menu(self, ev): menu = self.generate_context_menu() menu.exec_(self.mapToGlobal(ev.pos())) + def resizeEvent(self, event: QtGui.QResizeEvent): + if self.nested_panel: + # force this widget's container to give it enough space! + self.parent().setMinimumHeight(self.parent().minimumSizeHint().height()) + return super().resizeEvent(event) + class CompositeSignalPanel(SignalPanel): """ From b605ca7855699b3addca28bba059f8223d4847a8 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 13 Jun 2024 23:54:08 +0000 Subject: [PATCH 27/27] FIX: partial fix, top-level widgets resize properly when emptied now --- typhos/panel.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/typhos/panel.py b/typhos/panel.py index dcfafd4b..92240ff5 100644 --- a/typhos/panel.py +++ b/typhos/panel.py @@ -865,12 +865,31 @@ def open_context_menu(self, ev): menu = self.generate_context_menu() menu.exec_(self.mapToGlobal(ev.pos())) - def resizeEvent(self, event: QtGui.QResizeEvent): + def maybe_fix_parent_size(self): if self.nested_panel: - # force this widget's container to give it enough space! + # 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): """