Skip to content

Commit 9544789

Browse files
facontidavideDavide Faconticlaude
authored
update of plugin SDK (#96)
* feat(sdk): add pj.settings.v1 service + setButtonIconNamed dialog field Two additions plugins requested while porting the Mosaico toolbox to be Qt-free. pj.settings.v1 — an optional, QSettings-like key/value store, Qt-free for plugins. The whole service lives in the plugin SDK (pj_base), header-only: - C ABI PJ_settings_store_t/_vtable_t (plugin_data_api.h): string get/set, native string-list get/set, contains, remove; getter out-params valid until the next call on the store. - Plugin side — sdk::SettingsView + sdk::SettingsValue (plugin_data_api.hpp): QSettings-flavored setValue(key, v) overloads (string/int64/double/bool/ stringlist) and value(key).toString()/.toInt()/.toDouble()/.toBool() (QVariant-like), plus valueStringList/contains/remove. Scalars (de)serialize via to_chars/from_chars. - sdk::SettingsStoreService trait (service_traits.hpp). - Host side — sdk/settings_store_host.hpp (header-only): SettingsBackend interface (the GUI app implements it over QSettings), SettingsStoreHost adapter owning the getter scratch buffers, InMemorySettingsBackend for tests/headless hosts. - Unit test (pj_base/tests): scalar/list round-trips, defaults, bool parsing, contains/remove. setButtonIconNamed(widget, icon_id) — dialog field (button_icon_name) so plugins reference the host's themed icon set instead of shipping SVG bytes; resolution lives in the host. Mirrors setButtonIcon(svg). Docs: ARCHITECTURE service list + dialog-sdk-reference updated. Verified: ./build.sh builds core; settings_store_host_test passes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(dialog-protocol): de-Mosaico the dialog protocol Remove the Mosaico-specific surface that #93 added to the generic dialog protocol, keeping only generic, reusable building-block channels: - Drop the MetadataQueryBar channel entirely: setQuery{Keys,Operators,Values, Completions,Schema,Feedback} + their WidgetDataView getters, the QuerySelector event/builder, and the onQuerySelector callback + dispatch. The query bar is a single-plugin widget and no longer lives in the host; plugins own their query language and render feedback via generic text fields. - Drop the Mosaico "every_day" recurrence flag from DateRangeFilter, the dateRangeChanged event/builder, and onDateRangeChanged. A generic date-range picker emits only (from_iso, to_iso); recurrence is a plugin concern. - Rename SequencePicker -> DateRangePicker in comments/labels. - Add a generic setFieldValid(name, ok, tooltip) hook (+ fieldValid/ fieldValidTooltip readers): the plugin owns the validation rule, the host renders the indicator. This also fulfills the validation-indicator gap noted in toolbox-porting-gap-analysis.md. Update dialog-plugin-guide.md, dialog-sdk-reference.md, and toolbox-porting-gap-analysis.md to match. Builds clean; dialog-protocol tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sdk): surface settings read faults via Expected; bump to 0.4.0 SettingsView::value/valueStringList/contains now return PJ::Expected so a host backend fault is reported instead of being silently collapsed into an absent key. An unbound store (optional service) or a missing key stays a graceful default in a successful Expected, so !has_value() means exactly a real host error — consistent with the SDK's other fallible read views. Also correct the misleading "(v4)" label on the settings and colormap ABI service comments (both are protocol v1), and add test coverage for the exception→PJ_error_t path, unbound-view degradation, scratch-buffer read independence, and the new dialog fields (setButtonIconNamed, setFieldValid, 2-arg dateRangeChanged). Bump 0.3.1 -> 0.4.0: MINOR, since the dialog-protocol refactor removes source-visible SDK symbols. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dialog-protocol): generalize date hint to two-sided setDateRangePlaceholder Finish the Layer-1 de-Mosaico cleanup of the dialog protocol: replace the single-sided setDatePickerEarliest / picker_earliest accessor with a two-sided, Qt-idiomatic setDateRangePlaceholder(name, earliest_iso, latest_iso) (host readers dateRangeEarliest / dateRangeLatest, JSON date_range_earliest/latest). It stays a soft placeholder hint — mirroring QLineEdit::setPlaceholderText, not a selectable-range constraint — and now lets a DateRangePicker hint both ends of the dataset's available span. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Davide Faconti <dfaconti@aurynrobotics.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent cd03e22 commit 9544789

21 files changed

Lines changed: 946 additions & 155 deletions

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ endif()
196196
if(PJ_INSTALL_SDK)
197197
include(CMakePackageConfigHelpers)
198198

199-
set(PJ_PACKAGE_VERSION "0.3.1")
199+
set(PJ_PACKAGE_VERSION "0.4.0")
200200
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_core)
201201

202202
install(EXPORT plotjuggler_coreTargets

conanfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK)
88
plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog)
99
10-
A consuming Conan recipe declares e.g. `plotjuggler_core/0.3.1` and then:
10+
A consuming Conan recipe declares e.g. `plotjuggler_core/0.4.0` and then:
1111
1212
find_package(plotjuggler_core REQUIRED COMPONENTS plugin_sdk)
1313
target_link_libraries(my_plugin PRIVATE plotjuggler_core::plugin_sdk)
@@ -27,7 +27,7 @@
2727

2828
class PlotjugglerCoreConan(ConanFile):
2929
name = "plotjuggler_core"
30-
version = "0.3.1"
30+
version = "0.4.0"
3131
# Apache-2.0 covers pj_base + pj_plugins (the plugin-facing SDK);
3232
# MPL-2.0 covers pj_datastore (the storage engine). See LICENSE.
3333
license = "Apache-2.0 AND MPL-2.0"

docs/dialog-sdk-reference.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl
5353
| Method | Description |
5454
|--------|-------------|
5555
| `setButtonText(name, text)` | Set button label |
56-
| `setButtonIcon(name, svg_data)` | Set an inline SVG icon |
56+
| `setButtonIcon(name, svg_data)` | Set an inline SVG icon (custom/one-off) |
57+
| `setButtonIconNamed(name, icon_id)` | Set a button icon by id, resolved from the host's themed icon set (consistent tinting; unknown id → no icon) |
5758
| `setShortcut(name, key_sequence)` | Assign keyboard shortcut (e.g. `"Ctrl+A"`) |
5859
| `setFilePicker(name, text, filter, title)` | Turn into file picker |
5960
| `setFolderPicker(name, text, title)` | Turn into folder picker |
@@ -110,6 +111,7 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl
110111
| `setEnabled(name, bool)` | Enable/disable widget |
111112
| `setVisible(name, bool)` | Show/hide widget |
112113
| `setDropTarget(name, bool)` | Accept dropped item labels and emit `onItemsDropped` |
114+
| `setFieldValid(name, ok, tooltip)` | Inline valid/invalid indicator the plugin drives (optional tooltip) |
113115

114116
### Dialog-level Commands
115117

docs/toolbox-porting-gap-analysis.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,16 @@ The library tab has a green/red SVG circle indicator that validates Lua code in
277277
278278
This gives the user immediate feedback on library syntax errors before saving.
279279
280-
**SDK equivalent needed:**
280+
**SDK equivalent (now available):**
281281
```cpp
282-
WidgetData& setWidgetIndicator(std::string_view name,
283-
IndicatorState state, // OK, ERROR, WARNING
284-
std::string_view tooltip);
282+
WidgetData& setFieldValid(std::string_view name, bool ok, std::string_view tooltip = {});
285283
```
286284

287-
This is different from `setEnabled` — it is a visual status indicator that does not affect interactivity.
285+
This is different from `setEnabled` — it is a visual status indicator that does not
286+
affect interactivity. The plugin owns the validation rule (e.g. attempting to construct a
287+
`ReactiveLuaFunction` with the current code) and pushes the boolean result; the host
288+
renders the indicator. The green/red case maps directly onto the boolean — there is no
289+
separate WARNING state.
288290

289291
### 6.3 · Reactive execution tied to time slider (CRITICAL — Lua)
290292

@@ -450,9 +452,8 @@ WidgetData& setCodeLanguage(std::string_view name, std::string_view lang); // "l
450452
// DialogPluginTyped event handler:
451453
virtual bool onCodeChanged(std::string_view name, std::string_view code);
452454
453-
// Validation indicator:
454-
WidgetData& setWidgetIndicator(std::string_view name,
455-
IndicatorState state, std::string_view tooltip);
455+
// Validation indicator (available):
456+
WidgetData& setFieldValid(std::string_view name, bool ok, std::string_view tooltip = {});
456457
```
457458

458459
### 9.4 Time-synchronized reactive execution

pj_base/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ if(PJ_BUILD_TESTS)
6060
tests/span_test.cpp
6161
tests/expected_test.cpp
6262
tests/plugin_data_api_test.cpp
63+
tests/settings_store_host_test.cpp
6364
tests/data_source_protocol_test.cpp
6465
# TODO: data_source_plugin_base_test.cpp and
6566
# message_parser_plugin_base_test.cpp exercised old bind_write_host /

pj_base/include/pj_base/plugin_data_api.h

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ typedef struct {
726726
} PJ_object_read_host_t;
727727

728728
/**
729-
* Colormap registry service (v4).
729+
* Colormap registry service ("pj.colormap.v1", protocol_version 1).
730730
*
731731
* Independent host-provided service for toolbox plugins that want to
732732
* publish named colormap callbacks.
@@ -750,6 +750,50 @@ typedef struct {
750750
const PJ_colormap_registry_vtable_t* vtable;
751751
} PJ_colormap_registry_t;
752752

753+
/**
754+
* Settings store service ("pj.settings.v1", protocol_version 1).
755+
*
756+
* Optional host-provided key/value persistence, modeled on Qt's QSettings but
757+
* Qt-free. Plugins read/write small scalars (stored as strings) and string
758+
* lists keyed by a string; the host owns the backing store (e.g. QSettings in
759+
* the GUI app, a JSON file in a headless host) and namespaces keys per plugin.
760+
* All calls are [main-thread]. A string / list returned through an out-param
761+
* remains valid only until the next call on the same store.
762+
*/
763+
typedef struct PJ_settings_store_vtable_t {
764+
uint32_t protocol_version;
765+
uint32_t struct_size;
766+
767+
/* [main-thread] Read a string. *out_found reports whether the key exists; on
768+
* found, *out_value is valid until the next call on this store. */
769+
bool (*get_string)(
770+
void* ctx, PJ_string_view_t key, PJ_string_view_t* out_value, bool* out_found, PJ_error_t* out_error) PJ_NOEXCEPT;
771+
772+
/* [main-thread] Write a string (create or overwrite). */
773+
bool (*set_string)(void* ctx, PJ_string_view_t key, PJ_string_view_t value, PJ_error_t* out_error) PJ_NOEXCEPT;
774+
775+
/* [main-thread] Read a string list. On found, *out_items points to an array
776+
* of *out_count entries, valid until the next call on this store. */
777+
bool (*get_string_list)(
778+
void* ctx, PJ_string_view_t key, const PJ_string_view_t** out_items, size_t* out_count, bool* out_found,
779+
PJ_error_t* out_error) PJ_NOEXCEPT;
780+
781+
/* [main-thread] Write a string list (create or overwrite). */
782+
bool (*set_string_list)(
783+
void* ctx, PJ_string_view_t key, const PJ_string_view_t* items, size_t count, PJ_error_t* out_error) PJ_NOEXCEPT;
784+
785+
/* [main-thread] Report whether a key exists, in *out_present. */
786+
bool (*contains)(void* ctx, PJ_string_view_t key, bool* out_present, PJ_error_t* out_error) PJ_NOEXCEPT;
787+
788+
/* [main-thread] Remove a key (no-op if absent). */
789+
bool (*remove_key)(void* ctx, PJ_string_view_t key, PJ_error_t* out_error) PJ_NOEXCEPT;
790+
} PJ_settings_store_vtable_t;
791+
792+
typedef struct {
793+
void* ctx;
794+
const PJ_settings_store_vtable_t* vtable;
795+
} PJ_settings_store_t;
796+
753797
#ifdef __cplusplus
754798
}
755799
#endif

pj_base/include/pj_base/sdk/plugin_data_api.hpp

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Copyright 2026 Davide Faconti
33
// SPDX-License-Identifier: Apache-2.0
44

5+
#include <charconv>
6+
#include <cstdint>
57
#include <cstring>
68
#include <functional>
79
#include <initializer_list>
@@ -1284,4 +1286,217 @@ class ColorMapRegistryView {
12841286
PJ_colormap_registry_t registry_{};
12851287
};
12861288

1289+
// ---------------------------------------------------------------------------
1290+
// SettingsView — typed C++ view over PJ_settings_store_t
1291+
// ---------------------------------------------------------------------------
1292+
1293+
/// A read result from SettingsView, modeled on Qt's QVariant: it holds the
1294+
/// raw stored string (or nothing, if the key was absent) and converts on
1295+
/// demand with a caller-supplied default. Scalars are stored as strings by
1296+
/// SettingsView::setValue and parsed back here.
1297+
class SettingsValue {
1298+
public:
1299+
SettingsValue() = default;
1300+
explicit SettingsValue(std::optional<std::string> raw) : raw_(std::move(raw)) {}
1301+
1302+
/// True when the key was absent (no value stored).
1303+
[[nodiscard]] bool isNull() const noexcept {
1304+
return !raw_.has_value();
1305+
}
1306+
1307+
[[nodiscard]] std::string toString(std::string_view def = {}) const {
1308+
return raw_.has_value() ? *raw_ : std::string(def);
1309+
}
1310+
1311+
[[nodiscard]] std::int64_t toInt(std::int64_t def = 0) const {
1312+
return parse<std::int64_t>(def);
1313+
}
1314+
1315+
[[nodiscard]] double toDouble(double def = 0.0) const {
1316+
return parse<double>(def);
1317+
}
1318+
1319+
/// "true"/"1"/"on" → true; "false"/"0"/"off" → false; otherwise @p def.
1320+
[[nodiscard]] bool toBool(bool def = false) const {
1321+
if (!raw_.has_value()) {
1322+
return def;
1323+
}
1324+
const std::string& s = *raw_;
1325+
if (s == "true" || s == "1" || s == "on") {
1326+
return true;
1327+
}
1328+
if (s == "false" || s == "0" || s == "off") {
1329+
return false;
1330+
}
1331+
return def;
1332+
}
1333+
1334+
private:
1335+
template <typename T>
1336+
[[nodiscard]] T parse(T def) const {
1337+
if (!raw_.has_value()) {
1338+
return def;
1339+
}
1340+
T out{};
1341+
const char* begin = raw_->data();
1342+
const char* end = begin + raw_->size();
1343+
auto [ptr, ec] = std::from_chars(begin, end, out);
1344+
return (ec == std::errc{} && ptr == end) ? out : def;
1345+
}
1346+
1347+
std::optional<std::string> raw_;
1348+
};
1349+
1350+
/// C++ wrapper around PJ_settings_store_t — an optional, QSettings-like
1351+
/// key/value store the host may expose to plugins (service "pj.settings.v1").
1352+
/// Empty-constructible; `valid()` tells whether the host bound a store.
1353+
/// Scalars are stored as strings; reads return an Expected so a backend fault
1354+
/// is visible rather than silently masked as a missing key:
1355+
/// if (auto v = settings.value("count")) { int n = v->toInt(42); }
1356+
/// An unbound store or an absent key is a successful Expected holding a null
1357+
/// value/empty list/false — only a host backend fault yields `!has_value()`.
1358+
/// All calls are main-thread, mirroring QSettings usage.
1359+
class SettingsView {
1360+
public:
1361+
SettingsView() = default;
1362+
explicit SettingsView(PJ_settings_store_t store) : store_(store) {}
1363+
1364+
[[nodiscard]] bool valid() const noexcept {
1365+
return store_.vtable != nullptr && store_.ctx != nullptr;
1366+
}
1367+
1368+
// --- writes (QSettings setValue style; scalars serialized to string) ---
1369+
1370+
[[nodiscard]] Status setValue(std::string_view key, std::string_view value) const {
1371+
if (!valid() || store_.vtable->set_string == nullptr) {
1372+
return unexpected("settings store is not bound");
1373+
}
1374+
PJ_error_t err{};
1375+
if (!store_.vtable->set_string(store_.ctx, toAbiString(key), toAbiString(value), &err)) {
1376+
return unexpected(errorToString(err));
1377+
}
1378+
return okStatus();
1379+
}
1380+
1381+
/// const char* overload so a string literal binds to the string setter
1382+
/// rather than the bool one.
1383+
[[nodiscard]] Status setValue(std::string_view key, const char* value) const {
1384+
return setValue(key, std::string_view(value == nullptr ? "" : value));
1385+
}
1386+
1387+
[[nodiscard]] Status setValue(std::string_view key, std::int64_t value) const {
1388+
const std::string s = std::to_string(value);
1389+
return setValue(key, std::string_view(s));
1390+
}
1391+
1392+
[[nodiscard]] Status setValue(std::string_view key, int value) const {
1393+
return setValue(key, static_cast<std::int64_t>(value));
1394+
}
1395+
1396+
[[nodiscard]] Status setValue(std::string_view key, double value) const {
1397+
char buf[40];
1398+
auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), value);
1399+
if (ec != std::errc{}) {
1400+
return unexpected("settings: failed to format double value");
1401+
}
1402+
return setValue(key, std::string_view(buf, static_cast<std::size_t>(ptr - buf)));
1403+
}
1404+
1405+
[[nodiscard]] Status setValue(std::string_view key, bool value) const {
1406+
return setValue(key, std::string_view(value ? "true" : "false"));
1407+
}
1408+
1409+
[[nodiscard]] Status setValue(std::string_view key, const std::vector<std::string>& values) const {
1410+
if (!valid() || store_.vtable->set_string_list == nullptr) {
1411+
return unexpected("settings store is not bound");
1412+
}
1413+
std::vector<PJ_string_view_t> raw;
1414+
raw.reserve(values.size());
1415+
for (const auto& v : values) {
1416+
raw.push_back(toAbiString(v));
1417+
}
1418+
PJ_error_t err{};
1419+
if (!store_.vtable->set_string_list(store_.ctx, toAbiString(key), raw.data(), raw.size(), &err)) {
1420+
return unexpected(errorToString(err));
1421+
}
1422+
return okStatus();
1423+
}
1424+
1425+
// --- reads ---
1426+
1427+
/// Read a scalar as a QVariant-like SettingsValue. A failed Expected means
1428+
/// the host backend faulted; success holds a null value (isNull()) when the
1429+
/// store is unbound (optional service) or the key is absent. So `!has_value()`
1430+
/// is exactly a real host error, not a missing key.
1431+
[[nodiscard]] Expected<SettingsValue> value(std::string_view key) const {
1432+
if (!valid() || store_.vtable->get_string == nullptr) {
1433+
return SettingsValue{};
1434+
}
1435+
PJ_string_view_t out{};
1436+
bool found = false;
1437+
PJ_error_t err{};
1438+
if (!store_.vtable->get_string(store_.ctx, toAbiString(key), &out, &found, &err)) {
1439+
return unexpected(errorToString(err));
1440+
}
1441+
if (!found) {
1442+
return SettingsValue{};
1443+
}
1444+
// Copy out of the host's scratch buffer before it can be reused.
1445+
return SettingsValue{std::string(toStringView(out))};
1446+
}
1447+
1448+
/// Read a string list. A failed Expected means the host backend faulted;
1449+
/// success holds an empty vector when the store is unbound or the key is
1450+
/// absent.
1451+
[[nodiscard]] Expected<std::vector<std::string>> valueStringList(std::string_view key) const {
1452+
std::vector<std::string> result;
1453+
if (!valid() || store_.vtable->get_string_list == nullptr) {
1454+
return result;
1455+
}
1456+
const PJ_string_view_t* items = nullptr;
1457+
std::size_t count = 0;
1458+
bool found = false;
1459+
PJ_error_t err{};
1460+
if (!store_.vtable->get_string_list(store_.ctx, toAbiString(key), &items, &count, &found, &err)) {
1461+
return unexpected(errorToString(err));
1462+
}
1463+
if (!found) {
1464+
return result;
1465+
}
1466+
result.reserve(count);
1467+
for (std::size_t i = 0; i < count; ++i) {
1468+
result.emplace_back(toStringView(items[i]));
1469+
}
1470+
return result;
1471+
}
1472+
1473+
/// Report whether a key exists. A failed Expected means the host backend
1474+
/// faulted; success holds false when the store is unbound.
1475+
[[nodiscard]] Expected<bool> contains(std::string_view key) const {
1476+
if (!valid() || store_.vtable->contains == nullptr) {
1477+
return false;
1478+
}
1479+
bool present = false;
1480+
PJ_error_t err{};
1481+
if (!store_.vtable->contains(store_.ctx, toAbiString(key), &present, &err)) {
1482+
return unexpected(errorToString(err));
1483+
}
1484+
return present;
1485+
}
1486+
1487+
[[nodiscard]] Status remove(std::string_view key) const {
1488+
if (!valid() || store_.vtable->remove_key == nullptr) {
1489+
return unexpected("settings store is not bound");
1490+
}
1491+
PJ_error_t err{};
1492+
if (!store_.vtable->remove_key(store_.ctx, toAbiString(key), &err)) {
1493+
return unexpected(errorToString(err));
1494+
}
1495+
return okStatus();
1496+
}
1497+
1498+
private:
1499+
PJ_settings_store_t store_{};
1500+
};
1501+
12871502
} // namespace PJ::sdk

pj_base/include/pj_base/sdk/service_traits.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ struct ColorMapRegistryService {
158158
static_assert(detail::isValidServiceName(kName), "kName must match the pj naming rule");
159159
};
160160

161+
/// Optional QSettings-like key/value persistence exposed to any plugin family.
162+
/// Host-backed (QSettings in the GUI app, JSON in a headless host); keys are
163+
/// namespaced per plugin by the host.
164+
struct SettingsStoreService {
165+
static constexpr const char* kName = "pj.settings.v1";
166+
static constexpr uint32_t kMinVersion = 1;
167+
using Raw = PJ_settings_store_t;
168+
using Vtable = PJ_settings_store_vtable_t;
169+
using View = SettingsView;
170+
static_assert(detail::isValidServiceName(kName), "kName must match the pj naming rule");
171+
};
172+
161173
/// Runtime host exposed to DataSource plugins — progress, diagnostics,
162174
/// state notification, parser binding, modal message boxes.
163175
struct DataSourceRuntimeHostService {

0 commit comments

Comments
 (0)