0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

fix: adjust height of list widget

This commit is contained in:
2025-06-10 14:59:34 +02:00
committed by David Perl
parent 5e4c129af6
commit 11131ef14c
6 changed files with 55 additions and 41 deletions

View File

@ -72,7 +72,7 @@ class TypedForm(BECWidget, QWidget):
FormItemSpec(name=name, item_type=item_type, pretty_display=pretty_display) FormItemSpec(name=name, item_type=item_type, pretty_display=pretty_display)
for name, item_type in items # type: ignore for name, item_type in items # type: ignore
] ]
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
self._layout = QVBoxLayout() self._layout = QVBoxLayout()
self._layout.setContentsMargins(0, 0, 0, 0) self._layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self._layout) self.setLayout(self._layout)
@ -80,11 +80,9 @@ class TypedForm(BECWidget, QWidget):
self._enabled: bool = enabled self._enabled: bool = enabled
self._form_grid_container = QWidget(parent=self) self._form_grid_container = QWidget(parent=self)
self._form_grid_container.setSizePolicy( self._form_grid_container.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding
)
self._form_grid = QWidget(parent=self._form_grid_container) self._form_grid = QWidget(parent=self._form_grid_container)
self._form_grid.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self._form_grid.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
self._layout.addWidget(self._form_grid_container) self._layout.addWidget(self._form_grid_container)
self._form_grid_container.setLayout(QVBoxLayout()) self._form_grid_container.setLayout(QVBoxLayout())
self._form_grid.setLayout(self._new_grid_layout()) self._form_grid.setLayout(self._new_grid_layout())
@ -113,7 +111,7 @@ class TypedForm(BECWidget, QWidget):
grid.addWidget(label, row, 0) grid.addWidget(label, row, 0)
widget = self._widget_from_type(item, self._widget_types)(parent=self, spec=item) widget = self._widget_from_type(item, self._widget_types)(parent=self, spec=item)
widget.valueChanged.connect(self.value_changed) widget.valueChanged.connect(self.value_changed)
widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
grid.addWidget(widget, row, 1) grid.addWidget(widget, row, 1)
def enumerate_form_widgets(self): def enumerate_form_widgets(self):
@ -139,7 +137,7 @@ class TypedForm(BECWidget, QWidget):
old_layout.deleteLater() old_layout.deleteLater()
self._form_grid.deleteLater() self._form_grid.deleteLater()
self._form_grid = QWidget() self._form_grid = QWidget()
self._form_grid.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self._form_grid.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)
self._form_grid.setLayout(self._new_grid_layout()) self._form_grid.setLayout(self._new_grid_layout())
self._form_grid_container.layout().addWidget(self._form_grid) self._form_grid_container.layout().addWidget(self._form_grid)

View File

@ -12,7 +12,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined from pydantic_core import PydanticUndefined
from qtpy import QtCore from qtpy import QtCore
from qtpy.QtCore import QSize, Signal # type: ignore from qtpy.QtCore import QSize, Qt, Signal # type: ignore
from qtpy.QtGui import QFontMetrics from qtpy.QtGui import QFontMetrics
from qtpy.QtWidgets import ( from qtpy.QtWidgets import (
QApplication, QApplication,
@ -372,9 +372,11 @@ class ListFormItem(DynamicFormItem):
else: else:
self._types = _ItemAndWidgetType(str, QLineEdit, "") self._types = _ItemAndWidgetType(str, QLineEdit, "")
super().__init__(parent=parent, spec=spec) super().__init__(parent=parent, spec=spec)
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self._main_widget: QListWidget self._main_widget: QListWidget
self._data = [] self._data = []
self.setFixedHeight(QFontMetrics(self.font()).height() * 6) self._min_lines = 2 if spec.pretty_display else 4
self._repop(self._data)
def sizeHint(self): def sizeHint(self):
default = super().sizeHint() default = super().sizeHint()
@ -383,6 +385,7 @@ class ListFormItem(DynamicFormItem):
def _add_main_widget(self) -> None: def _add_main_widget(self) -> None:
self._main_widget = QListWidget() self._main_widget = QListWidget()
self._layout.addWidget(self._main_widget) self._layout.addWidget(self._main_widget)
self._layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self._add_buttons() self._add_buttons()
def _add_buttons(self): def _add_buttons(self):
@ -407,11 +410,13 @@ class ListFormItem(DynamicFormItem):
self._main_widget.clear() self._main_widget.clear()
for val in data: for val in data:
self._add_list_item(val) self._add_list_item(val)
self.scale_to_data()
def _add_data_item(self, val=None): def _add_data_item(self, val=None):
val = val or self._types.default val = val or self._types.default
self._data.append(val) self._data.append(val)
self._add_list_item(val) self._add_list_item(val)
self._repop(self._data)
def _add_list_item(self, val): def _add_list_item(self, val):
item = QListWidgetItem(self._main_widget) item = QListWidgetItem(self._main_widget)
@ -453,15 +458,26 @@ class ListFormItem(DynamicFormItem):
self._data = list(value) self._data = list(value)
self._repop(self._data) self._repop(self._data)
def _line_height(self):
return QFontMetrics(self._main_widget.font()).height()
def set_max_height_in_lines(self, lines: int):
outer_inc = 1 if self._spec.pretty_display else 3
self._main_widget.setFixedHeight(self._line_height() * max(lines, self._min_lines))
self._button_holder.setFixedHeight(self._line_height() * (max(lines, self._min_lines) + 1))
self.setFixedHeight(self._line_height() * (max(lines, self._min_lines) + outer_inc))
def scale_to_data(self, *_):
self.set_max_height_in_lines(self._main_widget.count() + 1)
class SetFormItem(ListFormItem): class SetFormItem(ListFormItem):
def _add_main_widget(self) -> None: def _add_main_widget(self) -> None:
super()._add_main_widget() super()._add_main_widget()
self._add_item_field = self._types.widget() self._add_item_field = self._types.widget()
self._buttons.addWidget(QLabel("Add new:")) self._buttons.addWidget(QLabel("Add new:"))
self._buttons.addWidget(self._add_item_field) self._buttons.addWidget(self._add_item_field)
self.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding) self.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Minimum)
@SafeSlot() @SafeSlot()
def _add_row(self): def _add_row(self):

View File

@ -113,11 +113,13 @@ class DictBackedTableModel(QAbstractTableModel):
@SafeSlot() @SafeSlot()
def add_row(self): def add_row(self):
self.insertRow(self.rowCount()) self.insertRow(self.rowCount())
self.dataChanged.emit(self.index(self.rowCount(), 0), self.index(self.rowCount(), 1), 0)
@SafeSlot(list) @SafeSlot(list)
def delete_rows(self, rows: list[int]): def delete_rows(self, rows: list[int]):
# delete from the end so indices stay correct # delete from the end so indices stay correct
for row in sorted(rows, reverse=True): for row in sorted(rows, reverse=True):
self.dataChanged.emit(self.index(row, 0), self.index(row, 1), 0)
self.removeRows(row, 1, QModelIndex()) self.removeRows(row, 1, QModelIndex())
def set_default(self, value: dict | None): def set_default(self, value: dict | None):
@ -154,16 +156,20 @@ class DictBackedTable(QWidget):
self._layout = QHBoxLayout() self._layout = QHBoxLayout()
self.setLayout(self._layout) self.setLayout(self._layout)
self._layout.setContentsMargins(0, 0, 0, 0)
self._table_model = DictBackedTableModel(initial_data) self._table_model = DictBackedTableModel(initial_data)
self._table_view = QTreeView() self._table_view = QTreeView()
self._table_view.setModel(self._table_model) self._table_view.setModel(self._table_model)
self.set_min_height_in_lines(max(5, len(initial_data))) self._min_lines = 3
self.set_max_height_in_lines(len(initial_data)) self.set_height_in_lines(len(initial_data))
self._table_view.setSizePolicy( self._table_view.setSizePolicy(
QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
) )
self._table_view.setAlternatingRowColors(True) self._table_view.setAlternatingRowColors(True)
self._table_view.setUniformRowHeights(True)
self._table_view.setWordWrap(False)
self._table_view.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) self._table_view.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
self._table_view.header().setSectionResizeMode(5, QtWidgets.QHeaderView.Stretch) self._table_view.header().setSectionResizeMode(5, QtWidgets.QHeaderView.Stretch)
self.autoscale = autoscale_to_data self.autoscale = autoscale_to_data
@ -218,19 +224,15 @@ class DictBackedTable(QWidget):
keys (list[str]): list of keys which are forbidden.""" keys (list[str]): list of keys which are forbidden."""
self._table_model.update_disallowed_keys(keys) self._table_model.update_disallowed_keys(keys)
def set_min_height_in_lines(self, lines: int): def set_height_in_lines(self, lines: int):
self._min_lines = lines
self._table_view.setMinimumHeight(QFontMetrics(self._table_view.font()).height() * lines)
def set_max_height_in_lines(self, lines: int):
self._table_view.setMaximumHeight( self._table_view.setMaximumHeight(
QFontMetrics(self._table_view.font()).height() * max(lines, self._min_lines) int(QFontMetrics(self._table_view.font()).height() * max(lines + 2, self._min_lines))
) )
@SafeSlot() @SafeSlot()
@SafeSlot(dict) @SafeSlot(dict)
def scale_to_data(self, *_): def scale_to_data(self, *_):
self.set_max_height_in_lines(self._table_model.length()) self.set_height_in_lines(self._table_model.length())
@SafeProperty(bool) @SafeProperty(bool)
def autoscale(self): # type: ignore def autoscale(self): # type: ignore

View File

@ -134,9 +134,7 @@ class DeviceConfigDialog(BECWidget, QDialog):
self.client.device_manager is not None self.client.device_manager is not None
and self._device in self.client.device_manager.devices and self._device in self.client.device_manager.devices
): ):
dev = self.client.device_manager.devices.get(self._device) self._initial_config = self.client.device_manager.devices.get(self._device)._config
dev.read_configuration(cached=False)
self._initial_config = dev._config
def _fill_form(self): def _fill_form(self):
self._form.set_data(DeviceConfigModel.model_validate(self._initial_config)) self._form.set_data(DeviceConfigModel.model_validate(self._initial_config))
@ -146,8 +144,11 @@ class DeviceConfigDialog(BECWidget, QDialog):
diff = { diff = {
k: v for k, v in new_config.items() if self._initial_config.get(k) != new_config.get(k) k: v for k, v in new_config.items() if self._initial_config.get(k) != new_config.get(k)
} }
# TODO: replace when https://github.com/bec-project/bec/issues/528 is resolved
if diff.get("deviceConfig") is not None: if diff.get("deviceConfig") is not None:
# TODO: special cased in some parts of device manager but not others, should
# be removed in config update as with below issue
diff["deviceConfig"].pop("device_access", None)
# TODO: replace when https://github.com/bec-project/bec/issues/528 is resolved
diff["deviceConfig"] = { diff["deviceConfig"] = {
k: literal_eval(str(v)) for k, v in diff["deviceConfig"].items() k: literal_eval(str(v)) for k, v in diff["deviceConfig"].items()
} }

View File

@ -78,7 +78,6 @@ class DeviceItem(ExpandableGroupFrame):
@SafeSlot(popup_error=True) @SafeSlot(popup_error=True)
def _reload_config(self, *_): def _reload_config(self, *_):
self.dev[self.device].read_configuration(cached=False)
self.set_display_config(self.dev[self.device]._config) self.set_display_config(self.dev[self.device]._config)
def set_display_config(self, config_dict: dict): def set_display_config(self, config_dict: dict):
@ -113,6 +112,7 @@ class DeviceItem(ExpandableGroupFrame):
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
import sys import sys
from unittest.mock import MagicMock
from qtpy.QtWidgets import QApplication from qtpy.QtWidgets import QApplication
@ -125,22 +125,20 @@ if __name__ == "__main__": # pragma: no cover
widget = QWidget() widget = QWidget()
layout = QHBoxLayout() layout = QHBoxLayout()
widget.setLayout(layout) widget.setLayout(layout)
item = DeviceItem(widget, "Device") mock_config = {
"name": "Test Device",
"enabled": True,
"deviceClass": "FakeDeviceClass",
"deviceConfig": {"kwarg1": "value1"},
"readoutPriority": "baseline",
"description": "A device for testing out a widget",
"readOnly": True,
"softwareTrigger": False,
"deviceTags": {"tag1", "tag2", "tag3"},
"userParameter": {"some_setting": "some_ value"},
}
item = DeviceItem(widget, "Device", {"Device": MagicMock(enabled=True, _config=mock_config)})
layout.addWidget(DarkModeButton()) layout.addWidget(DarkModeButton())
layout.addWidget(item) layout.addWidget(item)
item.set_display_config(
{
"name": "Test Device",
"enabled": True,
"deviceClass": "FakeDeviceClass",
"deviceConfig": {"kwarg1": "value1"},
"readoutPriority": "baseline",
"description": "A device for testing out a widget",
"readOnly": True,
"softwareTrigger": False,
"deviceTags": {"tag1", "tag2", "tag3"},
"userParameter": {"some_setting": "some_ value"},
}
)
widget.show() widget.show()
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

@ -89,7 +89,6 @@ def test_device_item_expansion(device_browser, qtbot):
form = widget._contents.layout().itemAt(0).widget() form = widget._contents.layout().itemAt(0).widget()
qtbot.waitUntil(lambda: isinstance(form, DeviceConfigForm), timeout=500) qtbot.waitUntil(lambda: isinstance(form, DeviceConfigForm), timeout=500)
assert widget.expanded assert widget.expanded
device_browser.dev["samx"].read_configuration.assert_called()
assert (name_field := form.widget_dict.get("name")) is not None assert (name_field := form.widget_dict.get("name")) is not None
qtbot.waitUntil(lambda: name_field.getValue() == "samx", timeout=500) qtbot.waitUntil(lambda: name_field.getValue() == "samx", timeout=500)
qtbot.mouseClick(widget._expansion_button, Qt.MouseButton.LeftButton) qtbot.mouseClick(widget._expansion_button, Qt.MouseButton.LeftButton)