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

type_name

"panel"

Plugin type identifier

is_singleton

True

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

  1. Plugin is instantiated on load (recommended: preload=True)

  2. get_panel_class() is called to get the BasePanel subclass

  3. Panel class is registered with PanelRegistry

  4. Panel can be instantiated via PanelRegistry.create(panel_id)

  5. 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

"devices"

Device monitoring and control

"analysis"

Data analysis tools

"acquisition"

Data acquisition controls

"tools"

General utilities

"logs"

Logging and debugging

Panel Metadata Fields

Field

Type

Required

Description

id

str

Yes

Unique identifier (e.g., “my.panels.viewer”)

name

str

Yes

Display name in menus

description

str

No

Tooltip description

category

str

No

Menu category (default: “tools”)

icon

str

No*

Icon name (qtawesome format). Effectively required for plugin panels — see Sidebar visibility below.

singleton

bool

No

Allow only one instance? (default: True)

default_area

str

No

Dock area: "left" (default) or "bottom" for sidebar panels. Do not use "center" for plugin panels — see below.

sidebar_group

str

No

Sidebar section grouping (default: “top”)

sidebar_order

int

No

Sort order among sidebar buttons

Two PanelMetadata choices make a plugin panel register successfully but invisibly — no error, no warning, no sidebar icon:

  1. 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).

  2. An empty icon renders 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!"))