1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-04 16:02:51 +01:00

scratch - device manager

This commit is contained in:
2025-08-18 09:59:54 +02:00
parent a2f8880459
commit fde7b4db6c

View File

@@ -0,0 +1,998 @@
from __future__ import annotations
from qtpy.QtCore import QSize, QSortFilterProxyModel, Qt
from qtpy.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QFormLayout,
QFrame,
QHBoxLayout,
QHeaderView,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMessageBox,
QPushButton,
QScrollArea,
QSizePolicy,
QSplitter,
QTableWidget,
QTableWidgetItem,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from thefuzz import fuzz
from bec_widgets.utils.bec_table import BECTable
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import ErrorPopupUtility, SafeSlot
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
class CheckBoxCenterWidget(QWidget):
"""Widget to center a checkbox in a table cell."""
def __init__(self, checked=False, parent=None):
super().__init__(parent)
layout = QHBoxLayout(self)
layout.setAlignment(Qt.AlignCenter)
layout.setContentsMargins(4, 0, 4, 0) # Reduced margins for more compact layout
self.checkbox = QCheckBox()
self.checkbox.setChecked(checked)
self.checkbox.setEnabled(False) # Read-only
# Store the value for sorting
self.value = checked
layout.addWidget(self.checkbox)
class TextLabelWidget(QWidget):
"""Widget to display text with word wrapping in a table cell."""
def __init__(self, text="", parent=None):
super().__init__(parent)
# Use a layout with minimal margins to maximize text display area
layout = QVBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(0)
# Create label with word wrap enabled
self.label = QLabel(text)
self.label.setWordWrap(True)
self.label.setTextInteractionFlags(Qt.TextSelectableByMouse)
self.label.setAlignment(Qt.AlignLeft | Qt.AlignTop) # Align to top-left
# Make sure label expands to fill available space
self.label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Store the text value for sorting
self.value = text
layout.addWidget(self.label)
# Make sure the widget itself uses an expanding size policy
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
# Ensure we have a reasonable height to start with
# This helps ensure text is visible before resizing calculations
min_height = 40 if text else 20
self.setMinimumHeight(min_height)
def setText(self, text):
"""Set the text of the label."""
self.label.setText(text)
self.value = text
# Trigger layout update
self.updateGeometry()
def sizeHint(self):
"""Provide a size hint based on the text content."""
# Get the width of our container (usually the table cell)
width = self.width() or 300
# If text is empty, return minimal size
if not self.value:
return QSize(width, 20)
# Calculate height for wrapped text
font_metrics = self.label.fontMetrics()
# Estimate how much space the text will need when wrapped
text_rect = font_metrics.boundingRect(
0,
0,
width - 10,
1000, # Width constraint, virtually unlimited height
Qt.TextWordWrap,
self.value,
)
# Add some padding
height = text_rect.height() + 8
return QSize(width, max(30, height))
def resizeEvent(self, event):
"""Handle resize events to ensure text is properly displayed."""
super().resizeEvent(event)
# When resized (especially width change), update layout to ensure text wrapping works
self.label.updateGeometry()
self.updateGeometry()
class SortableTableWidgetItem(QTableWidgetItem):
"""Table widget item that enables proper sorting for different data types."""
def __lt__(self, other):
"""Compare items for sorting."""
if self.text() == "Yes" and other.text() == "No":
return True
elif self.text() == "No" and other.text() == "Yes":
return False
else:
return self.text().lower() < other.text().lower()
class DeviceTagsWidget(BECWidget, QWidget):
"""Widget to display devices grouped by their tags in containers."""
def __init__(self, parent=None):
super().__init__(parent=parent)
self.layout = QVBoxLayout(self)
self.setLayout(self.layout)
# Title
self.title_label = QLabel("Device Tags")
self.title_label.setStyleSheet("font-weight: bold; font-size: 14px;")
self.layout.addWidget(self.title_label)
# Search bar for tags
self.search_layout = QHBoxLayout()
self.search_label = QLabel("Search:")
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Filter tags...")
self.search_input.setClearButtonEnabled(True)
self.search_input.textChanged.connect(self.filter_tags)
self.search_layout.addWidget(self.search_label)
self.search_layout.addWidget(self.search_input)
self.layout.addLayout(self.search_layout)
# Create a scroll area for tag containers
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
# Create a widget to hold all tag containers
self.scroll_widget = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_widget)
self.scroll_layout.setSpacing(10)
self.scroll_layout.setContentsMargins(5, 5, 5, 5)
self.scroll_layout.setAlignment(Qt.AlignTop)
self.scroll_area.setWidget(self.scroll_widget)
self.layout.addWidget(self.scroll_area)
# Initialize with empty data
self.all_devices = []
self.active_devices = []
self.device_tags = {} # Maps tag names to lists of device names
self.tag_containers = {} # Maps tag names to their container widgets
# Load initial data
self.update_tags()
def update_tags(self):
"""Update the tags containers with current device information."""
try:
# Get device config
config = self.client.device_manager._get_redis_device_config()
# Clear current data
self.all_devices = []
self.active_devices = []
self.device_tags = {}
# Process device config
for device_info in config:
device_name = device_info.get("name", "Unknown")
self.all_devices.append(device_name)
# Add to active devices if enabled
if device_info.get("enabled", False):
self.active_devices.append(device_name)
# Process device tags
tags = device_info.get("deviceTags", [])
for tag in tags:
if tag not in self.device_tags:
self.device_tags[tag] = []
self.device_tags[tag].append(device_name)
# Update the tag containers
self.populate_tag_containers()
except Exception as e:
ErrorPopupUtility().show_error_message(
"Device Tags Error", f"Error updating device tags: {str(e)}", self
)
def populate_tag_containers(self):
"""Populate the containers with current tag and device data."""
# Save current filter before clearing
current_filter = self.search_input.text() if hasattr(self, "search_input") else ""
# Clear existing containers
for i in reversed(range(self.scroll_layout.count())):
widget = self.scroll_layout.itemAt(i).widget()
if widget:
widget.setParent(None)
widget.deleteLater()
self.tag_containers = {}
# Add tag containers
for tag, devices in sorted(self.device_tags.items()):
# Create container frame for this tag
container = QFrame()
container.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
container.setStyleSheet(
"QFrame { background-color: palette(window); border: 1px solid palette(mid); border-radius: 4px; }"
)
container_layout = QVBoxLayout(container)
container_layout.setContentsMargins(10, 10, 10, 10)
# Add tag header with status indicator
header_layout = QHBoxLayout()
# Tag name label
tag_label = QLabel(tag)
tag_label.setStyleSheet("font-weight: bold;")
header_layout.addWidget(tag_label)
# Spacer to push status to the right
header_layout.addStretch()
# Status indicator
all_devices_count = len(devices)
active_devices_count = sum(1 for d in devices if d in self.active_devices)
if active_devices_count == 0:
status_text = "None"
status_color = "red"
elif active_devices_count == all_devices_count:
status_text = "All"
status_color = "green"
else:
status_text = f"{active_devices_count}/{all_devices_count}"
status_color = "orange"
status_label = QLabel(status_text)
status_label.setStyleSheet(f"color: {status_color}; font-weight: bold;")
header_layout.addWidget(status_label)
container_layout.addLayout(header_layout)
# Add divider line
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
container_layout.addWidget(line)
# Add device list
device_list = QListWidget()
device_list.setAlternatingRowColors(True)
device_list.setMaximumHeight(150) # Limit height
# Add devices to the list
for device_name in sorted(devices):
item = QListWidgetItem(device_name)
if device_name in self.active_devices:
item.setForeground(Qt.green)
else:
item.setForeground(Qt.red)
device_list.addItem(item)
container_layout.addWidget(device_list)
# Add to the scroll layout
self.scroll_layout.addWidget(container)
self.tag_containers[tag] = container
# Add a stretch at the end to push all containers to the top
self.scroll_layout.addStretch()
# Reapply filter if there was one
if current_filter:
self.filter_tags(current_filter)
@SafeSlot(str)
def filter_tags(self, text):
"""Filter the tag containers based on search text."""
if not hasattr(self, "tag_containers"):
return
text = text.lower()
# Show/hide tag containers based on filter
for tag, container in self.tag_containers.items():
if not text or text in tag.lower():
# Tag matches filter
container.show()
else:
# Check if any device in this tag matches
matches = False
for device in self.device_tags.get(tag, []):
if text in device.lower():
matches = True
break
container.setVisible(matches)
@SafeSlot()
def add_devices_by_tag(self):
"""Add devices with the selected tags to the active configuration."""
# This would be implemented for drag-and-drop in the future
@SafeSlot()
def remove_devices_by_tag(self):
"""Remove devices with the selected tags from the active configuration."""
# This would be implemented for drag-and-drop in the future
class DeviceManager(BECWidget, QWidget):
"""Widget to display the current device configuration in a table."""
def __init__(self, parent=None):
super().__init__(parent=parent)
# Main layout for the entire widget
self.main_layout = QHBoxLayout(self)
self.setLayout(self.main_layout)
# Create a splitter to hold the device tags widget and device table
self.splitter = QSplitter(Qt.Horizontal)
# Create device tags widget
self.device_tags_widget = DeviceTagsWidget(self)
self.splitter.addWidget(self.device_tags_widget)
# Create container for device table and its controls
self.table_container = QWidget()
self.layout = QVBoxLayout(self.table_container)
# Create search bar
self.search_layout = QHBoxLayout()
self.search_label = QLabel("Search:")
self.search_input = QLineEdit()
self.search_input.setPlaceholderText(
"Filter devices (approximate matching)..."
) # Default to fuzzy search
self.search_input.setClearButtonEnabled(True)
self.search_input.textChanged.connect(self.filter_devices)
self.search_layout.addWidget(self.search_label)
self.search_layout.addWidget(self.search_input)
# Add exact match toggle
self.fuzzy_toggle_layout = QHBoxLayout()
self.fuzzy_toggle_label = QLabel("Exact Match:")
self.fuzzy_toggle = ToggleSwitch()
self.fuzzy_toggle.setChecked(False) # Default to fuzzy search (toggle OFF)
self.fuzzy_toggle.stateChanged.connect(self.on_fuzzy_toggle_changed)
self.fuzzy_toggle.setToolTip(
"Toggle between approximate matching (OFF) and exact matching (ON)"
)
self.fuzzy_toggle_label.setToolTip(
"Toggle between approximate matching (OFF) and exact matching (ON)"
)
self.fuzzy_toggle_layout.addWidget(self.fuzzy_toggle_label)
self.fuzzy_toggle_layout.addWidget(self.fuzzy_toggle)
self.fuzzy_toggle_layout.addStretch()
# Add both search components to the layout
self.search_controls = QHBoxLayout()
self.search_controls.addLayout(self.search_layout)
self.search_controls.addSpacing(20) # Add some space between the search box and toggle
self.search_controls.addLayout(self.fuzzy_toggle_layout)
self.layout.addLayout(self.search_controls)
# Create table widget
self.device_table = BECTable()
self.device_table.setEditTriggers(QTableWidget.NoEditTriggers) # Make table read-only
self.device_table.setSelectionBehavior(QTableWidget.SelectRows)
self.device_table.setAlternatingRowColors(True)
self.layout.addWidget(self.device_table)
# Connect custom sorting handler
self.device_table.horizontalHeader().sectionClicked.connect(self.handle_header_click)
self.current_sort_section = 0
self.current_sort_order = Qt.AscendingOrder
# Make table resizable
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.device_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Don't stretch the last section to prevent it from changing width
self.device_table.horizontalHeader().setStretchLastSection(False)
self.device_table.verticalHeader().setVisible(False)
# Set up initial headers
self.headers = [
"Name",
"Device Class",
"Readout Priority",
"Enabled",
"Read Only",
"Documentation",
]
self.device_table.setColumnCount(len(self.headers))
self.device_table.setHorizontalHeaderLabels(self.headers)
# Set initial column resize modes
header = self.device_table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # Name
header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Device Class
header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Readout Priority
header.setSectionResizeMode(3, QHeaderView.Fixed) # Enabled
header.setSectionResizeMode(4, QHeaderView.Fixed) # Read Only
header.setSectionResizeMode(5, QHeaderView.Stretch) # Documentation
# Connect resize signal to adjust row heights when table is resized
self.device_table.horizontalHeader().sectionResized.connect(self.on_table_resized)
# Set fixed width for checkbox columns
self.device_table.setColumnWidth(3, 70) # Enabled column
self.device_table.setColumnWidth(4, 70) # Read Only column
# Ensure column widths stay fixed
header.setMinimumSectionSize(70)
header.setDefaultSectionSize(100)
# Enable sorting by clicking column headers
self.device_table.setSortingEnabled(False) # We'll handle sorting manually
self.device_table.horizontalHeader().setSortIndicatorShown(True)
self.device_table.horizontalHeader().setSectionsClickable(True)
# Add buttons for adding/removing devices
self.button_layout = QHBoxLayout()
# Add device button
self.add_device_button = QPushButton("Add Device")
self.add_device_button.clicked.connect(self.add_device)
self.button_layout.addWidget(self.add_device_button)
# Remove device button
self.remove_device_button = QPushButton("Remove Device")
self.remove_device_button.clicked.connect(self.remove_device)
self.button_layout.addWidget(self.remove_device_button)
# Add buttons to main layout
self.layout.addLayout(self.button_layout)
# Add the table container to the splitter
self.splitter.addWidget(self.table_container)
# Set initial sizes (30% for tags, 70% for table)
self.splitter.setSizes([300, 700])
# Add the splitter to the main layout
self.main_layout.addWidget(self.splitter)
# Connect signals between widgets
self.connect_signals()
# Load initial data
self.update_device_table()
def connect_signals(self):
"""Connect signals between the device table and tags widget."""
# Connect add devices by tag button to update the table
if hasattr(self.device_tags_widget, "add_tag_button"):
self.device_tags_widget.add_tag_button.clicked.connect(self.update_device_table)
# Connect remove devices by tag button to update the table
if hasattr(self.device_tags_widget, "remove_tag_button"):
self.device_tags_widget.remove_tag_button.clicked.connect(self.update_device_table)
@SafeSlot(int, int, int)
def on_table_resized(self, column, old_width, new_width):
"""Handle table column resize events to readjust row heights for text wrapping."""
# Only handle resizes of the documentation column
if column == 5:
# Update all rows with TextLabelWidgets in the documentation column
for row in range(self.device_table.rowCount()):
doc_widget = self.device_table.cellWidget(row, 5)
if doc_widget and isinstance(doc_widget, TextLabelWidget):
# Trigger recalculation of text wrapping
doc_widget.updateGeometry()
# Force the table to recalculate row heights
if doc_widget.value:
# Get text metrics
font_metrics = doc_widget.label.fontMetrics()
# Calculate new text height with word wrap
text_rect = font_metrics.boundingRect(
0,
0,
new_width - 10,
2000, # New width constraint
Qt.TextWordWrap,
doc_widget.value,
)
# Update row height
row_height = text_rect.height() + 16
self.device_table.setRowHeight(row, max(40, row_height))
@SafeSlot()
def update_device_table(self):
"""Update the device table with the current device configuration."""
try:
# Get device config (always a list of dictionaries)
config = self.client.device_manager._get_redis_device_config()
# Clear existing rows
self.device_table.setRowCount(0)
# Add devices to the table
for device_info in config:
row_position = self.device_table.rowCount()
self.device_table.insertRow(row_position)
# Set device name
self.device_table.setItem(
row_position, 0, SortableTableWidgetItem(device_info.get("name", "Unknown"))
)
# Set device class
device_class = device_info.get("deviceClass", "Unknown")
self.device_table.setItem(row_position, 1, SortableTableWidgetItem(device_class))
# Set readout priority
readout_priority = device_info.get("readoutPriority", "Unknown")
self.device_table.setItem(
row_position, 2, SortableTableWidgetItem(readout_priority)
)
# Set enabled status as checkbox
enabled_checkbox = CheckBoxCenterWidget(device_info.get("enabled", False))
self.device_table.setCellWidget(row_position, 3, enabled_checkbox)
# Set read-only status as checkbox
readonly_checkbox = CheckBoxCenterWidget(device_info.get("readOnly", False))
self.device_table.setCellWidget(row_position, 4, readonly_checkbox)
# Set documentation using text label widget with word wrap
documentation = device_info.get("documentation", "")
doc_widget = TextLabelWidget(documentation)
self.device_table.setCellWidget(row_position, 5, doc_widget)
# First, ensure the table is updated to show the new widgets
self.device_table.viewport().update()
# Force a layout update to get proper sizes
self.device_table.resizeRowsToContents()
# Then adjust row heights with better calculation for wrapped text
for row in range(self.device_table.rowCount()):
doc_widget = self.device_table.cellWidget(row, 5)
if doc_widget and isinstance(doc_widget, TextLabelWidget):
text = doc_widget.value
if text:
# Get the column width
col_width = self.device_table.columnWidth(5)
# Calculate appropriate height for the text
font_metrics = doc_widget.label.fontMetrics()
# Calculate text rectangle with word wrap
text_rect = font_metrics.boundingRect(
0,
0,
col_width - 10,
2000, # Width constraint with large height
Qt.TextWordWrap,
text,
)
# Set row height with additional padding
row_height = text_rect.height() + 16
self.device_table.setRowHeight(row, max(40, row_height))
# Update the widget to reflect the new size
doc_widget.updateGeometry()
# Apply current sort if any
if hasattr(self, "current_sort_section") and self.current_sort_section >= 0:
self.sort_table(self.current_sort_section, self.current_sort_order)
self.device_table.horizontalHeader().setSortIndicator(
self.current_sort_section, self.current_sort_order
)
# Reset the filter to make sure search works with new data
if hasattr(self, "search_input"):
current_filter = self.search_input.text()
if current_filter:
self.filter_devices(current_filter)
# Update the device tags widget
self.device_tags_widget.update_tags()
except Exception as e:
ErrorPopupUtility().show_error_message(
"Device Manager Error", f"Error updating device table: {str(e)}", self
)
@SafeSlot(bool)
def on_fuzzy_toggle_changed(self, enabled):
"""
Handle exact match toggle state change.
When toggle is ON (enabled=True): Use exact matching
When toggle is OFF (enabled=False): Use fuzzy/approximate matching
"""
# Update search mode label
if hasattr(self, "search_input"):
# Store original stylesheet to restore it later
original_style = self.search_input.styleSheet()
# Set placeholder text based on mode
if enabled: # Toggle ON = Exact match
self.search_input.setPlaceholderText("Filter devices (exact match)...")
print("Toggle switched ON: Using EXACT match mode")
else: # Toggle OFF = Approximate/fuzzy match
self.search_input.setPlaceholderText("Filter devices (approximate matching)...")
print("Toggle switched OFF: Using FUZZY match mode")
# Visual feedback - briefly highlight the search box with appropriate color
highlight_color = "#3498db" # Blue for feedback
self.search_input.setStyleSheet(f"border: 2px solid {highlight_color};")
# Create a one-time timer to restore the original style after a short delay
from qtpy.QtCore import QTimer
QTimer.singleShot(500, lambda: self.search_input.setStyleSheet(original_style))
# Log the toggle state for debugging
print(
f"Search mode changed: Exact match = {enabled}, Toggle isChecked = {self.fuzzy_toggle.isChecked()}"
)
# When toggle changes, reapply current search with new mode
current_text = self.search_input.text()
# Always reapply the filter, even if text is empty
# This ensures all rows are properly shown/hidden based on the new mode
self.filter_devices(current_text)
@SafeSlot(str)
def filter_devices(self, text):
"""Filter devices in the table based on exact or approximate matching."""
# Always show all rows when search is empty, regardless of match mode
if not text:
for row in range(self.device_table.rowCount()):
self.device_table.setRowHidden(row, False)
return
# Get current search mode
# When toggle is ON, we use exact match
# When toggle is OFF, we use fuzzy/approximate match
use_exact_match = hasattr(self, "fuzzy_toggle") and self.fuzzy_toggle.isChecked()
# Debug print to verify which mode is being used
print(f"Filtering with exact match: {use_exact_match}, search text: '{text}'")
# Threshold for fuzzy matching (0-100, higher is more strict)
threshold = 80
# Prepare search text (lowercase for case-insensitive search)
search_text = text.lower()
# Count of matched rows for feedback (but avoid double-counting)
visible_rows = 0
total_rows = self.device_table.rowCount()
# Filter rows using either exact or approximate matching
for row in range(total_rows):
row_visible = False
# Check name and device class columns (0 and 1)
for col in [0, 1]: # Name and Device Class columns
item = self.device_table.item(row, col)
if not item:
continue
cell_text = item.text().lower()
if use_exact_match:
# EXACT MATCH: Simple substring check
if search_text in cell_text:
row_visible = True
break
else:
# FUZZY MATCH: Use approximate matching
match_ratio = fuzz.partial_ratio(search_text, cell_text)
if match_ratio >= threshold:
row_visible = True
break
# Hide or show this row
self.device_table.setRowHidden(row, not row_visible)
# Count visible rows for potential feedback
if row_visible:
visible_rows += 1
@SafeSlot(int)
def handle_header_click(self, section):
"""Handle column header click to sort the table."""
# Toggle sort order if clicking the same section
if section == self.current_sort_section:
self.current_sort_order = (
Qt.DescendingOrder
if self.current_sort_order == Qt.AscendingOrder
else Qt.AscendingOrder
)
else:
self.current_sort_section = section
self.current_sort_order = Qt.AscendingOrder
# Update sort indicator
self.device_table.horizontalHeader().setSortIndicator(
self.current_sort_section, self.current_sort_order
)
# Perform the sort
self.sort_table(section, self.current_sort_order)
def sort_table(self, column, order):
"""Sort the table by the specified column and order."""
row_count = self.device_table.rowCount()
if row_count <= 1:
return # Nothing to sort
# Collect all rows for sorting
rows_data = []
for row in range(row_count):
# Create a safe copy of the row data
row_data = {}
row_data["items"] = []
row_data["widgets"] = []
row_data["hidden"] = self.device_table.isRowHidden(row)
row_data["sort_key"] = None
# Extract sort key for this row
if column in [3, 4]: # Checkbox columns
widget = self.device_table.cellWidget(row, column)
if widget and hasattr(widget, "value"):
row_data["sort_key"] = widget.value
else:
row_data["sort_key"] = False
else: # Text columns
item = self.device_table.item(row, column)
if item:
row_data["sort_key"] = item.text().lower()
else:
row_data["sort_key"] = ""
# Collect all items and widgets in the row
for col in range(self.device_table.columnCount()):
if col in [3, 4]: # Checkbox columns
widget = self.device_table.cellWidget(row, col)
if widget:
# Store the widget value to recreate it
is_checked = False
if hasattr(widget, "value"):
is_checked = widget.value
elif hasattr(widget, "checkbox"):
is_checked = widget.checkbox.isChecked()
row_data["widgets"].append((col, "checkbox", is_checked))
elif col == 5: # Documentation column with TextLabelWidget
widget = self.device_table.cellWidget(row, col)
if widget and isinstance(widget, TextLabelWidget):
text = widget.value
row_data["widgets"].append((col, "textlabel", text))
else:
row_data["widgets"].append((col, "textlabel", ""))
else:
item = self.device_table.item(row, col)
if item:
row_data["items"].append((col, item.text()))
else:
row_data["items"].append((col, ""))
rows_data.append(row_data)
# Sort the rows
reverse = order == Qt.DescendingOrder
sorted_rows = sorted(rows_data, key=lambda x: x["sort_key"], reverse=reverse)
# Rebuild the table with sorted data
self.device_table.setUpdatesEnabled(False) # Disable updates while rebuilding
# Clear and rebuild the table
self.device_table.clearContents()
self.device_table.setRowCount(row_count)
for row, row_data in enumerate(sorted_rows):
# Add text items
for col, text in row_data["items"]:
self.device_table.setItem(row, col, SortableTableWidgetItem(text))
# Add widgets
for col, widget_type, value in row_data["widgets"]:
if widget_type == "checkbox":
checkbox = CheckBoxCenterWidget(value)
self.device_table.setCellWidget(row, col, checkbox)
elif widget_type == "textlabel":
text_label = TextLabelWidget(value)
self.device_table.setCellWidget(row, col, text_label)
# Restore hidden state
self.device_table.setRowHidden(row, row_data["hidden"])
self.device_table.setUpdatesEnabled(True) # Re-enable updates
@SafeSlot()
def show_add_device_dialog(self):
"""Show the dialog for adding a new device."""
# Call the add_device method to handle the dialog and logic
self.add_device()
@SafeSlot()
def add_device(self):
"""Simulate adding a new device to the configuration."""
try:
# Create and show the add device dialog
dialog = AddDeviceDialog(self)
if dialog.exec():
# Get device config from dialog
device_config = dialog.get_device_config()
device_name = device_config.get("name")
# Print the action that would be taken (simulation only)
print(f"Would add device: {device_name} with config: {device_config}")
# Show simulation message
QMessageBox.information(
self,
"Device Addition Simulated",
f"Would add device: {device_name} (simulation only)",
)
# Update the device tags widget
self.device_tags_widget.update_tags()
except Exception as e:
ErrorPopupUtility().show_error_message(
"Device Manager Error", f"Error in add device simulation: {str(e)}", self
)
@SafeSlot()
def remove_device(self):
"""Simulate removing selected device(s) from the configuration."""
selected_rows = self.device_table.selectionModel().selectedRows()
if not selected_rows:
QMessageBox.information(self, "No Selection", "Please select a device to remove.")
return
# Confirm deletion
device_count = len(selected_rows)
message = f"Are you sure you want to remove {device_count} device{'s' if device_count > 1 else ''}?"
confirmation = QMessageBox.question(
self, "Confirm Removal", message, QMessageBox.Yes | QMessageBox.No
)
if confirmation == QMessageBox.Yes:
try:
# Get device names from selected rows
device_names = []
for index in selected_rows:
row = index.row()
device_name = self.device_table.item(row, 0).text()
device_names.append(device_name)
# Print removal action instead of actual removal
print(f"Would remove devices: {device_names}")
# Show simulation message
QMessageBox.information(
self,
"Device Removal Simulated",
f"Would remove {device_count} device{'s' if device_count > 1 else ''} (simulation only)",
)
# Update the device tags widget
self.device_tags_widget.update_tags()
except Exception as e:
ErrorPopupUtility().show_error_message(
"Device Manager Error", f"Error in remove device simulation: {str(e)}", self
)
class AddDeviceDialog(QDialog):
"""Dialog for adding a new device to the configuration."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Add New Device")
self.setMinimumWidth(400)
# Create layout
self.layout = QVBoxLayout(self)
self.form_layout = QFormLayout()
# Device name
self.name_input = QLineEdit()
self.form_layout.addRow("Device Name:", self.name_input)
# Device class
self.device_class_input = QLineEdit()
self.device_class_input.setText("ophyd_devices.SimPositioner")
self.form_layout.addRow("Device Class:", self.device_class_input)
# Readout priority
self.readout_priority_combo = QComboBox()
self.readout_priority_combo.addItems(["baseline", "monitored", "async", "on_request"])
self.form_layout.addRow("Readout Priority:", self.readout_priority_combo)
# Enabled checkbox
self.enabled_checkbox = QCheckBox()
self.enabled_checkbox.setChecked(True)
self.form_layout.addRow("Enabled:", self.enabled_checkbox)
# Read-only checkbox
self.readonly_checkbox = QCheckBox()
self.form_layout.addRow("Read Only:", self.readonly_checkbox)
# Documentation text
self.documentation_input = QLineEdit()
self.form_layout.addRow("Documentation:", self.documentation_input)
# Add form to layout
self.layout.addLayout(self.form_layout)
# Add buttons
self.button_layout = QHBoxLayout()
self.cancel_button = QPushButton("Cancel")
self.cancel_button.clicked.connect(self.reject)
self.add_button = QPushButton("Add Device")
self.add_button.clicked.connect(self.accept)
self.button_layout.addWidget(self.cancel_button)
self.button_layout.addWidget(self.add_button)
self.layout.addLayout(self.button_layout)
def get_device_config(self):
"""Get the device configuration from the dialog."""
return {
"name": self.name_input.text(),
"deviceClass": self.device_class_input.text(),
"readoutPriority": self.readout_priority_combo.currentText(),
"enabled": self.enabled_checkbox.isChecked(),
"readOnly": self.readonly_checkbox.isChecked(),
"documentation": self.documentation_input.text(),
"deviceConfig": {}, # Empty config for now
}
if __name__ == "__main__":
from qtpy.QtWidgets import QApplication
app = QApplication([])
window = DeviceManager()
window.show()
app.exec_()