--- name: create-setting-panel description: Use when creating property panels or configuration UI in the data-workbench Qt/C++ project. Triggers include "add a setting panel", "create property panel", "node settings", "workflow property editor", "chart item settings", or "app settings page". --- # Create Setting Panel ## Overview Setting panels in data-workbench use `DAPropertyPanelWidget` for property UI. Three patterns exist. Identify your target before implementing. ## Decision Tree ``` What are you configuring? QwtPlotItem (curve, bar, grid)? → Scenario A: ChartItem Panel (inherit DAChartItemSettingPanel) Arbitrary object (node, custom data model)? → Scenario B: Standalone Panel (inherit QWidget + DAPropertyPanelWidget) Application-level config (preferences, options)? → Scenario C: App Setting Page (inherit DAAbstractSettingPage) ``` ## Core Pattern — 7 Universal Steps These steps apply to **all** DAPropertyPanelWidget-based panels (Scenarios A and B): 1. **Define PropertyId enum** — values start from 1 2. **Create DAPropertyPanelWidget** in constructor 3. **Add properties** — call `addXxxProperty(PID_*, name, ...)` in `buildPropertyPanel()` - Group properties with `addCollapsibleGroup(title)` → `addXxxProperty(...)` → `endGroup()` - Nest sub-panels with `addSubPanel(id, groupName)` for independent property groups 4. **Connect signal chain** — `mPanel->propertyValueChanged` to handler slot 5. **Dispatch in handler** — `switch(propertyId)` with `case PID_*:` blocks 6. **Read values** — use `getxxxValue(PID_*)` in handler 7. **Write back** — apply to target + call target's refresh mechanism ### Grouping & Nesting Rules - After `addCollapsibleGroup(title)`, all subsequent `addXxxProperty()` calls go into that group automatically - Call `endGroup()` to exit the current group and return to root-level layout - Groups can be nested: call `addCollapsibleGroup()` inside another group - `addSubPanel(id, groupName)` creates a nested `DAPropertyPanelWidget` with its own property IDs - Sub-panel signals bubble up: `propertyValueChanged` from sub-panel forwards to parent panel automatically !!! danger "MANDATORY: Always call `endGroup()` immediately after `addCollapsibleGroup()`" Failing to call `endGroup()` will cause all subsequent properties and groups to be nested under the current group, resulting in broken layout and wrong hierarchy. Strictly follow: `addCollapsibleGroup(title)` → add properties → `endGroup()` ### Signal Chain | Scenario | Signal Chain | |----------|-------------| | ChartItem (A) | mPanel → base class forwards → `propertyValueChanged` → subclass slot | | Standalone (B) | 3-hop: `mPanel`→`onPanelPropertyValueChanged`→`emit`→`onPropertyValueChanged` ← P0 critical | | Nested sub-panel | subPanel `propertyValueChanged` → auto forwards to parent `propertyValueChanged` → no extra wiring needed | ## Scenario A: ChartItem Panel (QwtPlotItem) **Inheritance**: `DAChartItemSettingPanel` → `DAAbstractChartItemSettingWidget` → `QWidget` **Key pattern**: - `buildPropertyPanel()` is **pure virtual**, called at end of subclass constructor - `updateUI(QwtPlotItem*)` reads state back (`QSignalBlocker` recommended) - Signal chain is **automatic** via base class - Refresh by calling `replot()` **Skeleton**: ```cpp class MyPanel : public DAChartItemSettingPanel { Q_OBJECT public: enum PropertyID { PID_Title = 1, PID_Color = 2, PID_LineWidth = 3 }; explicit MyPanel(QWidget* parent = nullptr); void updateUI(QwtPlotItem* item) override; protected: void buildPropertyPanel() override; private Q_SLOTS: void onPropertyValueChanged(int propertyId); }; MyPanel::MyPanel(QWidget* parent) : DAChartItemSettingPanel(parent) { connect(this, &DAChartItemSettingPanel::propertyValueChanged, this, &MyPanel::onPropertyValueChanged); buildPropertyPanel(); } void MyPanel::buildPropertyPanel() { auto* pp = propertyPanel(); // Collapsible group for appearance properties pp->addCollapsibleGroup(tr("Appearance")); pp->addStringProperty(PID_Title, tr("Title")); pp->addColorProperty(PID_Color, tr("Color")); pp->addIntProperty(PID_LineWidth, tr("Line Width"), 1, 1, 100); pp->endGroup(); } ``` Register via `DAChartItemSettingPanelFactory::registerPanel(rtti, creator)`. **References**: `src/DAGui/ChartSetting/DAChartItemSettingPanel.h`, `src/DAGui/ChartSetting/DAChartCurveSettingPanel.{h,cpp}` ## Scenario B: Standalone Panel (Any Target) **Inheritance**: `QWidget` directly, owns `DAPropertyPanelWidget*` **Key pattern**: - `buildPropertyPanel()` is a **protected slot**, called directly in constructor - Constructor takes a target identifier (e.g., node type, object ID) - **Signal chain MUST be manually wired** — two connections (see skeleton) - Refresh by calling target's own notify/redraw mechanism (NOT `replot()`) **Skeleton** (workflow node example): ```cpp class DANodePropertyPanel : public QWidget { Q_OBJECT public: enum PropertyId { PID_Name = 1, PID_Color = 2 }; explicit DANodePropertyPanel(QWidget* parent = nullptr); void setTarget(DAWorkFlowNode* node); protected Q_SLOTS: void buildPropertyPanel(); void onPanelPropertyValueChanged(int propertyId); void onPropertyValueChanged(int propertyId); Q_SIGNALS: void propertyValueChanged(int propertyId); }; // Constructor: wire BOTH signal links DANodePropertyPanel::DANodePropertyPanel(QWidget* parent) : QWidget(parent) { mPanel = new DAPropertyPanelWidget(this); // Link 1: panel → forwarding slot connect(mPanel, &DAPropertyPanelWidget::propertyValueChanged, this, &DANodePropertyPanel::onPanelPropertyValueChanged); // Link 2: self → handler slot (P0: often forgotten) connect(this, &DANodePropertyPanel::propertyValueChanged, this, &DANodePropertyPanel::onPropertyValueChanged); buildPropertyPanel(); } void DANodePropertyPanel::buildPropertyPanel() { // Collapsible group for node info mPanel->addCollapsibleGroup(tr("Node Info")); mPanel->addStringProperty(PID_Name, tr("Name")); mPanel->addColorProperty(PID_Color, tr("Color")); mPanel->endGroup(); } ``` **Note**: Non-chart targets do not call `replot()`. Emit the target object's own change signal or call its refresh method instead. **References**: `src/DAGui/ChartSetting/DAChartAxisSettingPanel.{h,cpp}` (composition pattern), `src/DAGui/DAWorkFlowNodeItemSettingWidget.h` (target domain) ## Nested Sub-Panel Pattern Use `addSubPanel()` when a group of properties needs its own independent property ID namespace or separate update cycle. ```cpp void MyPanel::buildPropertyPanel() { auto* pp = propertyPanel(); // Root-level group pp->addCollapsibleGroup(tr("General")); pp->addStringProperty(PID_Title, tr("Title")); pp->endGroup(); // Nested sub-panel with its own property IDs auto* subPanel = pp->addSubPanel(SID_AxisSettings, tr("Axis Settings")); subPanel->addCollapsibleGroup(tr("X Axis")); subPanel->addDoubleProperty(PID_XMin, tr("Min"), 0.0); subPanel->addDoubleProperty(PID_XMax, tr("Max"), 100.0); subPanel->endGroup(); subPanel->addCollapsibleGroup(tr("Y Axis")); subPanel->addDoubleProperty(PID_YMin, tr("Min"), 0.0); subPanel->addDoubleProperty(PID_YMax, tr("Max"), 100.0); subPanel->endGroup(); // No extra signal wiring needed — sub-panel propertyValueChanged // bubbles up to parent panel automatically } // Access sub-panel later for state updates: void MyPanel::updateUI() { auto* subPanel = mPanel->getSubPanel(SID_AxisSettings); QSignalBlocker blocker(subPanel); subPanel->setDoubleValue(PID_XMin, ...); // ... } ``` **Key rules**: - Sub-panel IDs (`SID_*`) are separate from root panel IDs (`PID_*`), no collision - `getSubPanel(id)` returns the `DAPropertyPanelWidget*` for direct read/write - `getSubPanelId(subPanel)` reverse-lookups the ID from a pointer - Sub-panel `propertyValueChanged` auto-forwards to parent, no manual `connect` needed ## Using DAPropertyPanelContainerWidget `DAPropertyPanelContainerWidget` wraps `DAPropertyPanelWidget` inside a `QScrollArea` and proxies **all** its public API. Use it when the panel may grow tall and needs scrolling. ```cpp // Container delegates all DAPropertyPanelWidget methods: DAPropertyPanelContainerWidget* container = new DAPropertyPanelContainerWidget(this); container->addCollapsibleGroup(tr("Settings")); container->addStringProperty(PID_Name, tr("Name")); container->endGroup(); connect(container, &DAPropertyPanelContainerWidget::propertyValueChanged, ...); ``` **Reference**: `src/DACommonWidgets/DAPropertyPanelContainerWidget.h` ## Scenario C: App Setting Page **Inheritance**: `DAAbstractSettingPage` → `QWidget` **Key pattern**: - `apply()` is **pure virtual** — called on Apply/OK click - Emit `settingChanged()` on any property change (otherwise `apply()` is skipped) - Emit `settingApplyed()` after apply completes **Skeleton**: ```cpp class MyAppSettings : public DAAbstractSettingPage { Q_OBJECT public: explicit MyAppSettings(QWidget* parent = nullptr); void apply() override; QString getSettingPageTitle() const override; QIcon getSettingPageIcon() const override; }; ``` **Reference**: `src/DACommonWidgets/DAAbstractSettingPage.h` ## Key Differences | Aspect | ChartItem (A) | Standalone (B) | App Page (C) | |--------|--------------|----------------|--------------| | **Inherits** | DAChartItemSettingPanel | QWidget | DAAbstractSettingPage | | **Target** | QwtPlotItem via `setPlotItem()` | Any object via `setTarget()` | N/A | | **buildPropertyPanel()** | Pure virtual, called by subclass | Protected slot, called directly | N/A | | **Signal chain** | Automatic via base | Manual: 2 connections required | `settingChanged()` signal | | **Sub-panel signals** | Auto bubble-up to parent | Auto bubble-up to parent | N/A | | **Apply style** | Immediate (write + replot) | Immediate (write + notify) | Deferred (`apply()` on OK) | | **Qwt extras** | addCurveStyle/Axis/SymbolProperty | Not available | N/A | | **Factory** | DAChartItemSettingPanelFactory | Manually created | DASettingWidget manages | | **Grouping** | addCollapsibleGroup + endGroup | addCollapsibleGroup + endGroup | N/A | ## DAPropertyPanelWidget API Quick Reference ### Property Adding & Value Access | Method | Purpose | Read / Write | |--------|---------|-------------| | `addColorProperty` / `addFontProperty` / `addBrushProperty` | Color/Font/Brush | `getColorValue`/`setColorValue` etc. | | `addPenProperty` | Pen | `getPenValue`/`setPenValue` | | `addIntProperty` / `addDoubleProperty` / `addBoolProperty` | Numeric/Bool | `getIntValue`/`setIntValue` etc. | | `addStringProperty` / `addEnumProperty` | String/Enum | `getStringValue`/`setStringValue` etc. | | `addAlignmentProperty` / `addFilePathProperty` | Alignment/Path | `getAlignmentValue`/`setAlignmentValue` etc. | | `addProperty` | Custom Widget | `getPropertyItem(id)` | ### Grouping & Nesting | Method | Purpose | Notes | |--------|---------|-------| | `addCollapsibleGroup(title)` | Start collapsible group | Returns group ID (1-based). Subsequent addXxxProperty auto-tracked into this group | | `endGroup()` | Exit current group | Next addXxxProperty goes to root layout | | `addSubPanel(id, groupName)` | Create nested sub-panel | Returns `DAPropertyPanelWidget*` with signal bubble-up | | `getSubPanel(id)` | Get sub-panel by ID | Returns nullptr if not found | | `getSubPanelId(subPanel)` | Reverse-lookup sub-panel ID | Returns -1 if not found | | `getGroupPanel(groupId)` | Get group's internal panel | Returns `DAPropertyPanelWidget*` for the group | | `isGroupExpanded(groupId)` | Check group expand state | Returns true/false | | `setGroupExpanded(groupId, expanded)` | Set group expand state | Programmatic control of collapse/expand | | `addGroupLabel(text)` | **Deprecated** decorative label | Use `addCollapsibleGroup` instead. No collapse functionality | ### DAPropertyPanelContainerWidget `DAPropertyPanelContainerWidget` proxies **all** `DAPropertyPanelWidget` methods listed above. Use `rootPanel()` to access the underlying `DAPropertyPanelWidget` directly if needed. ## Compatibility Notes **QButtonGroup Qt5/Qt6 compat**: ```cpp #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) connect(group, QOverload::of(&QButtonGroup::buttonClicked), ...); #else connect(group, &QButtonGroup::idClicked, ...); #endif ``` **Qt5/Qt6 QButtonGroup addButton**: ```cpp #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) group->addButton(rb, id); #else group->setId(rb, id); group->addButton(rb); #endif ``` ## What NOT to Duplicate The 6-function lifecycle (`setTarget`, `getTarget`, `bindTarget`, `unbindTarget`, `updateUI`, `applySetting`) is documented in `docs/zh/dev-guide/ui/settingwidget-standard.md`. Reference that document — do not re-explain it here.