From eb9aa1a94cf6e2d2af9c79a4629938e917ac76bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20I=C3=B1igo=20Blasco?= Date: Fri, 26 Jun 2026 20:21:27 +0200 Subject: [PATCH] feat(dialog): radio-column + interactive sub-panel additions to the dialog protocol Two additive, tail-appended extensions to the dialog protocol/SDK: - Radio-column support in the table widget (an exclusive single-select column). - Interactive sub-panel protocol (requestSubPanel / closeSubPanel) for plugin-driven nested panels. Both are backward-compatible SDK additions; bumps the SDK to 0.14.0. --- CMakeLists.txt | 2 +- conanfile.py | 4 +-- docs/dialog-sdk-reference.md | 4 ++- .../pj_plugins/host/widget_data_view.hpp | 30 +++++++++++++++++ .../pj_plugins/host/widget_event_builder.hpp | 7 ++++ .../pj_plugins/sdk/dialog_plugin_typed.hpp | 9 +++++ .../include/pj_plugins/sdk/widget_data.hpp | 33 +++++++++++++++++++ .../include/pj_plugins/sdk/widget_event.hpp | 5 +++ recipe.yaml | 2 +- 9 files changed, 91 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b45781ba..a733b128 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,7 +118,7 @@ endif() if(PJ_INSTALL_SDK) include(CMakePackageConfigHelpers) - set(PJ_PACKAGE_VERSION "0.12.0") + set(PJ_PACKAGE_VERSION "0.14.0") set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_sdk) install(EXPORT plotjuggler_sdkTargets diff --git a/conanfile.py b/conanfile.py index 74fc5c95..9d6bc0ef 100644 --- a/conanfile.py +++ b/conanfile.py @@ -6,7 +6,7 @@ plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK) plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog) -A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.11.0` and then: +A consuming Conan recipe declares e.g. `plotjuggler_sdk/0.14.0` and then: find_package(plotjuggler_sdk REQUIRED COMPONENTS plugin_sdk) target_link_libraries(my_plugin PRIVATE plotjuggler_sdk::plugin_sdk) @@ -30,7 +30,7 @@ class PlotjugglerSdkConan(ConanFile): name = "plotjuggler_sdk" - version = "0.12.0" + version = "0.14.0" # Apache-2.0 covers the whole SDK (pj_base + pj_plugins). See LICENSE. license = "Apache-2.0" url = "https://github.com/PlotJuggler/plotjuggler_sdk" diff --git a/docs/dialog-sdk-reference.md b/docs/dialog-sdk-reference.md index 5f475e93..31b4d958 100644 --- a/docs/dialog-sdk-reference.md +++ b/docs/dialog-sdk-reference.md @@ -74,6 +74,7 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl | `setTableRows(name, vector>)` | Set row data | | `setSelectedRows(name, vector)` | Set selected row indices | | `setDisabledRows(name, vector)` | Grey out rows (non-selectable) | +| `setTableRadioColumn(name, column, checked_row)` | Render `column` as an exclusive radio group; `checked_row` is selected (-1 = none). Fires `onTableRadioSelected`. | ### QFrame Chart Container @@ -138,8 +139,9 @@ Override these in your `DialogPluginTyped` subclass. Return `true` when state ch | `onClicked(name)` | QPushButton | (no payload) | | `onFileSelected(name, path)` | QPushButton (file picker) | Selected file path | | `onFolderSelected(name, path)` | QPushButton (folder picker) | Selected folder path | -| `onSelectionChanged(name, items)` | QListWidget, QTableWidget | Vector of selected item texts | +| `onSelectionChanged(name, items)` | QListWidget, QTableWidget | Vector of selected item texts (table: column-0 text) | | `onItemDoubleClicked(name, index)` | QListWidget, QTableWidget | Row index of double-clicked item | +| `onTableRadioSelected(name, row)` | QTableWidget radio column | Row whose radio was clicked (see `setTableRadioColumn`) | | `onCodeChanged(name, code)` | QPlainTextEdit code editor | Edited code | | `onCodeChangedWithCursor(name, code, cursor)` | QPlainTextEdit code editor | Edited code + caret offset (`cursor < 0` when no opt-in / not reported); defaults to `onCodeChanged` | | `onItemsDropped(name, items)` | Any widget with `setDropTarget` | Dropped item labels | diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp index f5dcf3f7..0c6cef29 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp @@ -111,6 +111,15 @@ class WidgetDataView { return result; } + /// Column to render as an exclusive radio-button group (see setTableRadioColumn). + [[nodiscard]] std::optional tableRadioColumn(std::string_view name) const { + return getInt(name, "radio_column"); + } + /// Row whose radio is checked in the radio column (-1 = none). + [[nodiscard]] std::optional tableRadioCheckedRow(std::string_view name) const { + return getInt(name, "radio_checked_row"); + } + [[nodiscard]] std::optional> selectedRows(std::string_view name) const { const nlohmann::json* w = widget(name); if (!w) { @@ -441,6 +450,27 @@ class WidgetDataView { return ui_it->get(); } + /// Returns the interactive sub-panel UI XML if requestSubPanel was called, or + /// nullopt. The sub-panel is live (events flow back to the plugin); see + /// WidgetData::requestSubPanel. Observed by PanelEngine. + [[nodiscard]] std::optional subPanelUi() const { + auto it = data_.find("__request_sub_panel"); + if (it == data_.end() || !it->is_object()) { + return std::nullopt; + } + auto ui_it = it->find("ui"); + if (ui_it == it->end() || !ui_it->is_string()) { + return std::nullopt; + } + return ui_it->get(); + } + + /// True if WidgetData::closeSubPanel was called (dismiss the interactive sub-panel). + [[nodiscard]] bool subPanelClose() const { + auto it = data_.find("__request_sub_panel_close"); + return it != data_.end() && it->is_boolean() && it->get(); + } + /// Returns the close-reason string if WidgetData::requestClose was called, or /// nullopt. Observed by PanelEngine; ignored by DialogEngine. [[nodiscard]] std::optional requestClose() const { diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_event_builder.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_event_builder.hpp index 683741b1..ec831809 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_event_builder.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/host/widget_event_builder.hpp @@ -106,6 +106,13 @@ struct WidgetEventBuilder { return j.dump(); } + /// QTableWidget: a radio button in the radio column was selected (row index). + [[nodiscard]] static std::string tableRadioSelected(int row) { + nlohmann::json j; + j["table_radio_row"] = row; + return j.dump(); + } + /// Code editor: code changed. `cursor` is the caret offset (bytes) in the new /// text, or negative when unknown; it is serialized only when >= 0, so callers /// that omit it stay wire-compatible with readers that ignore the field. diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_typed.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_typed.hpp index 640f2f08..841f291c 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_typed.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_typed.hpp @@ -68,6 +68,12 @@ class DialogPluginTyped : public DialogPluginBase { return false; } + /// QTableWidget: a radio button in the table's radio column was selected + /// (see WidgetData::setTableRadioColumn). `row` is the newly-checked row. + virtual bool onTableRadioSelected(std::string_view /*widget_name*/, int /*row*/) { + return false; + } + virtual bool onCodeChanged(std::string_view /*widget_name*/, std::string_view /*code*/) { return false; } @@ -160,6 +166,9 @@ class DialogPluginTyped : public DialogPluginBase { if (auto v = event.headerSection()) { return onHeaderClicked(widget_name, *v); } + if (auto v = event.tableRadioRow()) { + return onTableRadioSelected(widget_name, *v); + } // value: try int first, then double if (auto v = event.valueInt()) { return onValueChanged(widget_name, *v); diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp index 9c51c0f2..55f699a5 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp @@ -177,6 +177,18 @@ class WidgetData { return *this; } + /// Render column `column` of a QTableWidget as an exclusive radio-button group: + /// one QRadioButton per row, only one on at a time. `checked_row` is the row + /// whose radio is selected (-1 for none). The cells in `column` carry the radio + /// instead of text. Clicking a radio fires onTableRadioSelected(name, row). + /// Re-send on every build to keep the checked row in sync. + WidgetData& setTableRadioColumn(std::string_view name, int column, int checked_row) { + auto& e = entry(name); + e["radio_column"] = column; + e["radio_checked_row"] = checked_row; + return *this; + } + // --- Chart (QFrame used as chart container) --- /// Set chart series data on a QFrame widget. The host will create or update @@ -461,6 +473,27 @@ class WidgetData { return *this; } + /// Request that the host open an INTERACTIVE sub-panel with the given UI XML. + /// Unlike requestSubDialog (a one-shot modal that only harvests inputs on OK), + /// the sub-panel is a live, non-blocking child: its widget events flow back to + /// the plugin through the normal handlers (onTextChanged / onSelectionChanged / + /// onItemDoubleClicked / onClicked, keyed by the sub-panel widgets' objectNames), + /// and the plugin keeps pushing WidgetData to it every tick (so previews/lists + /// update live). The host emits a synthetic onClicked("subPanelClosed") when the + /// user dismisses it. Send closeSubPanel() to dismiss it programmatically. + /// Only one sub-panel is open at a time; re-requesting while one is open is ignored. + WidgetData& requestSubPanel(std::string_view ui_xml) { + data_["__request_sub_panel"] = nlohmann::json{{"ui", ui_xml}}; + return *this; + } + + /// Request that the host close the interactive sub-panel opened by requestSubPanel + /// (e.g. after the user picked an item). No-op if none is open. + WidgetData& closeSubPanel() { + data_["__request_sub_panel_close"] = true; + return *this; + } + /// Request that the host close the panel hosting this plugin (PanelEngine). /// `reason` is a free-form plugin-defined string (e.g. "import_complete", /// "user_back", "error") forwarded to the host's onCloseRequested callback. diff --git a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_event.hpp b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_event.hpp index d0db1cc7..a241b0a7 100644 --- a/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_event.hpp +++ b/pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_event.hpp @@ -101,6 +101,11 @@ class WidgetEvent { return getInt("header_section"); } + /// QTableWidget: a radio button in the radio column was selected (row index) + std::optional tableRadioRow() const { + return getInt("table_radio_row"); + } + /// Code editor: code changed std::optional codeChanged() const { return getString("code_changed"); diff --git a/recipe.yaml b/recipe.yaml index 2439291f..1fa08def 100644 --- a/recipe.yaml +++ b/recipe.yaml @@ -1,7 +1,7 @@ schema_version: 1 context: - version: "0.11.0" + version: "0.14.0" package: name: plotjuggler_sdk