Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ endif()
if(PJ_INSTALL_SDK)
include(CMakePackageConfigHelpers)

set(PJ_PACKAGE_VERSION "0.5.0")
set(PJ_PACKAGE_VERSION "0.5.1")
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_core)

install(EXPORT plotjuggler_coreTargets
Expand Down
4 changes: 2 additions & 2 deletions conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,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_core/0.5.0` and then:
A consuming Conan recipe declares e.g. `plotjuggler_core/0.5.1` and then:

find_package(plotjuggler_core REQUIRED COMPONENTS plugin_sdk)
target_link_libraries(my_plugin PRIVATE plotjuggler_core::plugin_sdk)
Expand All @@ -27,7 +27,7 @@

class PlotjugglerCoreConan(ConanFile):
name = "plotjuggler_core"
version = "0.5.0"
version = "0.5.1"
# Apache-2.0 covers pj_base + pj_plugins (the plugin-facing SDK);
# MPL-2.0 covers pj_datastore (the storage engine). See LICENSE.
license = "Apache-2.0 AND MPL-2.0"
Expand Down
3 changes: 3 additions & 0 deletions docs/dialog-sdk-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl
| `setPlainText(name, text)` | Set plain text content |
| `setCodeContent(name, code)` | Set editable code content |
| `setCodeLanguage(name, lang)` | Set syntax highlighting language such as `"lua"` or `"python"` |
| `setCodeCursor(name, cursor)` | Move the caret to byte offset `cursor` (e.g. after inserting a completion) |
| `setCodeCaretTracking(name, enabled=true)` | Opt into caret tracking: report the caret on cursor moves too, not just edits |

### QTabWidget

Expand Down Expand Up @@ -139,6 +141,7 @@ Override these in your `DialogPluginTyped` subclass. Return `true` when state ch
| `onSelectionChanged(name, items)` | QListWidget, QTableWidget | Vector of selected item texts |
| `onItemDoubleClicked(name, index)` | QListWidget, QTableWidget | Row index of double-clicked item |
| `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 |
| `onChartViewChanged(name, x_min, x_max, y_min, y_max)` | QFrame chart container | Visible chart range |
| `onTabChanged(name, index)` | QTabWidget | New tab index |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ class WidgetDataView {
[[nodiscard]] std::optional<std::string> codeLanguage(std::string_view name) const {
return getString(name, "code_language");
}
/// Requested caret offset (bytes) for a code editor; the host moves the caret
/// here after applying new code content. Absent ⇒ leave the caret as-is.
[[nodiscard]] std::optional<int> codeCursor(std::string_view name) const {
return getInt(name, "code_cursor");
}
/// Whether this code editor opted into caret tracking (setCodeCaretTracking).
/// Absent ⇒ not requested; the host wires cursor-move events only when true.
[[nodiscard]] std::optional<bool> codeCaretTracking(std::string_view name) const {
return getBool(name, "code_caret_tracking");
}

// --- QLabel ---
[[nodiscard]] std::optional<std::string> label(std::string_view name) const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,15 @@ struct WidgetEventBuilder {
return j.dump();
}

/// Code editor: code changed
[[nodiscard]] static std::string codeChanged(std::string_view code) {
/// 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.
[[nodiscard]] static std::string codeChanged(std::string_view code, int cursor = -1) {
nlohmann::json j;
j["code_changed"] = code;
if (cursor >= 0) {
j["code_cursor"] = cursor;
}
return j.dump();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ class DialogPluginTyped : public DialogPluginBase {
return false;
}

/// Cursor-aware code change: `cursor` is the caret offset (bytes) in `code`,
/// or negative when the host didn't report one. The dispatch always calls
/// this; it defaults to onCodeChanged(name, code), so existing plugins keep
/// working. Override this (instead of onCodeChanged) to drive caret-aware
/// completion. A distinct name (rather than an overload) avoids the
/// overloaded-virtual hiding hazard.
///
/// The caret is only reported (and cursor-only moves only fire this at all)
/// for editors that opted in via WidgetData::setCodeCaretTracking. Without
/// opt-in this fires on text changes only, with cursor < 0 — so an editor
/// that merely validates code is not re-run on every cursor move.
virtual bool onCodeChangedWithCursor(std::string_view widget_name, std::string_view code, int /*cursor*/) {
return onCodeChanged(widget_name, code);
}

virtual bool onItemsDropped(std::string_view /*widget_name*/, const std::vector<std::string>& /*items*/) {
return false;
}
Expand Down Expand Up @@ -113,7 +128,7 @@ class DialogPluginTyped : public DialogPluginBase {
return onItemsDropped(widget_name, *v);
}
if (auto v = event.codeChanged()) {
return onCodeChanged(widget_name, *v);
return onCodeChangedWithCursor(widget_name, *v, event.codeCursor().value_or(-1));
}
if (auto v = event.text()) {
return onTextChanged(widget_name, *v);
Expand Down
18 changes: 18 additions & 0 deletions pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,24 @@ class WidgetData {
return *this;
}

/// Move the caret of a code editor to `cursor` (byte offset). Used after the
/// plugin programmatically rewrites the code (e.g. inserting a completion) so
/// the caret lands where the user expects rather than jumping to the start.
WidgetData& setCodeCursor(std::string_view name, int cursor) {
entry(name)["code_cursor"] = cursor;
return *this;
}

/// Opt this code editor into caret tracking. When enabled, the host reports
/// the caret offset on cursor moves as well as text edits (via
/// onCodeChangedWithCursor), so the plugin can drive caret-aware completion.
/// Editors that don't opt in only fire on text changes — the default — so an
/// editor that merely validates code isn't re-run on every cursor move.
WidgetData& setCodeCaretTracking(std::string_view name, bool enabled = true) {
entry(name)["code_caret_tracking"] = enabled;
return *this;
}

// --- QLabel ---
WidgetData& setLabel(std::string_view name, std::string_view text) {
entry(name)["label"] = text;
Expand Down
10 changes: 10 additions & 0 deletions pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_event.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ class WidgetEvent {
return getString("code_changed");
}

/// Caret offset (bytes) accompanying a codeChanged event, when the host
/// reported one. Absent for hosts/events that don't carry the cursor.
std::optional<int> codeCursor() const {
auto it = data_.find("code_cursor");
if (it == data_.end() || !it->is_number_integer()) {
return std::nullopt;
}
return it->get<int>();
}

/// Drag-and-drop: items dropped on a widget (curves, files, or any draggable payload).
std::optional<std::vector<std::string>> itemsDropped() const {
auto it = data_.find("items_dropped");
Expand Down
21 changes: 21 additions & 0 deletions pj_plugins/dialog_protocol/tests/dialog_plugin_typed_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ class RecordingPlugin : public PJ::DialogPluginTyped {
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);
last_text = std::string(code);
last_int = cursor;
return true;
}

// Recorded state
std::string last_handler;
std::string last_widget;
Expand Down Expand Up @@ -241,3 +249,16 @@ TEST_F(TypedDispatchTest, CurrentIndexTakesPriorityOverValue) {
EXPECT_TRUE(dispatch(plugin, "w", R"({"current_index": 1, "value": 5})"));
EXPECT_EQ(plugin.last_handler, "index_changed");
}

TEST_F(TypedDispatchTest, CodeChangedCarriesCursorToTypedHandler) {
EXPECT_TRUE(dispatch(plugin, "editor", R"({"code_changed": "robot ==", "code_cursor": 8})"));
EXPECT_EQ(plugin.last_handler, "code_changed");
EXPECT_EQ(plugin.last_text, "robot ==");
EXPECT_EQ(plugin.last_int, 8);
}

TEST_F(TypedDispatchTest, CodeChangedWithoutCursorPassesNegativeOne) {
EXPECT_TRUE(dispatch(plugin, "editor", R"({"code_changed": "x"})"));
EXPECT_EQ(plugin.last_handler, "code_changed");
EXPECT_EQ(plugin.last_int, -1);
}
22 changes: 22 additions & 0 deletions pj_plugins/dialog_protocol/tests/widget_data_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,25 @@ TEST(WidgetDataTest, Chaining) {
EXPECT_EQ(j["port"]["value"], 80);
EXPECT_EQ(j["tls"]["checked"], true);
}

TEST(WidgetDataTest, SetCodeCursor) {
WidgetData wd;
wd.setCodeContent("editor", "robot ==").setCodeCursor("editor", 8);
auto j = parse(wd);
EXPECT_EQ(j["editor"]["code_content"], "robot ==");
EXPECT_EQ(j["editor"]["code_cursor"], 8);
}

TEST(WidgetDataTest, SetCodeCaretTracking) {
WidgetData wd;
wd.setCodeCaretTracking("editor");
auto j = parse(wd);
EXPECT_EQ(j["editor"]["code_caret_tracking"], true);
}

TEST(WidgetDataTest, SetCodeCaretTrackingExplicitFalse) {
WidgetData wd;
wd.setCodeCaretTracking("editor", false);
auto j = parse(wd);
EXPECT_EQ(j["editor"]["code_caret_tracking"], false);
}
20 changes: 20 additions & 0 deletions pj_plugins/dialog_protocol/tests/widget_data_view_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,23 @@ TEST(WidgetDataViewTest, RawAccess) {
EXPECT_TRUE(raw.is_object());
EXPECT_EQ(raw["w"]["custom_field"], 99);
}

TEST(WidgetDataViewTest, CodeCursor) {
PJ::WidgetDataView v(R"({"editor": {"code_cursor": 12}})");
EXPECT_EQ(v.codeCursor("editor"), 12);
}

TEST(WidgetDataViewTest, CodeCursorAbsent) {
PJ::WidgetDataView v(R"({"editor": {"code_content": "x"}})");
EXPECT_FALSE(v.codeCursor("editor").has_value());
}

TEST(WidgetDataViewTest, CodeCaretTracking) {
PJ::WidgetDataView v(R"({"editor": {"code_caret_tracking": true}})");
EXPECT_EQ(v.codeCaretTracking("editor"), true);
}

TEST(WidgetDataViewTest, CodeCaretTrackingAbsent) {
PJ::WidgetDataView v(R"({"editor": {"code_content": "x"}})");
EXPECT_FALSE(v.codeCaretTracking("editor").has_value());
}
17 changes: 17 additions & 0 deletions pj_plugins/dialog_protocol/tests/widget_event_builder_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,20 @@ TEST(WidgetEventBuilderTest, TextChangedDoesNotTriggerOtherFields) {
EXPECT_FALSE(ev.checked().has_value());
EXPECT_FALSE(ev.clicked());
}

TEST(WidgetEventBuilderTest, CodeChangedWithCursor) {
std::string json = PJ::WidgetEventBuilder::codeChanged("robot ==", 8);
PJ::WidgetEvent ev(json);
ASSERT_TRUE(ev.codeChanged().has_value());
EXPECT_EQ(*ev.codeChanged(), "robot ==");
ASSERT_TRUE(ev.codeCursor().has_value());
EXPECT_EQ(*ev.codeCursor(), 8);
}

TEST(WidgetEventBuilderTest, CodeChangedWithoutCursorOmitsField) {
std::string json = PJ::WidgetEventBuilder::codeChanged("x");
PJ::WidgetEvent ev(json);
ASSERT_TRUE(ev.codeChanged().has_value());
EXPECT_EQ(*ev.codeChanged(), "x");
EXPECT_FALSE(ev.codeCursor().has_value());
}
2 changes: 1 addition & 1 deletion pj_plugins/docs/dialog-plugin-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ work like polling a server for available topics.
| QLabel | `setLabel` | (none — display only) |
| QListWidget | `setListItems`, `setSelectedItems` | `onSelectionChanged(name, items)`, `onItemDoubleClicked(name, index)` |
| QTableWidget | `setTableHeaders`, `setTableRows`, `setSelectedRows`, `setVisibleRows`, `setRowColor`, `setCellTooltip` | `onSelectionChanged(name, items)`, `onHeaderClicked(name, section)` |
| QPlainTextEdit | `setPlainText`, `setCodeContent`, `setCodeLanguage` | `onCodeChanged(name, code)` for code editors |
| 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) |
| RangeSlider (two-handle) | `setRangeSliderBounds`, `setRangeSliderValues`, `setRangeSliderTimeSpan` | `onRangeChanged(name, lower, upper)` |
Expand Down
Loading