mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-12-31 11:11:17 +01:00
refactor(device-manager-view): cleanup, minor bugfixes
This commit is contained in:
@@ -143,6 +143,9 @@ class ConfigChoiceDialog(QDialog):
|
||||
return self._result
|
||||
|
||||
|
||||
AVAILABLE_RESOURCE_IS_READY = False
|
||||
|
||||
|
||||
class DeviceManagerView(BECWidget, QWidget):
|
||||
|
||||
def __init__(self, parent=None, *args, **kwargs):
|
||||
@@ -159,15 +162,6 @@ class DeviceManagerView(BECWidget, QWidget):
|
||||
self.dock_manager.setStyleSheet("")
|
||||
self._root_layout.addWidget(self.dock_manager)
|
||||
|
||||
# # Available Resources Widget
|
||||
# self.available_devices = AvailableDeviceResources(
|
||||
# self, shared_selection_signal=self._shared_selection
|
||||
# )
|
||||
# self.available_devices_dock = QtAds.CDockWidget(
|
||||
# self.dock_manager, "Available Devices", self
|
||||
# )
|
||||
# self.available_devices_dock.setWidget(self.available_devices)
|
||||
|
||||
# Device Table View widget
|
||||
self.device_table_view = DeviceTableView(
|
||||
self, shared_selection_signal=self._shared_selection
|
||||
@@ -204,11 +198,7 @@ class DeviceManagerView(BECWidget, QWidget):
|
||||
self.help_inspector_dock = QtAds.CDockWidget(self.dock_manager, "Help Inspector", self)
|
||||
self.help_inspector_dock.setWidget(widget)
|
||||
|
||||
# # Hook inspector signals
|
||||
# def _class_cb(text: str):
|
||||
# print(text)
|
||||
|
||||
# # Register callback
|
||||
# Register callback
|
||||
self.help_inspector.bec_widget_help.connect(text_box.setMarkdown)
|
||||
|
||||
# Error Logs View
|
||||
@@ -247,57 +237,63 @@ class DeviceManagerView(BECWidget, QWidget):
|
||||
self.dock_manager.addDockWidgetTabToArea(self.error_logs_dock, area)
|
||||
|
||||
for dock in self.dock_manager.dockWidgets():
|
||||
# dock.setFeature(CDockWidget.DockWidgetDeleteOnClose, True)#TODO implement according to MonacoDock or AdvancedDockArea
|
||||
# dock.setFeature(CDockWidget.CustomCloseHandling, True) #TODO same
|
||||
dock.setFeature(CDockWidget.DockWidgetClosable, False)
|
||||
dock.setFeature(CDockWidget.DockWidgetFloatable, False)
|
||||
dock.setFeature(CDockWidget.DockWidgetMovable, False)
|
||||
|
||||
# TODO decide if we like to hide the title bars..
|
||||
# Fetch all dock areas of the dock widgets (on our case always one dock area)
|
||||
# for dock in self.dock_manager.dockWidgets():
|
||||
# if dock.objectName() in ["Help Inspector", "Error Logs"]:
|
||||
# continue
|
||||
# area = dock.dockAreaWidget()
|
||||
# area.titleBar().setVisible(False)
|
||||
|
||||
# Apply stretch after the layout is done
|
||||
self.set_default_view([2, 8, 2], [7, 3])
|
||||
# self.set_default_view([2, 8, 2], [2, 2, 4])
|
||||
|
||||
# Connect slots
|
||||
for signal, slots in [
|
||||
(
|
||||
self.device_table_view.selected_devices,
|
||||
(self.dm_config_view.on_select_config, self.dm_docs_view.on_select_config),
|
||||
),
|
||||
# (
|
||||
# self.available_devices.selected_devices,
|
||||
# (self.dm_config_view.on_select_config, self.dm_docs_view.on_select_config),
|
||||
# ),
|
||||
(
|
||||
self.ophyd_test_view.device_validated,
|
||||
(self.device_table_view.update_device_validation,),
|
||||
),
|
||||
# (
|
||||
# self.device_table_view.device_configs_changed,
|
||||
# (
|
||||
# self.ophyd_test_view.change_device_configs,
|
||||
# self.available_devices.mark_devices_used,
|
||||
# ),
|
||||
# ),
|
||||
# (
|
||||
# self.available_devices.add_selected_devices,
|
||||
# (self.device_table_view.add_device_configs,),
|
||||
# ),
|
||||
# (
|
||||
# self.available_devices.del_selected_devices,
|
||||
# (self.device_table_view.remove_device_configs,),
|
||||
# ),
|
||||
(
|
||||
self.device_table_view.device_configs_changed,
|
||||
(self.ophyd_test_view.change_device_configs,),
|
||||
),
|
||||
]:
|
||||
for slot in slots:
|
||||
signal.connect(slot)
|
||||
|
||||
# Once available resource is ready, add it to the view again
|
||||
if AVAILABLE_RESOURCE_IS_READY:
|
||||
# Available Resources Widget
|
||||
self.available_devices = AvailableDeviceResources(
|
||||
self, shared_selection_signal=self._shared_selection
|
||||
)
|
||||
self.available_devices_dock = QtAds.CDockWidget(
|
||||
self.dock_manager, "Available Devices", self
|
||||
)
|
||||
self.available_devices_dock.setWidget(self.available_devices)
|
||||
# Connect slots for available reosource
|
||||
for signal, slots in [
|
||||
(
|
||||
self.available_devices.selected_devices,
|
||||
(self.dm_config_view.on_select_config, self.dm_docs_view.on_select_config),
|
||||
),
|
||||
(
|
||||
self.device_table_view.device_configs_changed,
|
||||
(self.available_devices.mark_devices_used,),
|
||||
),
|
||||
(
|
||||
self.available_devices.add_selected_devices,
|
||||
(self.device_table_view.add_device_configs,),
|
||||
),
|
||||
(
|
||||
self.available_devices.del_selected_devices,
|
||||
(self.device_table_view.remove_device_configs,),
|
||||
),
|
||||
]:
|
||||
for slot in slots:
|
||||
signal.connect(slot)
|
||||
|
||||
# Add toolbar
|
||||
self._add_toolbar()
|
||||
|
||||
def _add_toolbar(self):
|
||||
@@ -421,13 +417,6 @@ class DeviceManagerView(BECWidget, QWidget):
|
||||
# Add load config from plugin dir
|
||||
self.toolbar.add_bundle(table_bundle)
|
||||
|
||||
# Most likly, no actions on available devices
|
||||
# Actions (vielleicht bundle fuer available devices )
|
||||
# - reset composed view
|
||||
# - add new device (EpicsMotor, EpicsMotorECMC, EpicsSignal, CustomDevice)
|
||||
# - remove device
|
||||
# - rerun validation (with/without connect)
|
||||
|
||||
# IO actions
|
||||
def _coming_soon(self):
|
||||
return QMessageBox.question(
|
||||
@@ -441,7 +430,6 @@ class DeviceManagerView(BECWidget, QWidget):
|
||||
@SafeSlot()
|
||||
def _load_file_action(self):
|
||||
"""Action for the 'load' action to load a config from disk for the io_bundle of the toolbar."""
|
||||
# Check if plugin repo is installed...
|
||||
try:
|
||||
plugin_path = plugin_repo_path()
|
||||
plugin_name = plugin_package_name()
|
||||
@@ -542,7 +530,6 @@ class DeviceManagerView(BECWidget, QWidget):
|
||||
file.write(yaml.dump(config))
|
||||
|
||||
# Table actions
|
||||
|
||||
@SafeSlot()
|
||||
def _reset_composed_view(self):
|
||||
"""Action for the 'reset_composed_view' action to reset the composed view."""
|
||||
@@ -554,13 +541,10 @@ class DeviceManagerView(BECWidget, QWidget):
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.device_table_view.clear_device_configs()
|
||||
|
||||
# TODO We want to have a combobox to choose from EpicsMotor, EpicsMotorECMC, EpicsSignal, EpicsSignalRO, and maybe EpicsSignalWithRBV and custom Device
|
||||
# For all default Epics devices, we would like to preselect relevant fields, and prompt them with the proper deviceConfig args already, i.e. 'prefix', 'read_pv', 'write_pv' etc..
|
||||
# For custom Device, they should receive all options. It might be cool to get a side panel with docstring view of the class upon inspecting it to make it easier in case deviceConfig entries are required..
|
||||
# TODO Bespoke Form to add a new device
|
||||
@SafeSlot()
|
||||
def _add_device_action(self):
|
||||
"""Action for the 'add_device' action to add a new device."""
|
||||
# Implement the logic to add a new device
|
||||
dialog = PresetClassDeviceConfigDialog(parent=self)
|
||||
dialog.accepted_data.connect(self._add_to_table_from_dialog)
|
||||
dialog.open()
|
||||
@@ -574,13 +558,12 @@ class DeviceManagerView(BECWidget, QWidget):
|
||||
"""Action for the 'remove_device' action to remove a device."""
|
||||
self.device_table_view.remove_selected_rows()
|
||||
|
||||
# TODO implement proper logic for validation. We should also carefully review how these jobs update the table, and how we can cancel pending validations
|
||||
# in case they are no longer relevant. We might want to 'block' the interactivity on the items for which validation runs with 'connect'!
|
||||
@SafeSlot()
|
||||
def _rerun_validation_action(self):
|
||||
@SafeSlot(bool)
|
||||
def _rerun_validation_action(self, connect: bool = True):
|
||||
"""Action for the 'rerun_validation' action to rerun validation on selected devices."""
|
||||
configs = self.device_table_view.table.selected_configs()
|
||||
self.ophyd_test_view.change_device_configs(configs, True, True)
|
||||
self.ophyd_test_view.change_device_configs(configs, True, connect)
|
||||
|
||||
####### Default view has to be done with setting up splitters ########
|
||||
def set_default_view(
|
||||
|
||||
@@ -9,55 +9,64 @@ CONFIG_DATA_ROLE: Final[int] = 118
|
||||
|
||||
# TODO 882 keep in sync with headers in device_table_view.py
|
||||
HEADERS_HELP_MD: dict[str, str] = {
|
||||
"status": (
|
||||
"## Status"
|
||||
"\n"
|
||||
"The current status of the device. Can be one of the following values: \n"
|
||||
"### **LOADED** \n The device with the specified configuration is loaded in the current config.\n"
|
||||
"### **CONNECT_READY** \n The device config is valid and the connection has been validated. It has not yet been loaded to the current config.\n"
|
||||
"### **CONNECT_FAILED** \n The device config is valid, but the connection could not be established.\n"
|
||||
"### **VALID** \n The device config is valid, but the connection has not yet been validated.\n"
|
||||
"### **INVALID** \n The device config is invalid and can not be loaded to the current config.\n"
|
||||
"status": "\n".join(
|
||||
[
|
||||
"## Status",
|
||||
"The current status of the device. Can be one of the following values: ",
|
||||
"### **LOADED** \n The device with the specified configuration is loaded in the current config.",
|
||||
"### **CONNECT_READY** \n The device config is valid and the connection has been validated. It has not yet been loaded to the current config.",
|
||||
"### **CONNECT_FAILED** \n The device config is valid, but the connection could not be established.",
|
||||
"### **VALID** \n The device config is valid, but the connection has not yet been validated.",
|
||||
"### **INVALID** \n The device config is invalid and can not be loaded to the current config.",
|
||||
]
|
||||
),
|
||||
"name": ("## Name " "\n" "The name of the device."),
|
||||
"deviceClass": (
|
||||
"## Device Class"
|
||||
"\n"
|
||||
"The device class specifies the type of the device. It will be used to create the instance."
|
||||
"name": "\n".join(["## Name ", "The name of the device."]),
|
||||
"deviceClass": "\n".join(
|
||||
[
|
||||
"## Device Class",
|
||||
"The device class specifies the type of the device. It will be used to create the instance.",
|
||||
]
|
||||
),
|
||||
"readoutPriority": (
|
||||
"## Readout Priority"
|
||||
"\n"
|
||||
"The readout priority of the device. Can be one of the following values: \n"
|
||||
"### **monitored** \n The monitored priority is used for devices that are read out during the scan (i.e. at every step) and whose value may change during the scan.\n"
|
||||
"### **baseline** \n The baseline priority is used for devices that are read out at the beginning of the scan and whose value does not change during the scan.\n"
|
||||
"### **async** \n The async priority is used for devices that are asynchronous to the monitored devices, and send their data independently.\n"
|
||||
"### **continuous** \n The continuous priority is used for devices that are read out continuously during the scan.\n"
|
||||
"### **on_request** \n The on_request priority is used for devices that should not be read out during the scan, yet are configured to be read out manually.\n"
|
||||
"readoutPriority": "\n".join(
|
||||
[
|
||||
"## Readout Priority",
|
||||
"The readout priority of the device. Can be one of the following values: ",
|
||||
"### **monitored** \n The monitored priority is used for devices that are read out during the scan (i.e. at every step) and whose value may change during the scan.",
|
||||
"### **baseline** \n The baseline priority is used for devices that are read out at the beginning of the scan and whose value does not change during the scan.",
|
||||
"### **async** \n The async priority is used for devices that are asynchronous to the monitored devices, and send their data independently.",
|
||||
"### **continuous** \n The continuous priority is used for devices that are read out continuously during the scan.",
|
||||
"### **on_request** \n The on_request priority is used for devices that should not be read out during the scan, yet are configured to be read out manually.",
|
||||
]
|
||||
),
|
||||
"deviceTags": (
|
||||
"## Device Tags"
|
||||
"\n"
|
||||
"A list of tags associated with the device. Tags can be used to group devices and filter them in the device manager."
|
||||
"deviceTags": "\n".join(
|
||||
[
|
||||
"## Device Tags",
|
||||
"A list of tags associated with the device. Tags can be used to group devices and filter them in the device manager.",
|
||||
]
|
||||
),
|
||||
"enabled": (
|
||||
"## Enabled"
|
||||
"\n"
|
||||
"Indicator whether the device is enabled or disabled. Disabled devices can not be used."
|
||||
"enabled": "\n".join(
|
||||
[
|
||||
"## Enabled",
|
||||
"Indicator whether the device is enabled or disabled. Disabled devices can not be used.",
|
||||
]
|
||||
),
|
||||
"readOnly": ("## Read Only" "\n" "Indicator that a device is read-only or can be modified."),
|
||||
"onFailure": (
|
||||
"## On Failure"
|
||||
"\n"
|
||||
"Specifies the behavior of the device in case of a failure. Can be one of the following values: \n"
|
||||
"### **buffer** \n The device readback will fall back to the last known value.\n"
|
||||
"### **retry** \n The device readback will be retried once, and raises an error if it fails again.\n"
|
||||
"### **raise** \n The device readback will raise immediately.\n"
|
||||
"readOnly": "\n".join(
|
||||
["## Read Only", "Indicator that a device is read-only or can be modified."]
|
||||
),
|
||||
"softwareTrigger": (
|
||||
"## Software Trigger"
|
||||
"\n"
|
||||
"Indicator whether the device receives a software trigger from BEC during a scan."
|
||||
"onFailure": "\n".join(
|
||||
[
|
||||
"## On Failure",
|
||||
"Specifies the behavior of the device in case of a failure. Can be one of the following values: ",
|
||||
"### **buffer** \n The device readback will fall back to the last known value.",
|
||||
"### **retry** \n The device readback will be retried once, and raises an error if it fails again.",
|
||||
"### **raise** \n The device readback will raise immediately.",
|
||||
]
|
||||
),
|
||||
"description": ("## Description" "\n" "A short description of the device."),
|
||||
"softwareTrigger": "\n".join(
|
||||
[
|
||||
"## Software Trigger",
|
||||
"Indicator whether the device receives a software trigger from BEC during a scan.",
|
||||
]
|
||||
),
|
||||
"description": "\n".join(["## Description", "A short description of the device."]),
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import textwrap
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Iterable, List
|
||||
from typing import TYPE_CHECKING, Any, Iterable, List, Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from bec_lib.atlas_models import Device
|
||||
@@ -46,7 +46,13 @@ USER_CHECK_DATA_ROLE = 101
|
||||
class DictToolTipDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Delegate that shows all key-value pairs of a rows's data as a YAML-like tooltip."""
|
||||
|
||||
def helpEvent(self, event, view, option, index):
|
||||
def helpEvent(
|
||||
self,
|
||||
event: QtCore.QEvent,
|
||||
view: QtWidgets.QAbstractItemView,
|
||||
option: QtWidgets.QStyleOptionViewItem,
|
||||
index: QModelIndex,
|
||||
):
|
||||
"""Override to show tooltip when hovering."""
|
||||
if event.type() != QtCore.QEvent.Type.ToolTip:
|
||||
return super().helpEvent(event, view, option, index)
|
||||
@@ -64,13 +70,23 @@ class CustomDisplayDelegate(DictToolTipDelegate):
|
||||
def displayText(self, value: Any, locale: QtCore.QLocale | QtCore.QLocale.Language) -> str:
|
||||
return ""
|
||||
|
||||
def _test_custom_paint(self, painter, option, index):
|
||||
def _test_custom_paint(
|
||||
self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QModelIndex
|
||||
):
|
||||
v = index.model().data(index, self._paint_test_role)
|
||||
return (v is not None), v
|
||||
|
||||
def _do_custom_paint(self, painter, option, index, value): ...
|
||||
def _do_custom_paint(
|
||||
self,
|
||||
painter: QtGui.QPainter,
|
||||
option: QtWidgets.QStyleOptionViewItem,
|
||||
index: QModelIndex,
|
||||
value: Any,
|
||||
): ...
|
||||
|
||||
def paint(self, painter, option, index) -> None:
|
||||
def paint(
|
||||
self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QModelIndex
|
||||
) -> None:
|
||||
(check, value) = self._test_custom_paint(painter, option, index)
|
||||
if not check:
|
||||
return super().paint(painter, option, index)
|
||||
@@ -83,14 +99,34 @@ class CustomDisplayDelegate(DictToolTipDelegate):
|
||||
class WrappingTextDelegate(CustomDisplayDelegate):
|
||||
"""A lightweight delegate that wraps text without expensive size recalculation."""
|
||||
|
||||
def __init__(self, parent=None, max_width=300, margin=6):
|
||||
def __init__(self, parent: BECTableView | None = None, max_width: int = 300, margin: int = 6):
|
||||
super().__init__(parent)
|
||||
self._parent = parent
|
||||
self.max_width = max_width
|
||||
self.margin = margin
|
||||
self._cache = {} # cache text metrics for performance
|
||||
self._wrapping_text_columns = None
|
||||
|
||||
def _do_custom_paint(self, painter, option, index, value: str):
|
||||
@property
|
||||
def wrapping_text_columns(self) -> List[int]:
|
||||
# Compute once, cache for later
|
||||
if self._wrapping_text_columns is None:
|
||||
self._wrapping_text_columns = []
|
||||
view = self._parent
|
||||
proxy: DeviceFilterProxyModel = self._parent.model()
|
||||
for col in range(proxy.columnCount()):
|
||||
delegate = view.itemDelegateForColumn(col)
|
||||
if isinstance(delegate, WrappingTextDelegate):
|
||||
self._wrapping_text_columns.append(col)
|
||||
return self._wrapping_text_columns
|
||||
|
||||
def _do_custom_paint(
|
||||
self,
|
||||
painter: QtGui.QPainter,
|
||||
option: QtWidgets.QStyleOptionViewItem,
|
||||
index: QModelIndex,
|
||||
value: str,
|
||||
):
|
||||
text = str(value)
|
||||
if not text:
|
||||
return
|
||||
@@ -101,30 +137,39 @@ class WrappingTextDelegate(CustomDisplayDelegate):
|
||||
cache_key = (text, option.rect.width())
|
||||
layout = self._cache.get(cache_key)
|
||||
if layout is None:
|
||||
layout = QtGui.QTextLayout(text, option.font)
|
||||
layout.beginLayout()
|
||||
height = 0
|
||||
while True:
|
||||
line = layout.createLine()
|
||||
if not line.isValid():
|
||||
break
|
||||
line.setLineWidth(option.rect.width() - self.margin)
|
||||
line.setPosition(QtCore.QPointF(self.margin / 2, height))
|
||||
height += line.height()
|
||||
layout.endLayout()
|
||||
layout = self._compute_layout(text, option)
|
||||
self._cache[cache_key] = layout
|
||||
|
||||
# # Draw background if selected
|
||||
# if option.state & QtWidgets.QStyle.State_Selected:
|
||||
# painter.fillRect(option.rect, option.palette.highlight())
|
||||
|
||||
# Draw text
|
||||
painter.setPen(option.palette.text().color())
|
||||
layout.draw(painter, option.rect.topLeft())
|
||||
|
||||
painter.restore()
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
def _compute_layout(
|
||||
self, text: str, option: QtWidgets.QStyleOptionViewItem
|
||||
) -> QtGui.QTextLayout:
|
||||
"""Compute and return the text layout for given text and option."""
|
||||
layout = self._get_layout(text, option.font)
|
||||
layout.beginLayout()
|
||||
height = 0
|
||||
max_lines = 100 # safety cap, should never be more than 100 lines..
|
||||
for _ in range(max_lines):
|
||||
line = layout.createLine()
|
||||
if not line.isValid():
|
||||
break
|
||||
line.setLineWidth(option.rect.width() - self.margin)
|
||||
line.setPosition(QtCore.QPointF(self.margin / 2, height))
|
||||
line_height = line.height()
|
||||
if line_height <= 0:
|
||||
break # avoid negative or zero height lines to be added
|
||||
height += line_height
|
||||
layout.endLayout()
|
||||
return layout
|
||||
|
||||
def _get_layout(self, text: str, font_option: QtGui.QFont) -> QtGui.QTextLayout:
|
||||
return QtGui.QTextLayout(text, font_option)
|
||||
|
||||
def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QModelIndex) -> QtCore.QSize:
|
||||
"""Return a cached or approximate height; avoids costly recomputation."""
|
||||
text = str(index.data(QtCore.Qt.DisplayRole) or "")
|
||||
view = self._parent
|
||||
@@ -151,14 +196,19 @@ class WrappingTextDelegate(CustomDisplayDelegate):
|
||||
lines = len(wrapped_lines)
|
||||
return QtCore.QSize(pixel_width, lines * (metrics.height()) + 2 * self.margin)
|
||||
|
||||
def estimate_chars_per_line(self, text: str, option, column_width: int) -> int:
|
||||
def estimate_chars_per_line(
|
||||
self, text: str, option: QtWidgets.QStyleOptionViewItem, column_width: int
|
||||
) -> int:
|
||||
"""Estimate number of characters that fit in a line for given width."""
|
||||
metrics = option.fontMetrics
|
||||
elided = metrics.elidedText(text, Qt.ElideRight, column_width)
|
||||
return len(elided.rstrip("…"))
|
||||
|
||||
@SafeSlot(int, int, int)
|
||||
def _on_section_resized(self, logical_index, old_size=None, new_size=None):
|
||||
@SafeSlot(int)
|
||||
def _on_section_resized(
|
||||
self, logical_index: int, old_size: int | None = None, new_size: int | None = None
|
||||
):
|
||||
"""Only update rows if a wrapped column was resized."""
|
||||
self._cache.clear()
|
||||
self._update_row_heights()
|
||||
@@ -167,17 +217,12 @@ class WrappingTextDelegate(CustomDisplayDelegate):
|
||||
"""Efficiently adjust row heights based on wrapped columns."""
|
||||
view = self._parent
|
||||
proxy = view.model()
|
||||
model = proxy.sourceModel()
|
||||
option = QtWidgets.QStyleOptionViewItem()
|
||||
view.initViewItemOption(option)
|
||||
# wrapping delegates
|
||||
wrap_delegate_columns = []
|
||||
for row in range(proxy.rowCount()):
|
||||
max_height = 18
|
||||
for column in [5, 6]: # TODO 884 don't hardcode columns.. to be improved
|
||||
for column in self.wrapping_text_columns:
|
||||
index = proxy.index(row, column)
|
||||
# model_index = proxy.mapToSource(index)
|
||||
# delegate = view.itemDelegateForColumn(model_index) or view.itemDelegate()
|
||||
delegate = view.itemDelegateForColumn(column)
|
||||
hint = delegate.sizeHint(option, index)
|
||||
max_height = max(max_height, hint.height())
|
||||
@@ -190,7 +235,7 @@ class CenterCheckBoxDelegate(CustomDisplayDelegate):
|
||||
|
||||
_paint_test_role = USER_CHECK_DATA_ROLE
|
||||
|
||||
def __init__(self, parent=None, colors=None):
|
||||
def __init__(self, parent: BECTableView | None = None, colors: AccentColors | None = None):
|
||||
super().__init__(parent)
|
||||
colors: AccentColors = colors if colors else get_accent_colors() # type: ignore
|
||||
_icon = partial(material_icon, size=(16, 16), color=colors.default, filled=True)
|
||||
@@ -203,13 +248,27 @@ class CenterCheckBoxDelegate(CustomDisplayDelegate):
|
||||
self._icon_checked = _icon("check_box")
|
||||
self._icon_unchecked = _icon("check_box_outline_blank")
|
||||
|
||||
def _do_custom_paint(self, painter, option, index, value):
|
||||
def _do_custom_paint(
|
||||
self,
|
||||
painter: QtGui.QPainter,
|
||||
option: QtWidgets.QStyleOptionViewItem,
|
||||
index: QModelIndex,
|
||||
value: Literal[
|
||||
Qt.CheckState.Checked | Qt.CheckState.Unchecked | Qt.CheckState.PartiallyChecked
|
||||
],
|
||||
):
|
||||
pixmap = self._icon_checked if value == Qt.CheckState.Checked else self._icon_unchecked
|
||||
pix_rect = pixmap.rect()
|
||||
pix_rect.moveCenter(option.rect.center())
|
||||
painter.drawPixmap(pix_rect.topLeft(), pixmap)
|
||||
|
||||
def editorEvent(self, event, model, option, index):
|
||||
def editorEvent(
|
||||
self,
|
||||
event: QtCore.QEvent,
|
||||
model: QtCore.QSortFilterProxyModel,
|
||||
option: QtWidgets.QStyleOptionViewItem,
|
||||
index: QModelIndex,
|
||||
):
|
||||
if event.type() != QtCore.QEvent.Type.MouseButtonRelease:
|
||||
return False
|
||||
current = model.data(index, USER_CHECK_DATA_ROLE)
|
||||
@@ -222,7 +281,7 @@ class CenterCheckBoxDelegate(CustomDisplayDelegate):
|
||||
class DeviceValidatedDelegate(CustomDisplayDelegate):
|
||||
"""Custom delegate for displaying validated device configurations."""
|
||||
|
||||
def __init__(self, parent=None, colors=None):
|
||||
def __init__(self, parent: BECTableView | None = None, colors: AccentColors | None = None):
|
||||
super().__init__(parent)
|
||||
colors = colors if colors else get_accent_colors()
|
||||
_icon = partial(material_icon, icon_name="circle", size=(12, 12), filled=True)
|
||||
@@ -241,7 +300,23 @@ class DeviceValidatedDelegate(CustomDisplayDelegate):
|
||||
ValidationStatus.FAILED: _icon(color=colors.emergency),
|
||||
}
|
||||
|
||||
def _do_custom_paint(self, painter, option, index, value):
|
||||
def _do_custom_paint(
|
||||
self,
|
||||
painter: QtGui.QPainter,
|
||||
option: QtWidgets.QStyleOptionViewItem,
|
||||
index: QModelIndex,
|
||||
value: Literal[0, 1, 2],
|
||||
):
|
||||
"""
|
||||
Paint the validation status icon centered in the cell.
|
||||
|
||||
Args:
|
||||
painter (QtGui.QPainter): The painter object.
|
||||
option (QtWidgets.QStyleOptionViewItem): The style options for the item.
|
||||
index (QModelIndex): The model index of the item.
|
||||
value (Literal[0,1,2]): The validation status value, where 0=Pending, 1=Valid, 2=Failed.
|
||||
Relates to ValidationStatus enum.
|
||||
"""
|
||||
if pixmap := self._icons.get(value):
|
||||
pix_rect = pixmap.rect()
|
||||
pix_rect.moveCenter(option.rect.center())
|
||||
@@ -258,7 +333,7 @@ class DeviceTableModel(QtCore.QAbstractTableModel):
|
||||
# tuple of list[dict[str, Any]] of configs which were added and bool True if added or False if removed
|
||||
configs_changed = QtCore.Signal(list, bool)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent: DeviceTableModel | None = None):
|
||||
super().__init__(parent)
|
||||
self._device_config: list[dict[str, Any]] = []
|
||||
self._validation_status: dict[str, ValidationStatus] = {}
|
||||
|
||||
@@ -43,7 +43,7 @@ def docstring_to_markdown(obj) -> str:
|
||||
# Highlight section headers for Markdown
|
||||
headers = ["Parameters", "Args", "Returns", "Raises", "Attributes", "Examples", "Notes"]
|
||||
for h in headers:
|
||||
doc = re.sub(rf"(?m)^({h})\s*:?\s*$", rf"### \1", text)
|
||||
text = re.sub(rf"(?m)^({h})\s*:?\s*$", rf"### \1", text)
|
||||
|
||||
# Preserve code blocks (4+ space indented lines)
|
||||
def fence_code(match: re.Match) -> str:
|
||||
|
||||
Reference in New Issue
Block a user