From 741d166457476d4ff95edc8af5be25ed1cd53a39 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 10 Oct 2023 00:28:54 -0700 Subject: [PATCH] [glass] NT view: enhance array support (#5732) - Break out array elements; this makes it easier to see array contents and allows plotting individual array elements - Provide array editor --- glass/src/libnt/native/cpp/NetworkTables.cpp | 573 +++++++++--------- .../glass/networktables/NetworkTables.h | 9 + 2 files changed, 309 insertions(+), 273 deletions(-) diff --git a/glass/src/libnt/native/cpp/NetworkTables.cpp b/glass/src/libnt/native/cpp/NetworkTables.cpp index 7318e22b7d..c1e8a36184 100644 --- a/glass/src/libnt/native/cpp/NetworkTables.cpp +++ b/glass/src/libnt/native/cpp/NetworkTables.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -58,35 +59,6 @@ static bool IsVisible(ShowCategory category, bool persistent, bool retained) { } } -static std::string BooleanArrayToString(std::span in) { - std::string rv; - wpi::raw_string_ostream os{rv}; - os << '['; - bool first = true; - for (auto v : in) { - if (!first) { - os << ','; - } - first = false; - if (v) { - os << "true"; - } else { - os << "false"; - } - } - os << ']'; - return rv; -} - -static std::string IntegerArrayToString(std::span in) { - return fmt::format("[{:d}]", fmt::join(in, ",")); -} - -template -static std::string FloatArrayToString(std::span in) { - return fmt::format("[{:.6f}]", fmt::join(in, ",")); -} - static std::string StringArrayToString(std::span in) { std::string rv; wpi::raw_string_ostream os{rv}; @@ -301,8 +273,7 @@ static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out, child.name = fmt::format("[{}]", i); child.path = fmt::format("{}{}", name, child.name); } - ++i; - UpdateJsonValueSource(&child, j[i], child.path, time); // recurse + UpdateJsonValueSource(&child, j[i++], child.path, time); // recurse } break; } @@ -333,62 +304,88 @@ static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out, } } +void NetworkTablesModel::ValueSource::UpdateDiscreteSource( + std::string_view name, double value, int64_t time, bool digital) { + valueChildren.clear(); + if (!source) { + source = std::make_unique(fmt::format("NT:{}", name)); + } + source->SetValue(value, time); + source->SetDigital(digital); +} + +template +void NetworkTablesModel::ValueSource::UpdateDiscreteArray( + std::string_view name, std::span arr, int64_t time, + MakeValue makeValue, bool digital) { + if (valueChildrenMap) { + valueChildren.clear(); + valueChildrenMap = false; + } + valueChildren.resize(arr.size()); + unsigned int i = 0; + for (auto&& child : valueChildren) { + if (child.name.empty()) { + child.name = fmt::format("[{}]", i); + child.path = fmt::format("{}{}", name, child.name); + } + child.value = makeValue(arr[i], time); + child.UpdateDiscreteSource(child.path, arr[i], time, digital); + ++i; + } +} + void NetworkTablesModel::ValueSource::UpdateFromValue( nt::Value&& v, std::string_view name, std::string_view typeStr) { value = v; switch (value.type()) { case NT_BOOLEAN: - valueChildren.clear(); - if (!source) { - source = std::make_unique(fmt::format("NT:{}", name)); - } - source->SetValue(value.GetBoolean() ? 1 : 0, value.last_change()); - source->SetDigital(true); + UpdateDiscreteSource(name, value.GetBoolean() ? 1 : 0, value.time(), + true); break; case NT_INTEGER: - valueChildren.clear(); - if (!source) { - source = std::make_unique(fmt::format("NT:{}", name)); - } - source->SetValue(value.GetInteger(), value.last_change()); - source->SetDigital(false); + UpdateDiscreteSource(name, value.GetInteger(), value.time()); break; case NT_FLOAT: - valueChildren.clear(); - if (!source) { - source = std::make_unique(fmt::format("NT:{}", name)); - } - source->SetValue(value.GetFloat(), value.last_change()); - source->SetDigital(false); + UpdateDiscreteSource(name, value.GetFloat(), value.time()); break; case NT_DOUBLE: - valueChildren.clear(); - if (!source) { - source = std::make_unique(fmt::format("NT:{}", name)); - } - source->SetValue(value.GetDouble(), value.last_change()); - source->SetDigital(false); + UpdateDiscreteSource(name, value.GetDouble(), value.time()); break; case NT_BOOLEAN_ARRAY: - valueChildren.clear(); - valueStr = BooleanArrayToString(value.GetBooleanArray()); + UpdateDiscreteArray(name, value.GetBooleanArray(), value.time(), + nt::Value::MakeBoolean, true); break; case NT_INTEGER_ARRAY: - valueChildren.clear(); - valueStr = IntegerArrayToString(value.GetIntegerArray()); + UpdateDiscreteArray(name, value.GetIntegerArray(), value.time(), + nt::Value::MakeInteger); break; case NT_FLOAT_ARRAY: - valueChildren.clear(); - valueStr = FloatArrayToString(value.GetFloatArray()); + UpdateDiscreteArray(name, value.GetFloatArray(), value.time(), + nt::Value::MakeFloat); break; case NT_DOUBLE_ARRAY: - valueChildren.clear(); - valueStr = FloatArrayToString(value.GetDoubleArray()); + UpdateDiscreteArray(name, value.GetDoubleArray(), value.time(), + nt::Value::MakeDouble); break; - case NT_STRING_ARRAY: - valueChildren.clear(); - valueStr = StringArrayToString(value.GetStringArray()); + case NT_STRING_ARRAY: { + auto arr = value.GetStringArray(); + if (valueChildrenMap) { + valueChildren.clear(); + valueChildrenMap = false; + } + valueChildren.resize(arr.size()); + unsigned int i = 0; + for (auto&& child : valueChildren) { + if (child.name.empty()) { + child.name = fmt::format("[{}]", i); + child.path = fmt::format("{}{}", name, child.name); + } + child.UpdateFromValue(nt::Value::MakeString(arr[i++], value.time()), + child.path, ""); + } break; + } case NT_STRING: if (typeStr == "json") { try { @@ -666,141 +663,6 @@ void NetworkTablesModel::UpdateClients(std::span data) { m_clients = std::move(newClients); } -static bool StringToBooleanArray(std::string_view in, std::vector* out) { - in = wpi::trim(in); - if (in.empty()) { - return false; - } - if (in.front() == '[') { - in.remove_prefix(1); - } - if (in.back() == ']') { - in.remove_suffix(1); - } - in = wpi::trim(in); - - wpi::SmallVector inSplit; - - wpi::split(in, inSplit, ',', -1, false); - for (auto val : inSplit) { - val = wpi::trim(val); - if (wpi::equals_lower(val, "true")) { - out->emplace_back(1); - } else if (wpi::equals_lower(val, "false")) { - out->emplace_back(0); - } else { - fmt::print(stderr, - "GUI: NetworkTables: Could not understand value '{}'\n", val); - return false; - } - } - - return true; -} - -static bool StringToIntegerArray(std::string_view in, - std::vector* out) { - in = wpi::trim(in); - if (in.empty()) { - return false; - } - if (in.front() == '[') { - in.remove_prefix(1); - } - if (in.back() == ']') { - in.remove_suffix(1); - } - in = wpi::trim(in); - - wpi::SmallVector inSplit; - - wpi::split(in, inSplit, ',', -1, false); - for (auto val : inSplit) { - if (auto num = wpi::parse_integer(wpi::trim(val), 0)) { - out->emplace_back(num.value()); - } else { - fmt::print(stderr, - "GUI: NetworkTables: Could not understand value '{}'\n", val); - return false; - } - } - - return true; -} - -template -static bool StringToFloatArray(std::string_view in, std::vector* out) { - in = wpi::trim(in); - if (in.empty()) { - return false; - } - if (in.front() == '[') { - in.remove_prefix(1); - } - if (in.back() == ']') { - in.remove_suffix(1); - } - in = wpi::trim(in); - - wpi::SmallVector inSplit; - - wpi::split(in, inSplit, ',', -1, false); - for (auto val : inSplit) { - if (auto num = wpi::parse_float(wpi::trim(val))) { - out->emplace_back(num.value()); - } else { - fmt::print(stderr, - "GUI: NetworkTables: Could not understand value '{}'\n", val); - return false; - } - } - - return true; -} - -static bool StringToStringArray(std::string_view in, - std::vector* out) { - in = wpi::trim(in); - if (in.empty()) { - return false; - } - if (in.front() == '[') { - in.remove_prefix(1); - } - if (in.back() == ']') { - in.remove_suffix(1); - } - in = wpi::trim(in); - - while (!in.empty()) { - if (in.front() != '"') { - fmt::print(stderr, "GUI: NetworkTables: Expected '\"'"); - return false; - } - in.remove_prefix(1); - wpi::SmallString<128> buf; - std::string_view val; - std::tie(val, in) = wpi::UnescapeCString(in, buf); - out->emplace_back(val); - if (!in.empty()) { - if (in.front() != '"') { - fmt::print(stderr, "GUI: NetworkTables: Error escaping string"); - return false; - } - in.remove_prefix(1); - in = wpi::ltrim(in); - } - if (!in.empty()) { - if (in.front() != ',') { - fmt::print(stderr, "GUI: NetworkTables: Expected ','"); - return false; - } - in.remove_prefix(1); - } - } - return true; -} - static void EmitEntryValueReadonly(const NetworkTablesModel::ValueSource& entry, const char* typeStr, NetworkTablesFlags flags) { @@ -841,24 +703,19 @@ static void EmitEntryValueReadonly(const NetworkTablesModel::ValueSource& entry, break; } case NT_BOOLEAN_ARRAY: - ImGui::LabelText(typeStr ? typeStr : "boolean[]", "%s", - entry.valueStr.c_str()); + ImGui::LabelText(typeStr ? typeStr : "boolean[]", "[]"); break; case NT_INTEGER_ARRAY: - ImGui::LabelText(typeStr ? typeStr : "int[]", "%s", - entry.valueStr.c_str()); + ImGui::LabelText(typeStr ? typeStr : "int[]", "[]"); break; case NT_FLOAT_ARRAY: - ImGui::LabelText(typeStr ? typeStr : "float[]", "%s", - entry.valueStr.c_str()); + ImGui::LabelText(typeStr ? typeStr : "float[]", "[]"); break; case NT_DOUBLE_ARRAY: - ImGui::LabelText(typeStr ? typeStr : "double[]", "%s", - entry.valueStr.c_str()); + ImGui::LabelText(typeStr ? typeStr : "double[]", "[]"); break; case NT_STRING_ARRAY: - ImGui::LabelText(typeStr ? typeStr : "string[]", "%s", - entry.valueStr.c_str()); + ImGui::LabelText(typeStr ? typeStr : "string[]", "[]"); break; case NT_RAW: ImGui::LabelText(typeStr ? typeStr : "raw", "[...]"); @@ -879,7 +736,147 @@ static char* GetTextBuffer(std::string_view in) { return textBuffer; } -static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry, +namespace { +class ArrayEditor { + public: + virtual ~ArrayEditor() = default; + virtual bool Emit() = 0; +}; + +template +class ArrayEditorImpl final : public ArrayEditor { + public: + ArrayEditorImpl(NetworkTablesModel& model, std::string name, + NetworkTablesFlags flags, std::span value) + : m_model{model}, + m_name{std::move(name)}, + m_flags{flags}, + m_arr{value.begin(), value.end()} {} + + bool Emit() final; + + private: + NetworkTablesModel& m_model; + std::string m_name; + NetworkTablesFlags m_flags; + std::vector m_arr; +}; + +template +bool ArrayEditorImpl::Emit() { + if (ImGui::BeginTable( + "arrayvalues", 1, + ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit | + ImGuiTableFlags_RowBg, + ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 16))) { + ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible + int toAdd = -1; + int toRemove = -1; + ImGuiListClipper clipper; + clipper.Begin(m_arr.size()); + while (clipper.Step()) { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::PushID(row); + char label[16]; + wpi::format_to_n_c_str(label, sizeof(label), "[{}]", row); + if constexpr (NTType == NT_BOOLEAN_ARRAY) { + static const char* boolOptions[] = {"false", "true"}; + ImGui::Combo(label, &m_arr[row], boolOptions, 2); + } else if constexpr (NTType == NT_FLOAT_ARRAY) { + ImGui::InputFloat(label, &m_arr[row], 0, 0, "%.6f"); + } else if constexpr (NTType == NT_DOUBLE_ARRAY) { + unsigned char precision = (m_flags & NetworkTablesFlags_Precision) >> + kNetworkTablesFlags_PrecisionBitShift; +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" +#endif + ImGui::InputDouble(label, &m_arr[row], 0, 0, + fmt::format("%.{}f", precision).c_str()); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + } else if constexpr (NTType == NT_INTEGER_ARRAY) { + ImGui::InputScalar(label, ImGuiDataType_S64, &m_arr[row]); + } else if constexpr (NTType == NT_STRING_ARRAY) { + ImGui::InputText(label, &m_arr[row]); + } + ImGui::SameLine(); + if (ImGui::SmallButton("+")) { + toAdd = row; + } + ImGui::SameLine(); + if (ImGui::SmallButton("-")) { + toRemove = row; + } + ImGui::PopID(); + } + } + if (toAdd != -1) { + m_arr.emplace(m_arr.begin() + toAdd); + } else if (toRemove != -1) { + m_arr.erase(m_arr.begin() + toRemove); + } + ImGui::EndTable(); + } + if (ImGui::Button("Add to end")) { + m_arr.emplace_back(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + return true; + } + ImGui::SameLine(); + if (ImGui::Button("Apply")) { + auto* entry = m_model.GetEntry(m_name); + if (!entry) { + entry = m_model.AddEntry( + nt::GetTopic(m_model.GetInstance().GetHandle(), m_name)); + } + if constexpr (NTType == NT_BOOLEAN_ARRAY) { + if (entry->publisher == 0) { + entry->publisher = + nt::Publish(entry->info.topic, NT_BOOLEAN_ARRAY, "boolean[]"); + } + nt::SetBooleanArray(entry->publisher, m_arr); + } else if constexpr (NTType == NT_FLOAT_ARRAY) { + if (entry->publisher == 0) { + entry->publisher = + nt::Publish(entry->info.topic, NT_FLOAT_ARRAY, "float[]"); + } + nt::SetFloatArray(entry->publisher, m_arr); + } else if constexpr (NTType == NT_DOUBLE_ARRAY) { + if (entry->publisher == 0) { + entry->publisher = + nt::Publish(entry->info.topic, NT_DOUBLE_ARRAY, "double[]"); + } + nt::SetDoubleArray(entry->publisher, m_arr); + } else if constexpr (NTType == NT_INTEGER_ARRAY) { + if (entry->publisher == 0) { + entry->publisher = + nt::Publish(entry->info.topic, NT_INTEGER_ARRAY, "int[]"); + } + nt::SetIntegerArray(entry->publisher, m_arr); + } else if constexpr (NTType == NT_STRING_ARRAY) { + if (entry->publisher == 0) { + entry->publisher = + nt::Publish(entry->info.topic, NT_STRING_ARRAY, "string[]"); + } + nt::SetStringArray(entry->publisher, m_arr); + } + return true; + } + return false; +} +} // namespace + +static ImGuiID gArrayEditorID; +static std::unique_ptr gArrayEditor; + +static void EmitEntryValueEditable(NetworkTablesModel* model, + NetworkTablesModel::Entry& entry, NetworkTablesFlags flags) { auto& val = entry.value; if (!val) { @@ -962,81 +959,69 @@ static void EmitEntryValueEditable(NetworkTablesModel::Entry& entry, } break; } - case NT_BOOLEAN_ARRAY: { - char* v = GetTextBuffer(entry.valueStr); - if (ImGui::InputText(typeStr ? typeStr : "boolean[]", v, kTextBufferSize, - ImGuiInputTextFlags_EnterReturnsTrue)) { - std::vector outv; - if (StringToBooleanArray(v, &outv)) { - if (entry.publisher == 0) { - entry.publisher = - nt::Publish(entry.info.topic, NT_BOOLEAN_ARRAY, "boolean[]"); - } - nt::SetBooleanArray(entry.publisher, outv); + case NT_BOOLEAN_ARRAY: + ImGui::LabelText("boolean[]", "[]"); + if (ImGui::BeginPopupContextItem("boolean[]")) { + if (ImGui::Selectable("Edit Array")) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, + entry.value.GetBooleanArray()); + ImGui::OpenPopup(gArrayEditorID); } + ImGui::EndPopup(); } break; - } - case NT_INTEGER_ARRAY: { - char* v = GetTextBuffer(entry.valueStr); - if (ImGui::InputText(typeStr ? typeStr : "int[]", v, kTextBufferSize, - ImGuiInputTextFlags_EnterReturnsTrue)) { - std::vector outv; - if (StringToIntegerArray(v, &outv)) { - if (entry.publisher == 0) { - entry.publisher = - nt::Publish(entry.info.topic, NT_INTEGER_ARRAY, "int[]"); - } - nt::SetIntegerArray(entry.publisher, outv); + case NT_INTEGER_ARRAY: + ImGui::LabelText("int[]", "[]"); + if (ImGui::BeginPopupContextItem("int[]")) { + if (ImGui::Selectable("Edit Array")) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, + entry.value.GetIntegerArray()); + ImGui::OpenPopup(gArrayEditorID); } + ImGui::EndPopup(); } break; - } - case NT_FLOAT_ARRAY: { - char* v = GetTextBuffer(entry.valueStr); - if (ImGui::InputText(typeStr ? typeStr : "float[]", v, kTextBufferSize, - ImGuiInputTextFlags_EnterReturnsTrue)) { - std::vector outv; - if (StringToFloatArray(v, &outv)) { - if (entry.publisher == 0) { - entry.publisher = - nt::Publish(entry.info.topic, NT_DOUBLE_ARRAY, "float[]"); - } - nt::SetFloatArray(entry.publisher, outv); + case NT_FLOAT_ARRAY: + ImGui::LabelText("float[]", "[]"); + if (ImGui::BeginPopupContextItem("float[]")) { + if (ImGui::Selectable("Edit Array")) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, entry.value.GetFloatArray()); + ImGui::OpenPopup(gArrayEditorID); } + ImGui::EndPopup(); } break; - } - case NT_DOUBLE_ARRAY: { - char* v = GetTextBuffer(entry.valueStr); - if (ImGui::InputText(typeStr ? typeStr : "double[]", v, kTextBufferSize, - ImGuiInputTextFlags_EnterReturnsTrue)) { - std::vector outv; - if (StringToFloatArray(v, &outv)) { - if (entry.publisher == 0) { - entry.publisher = - nt::Publish(entry.info.topic, NT_DOUBLE_ARRAY, "double[]"); - } - nt::SetDoubleArray(entry.publisher, outv); + case NT_DOUBLE_ARRAY: + ImGui::LabelText("double[]", "[]"); + if (ImGui::BeginPopupContextItem("double[]")) { + if (ImGui::Selectable("Edit Array")) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, entry.value.GetDoubleArray()); + ImGui::OpenPopup(gArrayEditorID); } + ImGui::EndPopup(); } break; - } - case NT_STRING_ARRAY: { - char* v = GetTextBuffer(entry.valueStr); - if (ImGui::InputText(typeStr ? typeStr : "string[]", v, kTextBufferSize, - ImGuiInputTextFlags_EnterReturnsTrue)) { - std::vector outv; - if (StringToStringArray(v, &outv)) { - if (entry.publisher == 0) { - entry.publisher = - nt::Publish(entry.info.topic, NT_STRING_ARRAY, "string[]"); - } - nt::SetStringArray(entry.publisher, outv); + case NT_STRING_ARRAY: + ImGui::LabelText("string[]", "[]"); + if (ImGui::BeginPopupContextItem("string[]")) { + if (ImGui::Selectable("Edit Array")) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, entry.value.GetStringArray()); + ImGui::OpenPopup(gArrayEditorID); } + ImGui::EndPopup(); + break; } break; - } case NT_RAW: ImGui::LabelText(typeStr ? typeStr : "raw", val.GetRaw().empty() ? "[]" : "[...]"); @@ -1228,13 +1213,45 @@ static void EmitEntry(NetworkTablesModel* model, // make it look like a normal label w/type ImGui::SetCursorPos(pos); ImGui::LabelText(entry.info.type_str.c_str(), "%s", ""); + if ((entry.value.IsBooleanArray() || entry.value.IsFloatArray() || + entry.value.IsDoubleArray() || entry.value.IsIntegerArray() || + entry.value.IsStringArray()) && + ImGui::BeginPopupContextItem(label)) { + if (ImGui::Selectable("Edit Array")) { + if (entry.value.IsBooleanArray()) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, + entry.value.GetBooleanArray()); + } else if (entry.value.IsFloatArray()) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, entry.value.GetFloatArray()); + } else if (entry.value.IsDoubleArray()) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, entry.value.GetDoubleArray()); + } else if (entry.value.IsIntegerArray()) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, + entry.value.GetIntegerArray()); + } else if (entry.value.IsStringArray()) { + gArrayEditor = + std::make_unique>( + *model, entry.info.name, flags, entry.value.GetStringArray()); + } + ImGui::OpenPopup(gArrayEditorID); + } + ImGui::EndPopup(); + } } else if (flags & NetworkTablesFlags_ReadOnly) { EmitEntryValueReadonly( entry, entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str(), flags); } else { - EmitEntryValueEditable(entry, flags); + EmitEntryValueEditable(model, entry, flags); } if (flags & NetworkTablesFlags_ShowProperties) { @@ -1480,6 +1497,16 @@ void glass::DisplayNetworkTablesInfo(NetworkTablesModel* model) { void glass::DisplayNetworkTables(NetworkTablesModel* model, NetworkTablesFlags flags) { + gArrayEditorID = ImGui::GetID("Array Editor"); + if (ImGui::BeginPopupModal("Array Editor", nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + if (!gArrayEditor || gArrayEditor->Emit()) { + ImGui::CloseCurrentPopup(); + gArrayEditor.release(); + } + ImGui::EndPopup(); + } + if (flags & NetworkTablesFlags_CombinedView) { DisplayTable(model, model->GetTreeRoot(), flags, ShowAll); } else { diff --git a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h index 232076782d..da8f9b5d49 100644 --- a/glass/src/libnt/native/include/glass/networktables/NetworkTables.h +++ b/glass/src/libnt/native/include/glass/networktables/NetworkTables.h @@ -48,6 +48,15 @@ class NetworkTablesModel : public Model { /** Whether or not the children represent a map */ bool valueChildrenMap = false; + + private: + void UpdateDiscreteSource(std::string_view name, double value, int64_t time, + bool digital = false); + + template + void UpdateDiscreteArray(std::string_view name, std::span arr, + int64_t time, MakeValue makeValue, + bool digital = false); }; struct EntryValueTreeNode : public ValueSource {