mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-09 00:02:10 +02:00
180 lines
6.0 KiB
Python
180 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
from qtpy.QtCore import Qt
|
|
from qtpy.QtWidgets import QSizePolicy, QSpacerItem, QSplitter, QVBoxLayout, QWidget
|
|
|
|
from bec_widgets.utils.bec_widget import BECWidget
|
|
from bec_widgets.utils.colors import get_theme_palette
|
|
from bec_widgets.widgets.containers.explorer.collapsible_tree_section import CollapsibleSection
|
|
|
|
|
|
class Explorer(BECWidget, QWidget):
|
|
"""
|
|
A widget that combines multiple collapsible sections for an explorer-like interface.
|
|
Each section can be expanded or collapsed, and sections can be reordered. The explorer
|
|
can contain also sub-explorers for nested structures.
|
|
"""
|
|
|
|
RPC = False
|
|
PLUGIN = False
|
|
|
|
def __init__(self, parent=None, **kwargs):
|
|
super().__init__(parent=parent, **kwargs)
|
|
|
|
# Main layout
|
|
self.main_layout = QVBoxLayout(self)
|
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
self.main_layout.setSpacing(0)
|
|
|
|
# Splitter for sections
|
|
self.splitter = QSplitter(Qt.Orientation.Vertical)
|
|
self.main_layout.addWidget(self.splitter)
|
|
|
|
# Spacer for when all sections are collapsed
|
|
self.expander = QSpacerItem(0, 0)
|
|
self.main_layout.addItem(self.expander)
|
|
|
|
# Registry of sections
|
|
self.sections: list[CollapsibleSection] = []
|
|
|
|
# Setup splitter styling
|
|
self._setup_splitter_styling()
|
|
|
|
def add_section(self, section: CollapsibleSection) -> None:
|
|
"""
|
|
Add a collapsible section to the explorer
|
|
|
|
Args:
|
|
section (CollapsibleSection): The section to add
|
|
"""
|
|
if not isinstance(section, CollapsibleSection):
|
|
raise TypeError("section must be an instance of CollapsibleSection")
|
|
|
|
if section in self.sections:
|
|
return
|
|
|
|
self.sections.append(section)
|
|
self.splitter.addWidget(section)
|
|
|
|
# Connect the section's toggle to update spacer
|
|
section.header_button.clicked.connect(self._update_spacer)
|
|
|
|
# Connect section reordering if supported
|
|
if hasattr(section, "section_reorder_requested"):
|
|
section.section_reorder_requested.connect(self._handle_section_reorder)
|
|
|
|
self._update_spacer()
|
|
|
|
def remove_section(self, section: CollapsibleSection) -> None:
|
|
"""
|
|
Remove a collapsible section from the explorer
|
|
|
|
Args:
|
|
section (CollapsibleSection): The section to remove
|
|
"""
|
|
if section not in self.sections:
|
|
return
|
|
self.sections.remove(section)
|
|
section.deleteLater()
|
|
section.close()
|
|
|
|
# Disconnect signals
|
|
try:
|
|
section.header_button.clicked.disconnect(self._update_spacer)
|
|
if hasattr(section, "section_reorder_requested"):
|
|
section.section_reorder_requested.disconnect(self._handle_section_reorder)
|
|
except RuntimeError:
|
|
# Signals already disconnected
|
|
pass
|
|
|
|
self._update_spacer()
|
|
|
|
def get_section(self, title: str) -> CollapsibleSection | None:
|
|
"""Get a section by its title"""
|
|
for section in self.sections:
|
|
if section.title == title:
|
|
return section
|
|
return None
|
|
|
|
def _setup_splitter_styling(self) -> None:
|
|
"""Setup the splitter styling with theme colors"""
|
|
palette = get_theme_palette()
|
|
separator_color = palette.mid().color()
|
|
|
|
self.splitter.setStyleSheet(
|
|
f"""
|
|
QSplitter::handle {{
|
|
height: 0.1px;
|
|
background-color: rgba({separator_color.red()}, {separator_color.green()}, {separator_color.blue()}, 60);
|
|
}}
|
|
"""
|
|
)
|
|
|
|
def _update_spacer(self) -> None:
|
|
"""Update the spacer size based on section states"""
|
|
any_expanded = any(section.expanded for section in self.sections)
|
|
|
|
if any_expanded:
|
|
self.expander.changeSize(0, 0, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
|
else:
|
|
self.expander.changeSize(
|
|
0, 10, QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Expanding
|
|
)
|
|
|
|
def _handle_section_reorder(self, source_title: str, target_title: str) -> None:
|
|
"""Handle reordering of sections"""
|
|
if source_title == target_title:
|
|
return
|
|
|
|
source_section = self.get_section(source_title)
|
|
target_section = self.get_section(target_title)
|
|
|
|
if not source_section or not target_section:
|
|
return
|
|
|
|
# Get current indices
|
|
source_index = self.splitter.indexOf(source_section)
|
|
target_index = self.splitter.indexOf(target_section)
|
|
|
|
if source_index == -1 or target_index == -1:
|
|
return
|
|
|
|
# Insert at target position
|
|
self.splitter.insertWidget(target_index, source_section)
|
|
|
|
# Update sections
|
|
self.sections.remove(source_section)
|
|
self.sections.insert(target_index, source_section)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import os
|
|
|
|
from qtpy.QtWidgets import QApplication, QLabel
|
|
|
|
from bec_widgets.widgets.containers.explorer.script_tree_widget import ScriptTreeWidget
|
|
|
|
app = QApplication([])
|
|
explorer = Explorer()
|
|
section = CollapsibleSection(title="SCRIPTS", indentation=0)
|
|
|
|
script_explorer = Explorer()
|
|
script_widget = ScriptTreeWidget()
|
|
local_scripts_section = CollapsibleSection(title="Local")
|
|
local_scripts_section.set_widget(script_widget)
|
|
script_widget.set_directory(os.path.abspath("./"))
|
|
script_explorer.add_section(local_scripts_section)
|
|
|
|
section.set_widget(script_explorer)
|
|
explorer.add_section(section)
|
|
shared_script_section = CollapsibleSection(title="Shared")
|
|
shared_script_widget = ScriptTreeWidget()
|
|
shared_script_widget.set_directory(os.path.abspath("./"))
|
|
shared_script_section.set_widget(shared_script_widget)
|
|
script_explorer.add_section(shared_script_section)
|
|
macros_section = CollapsibleSection(title="MACROS", indentation=0)
|
|
macros_section.set_widget(QLabel("Macros will be implemented later"))
|
|
explorer.add_section(macros_section)
|
|
explorer.show()
|
|
app.exec()
|