From b571c87b505140bfa0cc3884ec091890b220c19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20I=C3=B1igo=20Blasco?= Date: Sat, 27 Jun 2026 23:06:17 +0200 Subject: [PATCH] feat(dialog): add save-file picker to the dialog SDK Add WidgetData::setSaveFilePicker and the matching WidgetDataView accessors so a plugin can turn a button into a native "save as" chooser. The chosen path is delivered through the existing onFileSelected handler, so no new event type or typed handler is required. This is a backward-compatible API addition, so bump the SDK to 0.13.0 and bring recipe.yaml back in sync with the other version sources. Contents: - setSaveFilePicker(name, text, filter, title, default_suffix) -> "save_file_picker" action - WidgetDataView: isSaveFilePicker / saveFilePickerFilter / Title / DefaultSuffix - version 0.13.0 in conanfile.py, CMakeLists.txt, recipe.yaml + docstring example - docs: dialog-sdk-reference.md and dialog-plugin-guide.md --- CMakeLists.txt | 2 +- conanfile.py | 4 ++-- docs/dialog-sdk-reference.md | 5 ++-- .../pj_plugins/host/widget_data_view.hpp | 20 ++++++++++++++++ .../include/pj_plugins/sdk/widget_data.hpp | 16 +++++++++++++ pj_plugins/docs/dialog-plugin-guide.md | 23 +++++++++++++++++++ recipe.yaml | 2 +- 7 files changed, 66 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b45781ba..58620483 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.13.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..99eff96b 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.13.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.13.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..69c163c4 100644 --- a/docs/dialog-sdk-reference.md +++ b/docs/dialog-sdk-reference.md @@ -56,8 +56,9 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl | `setButtonIcon(name, svg_data)` | Set an inline SVG icon (custom/one-off) | | `setButtonIconNamed(name, icon_id)` | Set a button icon by id, resolved from the host's themed icon set (consistent tinting; unknown id → no icon) | | `setShortcut(name, key_sequence)` | Assign keyboard shortcut (e.g. `"Ctrl+A"`) | -| `setFilePicker(name, text, filter, title)` | Turn into file picker | +| `setFilePicker(name, text, filter, title)` | Turn into file picker (open existing) | | `setFolderPicker(name, text, title)` | Turn into folder picker | +| `setSaveFilePicker(name, text, filter, title, default_suffix="")` | Turn into save-file picker (type a name / pick location); delivered via `onFileSelected` | ### QListWidget @@ -136,7 +137,7 @@ Override these in your `DialogPluginTyped` subclass. Return `true` when state ch | `onValueChanged(name, int)` | QSpinBox | New integer value | | `onValueChanged(name, double)` | QDoubleSpinBox | New double value | | `onClicked(name)` | QPushButton | (no payload) | -| `onFileSelected(name, path)` | QPushButton (file picker) | Selected file path | +| `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 | | `onItemDoubleClicked(name, index)` | QListWidget, QTableWidget | Row index of double-clicked item | 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..99826d7a 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 @@ -288,6 +288,26 @@ class WidgetDataView { return getString(name, "title"); } + // --- Save-file picker --- + [[nodiscard]] bool isSaveFilePicker(std::string_view name) const { + const nlohmann::json* w = widget(name); + if (!w) { + return false; + } + auto it = w->find("action"); + return it != w->end() && it->is_string() && it->get() == "save_file_picker"; + } + + [[nodiscard]] std::optional saveFilePickerFilter(std::string_view name) const { + return getString(name, "filter"); + } + [[nodiscard]] std::optional saveFilePickerTitle(std::string_view name) const { + return getString(name, "title"); + } + [[nodiscard]] std::optional saveFilePickerDefaultSuffix(std::string_view name) const { + return getString(name, "default_suffix"); + } + // --- RangeSlider --- [[nodiscard]] std::optional rangeSliderMin(std::string_view name) const { return getInt(name, "range_min"); 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..02693b78 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 @@ -316,6 +316,22 @@ class WidgetData { return *this; } + /// Mark a button as a "save file" chooser: the host opens a native save dialog + /// (the user types a name and picks a location), then delivers the chosen path + /// as a fileSelected event — handle it in onFileSelected, like setFilePicker. + /// `default_suffix` is appended when the typed name carries no extension. + WidgetData& setSaveFilePicker( + std::string_view name, std::string_view button_text, std::string_view filter, std::string_view title, + std::string_view default_suffix = "") { + auto& e = entry(name); + e["button_text"] = button_text; + e["action"] = "save_file_picker"; + e["filter"] = filter; + e["title"] = title; + e["default_suffix"] = default_suffix; + return *this; + } + // --- QDateTimeEdit --- /// Set the displayed date+time as an ISO-8601 string (e.g. "2026-05-21T13:45:00"). /// Empty string clears any prior value (widget falls back to its default). diff --git a/pj_plugins/docs/dialog-plugin-guide.md b/pj_plugins/docs/dialog-plugin-guide.md index 7c09f8d9..b13312e9 100644 --- a/pj_plugins/docs/dialog-plugin-guide.md +++ b/pj_plugins/docs/dialog-plugin-guide.md @@ -354,6 +354,7 @@ work like polling a server for available topics. | QDoubleSpinBox | `setValue(double)` | `onValueChanged(name, double)` | | QPushButton | `setButtonText` | `onClicked(name)` | | QPushButton (file picker) | `setFilePicker` | `onFileSelected(name, path)` | +| QPushButton (save-file picker) | `setSaveFilePicker` | `onFileSelected(name, path)` | | QPushButton (folder picker) | `setFolderPicker` | `onFolderSelected(name, path)` | | QLabel | `setLabel` | (none — display only) | | QListWidget | `setListItems`, `setSelectedItems` | `onSelectionChanged(name, items)`, `onItemDoubleClicked(name, index)` | @@ -457,6 +458,28 @@ bool onFileSelected(std::string_view name, std::string_view path) override { } ``` +### Save-file picker + +For an *export* button, use `setSaveFilePicker()` instead. The host shows a +native save dialog (the user types a name and picks a location) and delivers the +chosen path through the same `onFileSelected()` handler — distinguish the button +by its `objectName`. The optional `default_suffix` is appended when the typed +name carries no extension. + +```cpp +// In widget_data(): +wd.setSaveFilePicker("export_btn", "Export...", "*.json", "Export Library", "json"); + +// Same handler as the file picker, routed by name: +bool onFileSelected(std::string_view name, std::string_view path) override { + if (name == "export_btn") { + writeLibraryTo(path); + return true; + } + return false; +} +``` + ### Dynamic visibility and enabled state Control widget visibility and enabled state from `widget_data()` to build diff --git a/recipe.yaml b/recipe.yaml index 2439291f..9a92d7bc 100644 --- a/recipe.yaml +++ b/recipe.yaml @@ -1,7 +1,7 @@ schema_version: 1 context: - version: "0.11.0" + version: "0.13.0" package: name: plotjuggler_sdk