diff --git a/CMakeLists.txt b/CMakeLists.txt index 5862048..a733b12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -118,7 +118,7 @@ endif() if(PJ_INSTALL_SDK) include(CMakePackageConfigHelpers) - set(PJ_PACKAGE_VERSION "0.13.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 d26c680..104accd 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.13.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,15 +30,15 @@ class PlotjugglerSdkConan(ConanFile): name = "plotjuggler_sdk" - # UNRELEASED BREAK: 0.13.0 unifies markers + transforms into the single host + # UNRELEASED BREAK: 0.13.0 unified markers + transforms into the single host # service `pj.data_processors.v1` via a `kind` discriminator (removed the old # `pj.markers.v1` and the interim `pj.generators.v1`; generalized # `create_data_processor`/`validate_data_processor_script` with kind/language/flags). - # This is an ABI/API change — normally MAJOR — but no PUBLIC tag ever shipped - # `pj.data_processors.v1`, so no released plugin breaks and 0.13.0 stays a valid - # pre-1.0 step. The FIRST public release that carries the unified - # `pj.data_processors.v1` MUST be tagged 1.0.0. See CHANGELOG.md. - version = "0.13.0" + # 0.14.0 then adds the dialog-protocol additions (radio column + interactive + # sub-panel). All pre-1.0 unreleased — no PUBLIC tag ever shipped + # `pj.data_processors.v1`, so no released plugin breaks. The FIRST public release + # that carries the unified `pj.data_processors.v1` MUST be tagged 1.0.0. See CHANGELOG.md. + 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 c7f8f58..20a9c48 100644 --- a/docs/dialog-sdk-reference.md +++ b/docs/dialog-sdk-reference.md @@ -75,6 +75,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 @@ -152,8 +153,9 @@ Override these in your `DialogPluginTyped` subclass. Return `true` when state ch | `onClicked(name)` | QPushButton | (no payload) | | `onFileSelected(name, path)` | QPushButton (file picker or save-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 f9c12c3..cef5baa 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 @@ -112,6 +112,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) { @@ -534,6 +543,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 4161837..49da89c 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 @@ -107,6 +107,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 64161c9..69c6df4 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; } @@ -168,6 +174,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 c79c5b8..74d41ec 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 @@ -274,6 +274,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 creates or updates a Qwt @@ -613,6 +625,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 4af4f42..d39b125 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 @@ -102,6 +102,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 9a92d7b..1fa08de 100644 --- a/recipe.yaml +++ b/recipe.yaml @@ -1,7 +1,7 @@ schema_version: 1 context: - version: "0.13.0" + version: "0.14.0" package: name: plotjuggler_sdk