PanelPlugin¶
Panel plugins add new UI panels (dock widgets) to the application.
Purpose¶
Use PanelPlugin when you want to:
Add a new tool panel to the application
Provide a custom view for data or devices
Create reusable UI components
Base Class¶
from lightfall.plugins.panel_plugin import PanelPlugin
Class Attributes¶
Attribute |
Value |
Description |
|---|---|---|
|
|
Plugin type identifier |
|
|
One plugin instance (panel class can create multiple instances) |
Required Methods¶
name (property)¶
Unique identifier for this panel plugin.
@property
def name(self) -> str:
return "my_panel"
get_panel_class()¶
Return the BasePanel subclass this plugin provides.
def get_panel_class(self) -> type[BasePanel]:
"""Return the panel class.
Returns:
A BasePanel subclass.
"""
from my_package.panels import MyPanel
return MyPanel
Optional Methods¶
panel_id (property)¶
Get the panel ID from the panel class metadata. Usually not overridden.
@property
def panel_id(self) -> str:
return self.get_panel_class().panel_metadata.id
Lifecycle¶
Plugin is instantiated on load (recommended:
preload=True)get_panel_class()is called to get theBasePanelsubclassPanel class is registered with
PanelRegistryPanel can be instantiated via
PanelRegistry.create(panel_id)Panel instances are managed by the docking system
Creating a Panel Class¶
Panel plugins wrap BasePanel subclasses. Here’s how to create one:
Panel Metadata¶
Every panel needs metadata:
from lightfall.ui.panels.base import BasePanel, PanelMetadata
class MyPanel(BasePanel):
panel_metadata = PanelMetadata(
id="my_package.panels.my_panel", # Unique identifier
name="My Panel", # Display name
description="Description of my panel", # Tooltip
category="tools", # Category for menu
icon="mdi.wrench", # Icon name (qtawesome)
singleton=True, # Only one instance allowed?
)
Panel Implementation¶
from PySide6.QtWidgets import QLabel, QVBoxLayout
from lightfall.ui.panels.base import BasePanel, PanelMetadata
class MyPanel(BasePanel):
"""A custom panel for displaying data."""
panel_metadata = PanelMetadata(
id="my_package.panels.data_viewer",
name="Data Viewer",
description="View and analyze data",
category="analysis",
icon="mdi.chart-line",
singleton=False, # Allow multiple instances
)
def __init__(self, parent=None):
super().__init__(parent)
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.addWidget(QLabel("Data Viewer Panel"))
# Add more widgets...
Complete Example¶
Panel Class¶
# my_package/panels/monitor_panel.py
"""Real-time device monitor panel."""
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QLabel, QTableWidget, QVBoxLayout
from lightfall.ui.panels.base import BasePanel, PanelMetadata
class MonitorPanel(BasePanel):
"""Panel for monitoring device values in real-time."""
panel_metadata = PanelMetadata(
id="my_package.panels.monitor",
name="Device Monitor",
description="Real-time device value monitoring",
category="devices",
icon="mdi.monitor-eye",
singleton=True,
)
def __init__(self, parent=None):
super().__init__(parent)
self._table: QTableWidget | None = None
self._timer: QTimer | None = None
self._setup_ui()
self._start_updates()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Header
header = QLabel("Device Monitor")
header.setStyleSheet("font-weight: bold; font-size: 14px;")
layout.addWidget(header)
# Table for values
self._table = QTableWidget()
self._table.setColumnCount(3)
self._table.setHorizontalHeaderLabels(["Device", "Value", "Status"])
layout.addWidget(self._table)
def _start_updates(self):
self._timer = QTimer(self)
self._timer.timeout.connect(self._update_values)
self._timer.start(1000) # Update every second
def _update_values(self):
# Update table with current device values
pass
def closeEvent(self, event):
if self._timer:
self._timer.stop()
super().closeEvent(event)
Plugin Class¶
# my_package/plugins/monitor_plugin.py
"""Monitor panel plugin."""
from typing import TYPE_CHECKING
from lightfall.plugins.panel_plugin import PanelPlugin
if TYPE_CHECKING:
from lightfall.ui.panels.base import BasePanel
class MonitorPanelPlugin(PanelPlugin):
"""Panel plugin providing the device monitor."""
@property
def name(self) -> str:
return "monitor"
def get_panel_class(self) -> type[BasePanel]:
from my_package.panels.monitor_panel import MonitorPanel
return MonitorPanel
Registration¶
Built-in Manifest¶
PluginEntry(
type_name="panel",
name="monitor",
import_path="my_package.plugins.monitor_plugin:MonitorPanelPlugin",
preload=True, # Recommended for panels
),
Why Preload?¶
Panels should use preload=True so they are registered with PanelRegistry before the main window is created. This ensures:
Panels appear in the View menu
Saved layouts can restore panels
Other plugins can reference the panel
Panel Categories¶
Common categories for organizing panels:
Category |
Purpose |
|---|---|
|
Device monitoring and control |
|
Data analysis tools |
|
Data acquisition controls |
|
General utilities |
|
Logging and debugging |
Panel Metadata Fields¶
Field |
Type |
Required |
Description |
|---|---|---|---|
|
|
Yes |
Unique identifier (e.g., “my.panels.viewer”) |
|
|
Yes |
Display name in menus |
|
|
No |
Tooltip description |
|
|
No |
Menu category (default: “tools”) |
|
|
No* |
Icon name (qtawesome format). Effectively required for plugin panels — see Sidebar visibility below. |
|
|
No |
Allow only one instance? (default: True) |
|
|
No |
Dock area: |
|
|
No |
Sidebar section grouping (default: “top”) |
|
|
No |
Sort order among sidebar buttons |
Sidebar visibility (gotchas)¶
Two PanelMetadata choices make a plugin panel register successfully but
invisibly — no error, no warning, no sidebar icon:
default_area="center"gets no sidebar button. The docking manager’s runtime registration handler (DockingManager._on_panel_registered) returns early for"center"panels without creating a button or deferring the panel."center"is reserved for the host’s own main-workspace content, not runtime-registered plugin panels. Use"left"(sidebar top section) or"bottom"(sidebar bottom section).An empty
iconrenders an invisible sidebar button. The button is created but has nothing to draw. Always set a qtawesome icon name (e.g."mdi.chart-line").
Pin both in a regression test in your plugin repo:
def test_metadata_sidebar_visible():
md = MyPanel.panel_metadata
assert md.default_area in {"left", "bottom"}
assert md.icon
(Real-world case: the lightfall-saxs panel shipped with
default_area="center" and passed every test while never appearing in the
sidebar — found 2026-07-27.)
Minimal Example¶
The simplest possible panel plugin:
# plugin.py
from lightfall.plugins.panel_plugin import PanelPlugin
class SimplePanelPlugin(PanelPlugin):
@property
def name(self) -> str:
return "simple"
def get_panel_class(self):
from my_package.panels import SimplePanel
return SimplePanel
# panels.py
from PySide6.QtWidgets import QLabel, QVBoxLayout
from lightfall.ui.panels.base import BasePanel, PanelMetadata
class SimplePanel(BasePanel):
panel_metadata = PanelMetadata(
id="my.simple",
name="Simple Panel",
icon="mdi.tools", # required in practice: empty icon = invisible sidebar button
default_area="left", # "left" or "bottom" only (see Sidebar visibility)
)
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.addWidget(QLabel("Hello, World!"))