// Copyright (c) FIRST and other WPILib contributors. // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. #include "glass/networktables/NetworkTables.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "glass/Context.h" #include "glass/DataSource.h" #include "glass/Storage.h" using namespace glass; using namespace mpack; namespace { enum ShowCategory { ShowPersistent, ShowRetained, ShowTransitory, ShowAll, }; } // namespace static bool IsVisible(ShowCategory category, bool persistent, bool retained) { switch (category) { case ShowPersistent: return persistent; case ShowRetained: return retained && !persistent; case ShowTransitory: return !retained && !persistent; case ShowAll: return true; default: return false; } } static std::string StringArrayToString(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; os << '"'; os.write_escaped(v); os << '"'; } os << ']'; return rv; } NetworkTablesModel::NetworkTablesModel() : NetworkTablesModel{nt::NetworkTableInstance::GetDefault()} {} NetworkTablesModel::NetworkTablesModel(nt::NetworkTableInstance inst) : m_inst{inst}, m_poller{inst} { m_poller.AddListener({{"", "$"}}, nt::EventFlags::kTopic | nt::EventFlags::kValueAll | nt::EventFlags::kImmediate); } NetworkTablesModel::Entry::~Entry() { if (publisher != 0) { nt::Unpublish(publisher); } } void NetworkTablesModel::Entry::UpdateInfo(nt::TopicInfo&& info_) { info = std::move(info_); properties = info.GetProperties(); persistent = false; auto it = properties.find("persistent"); if (it != properties.end()) { if (auto v = it->get_ptr()) { persistent = *v; } } retained = false; it = properties.find("retained"); if (it != properties.end()) { if (auto v = it->get_ptr()) { retained = *v; } } } static void UpdateMsgpackValueSource(NetworkTablesModel::ValueSource* out, mpack_reader_t& r, std::string_view name, int64_t time) { mpack_tag_t tag = mpack_read_tag(&r); switch (mpack_tag_type(&tag)) { case mpack::mpack_type_bool: out->UpdateFromValue( nt::Value::MakeBoolean(mpack_tag_bool_value(&tag), time), name, ""); break; case mpack::mpack_type_int: out->UpdateFromValue( nt::Value::MakeInteger(mpack_tag_int_value(&tag), time), name, ""); break; case mpack::mpack_type_uint: out->UpdateFromValue( nt::Value::MakeInteger(mpack_tag_uint_value(&tag), time), name, ""); break; case mpack::mpack_type_float: out->UpdateFromValue( nt::Value::MakeFloat(mpack_tag_float_value(&tag), time), name, ""); break; case mpack::mpack_type_double: out->UpdateFromValue( nt::Value::MakeDouble(mpack_tag_double_value(&tag), time), name, ""); break; case mpack::mpack_type_str: { std::string str; mpack_read_str(&r, &tag, &str); out->UpdateFromValue(nt::Value::MakeString(std::move(str), time), name, ""); break; } case mpack::mpack_type_bin: // just skip it mpack_skip_bytes(&r, mpack_tag_bin_length(&tag)); mpack_done_bin(&r); break; case mpack::mpack_type_array: { if (out->valueChildrenMap) { out->valueChildren.clear(); out->valueChildrenMap = false; } out->valueChildren.resize(mpack_tag_array_count(&tag)); unsigned int i = 0; for (auto&& child : out->valueChildren) { if (child.name.empty()) { child.name = fmt::format("[{}]", i); child.path = fmt::format("{}{}", name, child.name); } ++i; UpdateMsgpackValueSource(&child, r, child.path, time); // recurse } mpack_done_array(&r); break; } case mpack::mpack_type_map: { if (!out->valueChildrenMap) { out->valueChildren.clear(); out->valueChildrenMap = true; } wpi::StringMap elems; for (size_t i = 0, size = out->valueChildren.size(); i < size; ++i) { elems[out->valueChildren[i].name] = i; } bool added = false; uint32_t count = mpack_tag_map_count(&tag); for (uint32_t i = 0; i < count; ++i) { std::string key; if (mpack_expect_str(&r, &key) == mpack_ok) { auto it = elems.find(key); if (it != elems.end()) { auto& child = out->valueChildren[it->second]; UpdateMsgpackValueSource(&child, r, child.path, time); elems.erase(it); } else { added = true; out->valueChildren.emplace_back(); auto& child = out->valueChildren.back(); child.name = std::move(key); child.path = fmt::format("{}/{}", name, child.name); UpdateMsgpackValueSource(&child, r, child.path, time); } } } // erase unmatched keys out->valueChildren.erase( std::remove_if( out->valueChildren.begin(), out->valueChildren.end(), [&](const auto& child) { return elems.count(child.name) > 0; }), out->valueChildren.end()); if (added) { // sort by name std::sort(out->valueChildren.begin(), out->valueChildren.end(), [](const auto& a, const auto& b) { return a.name < b.name; }); } mpack_done_map(&r); break; } default: out->value = {}; mpack_done_type(&r, mpack_tag_type(&tag)); break; } } static void UpdateJsonValueSource(NetworkTablesModel::ValueSource* out, const wpi::json& j, std::string_view name, int64_t time) { switch (j.type()) { case wpi::json::value_t::object: { if (!out->valueChildrenMap) { out->valueChildren.clear(); out->valueChildrenMap = true; } wpi::StringMap elems; for (size_t i = 0, size = out->valueChildren.size(); i < size; ++i) { elems[out->valueChildren[i].name] = i; } bool added = false; for (auto&& kv : j.items()) { auto it = elems.find(kv.key()); if (it != elems.end()) { auto& child = out->valueChildren[it->second]; UpdateJsonValueSource(&child, kv.value(), child.path, time); elems.erase(it); } else { added = true; out->valueChildren.emplace_back(); auto& child = out->valueChildren.back(); child.name = kv.key(); child.path = fmt::format("{}/{}", name, child.name); UpdateJsonValueSource(&child, kv.value(), child.path, time); } } // erase unmatched keys out->valueChildren.erase( std::remove_if( out->valueChildren.begin(), out->valueChildren.end(), [&](const auto& child) { return elems.count(child.name) > 0; }), out->valueChildren.end()); if (added) { // sort by name std::sort(out->valueChildren.begin(), out->valueChildren.end(), [](const auto& a, const auto& b) { return a.name < b.name; }); } break; } case wpi::json::value_t::array: { if (out->valueChildrenMap) { out->valueChildren.clear(); out->valueChildrenMap = false; } out->valueChildren.resize(j.size()); unsigned int i = 0; for (auto&& child : out->valueChildren) { if (child.name.empty()) { child.name = fmt::format("[{}]", i); child.path = fmt::format("{}{}", name, child.name); } UpdateJsonValueSource(&child, j[i++], child.path, time); // recurse } break; } case wpi::json::value_t::string: out->UpdateFromValue( nt::Value::MakeString(j.get_ref(), time), name, ""); break; case wpi::json::value_t::boolean: out->UpdateFromValue(nt::Value::MakeBoolean(j.get(), time), name, ""); break; case wpi::json::value_t::number_integer: out->UpdateFromValue(nt::Value::MakeInteger(j.get(), time), name, ""); break; case wpi::json::value_t::number_unsigned: out->UpdateFromValue(nt::Value::MakeInteger(j.get(), time), name, ""); break; case wpi::json::value_t::number_float: out->UpdateFromValue(nt::Value::MakeDouble(j.get(), time), name, ""); break; default: out->value = {}; break; } } 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: UpdateDiscreteSource(name, value.GetBoolean() ? 1 : 0, value.time(), true); break; case NT_INTEGER: UpdateDiscreteSource(name, value.GetInteger(), value.time()); break; case NT_FLOAT: UpdateDiscreteSource(name, value.GetFloat(), value.time()); break; case NT_DOUBLE: UpdateDiscreteSource(name, value.GetDouble(), value.time()); break; case NT_BOOLEAN_ARRAY: UpdateDiscreteArray(name, value.GetBooleanArray(), value.time(), nt::Value::MakeBoolean, true); break; case NT_INTEGER_ARRAY: UpdateDiscreteArray(name, value.GetIntegerArray(), value.time(), nt::Value::MakeInteger); break; case NT_FLOAT_ARRAY: UpdateDiscreteArray(name, value.GetFloatArray(), value.time(), nt::Value::MakeFloat); break; case NT_DOUBLE_ARRAY: UpdateDiscreteArray(name, value.GetDoubleArray(), value.time(), nt::Value::MakeDouble); break; 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 { UpdateJsonValueSource(this, wpi::json::parse(value.GetString()), name, value.last_change()); } catch (wpi::json::exception&) { // ignore } } else { valueChildren.clear(); valueStr.clear(); wpi::raw_string_ostream os{valueStr}; os << '"'; os.write_escaped(value.GetString()); os << '"'; } break; case NT_RAW: if (typeStr == "msgpack") { mpack_reader_t r; mpack_reader_init_data(&r, value.GetRaw()); UpdateMsgpackValueSource(this, r, name, value.last_change()); mpack_reader_destroy(&r); } else { valueChildren.clear(); } break; default: valueChildren.clear(); break; } } void NetworkTablesModel::Update() { bool updateTree = false; for (auto&& event : m_poller.ReadQueue()) { if (auto info = event.GetTopicInfo()) { auto& entry = m_entries[info->topic]; if (event.flags & nt::EventFlags::kPublish) { if (!entry) { entry = std::make_unique(); m_sortedEntries.emplace_back(entry.get()); updateTree = true; } } if (event.flags & nt::EventFlags::kUnpublish) { // meta topic handling if (wpi::starts_with(info->name, '$')) { // meta topic handling if (info->name == "$clients") { m_clients.clear(); } else if (info->name == "$serverpub") { m_server.publishers.clear(); } else if (info->name == "$serversub") { m_server.subscribers.clear(); } else if (wpi::starts_with(info->name, "$clientpub$")) { auto it = m_clients.find(wpi::drop_front(info->name, 11)); if (it != m_clients.end()) { it->second.publishers.clear(); } } else if (wpi::starts_with(info->name, "$clientsub$")) { auto it = m_clients.find(wpi::drop_front(info->name, 11)); if (it != m_clients.end()) { it->second.subscribers.clear(); } } } auto it = std::find(m_sortedEntries.begin(), m_sortedEntries.end(), entry.get()); // will be removed completely below if (it != m_sortedEntries.end()) { *it = nullptr; } m_entries.erase(info->topic); updateTree = true; continue; } if (event.flags & nt::EventFlags::kProperties) { updateTree = true; } if (entry) { entry->UpdateTopic(std::move(event)); } } else if (auto valueData = event.GetValueEventData()) { auto& entry = m_entries[valueData->topic]; if (entry) { entry->UpdateFromValue(std::move(valueData->value), entry->info.name, entry->info.type_str); if (wpi::starts_with(entry->info.name, '$') && entry->value.IsRaw() && entry->info.type_str == "msgpack") { // meta topic handling if (entry->info.name == "$clients") { // need to remove deleted entries as UpdateClients() uses GetEntry() if (updateTree) { std::erase(m_sortedEntries, nullptr); } UpdateClients(entry->value.GetRaw()); } else if (entry->info.name == "$serverpub") { m_server.UpdatePublishers(entry->value.GetRaw()); } else if (entry->info.name == "$serversub") { m_server.UpdateSubscribers(entry->value.GetRaw()); } else if (wpi::starts_with(entry->info.name, "$clientpub$")) { auto it = m_clients.find(wpi::drop_front(entry->info.name, 11)); if (it != m_clients.end()) { it->second.UpdatePublishers(entry->value.GetRaw()); } } else if (wpi::starts_with(entry->info.name, "$clientsub$")) { auto it = m_clients.find(wpi::drop_front(entry->info.name, 11)); if (it != m_clients.end()) { it->second.UpdateSubscribers(entry->value.GetRaw()); } } } } } } // shortcut common case (updates) if (!updateTree) { return; } // remove deleted entries std::erase(m_sortedEntries, nullptr); RebuildTree(); } void NetworkTablesModel::RebuildTree() { // sort by name std::sort( m_sortedEntries.begin(), m_sortedEntries.end(), [](const auto& a, const auto& b) { return a->info.name < b->info.name; }); RebuildTreeImpl(&m_root, ShowAll); RebuildTreeImpl(&m_persistentRoot, ShowPersistent); RebuildTreeImpl(&m_retainedRoot, ShowRetained); RebuildTreeImpl(&m_transitoryRoot, ShowTransitory); } void NetworkTablesModel::RebuildTreeImpl(std::vector* tree, int category) { tree->clear(); wpi::SmallVector parts; for (auto& entry : m_sortedEntries) { if (!IsVisible(static_cast(category), entry->persistent, entry->retained)) { continue; } parts.clear(); wpi::split(entry->info.name, parts, '/', -1, false); // ignore a raw "/" key if (parts.empty()) { continue; } // get to leaf auto nodes = tree; for (auto part : wpi::drop_back(std::span{parts.begin(), parts.end()})) { auto it = std::find_if(nodes->begin(), nodes->end(), [&](const auto& node) { return node.name == part; }); if (it == nodes->end()) { nodes->emplace_back(part); // path is from the beginning of the string to the end of the current // part; this works because part is a reference to the internals of // entry->info.name nodes->back().path.assign( entry->info.name.data(), part.data() + part.size() - entry->info.name.data()); it = nodes->end() - 1; } nodes = &it->children; } auto it = std::find_if(nodes->begin(), nodes->end(), [&](const auto& node) { return node.name == parts.back(); }); if (it == nodes->end()) { nodes->emplace_back(parts.back()); // no need to set path, as it's identical to entry->name it = nodes->end() - 1; } it->entry = entry; } } bool NetworkTablesModel::Exists() { return true; } NetworkTablesModel::Entry* NetworkTablesModel::GetEntry(std::string_view name) { auto entryIt = std::lower_bound( m_sortedEntries.begin(), m_sortedEntries.end(), name, [](auto&& entry, auto&& name) { return entry->info.name < name; }); if (entryIt == m_sortedEntries.end() || (*entryIt)->info.name != name) { return nullptr; } return *entryIt; } NetworkTablesModel::Entry* NetworkTablesModel::AddEntry(NT_Topic topic) { auto& entry = m_entries[topic]; if (!entry) { entry = std::make_unique(); entry->info = nt::GetTopicInfo(topic); entry->properties = entry->info.GetProperties(); m_sortedEntries.emplace_back(entry.get()); } RebuildTree(); return entry.get(); } NetworkTablesModel::Client::Subscriber::Subscriber( nt::meta::ClientSubscriber&& oth) : ClientSubscriber{std::move(oth)}, topicsStr{StringArrayToString(topics)} {} void NetworkTablesModel::Client::UpdatePublishers( std::span data) { if (auto pubs = nt::meta::DecodeClientPublishers(data)) { publishers = std::move(*pubs); } else { fmt::print(stderr, "Failed to update publishers\n"); } } void NetworkTablesModel::Client::UpdateSubscribers( std::span data) { if (auto subs = nt::meta::DecodeClientSubscribers(data)) { subscribers.clear(); subscribers.reserve(subs->size()); for (auto&& sub : *subs) { subscribers.emplace_back(std::move(sub)); } } else { fmt::print(stderr, "Failed to update subscribers\n"); } } void NetworkTablesModel::UpdateClients(std::span data) { auto clientsArr = nt::meta::DecodeClients(data); if (!clientsArr) { return; } // we need to create a new map so deletions are reflected std::map> newClients; for (auto&& client : *clientsArr) { auto& newClient = newClients[client.id]; newClient = std::move(client); auto it = m_clients.find(newClient.id); if (it != m_clients.end()) { // transfer from existing newClient.publishers = std::move(it->second.publishers); newClient.subscribers = std::move(it->second.subscribers); } else { // initially populate if (Entry* entry = GetEntry(fmt::format("$clientpub${}", newClient.id))) { if (entry->value.IsRaw() && entry->info.type_str == "msgpack") { newClient.UpdatePublishers(entry->value.GetRaw()); } } if (Entry* entry = GetEntry(fmt::format("$clientsub${}", newClient.id))) { if (entry->value.IsRaw() && entry->info.type_str == "msgpack") { newClient.UpdateSubscribers(entry->value.GetRaw()); } } } } // replace map m_clients = std::move(newClients); } static void EmitEntryValueReadonly(const NetworkTablesModel::ValueSource& entry, const char* typeStr, NetworkTablesFlags flags) { auto& val = entry.value; if (!val) { return; } switch (val.type()) { case NT_BOOLEAN: ImGui::LabelText(typeStr ? typeStr : "boolean", "%s", val.GetBoolean() ? "true" : "false"); break; case NT_INTEGER: ImGui::LabelText(typeStr ? typeStr : "int", "%" PRId64, val.GetInteger()); break; case NT_FLOAT: ImGui::LabelText(typeStr ? typeStr : "double", "%.6f", val.GetFloat()); break; case NT_DOUBLE: { unsigned char precision = (flags & NetworkTablesFlags_Precision) >> kNetworkTablesFlags_PrecisionBitShift; #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif ImGui::LabelText(typeStr ? typeStr : "double", fmt::format("%.{}f", precision).c_str(), val.GetDouble()); #ifdef __GNUC__ #pragma GCC diagnostic pop #endif break; } case NT_STRING: { ImGui::LabelText(typeStr ? typeStr : "string", "%s", entry.valueStr.c_str()); break; } case NT_BOOLEAN_ARRAY: ImGui::LabelText(typeStr ? typeStr : "boolean[]", "[]"); break; case NT_INTEGER_ARRAY: ImGui::LabelText(typeStr ? typeStr : "int[]", "[]"); break; case NT_FLOAT_ARRAY: ImGui::LabelText(typeStr ? typeStr : "float[]", "[]"); break; case NT_DOUBLE_ARRAY: ImGui::LabelText(typeStr ? typeStr : "double[]", "[]"); break; case NT_STRING_ARRAY: ImGui::LabelText(typeStr ? typeStr : "string[]", "[]"); break; case NT_RAW: ImGui::LabelText(typeStr ? typeStr : "raw", "[...]"); break; default: ImGui::LabelText(typeStr ? typeStr : "other", "?"); break; } } static constexpr size_t kTextBufferSize = 4096; static char* GetTextBuffer(std::string_view in) { static char textBuffer[kTextBufferSize]; size_t len = (std::min)(in.size(), kTextBufferSize - 1); std::memcpy(textBuffer, in.data(), len); textBuffer[len] = '\0'; return textBuffer; } 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) { return; } const char* typeStr = entry.info.type_str.empty() ? nullptr : entry.info.type_str.c_str(); ImGui::PushID(entry.info.name.c_str()); switch (val.type()) { case NT_BOOLEAN: { static const char* boolOptions[] = {"false", "true"}; int v = val.GetBoolean() ? 1 : 0; if (ImGui::Combo(typeStr ? typeStr : "boolean", &v, boolOptions, 2)) { if (entry.publisher == 0) { entry.publisher = nt::Publish(entry.info.topic, NT_BOOLEAN, "boolean"); } nt::SetBoolean(entry.publisher, v); } break; } case NT_INTEGER: { int64_t v = val.GetInteger(); if (ImGui::InputScalar(typeStr ? typeStr : "int", ImGuiDataType_S64, &v, nullptr, nullptr, nullptr, ImGuiInputTextFlags_EnterReturnsTrue)) { if (entry.publisher == 0) { entry.publisher = nt::Publish(entry.info.topic, NT_INTEGER, "int"); } nt::SetInteger(entry.publisher, v); } break; } case NT_FLOAT: { float v = val.GetFloat(); if (ImGui::InputFloat(typeStr ? typeStr : "float", &v, 0, 0, "%.6f", ImGuiInputTextFlags_EnterReturnsTrue)) { if (entry.publisher == 0) { entry.publisher = nt::Publish(entry.info.topic, NT_FLOAT, "float"); } nt::SetFloat(entry.publisher, v); } break; } case NT_DOUBLE: { double v = val.GetDouble(); unsigned char precision = (flags & NetworkTablesFlags_Precision) >> kNetworkTablesFlags_PrecisionBitShift; #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" #endif if (ImGui::InputDouble(typeStr ? typeStr : "double", &v, 0, 0, fmt::format("%.{}f", precision).c_str(), ImGuiInputTextFlags_EnterReturnsTrue)) { if (entry.publisher == 0) { entry.publisher = nt::Publish(entry.info.topic, NT_DOUBLE, "double"); } nt::SetDouble(entry.publisher, v); } #ifdef __GNUC__ #pragma GCC diagnostic pop #endif break; } case NT_STRING: { char* v = GetTextBuffer(entry.valueStr); if (ImGui::InputText(typeStr ? typeStr : "string", v, kTextBufferSize, ImGuiInputTextFlags_EnterReturnsTrue)) { if (v[0] == '"') { if (entry.publisher == 0) { entry.publisher = nt::Publish(entry.info.topic, NT_STRING, "string"); } wpi::SmallString<128> buf; nt::SetString(entry.publisher, wpi::UnescapeCString(v + 1, buf).first); } } break; } 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: 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: 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: 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: 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() ? "[]" : "[...]"); break; case NT_RPC: ImGui::LabelText(typeStr ? typeStr : "rpc", "[...]"); break; default: ImGui::LabelText(typeStr ? typeStr : "other", "?"); break; } ImGui::PopID(); } static void CreateTopicMenuItem(NetworkTablesModel* model, std::string_view path, NT_Type type, const char* typeStr, bool enabled) { if (ImGui::MenuItem(typeStr, nullptr, false, enabled)) { auto entry = model->AddEntry(nt::GetTopic(model->GetInstance().GetHandle(), path)); if (entry->publisher == 0) { entry->publisher = nt::Publish(entry->info.topic, type, typeStr); // publish a default value so it's editable switch (type) { case NT_BOOLEAN: nt::SetDefaultBoolean(entry->publisher, false); break; case NT_INTEGER: nt::SetDefaultInteger(entry->publisher, 0); break; case NT_FLOAT: nt::SetDefaultFloat(entry->publisher, 0.0); break; case NT_DOUBLE: nt::SetDefaultDouble(entry->publisher, 0.0); break; case NT_STRING: nt::SetDefaultString(entry->publisher, ""); break; case NT_BOOLEAN_ARRAY: nt::SetDefaultBooleanArray(entry->publisher, {}); break; case NT_INTEGER_ARRAY: nt::SetDefaultIntegerArray(entry->publisher, {}); break; case NT_FLOAT_ARRAY: nt::SetDefaultFloatArray(entry->publisher, {}); break; case NT_DOUBLE_ARRAY: nt::SetDefaultDoubleArray(entry->publisher, {}); break; case NT_STRING_ARRAY: nt::SetDefaultStringArray(entry->publisher, {}); break; default: break; } } } } void glass::DisplayNetworkTablesAddMenu(NetworkTablesModel* model, std::string_view path, NetworkTablesFlags flags) { static char nameBuffer[kTextBufferSize]; if (ImGui::BeginMenu("Add new...")) { if (ImGui::IsWindowAppearing()) { nameBuffer[0] = '\0'; } ImGui::InputTextWithHint("New item name", "example", nameBuffer, kTextBufferSize); std::string fullNewPath; if (path == "/") { path = ""; } fullNewPath = fmt::format("{}/{}", path, nameBuffer); ImGui::Text("Adding: %s", fullNewPath.c_str()); ImGui::Separator(); auto entry = model->GetEntry(fullNewPath); bool exists = entry && entry->info.type != NT_Type::NT_UNASSIGNED; bool enabled = (flags & NetworkTablesFlags_CreateNoncanonicalKeys || nameBuffer[0] != '\0') && !exists; CreateTopicMenuItem(model, fullNewPath, NT_STRING, "string", enabled); CreateTopicMenuItem(model, fullNewPath, NT_INTEGER, "int", enabled); CreateTopicMenuItem(model, fullNewPath, NT_FLOAT, "float", enabled); CreateTopicMenuItem(model, fullNewPath, NT_DOUBLE, "double", enabled); CreateTopicMenuItem(model, fullNewPath, NT_BOOLEAN, "boolean", enabled); CreateTopicMenuItem(model, fullNewPath, NT_STRING_ARRAY, "string[]", enabled); CreateTopicMenuItem(model, fullNewPath, NT_INTEGER_ARRAY, "int[]", enabled); CreateTopicMenuItem(model, fullNewPath, NT_FLOAT_ARRAY, "float[]", enabled); CreateTopicMenuItem(model, fullNewPath, NT_DOUBLE_ARRAY, "double[]", enabled); CreateTopicMenuItem(model, fullNewPath, NT_BOOLEAN_ARRAY, "boolean[]", enabled); ImGui::EndMenu(); } } static void EmitParentContextMenu(NetworkTablesModel* model, const std::string& path, NetworkTablesFlags flags) { if (ImGui::BeginPopupContextItem(path.c_str())) { ImGui::Text("%s", path.c_str()); ImGui::Separator(); DisplayNetworkTablesAddMenu(model, path, flags); ImGui::EndPopup(); } } static void EmitValueName(DataSource* source, const char* name, const char* path) { if (source) { ImGui::Selectable(name); source->EmitDrag(); } else { ImGui::TextUnformatted(name); } if (ImGui::BeginPopupContextItem(path)) { ImGui::TextUnformatted(path); ImGui::EndPopup(); } } static void EmitValueTree( const std::vector& children, NetworkTablesFlags flags) { for (auto&& child : children) { ImGui::TableNextRow(); ImGui::TableNextColumn(); EmitValueName(child.source.get(), child.name.c_str(), child.path.c_str()); ImGui::TableNextColumn(); if (!child.valueChildren.empty()) { char label[64]; if (child.valueChildrenMap) { wpi::format_to_n_c_str(label, sizeof(label), "{{...}}##v_{}", child.name); } else { wpi::format_to_n_c_str(label, sizeof(label), "[...]##v_{}", child.name); } if (TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth)) { EmitValueTree(child.valueChildren, flags); TreePop(); } } else { EmitEntryValueReadonly(child, nullptr, flags); } } } static void EmitEntry(NetworkTablesModel* model, NetworkTablesModel::Entry& entry, const char* name, NetworkTablesFlags flags, ShowCategory category) { if (!IsVisible(category, entry.persistent, entry.retained)) { return; } bool valueChildrenOpen = false; ImGui::TableNextRow(); ImGui::TableNextColumn(); EmitValueName(entry.source.get(), name, entry.info.name.c_str()); ImGui::TableNextColumn(); if (!entry.valueChildren.empty()) { auto pos = ImGui::GetCursorPos(); char label[64]; if (entry.valueChildrenMap) { wpi::format_to_n_c_str(label, sizeof(label), "{{...}}##v_{}", entry.info.name.c_str()); } else { wpi::format_to_n_c_str(label, sizeof(label), "[...]##v_{}", entry.info.name.c_str()); } valueChildrenOpen = TreeNodeEx(label, ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_AllowItemOverlap); // 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(model, entry, flags); } if (flags & NetworkTablesFlags_ShowProperties) { ImGui::TableNextColumn(); ImGui::Text("%s", entry.info.properties.c_str()); if (ImGui::BeginPopupContextItem(entry.info.name.c_str())) { if (ImGui::Checkbox("persistent", &entry.persistent)) { nt::SetTopicPersistent(entry.info.topic, entry.persistent); } if (ImGui::Checkbox("retained", &entry.retained)) { if (entry.retained) { nt::SetTopicProperty(entry.info.topic, "retained", true); } else { nt::DeleteTopicProperty(entry.info.topic, "retained"); } } ImGui::EndPopup(); } } if (flags & NetworkTablesFlags_ShowTimestamp) { ImGui::TableNextColumn(); if (entry.value) { ImGui::Text("%f", (entry.value.last_change() * 1.0e-6) - (GetZeroTime() * 1.0e-6)); } else { ImGui::TextUnformatted(""); } } if (flags & NetworkTablesFlags_ShowServerTimestamp) { ImGui::TableNextColumn(); if (entry.value && entry.value.server_time() != 0) { if (entry.value.server_time() == 1) { ImGui::TextUnformatted("---"); } else { ImGui::Text("%f", entry.value.server_time() * 1.0e-6); } } else { ImGui::TextUnformatted(""); } } if (valueChildrenOpen) { EmitValueTree(entry.valueChildren, flags); TreePop(); } } static void EmitTree(NetworkTablesModel* model, const std::vector& tree, NetworkTablesFlags flags, ShowCategory category, bool root) { for (auto&& node : tree) { if (root && (flags & NetworkTablesFlags_ShowSpecial) == 0 && wpi::starts_with(node.name, '$')) { continue; } if (node.entry) { EmitEntry(model, *node.entry, node.name.c_str(), flags, category); } if (!node.children.empty()) { ImGui::TableNextRow(); ImGui::TableNextColumn(); bool open = TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanFullWidth); EmitParentContextMenu(model, node.path, flags); if (open) { EmitTree(model, node.children, flags, category, false); TreePop(); } } } } static void DisplayTable(NetworkTablesModel* model, const std::vector& tree, NetworkTablesFlags flags, ShowCategory category) { if (tree.empty()) { return; } const bool showProperties = (flags & NetworkTablesFlags_ShowProperties); const bool showTimestamp = (flags & NetworkTablesFlags_ShowTimestamp); const bool showServerTimestamp = (flags & NetworkTablesFlags_ShowServerTimestamp); ImGui::BeginTable("values", 2 + (showProperties ? 1 : 0) + (showTimestamp ? 1 : 0) + (showServerTimestamp ? 1 : 0), ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInner); ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 0.35f * ImGui::GetWindowWidth()); ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 12 * ImGui::GetFontSize()); if (showProperties) { ImGui::TableSetupColumn("Properties", ImGuiTableColumnFlags_WidthFixed, 12 * ImGui::GetFontSize()); } if (showTimestamp) { ImGui::TableSetupColumn("Time"); } if (showServerTimestamp) { ImGui::TableSetupColumn("Server Time"); } ImGui::TableHeadersRow(); if (flags & NetworkTablesFlags_TreeView) { switch (category) { case ShowPersistent: PushID("persistent"); break; case ShowRetained: PushID("retained"); break; case ShowTransitory: PushID("transitory"); break; default: break; } EmitTree(model, tree, flags, category, true); if (category != ShowAll) { PopID(); } } else { for (auto entry : model->GetEntries()) { if ((flags & NetworkTablesFlags_ShowSpecial) != 0 || !wpi::starts_with(entry->info.name, '$')) { EmitEntry(model, *entry, entry->info.name.c_str(), flags, category); } } } ImGui::EndTable(); } static void DisplayClient(const NetworkTablesModel::Client& client) { if (CollapsingHeader("Publishers")) { ImGui::BeginTable("publishers", 2, ImGuiTableFlags_Resizable); ImGui::TableSetupColumn("UID", ImGuiTableColumnFlags_WidthFixed, 10 * ImGui::GetFontSize()); ImGui::TableSetupColumn("Topic"); ImGui::TableHeadersRow(); for (auto&& pub : client.publishers) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("%" PRId64, pub.uid); ImGui::TableNextColumn(); ImGui::Text("%s", pub.topic.c_str()); } ImGui::EndTable(); } if (CollapsingHeader("Subscribers")) { ImGui::BeginTable( "subscribers", 6, ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp); ImGui::TableSetupColumn("UID", ImGuiTableColumnFlags_WidthFixed, 10 * ImGui::GetFontSize()); ImGui::TableSetupColumn("Topics", ImGuiTableColumnFlags_WidthStretch, 6.0f); ImGui::TableSetupColumn("Periodic", ImGuiTableColumnFlags_WidthStretch, 1.0f); ImGui::TableSetupColumn("Topics Only", ImGuiTableColumnFlags_WidthStretch, 1.0f); ImGui::TableSetupColumn("Send All", ImGuiTableColumnFlags_WidthStretch, 1.0f); ImGui::TableSetupColumn("Prefix Match", ImGuiTableColumnFlags_WidthStretch, 1.0f); ImGui::TableHeadersRow(); for (auto&& sub : client.subscribers) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("%" PRId64, sub.uid); ImGui::TableNextColumn(); ImGui::Text("%s", sub.topicsStr.c_str()); ImGui::TableNextColumn(); ImGui::Text("%0.3f", sub.options.periodic); ImGui::TableNextColumn(); ImGui::Text(sub.options.topicsOnly ? "Yes" : "No"); ImGui::TableNextColumn(); ImGui::Text(sub.options.sendAll ? "Yes" : "No"); ImGui::TableNextColumn(); ImGui::Text(sub.options.prefixMatch ? "Yes" : "No"); } ImGui::EndTable(); } } void glass::DisplayNetworkTablesInfo(NetworkTablesModel* model) { auto inst = model->GetInstance(); if (CollapsingHeader("Connections")) { ImGui::BeginTable("connections", 4, ImGuiTableFlags_Resizable); ImGui::TableSetupColumn("Id"); ImGui::TableSetupColumn("Address"); ImGui::TableSetupColumn("Updated"); ImGui::TableSetupColumn("Proto"); ImGui::TableSetupScrollFreeze(1, 0); ImGui::TableHeadersRow(); for (auto&& i : inst.GetConnections()) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("%s", i.remote_id.c_str()); ImGui::TableNextColumn(); ImGui::Text("%s", i.remote_ip.c_str()); ImGui::TableNextColumn(); ImGui::Text("%llu", static_cast( // NOLINT(runtime/int) i.last_update)); ImGui::TableNextColumn(); ImGui::Text("%d.%d", i.protocol_version >> 8, i.protocol_version & 0xff); } ImGui::EndTable(); } auto netMode = inst.GetNetworkMode(); if (netMode == NT_NET_MODE_SERVER || netMode == NT_NET_MODE_CLIENT4) { if (CollapsingHeader("Server")) { PushID("Server"); ImGui::Indent(); DisplayClient(model->GetServer()); ImGui::Unindent(); PopID(); } if (CollapsingHeader("Clients")) { ImGui::Indent(); for (auto&& client : model->GetClients()) { if (CollapsingHeader(client.second.id.c_str())) { PushID(client.second.id.c_str()); ImGui::Indent(); ImGui::Text("%s (version %u.%u)", client.second.conn.c_str(), client.second.version >> 8, client.second.version & 0xff); DisplayClient(client.second); ImGui::Unindent(); PopID(); } } ImGui::Unindent(); } } } 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 { if (CollapsingHeader("Persistent Values", ImGuiTreeNodeFlags_DefaultOpen)) { DisplayTable(model, model->GetPersistentTreeRoot(), flags, ShowPersistent); } if (CollapsingHeader("Retained Values", ImGuiTreeNodeFlags_DefaultOpen)) { DisplayTable(model, model->GetRetainedTreeRoot(), flags, ShowRetained); } if (CollapsingHeader("Transitory Values", ImGuiTreeNodeFlags_DefaultOpen)) { DisplayTable(model, model->GetTransitoryTreeRoot(), flags, ShowTransitory); } } } void NetworkTablesFlagsSettings::Update() { if (!m_pTreeView) { auto& storage = GetStorage(); m_pTreeView = &storage.GetBool("tree", m_defaultFlags & NetworkTablesFlags_TreeView); m_pCombinedView = &storage.GetBool( "combined", m_defaultFlags & NetworkTablesFlags_CombinedView); m_pShowSpecial = &storage.GetBool( "special", m_defaultFlags & NetworkTablesFlags_ShowSpecial); m_pShowProperties = &storage.GetBool( "properties", m_defaultFlags & NetworkTablesFlags_ShowProperties); m_pShowTimestamp = &storage.GetBool( "timestamp", m_defaultFlags & NetworkTablesFlags_ShowTimestamp); m_pShowServerTimestamp = &storage.GetBool( "serverTimestamp", m_defaultFlags & NetworkTablesFlags_ShowServerTimestamp); m_pCreateNoncanonicalKeys = &storage.GetBool( "createNonCanonical", m_defaultFlags & NetworkTablesFlags_CreateNoncanonicalKeys); m_pPrecision = &storage.GetInt( "precision", (m_defaultFlags & NetworkTablesFlags_Precision) >> kNetworkTablesFlags_PrecisionBitShift); } m_flags &= ~( NetworkTablesFlags_TreeView | NetworkTablesFlags_CombinedView | NetworkTablesFlags_ShowSpecial | NetworkTablesFlags_ShowProperties | NetworkTablesFlags_ShowTimestamp | NetworkTablesFlags_ShowServerTimestamp | NetworkTablesFlags_CreateNoncanonicalKeys | NetworkTablesFlags_Precision); m_flags |= (*m_pTreeView ? NetworkTablesFlags_TreeView : 0) | (*m_pCombinedView ? NetworkTablesFlags_CombinedView : 0) | (*m_pShowSpecial ? NetworkTablesFlags_ShowSpecial : 0) | (*m_pShowProperties ? NetworkTablesFlags_ShowProperties : 0) | (*m_pShowTimestamp ? NetworkTablesFlags_ShowTimestamp : 0) | (*m_pShowServerTimestamp ? NetworkTablesFlags_ShowServerTimestamp : 0) | (*m_pCreateNoncanonicalKeys ? NetworkTablesFlags_CreateNoncanonicalKeys : 0) | (*m_pPrecision << kNetworkTablesFlags_PrecisionBitShift); } void NetworkTablesFlagsSettings::DisplayMenu() { if (!m_pTreeView) { return; } ImGui::MenuItem("Tree View", "", m_pTreeView); ImGui::MenuItem("Combined View", "", m_pCombinedView); ImGui::MenuItem("Show Special", "", m_pShowSpecial); ImGui::MenuItem("Show Properties", "", m_pShowProperties); ImGui::MenuItem("Show Timestamp", "", m_pShowTimestamp); ImGui::MenuItem("Show Server Timestamp", "", m_pShowServerTimestamp); if (ImGui::BeginMenu("Decimal Precision")) { static const char* precisionOptions[] = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}; for (int i = 1; i <= 10; i++) { if (ImGui::MenuItem(precisionOptions[i - 1], nullptr, i == *m_pPrecision)) { *m_pPrecision = i; } } ImGui::EndMenu(); } ImGui::Separator(); ImGui::MenuItem("Allow creation of non-canonical keys", "", m_pCreateNoncanonicalKeys); } void NetworkTablesView::Display() { m_flags.Update(); DisplayNetworkTables(m_model, m_flags.GetFlags()); } void NetworkTablesView::Settings() { m_flags.DisplayMenu(); DisplayNetworkTablesAddMenu(m_model, {}, m_flags.GetFlags()); } bool NetworkTablesView::HasSettings() { return true; }