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:
@ -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_())
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user