0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 11:41:49 +02:00

WIP EXPANSION PANEL: Plugin accept other widgets

This commit is contained in:
2025-01-31 20:27:30 +01:00
parent 6d8bc4a750
commit 4fb5922b4a
2 changed files with 94 additions and 74 deletions

View File

@ -1,30 +1,29 @@
import sys
from PySide6.QtWidgets import QSizePolicy
from qtpy.QtCore import QEvent, Qt
from qtpy.QtWidgets import (
QApplication,
QWidget,
QVBoxLayout,
QFrame,
QHBoxLayout,
QFrame,
QPushButton,
QLabel,
QFormLayout,
QComboBox,
QLineEdit,
QSpinBox,
QToolBox,
QColorDialog,
QSizePolicy,
)
from qtpy.QtCore import Qt
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import set_theme
class ExpansionPanel(BECWidget, QWidget):
"""
A collapsible container widget for use in Qt Designer.
This version avoids the traceback by deferring eventFilter installation
until after header_frame and content_frame exist, and by checking they're
not None before referencing them.
"""
PLUGIN = True
RPC = False
@ -35,7 +34,6 @@ class ExpansionPanel(BECWidget, QWidget):
client=None,
gui_id: str | None = None,
title="Panel",
color: str | None = "#C0C0C0",
expanded=False,
) -> None:
if config is None:
@ -48,55 +46,71 @@ class ExpansionPanel(BECWidget, QWidget):
# Properties
self._expanded = expanded
self.header_frame = None
self.content_frame = None
# Main vertical layout: header + content
# Setup the main layout
self._main_layout = QVBoxLayout(self)
self._main_layout.setContentsMargins(0, 0, 0, 0)
self._main_layout.setSpacing(0)
# Header
# Create the header
self._init_header(title)
# Create the content
self._init_content()
# Make sure the content is initially visible or hidden
self.content_frame.setVisible(expanded)
# Defer installing the event filter until everything is ready
self.installEventFilter(self)
def _init_header(self, title):
"""
Create the header frame with arrow button and label.
"""
self.header_frame = QFrame(self)
self.header_frame.setObjectName("headerFrame")
self.header_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.header_frame.setStyleSheet(
"""
#headerFrame {
border: 1px solid #C0C0C0;
border-radius: 5;
}
#headerFrame {
border: 1px solid #C0C0C0;
border-radius: 4px;
}
"""
) # TODO think about the colors of the header frame and rounding the corners
)
header_layout = QHBoxLayout(self.header_frame)
header_layout.setContentsMargins(3, 2, 3, 2)
header_layout.setSpacing(3)
header_layout.setSpacing(5)
# Toggle button (arrow)
self.btn_toggle = QPushButton("" if expanded else "", self.header_frame)
self.btn_toggle = QPushButton("" if self._expanded else "", self.header_frame)
self.btn_toggle.setFixedSize(25, 25)
self.btn_toggle.setStyleSheet("border: none; font-weight: bold;")
self.btn_toggle.clicked.connect(self.toggle)
header_layout.addWidget(self.btn_toggle, alignment=Qt.AlignVCenter)
# Title label
self.label_title = QLabel(title, self.header_frame)
self.label_title.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
header_layout.addWidget(self.label_title, alignment=Qt.AlignVCenter | Qt.AlignLeft)
# Spacer stretch
header_layout.addStretch()
self._main_layout.addWidget(self.header_frame)
# Content area
def _init_content(self):
"""
Create the collapsible content frame, with its own QVBoxLayout.
"""
self.content_frame = QFrame(self)
self.content_frame.setObjectName("ContentFrame")
self.content_frame.setStyleSheet(
"""
#ContentFrame {
border: 1px solid #C0C0C0;
border-radius: 5;
}
#ContentFrame {
border: 1px solid #C0C0C0;
border-radius: 4px;
}
"""
)
self.content_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
@ -106,13 +120,12 @@ class ExpansionPanel(BECWidget, QWidget):
self._main_layout.addWidget(self.content_frame)
self.content_frame.setVisible(expanded)
@SafeSlot()
def toggle(self):
"""Collapse or expand the content area."""
self._expanded = not self._expanded
self.content_frame.setVisible(self._expanded)
if self.content_frame:
self.content_frame.setVisible(self._expanded)
self.btn_toggle.setText("" if self._expanded else "")
@SafeProperty(bool)
@ -126,11 +139,12 @@ class ExpansionPanel(BECWidget, QWidget):
@SafeProperty(str)
def title(self):
return self.label_title.text()
return self.label_title.text() if self.label_title else ""
@title.setter
def title(self, value: str):
self.label_title.setText(value)
if self.label_title:
self.label_title.setText(value)
@SafeProperty("QColor")
def label_color(self):
@ -142,34 +156,41 @@ class ExpansionPanel(BECWidget, QWidget):
@property
def content_layout(self) -> QVBoxLayout:
"""
Return the layout of the content frame,
so you can add sub-widgets at any time.
"""
"""Return the layout of the content frame for programmatic additions."""
return self._content_layout
class DemoApp(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QVBoxLayout(self)
self.panel1 = ExpansionPanel(title="Panel 1", expanded=False)
self.layout.addWidget(self.panel1)
btn1 = QPushButton("Button 1")
self.panel1._content_layout.addWidget(btn1)
self.panel2 = ExpansionPanel(title="Panel 2", color="#FF0000")
self.layout.addWidget(self.panel2)
self.panel3 = ExpansionPanel(title="Panel 3", color="#00FF00", expanded=False)
self.layout.addWidget(self.panel3)
def event(self, e):
"""
Override event() to detect when child widgets are added by Designer,
so we can place them into the content layout if they are not the header frame
or content frame themselves.
"""
if e.type() == QEvent.ChildAdded:
child_obj = e.child()
# Only process if we have a valid child widget
if child_obj is not None and isinstance(child_obj, QWidget):
# Also check if we have valid references to header_frame & content_frame
if self.header_frame is not None and self.content_frame is not None:
# If it's not our known frames, place it inside the content layout
if child_obj not in (self.header_frame, self.content_frame):
self._content_layout.addWidget(child_obj)
return super().event(e)
if __name__ == "__main__":
# Quick test if not using Designer
from qtpy.QtWidgets import QApplication, QVBoxLayout, QPushButton
app = QApplication(sys.argv)
set_theme("dark")
panel = DemoApp()
panel.show()
panel = ExpansionPanel(title="Test Panel", expanded=True)
panel.content_layout.addWidget(QPushButton("Test Button 1"))
panel.content_layout.addWidget(QPushButton("Test Button 2"))
container = QWidget()
lay = QVBoxLayout(container)
lay.addWidget(panel)
container.resize(400, 300)
container.show()
sys.exit(app.exec_())

View File

@ -1,54 +1,53 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from bec_widgets.utils.bec_designer import designer_material_icon
# Make sure the path below is correct
from bec_widgets.widgets.containers.expantion_panel.expansion_panel import ExpansionPanel
DOM_XML = """
<ui language='c++'>
<widget class='ExpansionPanel' name='expansion_panel'>
</widget>
<widget class='ExpansionPanel' name='expansion_panel'/>
</ui>
"""
class ExpansionPanelPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
class ExpansionPanelPlugin(QDesignerCustomWidgetInterface):
def __init__(self):
super().__init__()
self._form_editor = None
self.initialized = False
def createWidget(self, parent):
t = ExpansionPanel(parent)
return t
return ExpansionPanel(parent=parent)
def domXml(self):
return DOM_XML
def group(self):
return ""
return "BEC Widgets"
def icon(self):
return designer_material_icon(ExpansionPanel.ICON_NAME)
def includeFile(self):
return "expansion_panel"
return "bec_widgets.widgets.containers.expantion_panel.expansion_panel"
def initialize(self, form_editor):
self._form_editor = form_editor
if self.initialized:
return
self.initialized = True
def isContainer(self):
return True
return True # crucial for Designer to allow dropping child widgets
def isInitialized(self):
return self._form_editor is not None
return self.initialized
def name(self):
return "ExpansionPanel"
def toolTip(self):
return "ExpansionPanel"
return "A collapsible panel container widget"
def whatsThis(self):
return self.toolTip()