Source code for jdxi_editor.ui.widgets.editor.section_base

"""
Section Base Widget

This module provides the `SectionBaseWidget` class, which provides a common base
for all editor sections. It automatically adds standardized icon rows based on
section type, ensuring consistency across all tabs.

Classes:
--------
- `SectionBaseWidget`: Base widget that automatically adds icon rows to sections.

Features:
---------
- Automatic icon row addition based on section type
- Consistent layout structure
- Theme-aware styling (analog vs regular)
- Easy to subclass for new sections

Usage Example:
--------------
    # In a section's __init__ or setup_ui method:

    class MySection(SectionBaseWidget):
        def __init__(self, ...):
            super().__init__(icon_type="adsr", analog=False)
            # Your initialization code
            self.setup_ui()

        def setup_ui(self):
            layout = self.get_layout()  # Gets the QVBoxLayout with icon row already added
            # Add your controls to layout
            layout.addWidget(my_widget)

"""

from typing import Any, Callable, Literal, Optional

from decologr import Decologr as log
from PySide6.QtCore import QSize
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QButtonGroup, QHBoxLayout, QLabel, QPushButton, QTabWidget

from jdxi_editor.midi.data.address.address import JDXiSysExAddress
from jdxi_editor.midi.data.analog.lfo import AnalogLFOShape
from jdxi_editor.midi.data.digital.filter import DigitalFilterMode
from jdxi_editor.midi.data.digital.lfo import DigitalLFOShape
from jdxi_editor.midi.data.parameter.analog.address import AnalogParam
from jdxi_editor.midi.data.parameter.analog.spec import AnalogFilterMode
from jdxi_editor.midi.data.parameter.digital import DigitalPartialParam
from jdxi_editor.midi.data.parameter.digital.spec import (
    JDXiMidiDigital,
    TabDefinitionMixin,
)
from jdxi_editor.midi.io.helper import MidiIOHelper
from jdxi_editor.ui.adsr.spec import ADSRSpec, ADSRStage
from jdxi_editor.ui.common import JDXi, QVBoxLayout, QWidget
from jdxi_editor.ui.editors.synth.base import SynthBase
from jdxi_editor.ui.widgets.adsr.adsr import ADSR
from jdxi_editor.ui.widgets.editor.helper import (
    create_button_with_icon,
    create_envelope_group,
    create_group_with_layout,
    create_icon_from_qta,
    create_layout_with_child,
    create_layout_with_items,
    create_widget_with_layout,
    transfer_layout_items,
)
from jdxi_editor.ui.widgets.editor.icon_type import IconType
from jdxi_editor.ui.widgets.spec import ComboBoxSpec, SliderSpec, SwitchSpec
from picomidi.sysex.parameter.address import AddressParameter


[docs] class SectionBaseWidget(SynthBase): """ Base widget for editor sections that automatically adds icon rows. This widget standardizes section structure by automatically adding appropriate icon rows based on section type, reducing boilerplate and ensuring consistency. """ from jdxi_editor.ui.editors.base.layout.spec import LayoutSpec
[docs] WAVEFORM_SPECS: list[SliderSpec] = []
[docs] spec: LayoutSpec | None = None
[docs] BUTTON_ENABLE_RULES: dict[Any, list[str]] = {}
[docs] BUTTON_SPECS: list = [] # optional waveform/mode/shape buttons
[docs] SYNTH_SPEC = JDXiMidiDigital
def __init__( self, send_midi_parameter: Callable = None, midi_helper: Optional[MidiIOHelper] = None, address: JDXiSysExAddress = None, parent: Optional[QWidget] = None, icons_row_type: Literal[ IconType.ADSR, IconType.OSCILLATOR, IconType.GENERIC, IconType.NONE ] = "adsr", analog: bool = False, controls=None, ): """ Initialize the SectionBaseWidget. :param parent: Parent widget :param icons_row_type: Type of icon row to add ("adsr", "oscillator", "generic", or "none") :param analog: Whether to apply analog-specific styling :param midi_helper: Optional MIDI helper for communication :param controls: Optional dict-like for parameter widgets; passed to SynthBase when provided. """ super().__init__(midi_helper=midi_helper, parent=parent, controls=controls)
[docs] self.wave_shape_param: list | None = None
[docs] self.wave_shape_buttons = None
# Only set default if subclass (e.g. oscillator/filter) did not set wave_shapes before super().__init__() if not hasattr(self, "wave_shapes"): self.wave_shapes: list | None = None
[docs] self.wave_shape_icon_map: dict | None = None
[docs] self.analog: bool = analog
[docs] self.icons_row_type = icons_row_type
[docs] self._layout: Optional[QVBoxLayout] = None
[docs] self._icon_added: bool = False
[docs] self.midi_helper = midi_helper
[docs] self.address: JDXiSysExAddress | None = address
[docs] self._set_param = send_midi_parameter
[docs] self.tab_widget = None
[docs] self.adsr_widget = None
[docs] self.amp_control_widgets = []
[docs] self.button_widgets = {}
self.button_widgets: dict[Any, QPushButton] = {}
[docs] self.slider_widgets: dict[Any, QWidget] = {}
self.build_widgets() skip_base_setup = getattr(self.__class__, "SKIP_BASE_SETUP_UI", False) if not skip_base_setup: self._setup_ui() if self._get_button_specs(): self._initialize_button_states() # ------------------------------- # Layout & Tabs # -------------------------------
[docs] def _setup_ui(self): """Assemble section UI""" layout = self.create_layout() if self.button_widgets: button_layout = self._create_button_row_layout() if button_layout is not None: layout.addLayout(button_layout) self._create_tab_widget() layout.addWidget(self.tab_widget)
[docs] def get_layout( self, margins: tuple[int, int, int, int] = None, spacing: int = None ) -> QVBoxLayout: """ Get or create the main layout for this section. If the layout doesn't exist, creates it and adds the icon row. This should be called at the start of setup_ui() or init_ui(). :param margins: Optional tuple of (left, top, right, bottom) margins :param spacing: Optional spacing between widgets :return: The main QVBoxLayout """ if self._layout is None: self._layout = QVBoxLayout() self.setLayout(self._layout) # Set margins and spacing if provided if margins is not None: # Margins class now has __iter__, so it can be unpacked directly # Also handles tuples/lists for backward compatibility self._layout.setContentsMargins(*margins) if spacing is not None: self._layout.setSpacing(spacing) # Apply styling JDXi.UI.Theme.apply_adsr_style(self, analog=self.analog) # Add icon row if not disabled if self.icons_row_type != IconType.NONE and not self._icon_added: self._add_icon_row() self._icon_added = True return self._layout
# ------------------------------- # Build Widgets # -------------------------------
[docs] def build_widgets(self): """Build sliders, switches, combo boxes, buttons, and ADSR""" self._build_widgets() # Optional waveform / mode / shape buttons if self._get_button_specs(): self._create_waveform_buttons() # Optional ADSR envelope: # - Most sections advertise ADSR via class-level spec_adsr # - Some newer specs may carry an `adsr` field on `self.spec` if getattr(self, "spec", None) is not None: adsr_spec = getattr(self.spec, "adsr", None) if adsr_spec: self._create_adsr()
[docs] def _create_tab_widget(self): """Override to add Pan slider in its own horizontal layout""" self.tab_widget = QTabWidget() controls_widget = self._create_controls_widget() self._add_tab(key=self.SYNTH_SPEC.Amp.Tab.CONTROLS, widget=controls_widget) # --- ADSR tab if any if self.adsr_widget: adsr_group = create_envelope_group( adsr_widget=self.adsr_widget, analog=self.analog ) self._add_tab(key=self.SYNTH_SPEC.Amp.Tab.ADSR, widget=adsr_group)
[docs] def generate_mode_button_specs(self): """formerly generate_wave_shapes before generalization""" raise NotImplementedError( "generate_mode_button_specs must be implemented by subclass" )
[docs] def _create_controls_widget(self) -> QWidget: """Controls tab""" controls_layout = create_layout_with_items(self.amp_control_widgets) return create_widget_with_layout(controls_layout)
[docs] def _get_param_specs(self) -> list: """Return the main list of specs for widget creation. Supports SLIDER_GROUPS as dict or object with .controls.""" groups = self.spec if hasattr(groups, "get"): return groups.get("controls", []) if hasattr(groups, "controls"): return groups.controls return []
[docs] def _build_widgets(self): """Create widgets from SLIDER_GROUPS['controls'] (sliders, switches, combos).""" for spec in self._get_param_specs(): if isinstance(spec, SliderSpec): widget = self._create_parameter_slider( spec.param, spec.label, vertical=spec.vertical ) elif isinstance(spec, SwitchSpec): widget = self._create_parameter_switch( spec.param, spec.label, spec.options ) elif isinstance(spec, ComboBoxSpec): widget = self._create_parameter_combo_box( spec.param, spec.label, options=spec.options ) else: continue self.controls[spec.param] = widget self.amp_control_widgets.append(widget)
[docs] def _update_button_enabled_states(self, button_param): """Enable/disable controls based on BUTTON_ENABLE_RULES""" def _widget_for_key(key): if hasattr(self, "_resolve_rule_widget"): return self._resolve_rule_widget(key) return getattr(self, key, None) # --- Disable all first for attrs in self.BUTTON_ENABLE_RULES.values(): for key in attrs: widget = _widget_for_key(key) if widget is not None: widget.setEnabled(False) # --- Enable per selected button for key in self.BUTTON_ENABLE_RULES.get(button_param, []): widget = _widget_for_key(key) if widget is not None: widget.setEnabled(True)
[docs] def _initialize_button_states(self): """Set initial button state (first in wave_shapes / BUTTON_SPECS). Uses spec.param (SliderSpec and WaveShapeSpec both expose .param).""" specs = self._get_button_specs() if specs: first_param = getattr(specs[0], "param", None) if first_param is not None: self._on_button_selected(first_param)
[docs] def _create_button_row_layout(self): """Create layout for button row. Override in subclasses.""" if not self.button_widgets: return None widget_layout = create_layout_with_items(list(self.button_widgets.values())) layout = create_layout_with_child(widget_layout) return layout
[docs] def _create_adsr(self): """Create ADSR widget from spec_adsr""" attack_key = ADSRStage.ATTACK decay_key = ADSRStage.DECAY sustain_key = ADSRStage.SUSTAIN release_key = ADSRStage.RELEASE peak_key = ADSRStage.DEPTH def get_param(spec_or_param): """Extract parameter from ADSRSpec or return parameter directly""" if isinstance(spec_or_param, ADSRSpec): return spec_or_param.param return spec_or_param attack_spec = self.spec_adsr.get(attack_key) decay_spec = self.spec_adsr.get(decay_key) sustain_spec = self.spec_adsr.get(sustain_key) release_spec = self.spec_adsr.get(release_key) peak_spec = self.spec_adsr.get(peak_key) if peak_key else None attack_param = get_param(attack_spec) if attack_spec else None decay_param = get_param(decay_spec) if decay_spec else None sustain_param = get_param(sustain_spec) if sustain_spec else None release_param = get_param(release_spec) if release_spec else None peak_param = get_param(peak_spec) if peak_spec else None self.adsr_widget = ADSR( attack_param=attack_param, decay_param=decay_param, sustain_param=sustain_param, release_param=release_param, peak_param=peak_param, midi_helper=self.midi_helper, create_parameter_slider=self._create_parameter_slider, controls=self.controls, address=self.address, analog=self.analog, )
[docs] def _add_centered_row(self, *widgets: QWidget) -> None: """add centered row""" row = QHBoxLayout() row.addStretch() for w in widgets: row.addWidget(w) row.addStretch() self.get_layout().addLayout(row)
[docs] def _get_button_specs(self): """Return list of button specs: wave_shapes when set by subclass, else BUTTON_SPECS.""" return getattr(self, "wave_shapes", None) or self.BUTTON_SPECS
[docs] def _resolve_icon(self, icon_name: str | None) -> QIcon | None: """resolve icon""" if not icon_name: return None # 1 — generated waveform icon try: icon = JDXi.UI.Icon.generate_waveform_icon_by_name(None, icon_name) if icon and not icon.isNull(): return icon except Exception: pass # 2 — cached/generated registry icon try: icon = JDXi.UI.Icon.get_generated_icon(icon_name) if icon and not icon.isNull(): return icon except Exception: pass # 3 — QTA fallback try: from jdxi_editor.ui.widgets.editor.helper import create_icon_from_qta icon = create_icon_from_qta(icon_name) if icon and not icon.isNull(): return icon except Exception: pass return None
[docs] def _build_wave_button(self, spec) -> QPushButton: """Build wave button""" label = getattr(spec, "label", getattr(spec, "name", "Button")) btn = QPushButton(label) btn.setCheckable(True) JDXi.UI.Theme.apply_button_rect(btn, analog=self.analog) btn.setFixedSize( JDXi.UI.Dimensions.WaveformIcon.WIDTH, JDXi.UI.Dimensions.WaveformIcon.HEIGHT, ) icon = self._resolve_icon(getattr(spec, "icon_name", None)) if icon: btn.setIcon(icon) btn.setIconSize( QSize( JDXi.UI.Dimensions.LFOIcon.WIDTH, JDXi.UI.Dimensions.LFOIcon.HEIGHT ) ) btn.clicked.connect(lambda _, p=spec.param: self._on_button_selected(p)) return btn
[docs] def _register_button(self, spec, btn: QPushButton): """register button""" self.button_widgets[spec.param] = btn if not isinstance(spec.param, DigitalFilterMode): self.controls[spec.param] = btn
[docs] def _create_waveform_buttons(self): """Create mode/waveform/shape buttons from wave_shapes or BUTTON_SPECS.""" specs = self._get_button_specs() if not specs: return for spec in specs: btn = self._build_wave_button(spec) self._register_button(spec, btn) # compatibility with DigitalFilterSection if specs and isinstance(specs[0].param, DigitalFilterMode): self.filter_mode_buttons = self.button_widgets
[docs] def _add_icon_row(self) -> None: """Add the appropriate icon row based on icon_type""" if self._layout is None: return # --- Create a container layout to avoid "already has a parent" errors icon_row_container = QHBoxLayout() if self.icons_row_type == IconType.ADSR: icon_hlayout = JDXi.UI.Icon.create_adsr_icons_row() elif self.icons_row_type == IconType.OSCILLATOR: icon_hlayout = JDXi.UI.Icon.create_oscillator_icons_row() elif self.icons_row_type == IconType.GENERIC: icon_hlayout = JDXi.UI.Icon.create_generic_musical_icon_row() else: return # --- IconType.NONE or unknown # --- Transfer all items from icon_hlayout to icon_row_container transfer_layout_items(icon_hlayout, icon_row_container) self._layout.addLayout(icon_row_container)
[docs] def _create_adsr_group(self): """Create amp ADSR envelope using standardized helper""" self.adsr_widget = self.build_adsr_widget() self.adsr_group = create_envelope_group( name="Envelope", adsr_widget=self.adsr_widget, analog=self.analog )
[docs] def build_adsr_widget(self) -> ADSR: """build ADSR widget""" # --- Extract parameters from ADSRSpec objects def get_param(spec_or_param): """Extract parameter from ADSRSpec or return parameter directly""" if isinstance(spec_or_param, ADSRSpec): return spec_or_param.param return spec_or_param attack_spec = self.spec.adsr.get(ADSRStage.ATTACK) decay_spec = self.spec.adsr.get(ADSRStage.DECAY) sustain_spec = self.spec.adsr.get(ADSRStage.SUSTAIN) release_spec = self.spec.adsr.get(ADSRStage.RELEASE) peak_spec = self.spec.adsr.get(ADSRStage.DEPTH) attack_param = get_param(attack_spec) if attack_spec else None decay_param = get_param(decay_spec) if decay_spec else None sustain_param = get_param(sustain_spec) if sustain_spec else None release_param = get_param(release_spec) if release_spec else None peak_param = get_param(peak_spec) if peak_spec else None amp_env_adsr_widget = ADSR( attack_param=attack_param, decay_param=decay_param, sustain_param=sustain_param, release_param=release_param, peak_param=peak_param, midi_helper=self.midi_helper, create_parameter_slider=self._create_parameter_slider, address=self.address, controls=self.controls, analog=self.analog, ) return amp_env_adsr_widget
[docs] def setup_ui(self) -> None: """ Setup the UI for this section. Subclasses should override this method and call get_layout() at the start to ensure the icon row is added. Example: def setup_ui(self): layout = self.get_layout() layout.addWidget(my_widget) """ # --- Default implementation - subclasses should override self.get_layout()
[docs] def init_ui(self) -> None: """ Initialize the UI for this section. Alias for setup_ui() for sections that use init_ui() naming. Subclasses can override either setup_ui() or init_ui(). """ self.setup_ui()
[docs] def _add_tab( self, *, key: TabDefinitionMixin, widget: QWidget, ) -> None: """Add a tab using TabDefinitionMixin pattern""" from jdxi_editor.midi.data.digital.oscillator import WaveForm # --- Handle both regular icons and generated waveform icons waveform_type_values = { WaveForm.ADSR, WaveForm.UPSAW, WaveForm.SQUARE, WaveForm.PWSQU, WaveForm.TRIANGLE, WaveForm.SINE, WaveForm.SAW, WaveForm.SPSAW, WaveForm.PCM, WaveForm.NOISE, WaveForm.LPF_FILTER, WaveForm.HPF_FILTER, WaveForm.BYPASS_FILTER, WaveForm.BPF_FILTER, WaveForm.FILTER_SINE, } # --- Find the tab widget (could be tab_widget or oscillator_tab_widget, etc.) tab_widget = None if hasattr(self, "tab_widget") and self.tab_widget is not None: tab_widget = self.tab_widget else: self.tab_widget = QTabWidget() tab_widget = self.tab_widget # --- Handle icon - could be a string (qtawesome icon name) or WaveformType value if isinstance(key.icon, str) and key.icon in waveform_type_values: # --- Use generated icon for waveform types icon = JDXi.UI.Icon.get_generated_icon(key.icon) elif isinstance(key.icon, str) and key.icon.startswith("mdi."): # --- Direct qtawesome icon name (e.g., "mdi.numeric-1-circle-outline") icon = JDXi.UI.Icon.get_icon(key.icon, color=JDXi.UI.Style.GREY) else: # --- Use regular icon from registry icon = JDXi.UI.Icon.get_icon(key.icon, color=JDXi.UI.Style.GREY) widget.setMaximumHeight(JDXi.UI.Dimensions.EDITOR.HEIGHT) tab_widget.addTab(widget, icon, key.label) setattr(self, key.attr_name, widget)
[docs] def create_layout(self): """create main rows layout""" layout = self.get_layout( margins=JDXi.UI.Dimensions.EDITOR_DIGITAL.MARGINS, spacing=JDXi.UI.Dimensions.EDITOR_DIGITAL.SPACING, ) layout.addSpacing(JDXi.UI.Dimensions.EDITOR_DIGITAL.SPACING) return layout
# ------------------------------- # Button Logic # -------------------------------
[docs] def _on_button_selected(self, button_param): """Handle button selection & enabling dependent widgets""" if button_param is None: return for btn in self.button_widgets.values(): btn.setChecked(False) JDXi.UI.Theme.apply_button_rect(btn, analog=self.analog) selected_btn = self.button_widgets.get(button_param) if selected_btn is None: return selected_btn.setChecked(True) JDXi.UI.Theme.apply_button_active(selected_btn, analog=self.analog) self._update_button_enabled_states(button_param) if self._set_param: # --- Map filter mode enums to their corresponding parameter if isinstance(button_param, DigitalFilterMode): # --- Filter mode buttons map to FILTER_MODE_SWITCH parameter actual_param = DigitalPartialParam.FILTER_MODE_SWITCH param_value = button_param.value elif isinstance(button_param, AnalogFilterMode): # --- Filter mode buttons map to FILTER_MODE_SWITCH parameter actual_param = AnalogParam.FILTER_MODE_SWITCH param_value = button_param.value else: # --- For other button types (like waveform), use the param directly actual_param = button_param param_value = getattr(button_param, "value", button_param) if actual_param is None: return # --- Ensure we have a valid AddressParameter before sending if not isinstance(actual_param, AddressParameter): from decologr import Decologr as log log.error( f"Cannot send MIDI: {button_param} is not an AddressParameter (got {type(button_param)})" ) return self._set_param(actual_param, param_value)
[docs] def _add_widget_rows(self, layout: QHBoxLayout, rows: list[list[QWidget]]): """add a list of rows of widgets to a layout""" for row in rows: layout.addLayout(create_layout_with_items(row))
[docs] def _add_group_with_widget_rows(self, label: str, rows: list[list[QWidget]]): """Create a group box, populate it with rows of widgets, and add it to the parent layout.""" group, group_layout = create_group_with_layout(label=label) self._add_widget_rows(group_layout, rows) group_layout.addStretch() layout = self.get_layout() layout.addWidget(group) JDXi.UI.Theme.apply_adsr_style(group, analog=self.analog)
[docs] def _create_shape_row(self): """Shape and sync controls""" shape_label = QLabel("Shape") layout_widgets = [shape_label] self.wave_shape_group = QButtonGroup(self) self.wave_shape_group.setExclusive(True) for wave in self.wave_shapes: icon = create_icon_from_qta(wave.icon) btn = create_button_with_icon( icon_name=wave.shape.display_name, icon=icon, button_dimensions=JDXi.UI.Dimensions.WaveformIcon, icon_dimensions=JDXi.UI.Dimensions.LFOIcon, ) btn.setCheckable(True) JDXi.UI.Theme.apply_button_rect(btn, analog=self.analog) self.wave_shape_group.addButton(btn, wave.shape.value) self.wave_shape_buttons[wave.shape] = btn layout_widgets.append(btn) self.wave_shape_group.idToggled.connect(self._on_shape_group_changed) return create_layout_with_items(layout_widgets)
[docs] def _apply_wave_shape_style(self, active_shape): """Apply wave shape style (match Digital Filter section mode buttons).""" for shape, btn in self.wave_shape_buttons.items(): btn.setChecked(shape == active_shape) if shape == active_shape: JDXi.UI.Theme.apply_button_active(btn, analog=self.analog) else: JDXi.UI.Theme.apply_button_rect(btn, analog=self.analog)
[docs] def _on_shape_group_changed(self, shape_value: int, checked: bool) -> None: """on shape group changed""" log.message( "[LFO Shape] _on_shape_group_changed shape_value %s, checked: %s section: %s", shape_value, checked, self.__class__.__name__, scope=self.__class__.__name__, ) if not checked: return try: shape = self.SYNTH_SPEC.LFO.Shape(shape_value) self.set_wave_shape(shape, send_midi=True) except Exception as ex: log.error( "[SectionBaseWidget] [_on_shape_group_changed] error %s occurred", ex, scope=self.__class__.__name__, )
[docs] def _get_wave_shape_button(self, shape: DigitalLFOShape | AnalogLFOShape): """get wave shape button""" btn = self.wave_shape_buttons.get(shape) if btn is None: log.warning( "[LFO Shape] No button for %s (section=%s)", shape, self.__class__.__name__, scope=self.__class__.__name__, ) return btn
[docs] def set_wave_shape( self, shape: DigitalLFOShape | AnalogLFOShape, send_midi: bool = False ): """Update UI + optionally send MIDI""" btn = self._get_wave_shape_button(shape) log.message(f"selected {shape}", scope="set_wave_shape") if not btn: return self._apply_wave_shape_ui(btn, shape) if send_midi: self._send_wave_shape_midi(shape)
[docs] def _send_wave_shape_midi(self, shape: DigitalLFOShape | AnalogLFOShape): """Send Wave Shape""" if self._set_param: address = getattr(self, "address", None) log.message( "[LFO Shape] sending MIDI param: %s value %s address %s section %s", getattr(self.wave_shape_param, "name", self.wave_shape_param), shape.value, address, self.__class__.__name__, scope=self.__class__.__name__, ) if not self._set_param(self.wave_shape_param, shape.value, address): log.warning( f"Failed to set Mod LFO shape to {shape.name}", scope=self.__class__.__name__, ) elif not self._set_param: log.warning( "[LFO Shape] send_midi=True but send_midi_parameter is not set (section=%s)", self.__class__.__name__, scope=self.__class__.__name__, )
[docs] def _apply_wave_shape_ui( self, btn: Any | None, shape: DigitalLFOShape | AnalogLFOShape ): """Apply wave shape UI""" # --- Prevent recursive signals when updating from MIDI self.wave_shape_group.blockSignals(True) btn.setChecked(True) self.wave_shape_group.blockSignals(False) self._apply_wave_shape_style(shape)
[docs] def _wrap_row(self, widgets: list[QWidget]) -> QWidget: """ Convert a list of controls into a QWidget row container. Qt rule: layouts cannot be inserted where a QWidget is required (tabs, group boxes, pages). So we wrap the layout inside a QWidget. """ row_widget = QWidget() row_layout = create_layout_with_items(widgets) row_widget.setLayout(row_layout) return row_widget
[docs] def _build_sliders(self, specs: list["SliderSpec"]): """build sliders""" return [ self._create_parameter_slider( spec.param, spec.label, vertical=spec.vertical ) for spec in specs ]
[docs] def _build_combo_boxes(self, specs: list["ComboBoxSpec"]): """build combo boxes""" return [ self._create_parameter_combo_box( spec.param, spec.label, spec.options, spec.values ) for spec in specs ]
[docs] def _build_switches(self, specs: list["SwitchSpec"]): return [ self._create_parameter_switch(spec.param, spec.label, spec.options) for spec in specs ]
[docs] def _build_layout_spec(self): """Build layout spec""" raise NotImplementedError("Should be implemented in subclass")