diff --git a/CHANGELOG.md b/CHANGELOG.md index de71eef..1d7287c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to `plotjuggler_sdk` are recorded here. Versioning policy is in [`CLAUDE.md`](./CLAUDE.md) → "Release Versioning". +## [0.18.0] + +### Feature: QDateTimeEdit event surface (MINOR) + +The dialog protocol's QDateTimeEdit setters (`setDateTime` / `setDateTimeRange`, +shipped earlier) gain their missing event direction, so the widget becomes an +input, not just a display (backward-compatible JSON addition; no C ABI change, +`PJ_DIALOG_PROTOCOL_VERSION` unchanged): + +- `WidgetEvent` key: `datetime_iso` (string — the edited datetime, wall-clock + local ISO-8601; fractional seconds only for ms-precision editors), with + `WidgetEventBuilder::dateTimeChanged()` on the host side, + `WidgetEvent::dateTimeChanged()` on the plugin side, and typed dispatch via + `DialogPluginTyped::onDateTimeChanged()`. +- Docs: `dialog-sdk-reference.md` gains the previously missing QDateTimeEdit + section; the setter contract is clarified (empty/unparsable strings are + ignored — the widget keeps its current value; `QDateEdit`/`QTimeEdit` + subclasses share the binding). + ## [0.17.0] ### Feature: dialog-protocol additions for deletable lists and chart/list placeholders (MINOR) diff --git a/CMakeLists.txt b/CMakeLists.txt index 43d2dae..afa90a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,7 +126,7 @@ endif() if(PJ_INSTALL_SDK) include(CMakePackageConfigHelpers) - set(PJ_PACKAGE_VERSION "0.17.0") + set(PJ_PACKAGE_VERSION "0.18.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 e38b421..506d6fd 100644 --- a/conanfile.py +++ b/conanfile.py @@ -50,7 +50,10 @@ class PlotjugglerSdkConan(ConanFile): # 0.17.0 extends the dialog protocol with backward-compatible additions # (list_deletable / list_placeholder / chart_placeholder keys and the # item_delete_index event) — MINOR. See CHANGELOG.md. - version = "0.17.0" + # 0.18.0 adds the QDateTimeEdit event surface (datetime_iso event, + # WidgetEvent::dateTimeChanged, DialogPluginTyped::onDateTimeChanged) — + # MINOR, header-only additions. See CHANGELOG.md. + version = "0.18.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 aff01a5..82b9c86 100644 --- a/docs/dialog-sdk-reference.md +++ b/docs/dialog-sdk-reference.md @@ -104,6 +104,17 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl |--------|-------------| | `setTabIndex(name, int)` | Set active tab index | +### QDateTimeEdit + +Also binds the `QDateEdit` / `QTimeEdit` subclasses. Datetimes are wall-clock +local time exchanged verbatim; events always carry a full ISO datetime, with +fractional seconds only when the editor's display format includes them. + +| Method | Description | +|--------|-------------| +| `setDateTime(name, iso8601)` | Set the displayed datetime (ISO-8601, e.g. `"2026-05-21T13:45:00"`); empty/unparsable strings are ignored | +| `setDateTimeRange(name, min_iso, max_iso)` | Set the allowed [min, max] datetime range | + ### QDialogButtonBox | Method | Description | @@ -165,6 +176,7 @@ Override these in your `DialogPluginTyped` subclass. Return `true` when state ch | `onChartViewChanged(name, x_min, x_max, y_min, y_max)` | QFrame chart container | Visible chart range | | `onMarkerTimelineChanged(name, marks)` | MarkerTimeline | Full `std::vector` set after a drag/resize/delete | | `onTabChanged(name, index)` | QTabWidget | New tab index | +| `onDateTimeChanged(name, iso8601)` | QDateTimeEdit (incl. QDateEdit/QTimeEdit) | Edited datetime as ISO-8601 string (local wall-clock) | --- 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 cb851a4..45504e6 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 @@ -149,6 +149,14 @@ struct WidgetEventBuilder { return j.dump(); } + /// QDateTimeEdit: displayed datetime edited (ISO-8601, e.g. "2026-05-21T13:45:00"). + /// Wall-clock local time; include fractional seconds only when meaningful. + [[nodiscard]] static std::string dateTimeChanged(std::string_view iso8601) { + nlohmann::json j; + j["datetime_iso"] = iso8601; + return j.dump(); + } + /// RangeSlider: lower/upper handle position changed (slider units). [[nodiscard]] static std::string rangeChanged(int lower, int upper) { nlohmann::json j; 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 d41188d..f7d4b75 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 @@ -127,6 +127,13 @@ class DialogPluginTyped : public DialogPluginBase { return false; } + /// QDateTimeEdit (and its QDateEdit/QTimeEdit subclasses): the displayed + /// datetime was edited. `iso8601` is wall-clock local time, always a full + /// datetime; fractional seconds appear only for ms-precision editors. + virtual bool onDateTimeChanged(std::string_view /*widget_name*/, std::string_view /*iso8601*/) { + return false; + } + private: /// Parses event_json and dispatches to the appropriate typed virtual above. bool onWidgetEvent(std::string_view widget_name, std::string_view event_json) final { @@ -144,6 +151,9 @@ class DialogPluginTyped : public DialogPluginBase { if (auto v = event.dateRangeChanged()) { return onDateRangeChanged(widget_name, v->from_iso, v->to_iso); } + if (auto v = event.dateTimeChanged()) { + return onDateTimeChanged(widget_name, *v); + } if (auto v = event.itemsDropped()) { return onItemsDropped(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 88f82b8..6c206f1 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 @@ -479,7 +479,10 @@ class WidgetData { // --- 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). + /// Datetimes are exchanged verbatim as wall-clock values: a suffixless string is + /// host-local time; an explicit UTC offset is honored on parse. Empty or + /// unparsable strings are ignored — the widget keeps its current value (a + /// QDateTimeEdit always displays one). WidgetData& setDateTime(std::string_view name, std::string_view iso8601) { entry(name)["datetime"] = std::string(iso8601); return *this; 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 5c62f65..8677c8d 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 @@ -157,6 +157,11 @@ class WidgetEvent { return DateRangeFilter{from->get(), to->get()}; } + /// QDateTimeEdit: edited datetime as an ISO-8601 string. + std::optional dateTimeChanged() const { + return getString("datetime_iso"); + } + /// RangeSlider: lower/upper handle positions (slider units). struct RangeValues { int lower; diff --git a/pj_plugins/dialog_protocol/tests/dialog_plugin_typed_test.cpp b/pj_plugins/dialog_protocol/tests/dialog_plugin_typed_test.cpp index 1f5cb2d..1a40872 100644 --- a/pj_plugins/dialog_protocol/tests/dialog_plugin_typed_test.cpp +++ b/pj_plugins/dialog_protocol/tests/dialog_plugin_typed_test.cpp @@ -95,6 +95,13 @@ class RecordingPlugin : public PJ::DialogPluginTyped { return true; } + bool onDateTimeChanged(std::string_view widget_name, std::string_view iso8601) override { + last_handler = "date_time_changed"; + last_widget = std::string(widget_name); + last_text = std::string(iso8601); + return true; + } + bool onCodeChangedWithCursor(std::string_view widget_name, std::string_view code, int cursor) override { last_handler = "code_changed"; last_widget = std::string(widget_name); @@ -220,6 +227,13 @@ TEST_F(TypedDispatchTest, DateRangeChanged) { EXPECT_EQ(plugin_.last_date_to, ""); } +TEST_F(TypedDispatchTest, DateTimeChanged) { + EXPECT_TRUE(dispatch(plugin_, "startTime", R"({"datetime_iso": "2026-01-02T03:04:05"})")); + EXPECT_EQ(plugin_.last_handler, "date_time_changed"); + EXPECT_EQ(plugin_.last_widget, "startTime"); + EXPECT_EQ(plugin_.last_text, "2026-01-02T03:04:05"); +} + // --- Edge cases --- TEST_F(TypedDispatchTest, UnrecognizedFieldReturnsFalse) { diff --git a/pj_plugins/dialog_protocol/tests/widget_event_builder_test.cpp b/pj_plugins/dialog_protocol/tests/widget_event_builder_test.cpp index 1f05fb2..b3ddf1d 100644 --- a/pj_plugins/dialog_protocol/tests/widget_event_builder_test.cpp +++ b/pj_plugins/dialog_protocol/tests/widget_event_builder_test.cpp @@ -135,6 +135,25 @@ TEST(WidgetEventTest_DateRange, MissingFieldYieldsNullopt) { EXPECT_FALSE(ev.dateRangeChanged().has_value()); } +TEST(WidgetEventBuilderTest, DateTimeChanged) { + std::string json = PJ::WidgetEventBuilder::dateTimeChanged("2026-05-21T13:45:00"); + PJ::WidgetEvent ev(json); + ASSERT_TRUE(ev.dateTimeChanged().has_value()); + EXPECT_EQ(*ev.dateTimeChanged(), "2026-05-21T13:45:00"); + // A datetime event must not be routed as plain text. + EXPECT_FALSE(ev.text().has_value()); +} + +TEST(WidgetEventTest_DateTime, MissingFieldYieldsNullopt) { + PJ::WidgetEvent ev("{}"); + EXPECT_FALSE(ev.dateTimeChanged().has_value()); +} + +TEST(WidgetEventTest_DateTime, WrongTypeYieldsNullopt) { + PJ::WidgetEvent ev(R"({"datetime_iso": 42})"); + EXPECT_FALSE(ev.dateTimeChanged().has_value()); +} + // --- Verify JSON is parseable and contains only expected fields --- TEST(WidgetEventBuilderTest, ClickedHasNoExtraFields) { diff --git a/pj_plugins/docs/dialog-plugin-guide.md b/pj_plugins/docs/dialog-plugin-guide.md index b13312e..c3e81ab 100644 --- a/pj_plugins/docs/dialog-plugin-guide.md +++ b/pj_plugins/docs/dialog-plugin-guide.md @@ -361,7 +361,7 @@ work like polling a server for available topics. | QTableWidget | `setTableHeaders`, `setTableRows`, `setSelectedRows`, `setVisibleRows`, `setRowColor`, `setCellTooltip` | `onSelectionChanged(name, items)`, `onHeaderClicked(name, section)` | | QPlainTextEdit | `setPlainText`, `setCodeContent`, `setCodeLanguage`, `setCodeCursor`, `setCodeCaretTracking` | `onCodeChanged(name, code)`, or `onCodeChangedWithCursor(name, code, cursor)` when the editor opts into caret tracking | | QFrame (chart container) | `setChartSeries`, `clearChart`, `setChartZoomEnabled` | `onChartViewChanged(name, x_min, x_max, y_min, y_max)` | -| QDateTimeEdit | `setDateTime`, `setDateTimeRange` | (none — input only) | +| QDateTimeEdit (incl. QDateEdit/QTimeEdit) | `setDateTime`, `setDateTimeRange` | `onDateTimeChanged(name, iso8601)` | | RangeSlider (two-handle) | `setRangeSliderBounds`, `setRangeSliderValues`, `setRangeSliderTimeSpan` | `onRangeChanged(name, lower, upper)` | | DateRangePicker (date range) | `setDateRangePlaceholder` | `onDateRangeChanged(name, from_iso, to_iso)` | | QTabWidget | `setTabIndex` | `onTabChanged(name, index)` | diff --git a/recipe.yaml b/recipe.yaml index 8361f0d..72ded35 100644 --- a/recipe.yaml +++ b/recipe.yaml @@ -1,7 +1,7 @@ schema_version: 1 context: - version: "0.17.0" + version: "0.18.0" package: name: plotjuggler_sdk