[glass] Add glass: an application for display of robot data

This reuses many pieces of the current simulation GUI.  The common pieces have
been refactored into the libglass library.

The libglass library is designed to be usable for other standalone data
visualization applications (e.g. viewing data logs).

The name "glass" comes from "glass cockpit", as the application features
several multi-function displays that can be adjusted to display robot
information as needed.
This commit is contained in:
Peter Johnson
2020-09-12 10:55:46 -07:00
parent 727940d847
commit 2a5ca77454
151 changed files with 10386 additions and 4565 deletions

View File

@@ -0,0 +1,422 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/Context.h"
#include <algorithm>
#include <cinttypes>
#include <cstdio>
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_stdlib.h>
#include <wpi/StringRef.h>
#include <wpigui.h>
#include "glass/ContextInternal.h"
using namespace glass;
Context* glass::gContext;
static bool ConvertInt(Storage::Value* value) {
value->type = Storage::Value::kInt;
if (value->stringVal.empty()) {
return false;
} else {
if (wpi::StringRef{value->stringVal}.getAsInteger(10, value->intVal))
return false;
}
return true;
}
static bool ConvertInt64(Storage::Value* value) {
value->type = Storage::Value::kInt64;
if (value->stringVal.empty()) {
return false;
} else {
if (wpi::StringRef{value->stringVal}.getAsInteger(10, value->int64Val))
return false;
}
return true;
}
static bool ConvertBool(Storage::Value* value) {
value->type = Storage::Value::kBool;
if (value->stringVal.empty()) {
return false;
} else {
int val;
if (wpi::StringRef{value->stringVal}.getAsInteger(10, val)) {
return false;
}
value->boolVal = (val != 0);
}
return true;
}
static bool ConvertFloat(Storage::Value* value) {
value->type = Storage::Value::kFloat;
if (value->stringVal.empty()) {
return false;
} else {
if (std::sscanf(value->stringVal.c_str(), "%f", &value->floatVal) != 1)
return false;
}
return true;
}
static bool ConvertDouble(Storage::Value* value) {
value->type = Storage::Value::kDouble;
if (value->stringVal.empty()) {
return false;
} else {
if (std::sscanf(value->stringVal.c_str(), "%lf", &value->doubleVal) != 1)
return false;
}
return true;
}
static void* GlassStorageReadOpen(ImGuiContext*, ImGuiSettingsHandler* handler,
const char* name) {
auto ctx = static_cast<Context*>(handler->UserData);
auto& storage = ctx->storage[name];
if (!storage) storage = std::make_unique<Storage>();
return storage.get();
}
static void GlassStorageReadLine(ImGuiContext*, ImGuiSettingsHandler*,
void* entry, const char* line) {
auto storage = static_cast<Storage*>(entry);
auto [key, val] = wpi::StringRef{line}.split('=');
auto& keys = storage->GetKeys();
auto& values = storage->GetValues();
auto it = std::find(keys.begin(), keys.end(), key);
if (it == keys.end()) {
keys.emplace_back(key);
values.emplace_back(std::make_unique<Storage::Value>(val));
} else {
auto& value = *values[it - keys.begin()];
value.stringVal = val;
switch (value.type) {
case Storage::Value::kInt:
ConvertInt(&value);
break;
case Storage::Value::kInt64:
ConvertInt64(&value);
break;
case Storage::Value::kBool:
ConvertBool(&value);
break;
case Storage::Value::kFloat:
ConvertFloat(&value);
break;
case Storage::Value::kDouble:
ConvertDouble(&value);
break;
default:
break;
}
}
}
static void GlassStorageWriteAll(ImGuiContext*, ImGuiSettingsHandler* handler,
ImGuiTextBuffer* out_buf) {
auto ctx = static_cast<Context*>(handler->UserData);
// sort for output
std::vector<wpi::StringMapConstIterator<std::unique_ptr<Storage>>> sorted;
for (auto it = ctx->storage.begin(); it != ctx->storage.end(); ++it) {
sorted.emplace_back(it);
}
std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) {
return a->getKey() < b->getKey();
});
for (auto&& entryIt : sorted) {
auto& entry = *entryIt;
out_buf->append("[GlassStorage][");
out_buf->append(entry.first().begin(), entry.first().end());
out_buf->append("]\n");
auto& keys = entry.second->GetKeys();
auto& values = entry.second->GetValues();
for (size_t i = 0; i < keys.size(); ++i) {
out_buf->append(keys[i].data(), keys[i].data() + keys[i].size());
out_buf->append("=");
auto& value = *values[i];
switch (value.type) {
case Storage::Value::kInt:
out_buf->appendf("%d\n", value.intVal);
break;
case Storage::Value::kInt64:
out_buf->appendf("%" PRId64 "\n", value.int64Val);
break;
case Storage::Value::kBool:
out_buf->appendf("%d\n", value.boolVal ? 1 : 0);
break;
case Storage::Value::kFloat:
out_buf->appendf("%f\n", value.floatVal);
break;
case Storage::Value::kDouble:
out_buf->appendf("%f\n", value.doubleVal);
break;
case Storage::Value::kNone:
case Storage::Value::kString:
out_buf->append(value.stringVal.data(),
value.stringVal.data() + value.stringVal.size());
out_buf->append("\n");
break;
}
}
out_buf->append("\n");
}
}
static void Initialize(Context* ctx) {
wpi::gui::AddInit([=] {
ImGuiSettingsHandler ini_handler;
ini_handler.TypeName = "GlassStorage";
ini_handler.TypeHash = ImHashStr("GlassStorage");
ini_handler.ReadOpenFn = GlassStorageReadOpen;
ini_handler.ReadLineFn = GlassStorageReadLine;
ini_handler.WriteAllFn = GlassStorageWriteAll;
ini_handler.UserData = ctx;
ImGui::GetCurrentContext()->SettingsHandlers.push_back(ini_handler);
ctx->sources.Initialize();
});
}
static void Shutdown(Context* ctx) {}
Context* glass::CreateContext() {
Context* ctx = new Context;
if (!gContext) SetCurrentContext(ctx);
Initialize(ctx);
return ctx;
}
void glass::DestroyContext(Context* ctx) {
if (!ctx) ctx = gContext;
Shutdown(ctx);
if (gContext == ctx) SetCurrentContext(nullptr);
delete ctx;
}
Context* glass::GetCurrentContext() { return gContext; }
void glass::SetCurrentContext(Context* ctx) { gContext = ctx; }
Storage::Value& Storage::GetValue(wpi::StringRef key) {
auto it = std::find(m_keys.begin(), m_keys.end(), key);
if (it == m_keys.end()) {
m_keys.emplace_back(key);
m_values.emplace_back(std::make_unique<Value>());
return *m_values.back();
} else {
return *m_values[it - m_keys.begin()];
}
}
#define DEFUN(CapsName, LowerName, CType) \
CType Storage::Get##CapsName(wpi::StringRef key, CType defaultVal) const { \
auto it = std::find(m_keys.begin(), m_keys.end(), key); \
if (it == m_keys.end()) return defaultVal; \
Value& value = *m_values[it - m_keys.begin()]; \
if (value.type != Value::k##CapsName) { \
if (!Convert##CapsName(&value)) value.LowerName##Val = defaultVal; \
} \
return value.LowerName##Val; \
} \
\
void Storage::Set##CapsName(wpi::StringRef key, CType val) { \
auto it = std::find(m_keys.begin(), m_keys.end(), key); \
if (it == m_keys.end()) { \
m_keys.emplace_back(key); \
m_values.emplace_back(std::make_unique<Value>()); \
m_values.back()->type = Value::k##CapsName; \
m_values.back()->LowerName##Val = val; \
} else { \
Value& value = *m_values[it - m_keys.begin()]; \
value.type = Value::k##CapsName; \
value.LowerName##Val = val; \
} \
} \
\
CType* Storage::Get##CapsName##Ref(wpi::StringRef key, CType defaultVal) { \
auto it = std::find(m_keys.begin(), m_keys.end(), key); \
if (it == m_keys.end()) { \
m_keys.emplace_back(key); \
m_values.emplace_back(std::make_unique<Value>()); \
m_values.back()->type = Value::k##CapsName; \
m_values.back()->LowerName##Val = defaultVal; \
return &m_values.back()->LowerName##Val; \
} else { \
Value& value = *m_values[it - m_keys.begin()]; \
if (value.type != Value::k##CapsName) { \
if (!Convert##CapsName(&value)) value.LowerName##Val = defaultVal; \
} \
return &value.LowerName##Val; \
} \
}
DEFUN(Int, int, int)
DEFUN(Int64, int64, int64_t)
DEFUN(Bool, bool, bool)
DEFUN(Float, float, float)
DEFUN(Double, double, double)
std::string Storage::GetString(wpi::StringRef key,
const std::string& defaultVal) const {
auto it = std::find(m_keys.begin(), m_keys.end(), key);
if (it == m_keys.end()) return defaultVal;
Value& value = *m_values[it - m_keys.begin()];
value.type = Value::kString;
return value.stringVal;
}
void Storage::SetString(wpi::StringRef key, const wpi::Twine& val) {
auto it = std::find(m_keys.begin(), m_keys.end(), key);
if (it == m_keys.end()) {
m_keys.emplace_back(key);
m_values.emplace_back(std::make_unique<Value>(val));
m_values.back()->type = Value::kString;
} else {
Value& value = *m_values[it - m_keys.begin()];
value.type = Value::kString;
value.stringVal = val.str();
}
}
std::string* Storage::GetStringRef(wpi::StringRef key,
wpi::StringRef defaultVal) {
auto it = std::find(m_keys.begin(), m_keys.end(), key);
if (it == m_keys.end()) {
m_keys.emplace_back(key);
m_values.emplace_back(std::make_unique<Value>(defaultVal));
m_values.back()->type = Value::kString;
return &m_values.back()->stringVal;
} else {
Value& value = *m_values[it - m_keys.begin()];
value.type = Value::kString;
return &value.stringVal;
}
}
Storage& glass::GetStorage() {
auto& storage = gContext->storage[gContext->curId];
if (!storage) storage = std::make_unique<Storage>();
return *storage;
}
Storage& glass::GetStorage(wpi::StringRef id) {
auto& storage = gContext->storage[id];
if (!storage) storage = std::make_unique<Storage>();
return *storage;
}
static void PushIDStack(wpi::StringRef label_id) {
gContext->idStack.emplace_back(gContext->curId.size());
auto [label, id] = wpi::StringRef{label_id}.split("###");
// if no ###id, use label as id
if (id.empty()) id = label;
if (!gContext->curId.empty()) gContext->curId += "###";
gContext->curId += id;
}
static void PopIDStack() {
gContext->curId.resize(gContext->idStack.back());
gContext->idStack.pop_back();
}
bool glass::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) {
PushIDStack(name);
return ImGui::Begin(name, p_open, flags);
}
void glass::End() {
ImGui::End();
PopIDStack();
}
bool glass::BeginChild(const char* str_id, const ImVec2& size, bool border,
ImGuiWindowFlags flags) {
PushIDStack(str_id);
return ImGui::BeginChild(str_id, size, border, flags);
}
void glass::EndChild() {
ImGui::EndChild();
PopIDStack();
}
bool glass::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) {
wpi::SmallString<64> openKey;
auto [name, id] = wpi::StringRef{label}.split("###");
// if no ###id, use name as id
if (id.empty()) id = name;
openKey = id;
openKey += "###open";
bool* open = GetStorage().GetBoolRef(openKey);
*open = ImGui::CollapsingHeader(
label, flags | (*open ? ImGuiTreeNodeFlags_DefaultOpen : 0));
return *open;
}
bool glass::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) {
PushIDStack(label);
bool* open = GetStorage().GetBoolRef("open");
*open = ImGui::TreeNodeEx(
label, flags | (*open ? ImGuiTreeNodeFlags_DefaultOpen : 0));
if (!*open) PopIDStack();
return *open;
}
void glass::TreePop() {
ImGui::TreePop();
PopIDStack();
}
void glass::PushID(const char* str_id) {
PushIDStack(str_id);
ImGui::PushID(str_id);
}
void glass::PushID(const char* str_id_begin, const char* str_id_end) {
PushIDStack(wpi::StringRef(str_id_begin, str_id_end - str_id_begin));
ImGui::PushID(str_id_begin, str_id_end);
}
void glass::PushID(int int_id) {
char buf[16];
std::snprintf(buf, sizeof(buf), "%d", int_id);
PushIDStack(buf);
ImGui::PushID(int_id);
}
void glass::PopID() {
ImGui::PopID();
PopIDStack();
}
bool glass::PopupEditName(const char* label, std::string* name) {
bool rv = false;
if (ImGui::BeginPopupContextItem(label)) {
ImGui::Text("Edit name:");
if (ImGui::InputText("##editname", name)) {
rv = true;
}
if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
return rv;
}

View File

@@ -0,0 +1,142 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/DataSource.h"
#include "glass/ContextInternal.h"
using namespace glass;
wpi::sig::Signal<const char*, DataSource*> DataSource::sourceCreated;
DataSource::DataSource(const wpi::Twine& id) : m_id{id.str()} {
auto it = gContext->sources.try_emplace(m_id, this);
auto& srcName = it.first->getValue();
m_name = srcName.name.get();
if (!srcName.source) srcName.source = this;
sourceCreated(m_id.c_str(), this);
}
DataSource::DataSource(const wpi::Twine& id, int index)
: DataSource{id + wpi::Twine('[') + wpi::Twine(index) + wpi::Twine(']')} {}
DataSource::DataSource(const wpi::Twine& id, int index, int index2)
: DataSource{id + wpi::Twine('[') + wpi::Twine(index) + wpi::Twine(',') +
wpi::Twine(index2) + wpi::Twine(']')} {}
DataSource::~DataSource() {
if (!gContext) return;
auto it = gContext->sources.find(m_id);
if (it == gContext->sources.end()) return;
auto& srcName = it->getValue();
if (srcName.source == this) srcName.source = nullptr;
}
void DataSource::SetName(const wpi::Twine& name) { m_name->SetName(name); }
const char* DataSource::GetName() const { return m_name->GetName(); }
void DataSource::PushEditNameId(int index) { m_name->PushEditNameId(index); }
void DataSource::PushEditNameId(const char* name) {
m_name->PushEditNameId(name);
}
bool DataSource::PopupEditName(int index) {
return m_name->PopupEditName(index);
}
bool DataSource::PopupEditName(const char* name) {
return m_name->PopupEditName(name);
}
bool DataSource::InputTextName(const char* label_id,
ImGuiInputTextFlags flags) {
return m_name->InputTextName(label_id, flags);
}
void DataSource::LabelText(const char* label, const char* fmt, ...) const {
va_list args;
va_start(args, fmt);
LabelTextV(label, fmt, args);
va_end(args);
}
// Add a label+text combo aligned to other label+value widgets
void DataSource::LabelTextV(const char* label, const char* fmt,
va_list args) const {
ImGui::PushID(label);
ImGui::LabelTextV("##input", fmt, args);
ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::Selectable(label);
ImGui::PopID();
EmitDrag();
}
bool DataSource::Combo(const char* label, int* current_item,
const char* const items[], int items_count,
int popup_max_height_in_items) const {
ImGui::PushID(label);
bool rv = ImGui::Combo("##input", current_item, items, items_count,
popup_max_height_in_items);
ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::Selectable(label);
EmitDrag();
ImGui::PopID();
return rv;
}
bool DataSource::SliderFloat(const char* label, float* v, float v_min,
float v_max, const char* format,
float power) const {
ImGui::PushID(label);
bool rv = ImGui::SliderFloat("##input", v, v_min, v_max, format, power);
ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::Selectable(label);
EmitDrag();
ImGui::PopID();
return rv;
}
bool DataSource::InputDouble(const char* label, double* v, double step,
double step_fast, const char* format,
ImGuiInputTextFlags flags) const {
ImGui::PushID(label);
bool rv = ImGui::InputDouble("##input", v, step, step_fast, format, flags);
ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::Selectable(label);
EmitDrag();
ImGui::PopID();
return rv;
}
bool DataSource::InputInt(const char* label, int* v, int step, int step_fast,
ImGuiInputTextFlags flags) const {
ImGui::PushID(label);
bool rv = ImGui::InputInt("##input", v, step, step_fast, flags);
ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::Selectable(label);
EmitDrag();
ImGui::PopID();
return rv;
}
void DataSource::EmitDrag(ImGuiDragDropFlags flags) const {
if (ImGui::BeginDragDropSource(flags)) {
auto self = this;
ImGui::SetDragDropPayload("DataSource", &self, sizeof(self));
const char* name = GetName();
ImGui::TextUnformatted(name[0] == '\0' ? m_id.c_str() : name);
ImGui::EndDragDropSource();
}
}
DataSource* DataSource::Find(wpi::StringRef id) {
auto it = gContext->sources.find(id);
if (it == gContext->sources.end()) return nullptr;
return it->getValue().source;
}

View File

@@ -0,0 +1,52 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/MainMenuBar.h"
#include <cstdio>
#include <wpigui.h>
using namespace glass;
void MainMenuBar::AddMainMenu(std::function<void()> menu) {
if (menu) m_menus.emplace_back(std::move(menu));
}
void MainMenuBar::AddOptionMenu(std::function<void()> menu) {
if (menu) m_optionMenus.emplace_back(std::move(menu));
}
void MainMenuBar::Display() {
ImGui::BeginMainMenuBar();
if (!m_optionMenus.empty()) {
if (ImGui::BeginMenu("Options")) {
for (auto&& menu : m_optionMenus) {
if (menu) menu();
}
ImGui::EndMenu();
}
}
wpi::gui::EmitViewMenu();
for (auto&& menu : m_menus) {
if (menu) menu();
}
#if 0
char str[64];
std::snprintf(str, sizeof(str), "%.3f ms/frame (%.1f FPS)",
1000.0f / ImGui::GetIO().Framerate,
ImGui::GetIO().Framerate);
ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::CalcTextSize(str).x -
10);
ImGui::Text("%s", str);
#endif
ImGui::EndMainMenuBar();
}

View File

@@ -0,0 +1,12 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/Model.h"
using namespace glass;
bool Model::IsReadOnly() { return false; }

View File

@@ -0,0 +1,30 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/View.h"
using namespace glass;
namespace {
class FunctionView : public View {
public:
explicit FunctionView(wpi::unique_function<void()> display)
: m_display(std::move(display)) {}
void Display() override { m_display(); }
private:
wpi::unique_function<void()> m_display;
};
} // namespace
std::unique_ptr<View> glass::MakeFunctionView(
wpi::unique_function<void()> display) {
return std::make_unique<FunctionView>(std::move(display));
}
void View::Hidden() {}

View File

@@ -0,0 +1,102 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/Window.h"
#include <imgui_internal.h>
#include <wpi/StringRef.h>
#include "glass/Context.h"
using namespace glass;
void Window::SetVisibility(Visibility visibility) {
switch (visibility) {
case kHide:
m_visible = false;
m_enabled = true;
break;
case kShow:
m_visible = true;
m_enabled = true;
break;
case kDisabled:
m_enabled = false;
break;
}
}
void Window::Display() {
if (!m_view) return;
if (!m_visible || !m_enabled) {
PushID(m_id);
m_view->Hidden();
PopID();
return;
}
if (m_posCond != 0) ImGui::SetNextWindowPos(m_pos, m_posCond);
if (m_sizeCond != 0) ImGui::SetNextWindowSize(m_size, m_sizeCond);
if (m_setPadding) ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, m_padding);
char label[128];
std::snprintf(label, sizeof(label), "%s###%s",
m_name.empty() ? m_id.c_str() : m_name.c_str(), m_id.c_str());
if (Begin(label, &m_visible, m_flags)) {
if (m_renamePopupEnabled) PopupEditName(nullptr, &m_name);
m_view->Display();
} else {
m_view->Hidden();
}
End();
if (m_setPadding) ImGui::PopStyleVar();
}
bool Window::DisplayMenuItem(const char* label) {
bool wasVisible = m_visible;
ImGui::MenuItem(
label ? label : (m_name.empty() ? m_id.c_str() : m_name.c_str()), nullptr,
&m_visible, m_enabled);
return !wasVisible && m_visible;
}
void Window::ScaleDefault(float scale) {
if ((m_posCond & ImGuiCond_FirstUseEver) != 0) {
m_pos.x *= scale;
m_pos.y *= scale;
}
if ((m_sizeCond & ImGuiCond_FirstUseEver) != 0) {
m_size.x *= scale;
m_size.y *= scale;
}
}
void Window::IniReadLine(const char* lineStr) {
wpi::StringRef line{lineStr};
auto [name, value] = line.split('=');
name = name.trim();
value = value.trim();
if (name == "name") {
m_name = value;
} else if (name == "visible") {
int num;
if (value.getAsInteger(10, num)) return;
m_visible = num;
} else if (name == "enabled") {
int num;
if (value.getAsInteger(10, num)) return;
m_enabled = num;
}
}
void Window::IniWriteAll(const char* typeName, ImGuiTextBuffer* out_buf) {
out_buf->appendf("[%s][%s]\nname=%s\nvisible=%d\nenabled=%d\n\n", typeName,
m_id.c_str(), m_name.c_str(), m_visible ? 1 : 0,
m_enabled ? 1 : 0);
}

View File

@@ -0,0 +1,107 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/WindowManager.h"
#include <algorithm>
#include <wpi/StringRef.h>
#include <wpi/raw_ostream.h>
#include <wpigui.h>
using namespace glass;
WindowManager::WindowManager(const wpi::Twine& iniName)
: m_iniSaver{iniName, this} {}
// read/write open state to ini file
void* WindowManager::IniSaver::IniReadOpen(const char* name) {
return m_manager->GetOrAddWindow(name, true);
}
void WindowManager::IniSaver::IniReadLine(void* entry, const char* lineStr) {
static_cast<Window*>(entry)->IniReadLine(lineStr);
}
void WindowManager::IniSaver::IniWriteAll(ImGuiTextBuffer* out_buf) {
const char* typeName = GetTypeName();
for (auto&& window : m_manager->m_windows) {
window->IniWriteAll(typeName, out_buf);
}
}
Window* WindowManager::AddWindow(wpi::StringRef id,
wpi::unique_function<void()> display) {
auto win = GetOrAddWindow(id, false);
if (!win) return nullptr;
if (win->HasView()) {
wpi::errs() << "GUI: ignoring duplicate window '" << id << "'\n";
return nullptr;
}
win->SetView(MakeFunctionView(std::move(display)));
return win;
}
Window* WindowManager::AddWindow(wpi::StringRef id,
std::unique_ptr<View> view) {
auto win = GetOrAddWindow(id, false);
if (!win) return nullptr;
if (win->HasView()) {
wpi::errs() << "GUI: ignoring duplicate window '" << id << "'\n";
return nullptr;
}
win->SetView(std::move(view));
return win;
}
Window* WindowManager::GetOrAddWindow(wpi::StringRef id, bool duplicateOk) {
// binary search
auto it = std::lower_bound(
m_windows.begin(), m_windows.end(), id,
[](const auto& elem, wpi::StringRef s) { return elem->GetId() < s; });
if (it != m_windows.end() && (*it)->GetId() == id) {
if (!duplicateOk) {
wpi::errs() << "GUI: ignoring duplicate window '" << id << "'\n";
return nullptr;
}
return it->get();
}
// insert before (keeps sort)
return m_windows.emplace(it, std::make_unique<Window>(id))->get();
}
Window* WindowManager::GetWindow(wpi::StringRef id) {
// binary search
auto it = std::lower_bound(
m_windows.begin(), m_windows.end(), id,
[](const auto& elem, wpi::StringRef s) { return elem->GetId() < s; });
if (it == m_windows.end() || (*it)->GetId() != id) return nullptr;
return it->get();
}
void WindowManager::GlobalInit() {
wpi::gui::AddInit([this] { m_iniSaver.Initialize(); });
wpi::gui::AddWindowScaler([this](float scale) {
// scale default window positions
for (auto&& window : m_windows) {
window->ScaleDefault(scale);
}
});
wpi::gui::AddLateExecute([this] { DisplayWindows(); });
}
void WindowManager::DisplayMenu() {
for (auto&& window : m_windows) {
window->DisplayMenuItem();
}
}
void WindowManager::DisplayWindows() {
for (auto&& window : m_windows) {
window->Display();
}
}

View File

@@ -0,0 +1,51 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/Accelerometer.h"
#include "glass/DataSource.h"
#include "glass/other/DeviceTree.h"
using namespace glass;
void glass::DisplayAccelerometerDevice(AccelerometerModel* model) {
if (!model->Exists()) return;
if (BeginDevice("BuiltInAccel")) {
// Range
{
int value = model->GetRange();
static const char* rangeOptions[] = {"2G", "4G", "8G"};
DeviceEnum("Range", true, &value, rangeOptions, 3);
}
// X Accel
if (auto xData = model->GetXData()) {
double value = xData->GetValue();
if (DeviceDouble("X Accel", false, &value, xData)) {
model->SetX(value);
}
}
// Y Accel
if (auto yData = model->GetYData()) {
double value = yData->GetValue();
if (DeviceDouble("Y Accel", false, &value, yData)) {
model->SetY(value);
}
}
// Z Accel
if (auto zData = model->GetZData()) {
double value = zData->GetValue();
if (DeviceDouble("Z Accel", false, &value, zData)) {
model->SetZ(value);
}
}
EndDevice();
}
}

View File

@@ -0,0 +1,41 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/AnalogGyro.h"
#include "glass/DataSource.h"
#include "glass/other/DeviceTree.h"
using namespace glass;
void glass::DisplayAnalogGyroDevice(AnalogGyroModel* model, int index) {
char name[32];
std::snprintf(name, sizeof(name), "AnalogGyro[%d]", index);
if (BeginDevice(name)) {
// angle
if (auto angleData = model->GetAngleData()) {
double value = angleData->GetValue();
if (DeviceDouble("Angle", false, &value, angleData)) {
model->SetAngle(value);
}
}
// rate
if (auto rateData = model->GetRateData()) {
double value = rateData->GetValue();
if (DeviceDouble("Rate", false, &value, rateData)) {
model->SetRate(value);
}
}
EndDevice();
}
}
void glass::DisplayAnalogGyrosDevice(AnalogGyrosModel* model) {
model->ForEachAnalogGyro(
[&](AnalogGyroModel& gyro, int i) { DisplayAnalogGyroDevice(&gyro, i); });
}

View File

@@ -0,0 +1,66 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/AnalogInput.h"
#include <imgui.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
using namespace glass;
void glass::DisplayAnalogInput(AnalogInputModel* model, int index) {
auto voltageData = model->GetVoltageData();
if (!voltageData) return;
// build label
std::string* name = GetStorage().GetStringRef("name");
char label[128];
if (!name->empty()) {
std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), index);
} else {
std::snprintf(label, sizeof(label), "In[%d]###name", index);
}
if (model->IsGyro()) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
ImGui::LabelText(label, "AnalogGyro[%d]", index);
ImGui::PopStyleColor();
} else if (auto simDevice = model->GetSimDevice()) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
ImGui::LabelText(label, "%s", simDevice);
ImGui::PopStyleColor();
} else {
float val = voltageData->GetValue();
if (voltageData->SliderFloat(label, &val, 0.0, 5.0)) model->SetVoltage(val);
}
// context menu to change name
if (PopupEditName("name", name)) voltageData->SetName(name->c_str());
}
void glass::DisplayAnalogInputs(AnalogInputsModel* model,
wpi::StringRef noneMsg) {
ImGui::Text("(Use Ctrl+Click to edit value)");
bool hasAny = false;
bool first = true;
model->ForEachAnalogInput([&](AnalogInputModel& input, int i) {
if (!first) {
ImGui::Spacing();
ImGui::Spacing();
} else {
first = false;
}
PushID(i);
DisplayAnalogInput(&input, i);
PopID();
hasAny = true;
});
if (!hasAny && !noneMsg.empty())
ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end());
}

View File

@@ -0,0 +1,47 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/AnalogOutput.h"
#include "glass/Context.h"
#include "glass/DataSource.h"
#include "glass/other/DeviceTree.h"
using namespace glass;
void glass::DisplayAnalogOutputsDevice(AnalogOutputsModel* model) {
int count = 0;
model->ForEachAnalogOutput([&](auto&, int) { ++count; });
if (count == 0) return;
if (BeginDevice("Analog Outputs")) {
model->ForEachAnalogOutput([&](auto& analogOut, int i) {
auto analogOutData = analogOut.GetVoltageData();
if (!analogOutData) return;
PushID(i);
// build label
std::string* name = GetStorage().GetStringRef("name");
char label[128];
if (!name->empty()) {
std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), i);
} else {
std::snprintf(label, sizeof(label), "Out[%d]###name", i);
}
double value = analogOutData->GetValue();
DeviceDouble(label, true, &value, analogOutData);
if (PopupEditName("name", name)) {
if (analogOutData) analogOutData->SetName(name->c_str());
}
PopID();
});
EndDevice();
}
}

View File

@@ -0,0 +1,118 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/DIO.h"
#include <imgui.h>
#include "glass/DataSource.h"
#include "glass/hardware/Encoder.h"
#include "glass/support/IniSaverInfo.h"
using namespace glass;
static void LabelSimDevice(const char* name, const char* simDeviceName) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
ImGui::LabelText(name, "%s", simDeviceName);
ImGui::PopStyleColor();
}
void DisplayDIOImpl(DIOModel* model, int index, bool outputsEnabled) {
auto dpwm = model->GetDPWM();
auto dutyCycle = model->GetDutyCycle();
auto encoder = model->GetEncoder();
auto dioData = model->GetValueData();
auto dpwmData = dpwm ? dpwm->GetValueData() : nullptr;
auto dutyCycleData = dutyCycle ? dutyCycle->GetValueData() : nullptr;
bool exists = model->Exists();
auto& info = dioData->GetNameInfo();
char label[128];
if (exists && dpwmData) {
dpwmData->GetNameInfo().GetLabel(label, sizeof(label), "PWM", index);
if (auto simDevice = dpwm->GetSimDevice()) {
LabelSimDevice(label, simDevice);
} else {
dpwmData->LabelText(label, "%0.3f", dpwmData->GetValue());
}
} else if (exists && encoder) {
info.GetLabel(label, sizeof(label), " In", index);
if (auto simDevice = encoder->GetSimDevice()) {
LabelSimDevice(label, simDevice);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
ImGui::LabelText(label, "Encoder[%d,%d]", encoder->GetChannelA(),
encoder->GetChannelB());
ImGui::PopStyleColor();
}
} else if (exists && dutyCycleData) {
dutyCycleData->GetNameInfo().GetLabel(label, sizeof(label), "Dty", index);
if (auto simDevice = dutyCycle->GetSimDevice()) {
LabelSimDevice(label, simDevice);
} else {
double val = dutyCycleData->GetValue();
if (dutyCycleData->InputDouble(label, &val)) {
dutyCycle->SetValue(val);
}
}
} else {
const char* name = model->GetName();
if (name[0] != '\0')
info.GetLabel(label, sizeof(label), name);
else
info.GetLabel(label, sizeof(label), model->IsInput() ? " In" : "Out",
index);
if (auto simDevice = model->GetSimDevice()) {
LabelSimDevice(label, simDevice);
} else {
if (!exists) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
dioData->LabelText(label, "unknown");
ImGui::PopStyleColor();
} else if (model->IsReadOnly()) {
dioData->LabelText(
label, "%s",
outputsEnabled ? (dioData->GetValue() != 0 ? "1 (high)" : "0 (low)")
: "1 (disabled)");
} else {
static const char* options[] = {"0 (low)", "1 (high)"};
int val = dioData->GetValue() != 0 ? 1 : 0;
if (dioData->Combo(label, &val, options, 2)) {
model->SetValue(val);
}
}
}
}
if (info.PopupEditName(index)) {
if (dpwmData) dpwmData->SetName(info.GetName());
if (dutyCycleData) dutyCycleData->SetName(info.GetName());
}
}
void glass::DisplayDIO(DIOModel* model, int index, bool outputsEnabled) {
ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
DisplayDIOImpl(model, index, outputsEnabled);
ImGui::PopItemWidth();
}
void glass::DisplayDIOs(DIOsModel* model, bool outputsEnabled,
wpi::StringRef noneMsg) {
bool hasAny = false;
ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
model->ForEachDIO([&](DIOModel& dio, int i) {
hasAny = true;
ImGui::PushID(i);
DisplayDIOImpl(&dio, i, outputsEnabled);
ImGui::PopID();
});
ImGui::PopItemWidth();
if (!hasAny && !noneMsg.empty())
ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end());
}

View File

@@ -0,0 +1,165 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/Encoder.h"
#include <imgui.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
using namespace glass;
void EncoderModel::SetName(const wpi::Twine& name) {
if (name.str().empty()) {
if (auto distancePerPulse = GetDistancePerPulseData()) {
distancePerPulse->SetName("");
}
if (auto count = GetCountData()) {
count->SetName("");
}
if (auto period = GetPeriodData()) {
period->SetName("");
}
if (auto direction = GetDirectionData()) {
direction->SetName("");
}
if (auto distance = GetDistanceData()) {
distance->SetName("");
}
if (auto rate = GetRateData()) {
rate->SetName("");
}
} else {
if (auto distancePerPulse = GetDistancePerPulseData()) {
distancePerPulse->SetName(name + " Distance/Count");
}
if (auto count = GetCountData()) {
count->SetName(name + " Count");
}
if (auto period = GetPeriodData()) {
period->SetName(name + " Period");
}
if (auto direction = GetDirectionData()) {
direction->SetName(name + " Direction");
}
if (auto distance = GetDistanceData()) {
distance->SetName(name + " Distance");
}
if (auto rate = GetRateData()) {
rate->SetName(name + " Rate");
}
}
}
void glass::DisplayEncoder(EncoderModel* model) {
if (auto simDevice = model->GetSimDevice()) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
ImGui::TextUnformatted(simDevice);
ImGui::PopStyleColor();
return;
}
int chA = model->GetChannelA();
int chB = model->GetChannelB();
// build header label
std::string* name = GetStorage().GetStringRef("name");
char label[128];
if (!name->empty()) {
std::snprintf(label, sizeof(label), "%s [%d,%d]###name", name->c_str(), chA,
chB);
} else {
std::snprintf(label, sizeof(label), "Encoder[%d,%d]###name", chA, chB);
}
// header
bool open = CollapsingHeader(label);
// context menu to change name
if (PopupEditName("name", name)) {
model->SetName(name->c_str());
}
if (!open) return;
ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
// distance per pulse
if (auto distancePerPulseData = model->GetDistancePerPulseData()) {
double value = distancePerPulseData->GetValue();
distancePerPulseData->LabelText("Dist/Count", "%.6f", value);
}
// count
if (auto countData = model->GetCountData()) {
int value = countData->GetValue();
if (ImGui::InputInt("##input", &value)) model->SetCount(value);
ImGui::SameLine();
if (ImGui::Button("Reset")) {
model->SetCount(0);
}
ImGui::SameLine();
ImGui::Selectable("Count");
countData->EmitDrag();
}
// max period
{
double maxPeriod = model->GetMaxPeriod();
ImGui::LabelText("Max Period", "%.6f", maxPeriod);
}
// period
if (auto periodData = model->GetPeriodData()) {
double value = periodData->GetValue();
if (periodData->InputDouble("Period", &value, 0, 0, "%.6g")) {
model->SetPeriod(value);
}
}
// reverse direction
ImGui::LabelText("Reverse Direction", "%s",
model->GetReverseDirection() ? "true" : "false");
// direction
if (auto directionData = model->GetDirectionData()) {
static const char* options[] = {"reverse", "forward"};
int value = directionData->GetValue() ? 1 : 0;
if (directionData->Combo("Direction", &value, options, 2)) {
model->SetDirection(value != 0);
}
}
// distance
if (auto distanceData = model->GetDistanceData()) {
double value = distanceData->GetValue();
if (distanceData->InputDouble("Distance", &value, 0, 0, "%.6g")) {
model->SetDistance(value);
}
}
// rate
if (auto rateData = model->GetRateData()) {
double value = rateData->GetValue();
if (rateData->InputDouble("Rate", &value, 0, 0, "%.6g")) {
model->SetRate(value);
}
}
ImGui::PopItemWidth();
}
void glass::DisplayEncoders(EncodersModel* model, wpi::StringRef noneMsg) {
bool hasAny = false;
model->ForEachEncoder([&](EncoderModel& encoder, int i) {
hasAny = true;
PushID(i);
DisplayEncoder(&encoder);
PopID();
});
if (!hasAny && !noneMsg.empty())
ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end());
}

View File

@@ -0,0 +1,91 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/LEDDisplay.h"
#include "glass/Context.h"
#include "glass/support/ExtraGuiWidgets.h"
using namespace glass;
namespace {
struct IndicatorData {
std::vector<int> values;
std::vector<ImU32> colors;
};
} // namespace
void glass::DisplayLEDDisplay(LEDDisplayModel* model, int index) {
wpi::SmallVector<LEDDisplayModel::Data, 64> dataBuf;
auto data = model->GetData(dataBuf);
int length = data.size();
bool running = model->IsRunning();
auto& storage = GetStorage();
int* numColumns = storage.GetIntRef("columns", 10);
bool* serpentine = storage.GetBoolRef("serpentine", false);
int* order = storage.GetIntRef("order", LEDConfig::RowMajor);
int* start = storage.GetIntRef("start", LEDConfig::UpperLeft);
ImGui::PushItemWidth(ImGui::GetFontSize() * 6);
ImGui::LabelText("Length", "%d", length);
ImGui::LabelText("Running", "%s", running ? "Yes" : "No");
ImGui::InputInt("Columns", numColumns);
{
static const char* options[] = {"Row Major", "Column Major"};
ImGui::Combo("Order", order, options, 2);
}
{
static const char* options[] = {"Upper Left", "Lower Left", "Upper Right",
"Lower Right"};
ImGui::Combo("Start", start, options, 4);
}
ImGui::Checkbox("Serpentine", serpentine);
if (*numColumns < 1) *numColumns = 1;
ImGui::PopItemWidth();
// show as LED indicators
auto iData = storage.GetData<IndicatorData>();
if (!iData) {
storage.SetData(std::make_shared<IndicatorData>());
iData = storage.GetData<IndicatorData>();
}
if (length > static_cast<int>(iData->values.size()))
iData->values.resize(length);
if (length > static_cast<int>(iData->colors.size()))
iData->colors.resize(length);
if (!running) {
iData->colors[0] = IM_COL32(128, 128, 128, 255);
for (int j = 0; j < length; ++j) iData->values[j] = -1;
} else {
for (int j = 0; j < length; ++j) {
iData->values[j] = j + 1;
iData->colors[j] = IM_COL32(data[j].r, data[j].g, data[j].b, 255);
}
}
LEDConfig config;
config.serpentine = *serpentine;
config.order = static_cast<LEDConfig::Order>(*order);
config.start = static_cast<LEDConfig::Start>(*start);
DrawLEDs(iData->values.data(), length, *numColumns, iData->colors.data(), 0,
0, config);
}
void glass::DisplayLEDDisplays(LEDDisplaysModel* model) {
bool hasAny = false;
model->ForEachLEDDisplay([&](LEDDisplayModel& display, int i) {
hasAny = true;
if (model->GetNumLEDDisplays() > 1) ImGui::Text("LEDs[%d]", i);
PushID(i);
DisplayLEDDisplay(&display, i);
PopID();
});
if (!hasAny) ImGui::Text("No addressable LEDs");
}

View File

@@ -0,0 +1,150 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/PCM.h"
#include <cstdio>
#include <cstring>
#include <imgui.h>
#include <wpi/SmallVector.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
#include "glass/other/DeviceTree.h"
#include "glass/support/ExtraGuiWidgets.h"
#include "glass/support/IniSaverInfo.h"
using namespace glass;
bool glass::DisplayPCMSolenoids(PCMModel* model, int index,
bool outputsEnabled) {
wpi::SmallVector<int, 16> channels;
model->ForEachSolenoid([&](SolenoidModel& solenoid, int j) {
if (auto data = solenoid.GetOutputData()) {
if (j >= static_cast<int>(channels.size())) channels.resize(j + 1);
channels[j] = (outputsEnabled && data->GetValue()) ? 1 : -1;
}
});
if (channels.empty()) return false;
// show nonexistent channels as empty
for (auto&& ch : channels) {
if (ch == 0) ch = -2;
}
// build header label
std::string* name = GetStorage().GetStringRef("name");
char label[128];
if (!name->empty()) {
std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), index);
} else {
std::snprintf(label, sizeof(label), "PCM[%d]###name", index);
}
// header
bool open = CollapsingHeader(label);
PopupEditName("name", name);
ImGui::SetItemAllowOverlap();
ImGui::SameLine();
// show channels as LED indicators
static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255),
IM_COL32(128, 128, 128, 255)};
DrawLEDs(channels.data(), channels.size(), channels.size(), colors);
if (open) {
ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
model->ForEachSolenoid([&](SolenoidModel& solenoid, int j) {
if (auto data = solenoid.GetOutputData()) {
PushID(j);
char solenoidName[64];
auto& info = data->GetNameInfo();
info.GetLabel(solenoidName, sizeof(solenoidName), "Solenoid", j);
data->LabelText(solenoidName, "%s", channels[j] == 1 ? "On" : "Off");
info.PopupEditName(j);
PopID();
}
});
ImGui::PopItemWidth();
}
return true;
}
void glass::DisplayPCMsSolenoids(PCMsModel* model, bool outputsEnabled,
wpi::StringRef noneMsg) {
bool hasAny = false;
model->ForEachPCM([&](PCMModel& pcm, int i) {
PushID(i);
if (DisplayPCMSolenoids(&pcm, i, outputsEnabled)) hasAny = true;
PopID();
});
if (!hasAny && !noneMsg.empty())
ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end());
}
void glass::DisplayCompressorDevice(PCMModel* model, int index,
bool outputsEnabled) {
auto compressor = model->GetCompressor();
if (!compressor || !compressor->Exists()) return;
DisplayCompressorDevice(compressor, index, outputsEnabled);
}
void glass::DisplayCompressorDevice(CompressorModel* model, int index,
bool outputsEnabled) {
char name[32];
std::snprintf(name, sizeof(name), "Compressor[%d]", index);
if (BeginDevice(name)) {
// output enabled
if (auto runningData = model->GetRunningData()) {
bool value = outputsEnabled && runningData->GetValue();
if (DeviceBoolean("Running", false, &value, runningData)) {
model->SetRunning(value);
}
}
// closed loop enabled
if (auto enabledData = model->GetEnabledData()) {
int value = enabledData->GetValue() ? 1 : 0;
static const char* enabledOptions[] = {"disabled", "enabled"};
if (DeviceEnum("Closed Loop", true, &value, enabledOptions, 2,
enabledData)) {
model->SetEnabled(value != 0);
}
}
// pressure switch
if (auto pressureSwitchData = model->GetPressureSwitchData()) {
int value = pressureSwitchData->GetValue() ? 1 : 0;
static const char* switchOptions[] = {"full", "low"};
if (DeviceEnum("Pressure", false, &value, switchOptions, 2,
pressureSwitchData)) {
model->SetPressureSwitch(value != 0);
}
}
// compressor current
if (auto currentData = model->GetCurrentData()) {
double value = currentData->GetValue();
if (DeviceDouble("Current (A)", false, &value, currentData)) {
model->SetCurrent(value);
}
}
EndDevice();
}
}
void glass::DisplayCompressorsDevice(PCMsModel* model, bool outputsEnabled) {
model->ForEachPCM([&](PCMModel& pcm, int i) {
DisplayCompressorDevice(&pcm, i, outputsEnabled);
});
}

View File

@@ -0,0 +1,92 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/PDP.h"
#include <algorithm>
#include <cstdio>
#include <imgui.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
#include "glass/support/IniSaverInfo.h"
using namespace glass;
static float DisplayChannel(PDPModel& pdp, int channel) {
float width = 0;
if (auto currentData = pdp.GetCurrentData(channel)) {
ImGui::PushID(channel);
auto& leftInfo = currentData->GetNameInfo();
char name[64];
leftInfo.GetLabel(name, sizeof(name), "", channel);
double val = currentData->GetValue();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
if (currentData->InputDouble(name, &val, 0, 0, "%.3f"))
pdp.SetCurrent(channel, val);
width = ImGui::GetItemRectSize().x;
leftInfo.PopupEditName(channel);
ImGui::PopID();
}
return width;
}
void glass::DisplayPDP(PDPModel* model, int index) {
char name[128];
std::snprintf(name, sizeof(name), "PDP[%d]", index);
if (CollapsingHeader(name)) {
// temperature
if (auto tempData = model->GetTemperatureData()) {
double value = tempData->GetValue();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
if (tempData->InputDouble("Temp", &value, 0, 0, "%.3f")) {
model->SetTemperature(value);
}
}
// voltage
if (auto voltageData = model->GetVoltageData()) {
double value = voltageData->GetValue();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
if (voltageData->InputDouble("Voltage", &value, 0, 0, "%.3f")) {
model->SetVoltage(value);
}
}
// channel currents; show as two columns laid out like PDP
const int numChannels = model->GetNumChannels();
ImGui::Text("Channel Current (A)");
ImGui::Columns(2, "channels", false);
float maxWidth = ImGui::GetFontSize() * 13;
for (int left = 0, right = numChannels - 1; left < right; ++left, --right) {
float leftWidth = DisplayChannel(*model, left);
ImGui::NextColumn();
float rightWidth = DisplayChannel(*model, right);
ImGui::NextColumn();
float width =
(std::max)(leftWidth, rightWidth) * 2 + ImGui::GetFontSize() * 4;
if (width > maxWidth) maxWidth = width;
}
ImGui::Columns(1);
ImGui::Dummy(ImVec2(maxWidth, 0));
}
}
void glass::DisplayPDPs(PDPsModel* model, wpi::StringRef noneMsg) {
bool hasAny = false;
model->ForEachPDP([&](PDPModel& pdp, int i) {
hasAny = true;
PushID(i);
DisplayPDP(&pdp, i);
PopID();
});
if (!hasAny && !noneMsg.empty())
ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end());
}

View File

@@ -0,0 +1,62 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/PWM.h"
#include <imgui.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
using namespace glass;
void glass::DisplayPWM(PWMModel* model, int index, bool outputsEnabled) {
auto data = model->GetSpeedData();
if (!data) return;
// build label
std::string* name = GetStorage().GetStringRef("name");
char label[128];
if (!name->empty()) {
std::snprintf(label, sizeof(label), "%s [%d]###name", name->c_str(), index);
} else {
std::snprintf(label, sizeof(label), "PWM[%d]###name", index);
}
int led = model->GetAddressableLED();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
if (led >= 0) {
ImGui::LabelText(label, "LED[%d]", led);
} else {
float val = outputsEnabled ? data->GetValue() : 0;
data->LabelText(label, "%0.3f", val);
}
if (PopupEditName("name", name)) {
data->SetName(name->c_str());
}
}
void glass::DisplayPWMs(PWMsModel* model, bool outputsEnabled,
wpi::StringRef noneMsg) {
bool hasAny = false;
bool first = true;
model->ForEachPWM([&](PWMModel& pwm, int i) {
hasAny = true;
PushID(i);
if (!first)
ImGui::Separator();
else
first = false;
DisplayPWM(&pwm, i, outputsEnabled);
PopID();
});
if (!hasAny && !noneMsg.empty())
ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end());
}

View File

@@ -0,0 +1,74 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/Relay.h"
#include <imgui.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
#include "glass/support/ExtraGuiWidgets.h"
using namespace glass;
void glass::DisplayRelay(RelayModel* model, int index, bool outputsEnabled) {
auto forwardData = model->GetForwardData();
auto reverseData = model->GetReverseData();
if (!forwardData && !reverseData) {
return;
}
bool forward = false;
bool reverse = false;
if (outputsEnabled) {
if (forwardData) forward = forwardData->GetValue();
if (reverseData) reverse = reverseData->GetValue();
}
std::string* name = GetStorage().GetStringRef("name");
ImGui::PushID("name");
if (!name->empty())
ImGui::Text("%s [%d]", name->c_str(), index);
else
ImGui::Text("Relay[%d]", index);
ImGui::PopID();
if (PopupEditName("name", name)) {
if (forwardData) forwardData->SetName(name->c_str());
if (reverseData) reverseData->SetName(name->c_str());
}
ImGui::SameLine();
// show forward and reverse as LED indicators
static const ImU32 colors[] = {IM_COL32(255, 255, 102, 255),
IM_COL32(255, 0, 0, 255),
IM_COL32(128, 128, 128, 255)};
int values[2] = {reverseData ? (reverse ? 2 : -2) : -3,
forwardData ? (forward ? 1 : -1) : -3};
DataSource* sources[2] = {reverseData, forwardData};
DrawLEDSources(values, sources, 2, 2, colors);
}
void glass::DisplayRelays(RelaysModel* model, bool outputsEnabled,
wpi::StringRef noneMsg) {
bool hasAny = false;
bool first = true;
model->ForEachRelay([&](RelayModel& relay, int i) {
hasAny = true;
if (!first)
ImGui::Separator();
else
first = false;
PushID(i);
DisplayRelay(&relay, i, outputsEnabled);
PopID();
});
if (!hasAny && !noneMsg.empty())
ImGui::TextUnformatted(noneMsg.begin(), noneMsg.end());
}

View File

@@ -0,0 +1,87 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/hardware/RoboRio.h"
#include <imgui.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
using namespace glass;
static void DisplayRail(RoboRioRailModel& rail, const char* name) {
if (CollapsingHeader(name)) {
ImGui::PushID(name);
if (auto data = rail.GetVoltageData()) {
double val = data->GetValue();
if (data->InputDouble("Voltage (V)", &val)) {
rail.SetVoltage(val);
}
}
if (auto data = rail.GetCurrentData()) {
double val = data->GetValue();
if (data->InputDouble("Current (A)", &val)) {
rail.SetCurrent(val);
}
}
if (auto data = rail.GetActiveData()) {
static const char* options[] = {"inactive", "active"};
int val = data->GetValue() ? 1 : 0;
if (data->Combo("Active", &val, options, 2)) {
rail.SetActive(val);
}
}
if (auto data = rail.GetFaultsData()) {
int val = data->GetValue();
if (data->InputInt("Faults", &val)) {
rail.SetFaults(val);
}
}
ImGui::PopID();
}
}
void glass::DisplayRoboRio(RoboRioModel* model) {
ImGui::Button("User Button");
model->SetUserButton(ImGui::IsItemActive());
ImGui::PushItemWidth(ImGui::GetFontSize() * 8);
if (CollapsingHeader("RoboRIO Input")) {
ImGui::PushID("RoboRIO Input");
if (auto data = model->GetVInVoltageData()) {
double val = data->GetValue();
if (data->InputDouble("Voltage (V)", &val)) {
model->SetVInVoltage(val);
}
}
if (auto data = model->GetVInCurrentData()) {
double val = data->GetValue();
if (data->InputDouble("Current (A)", &val)) {
model->SetVInCurrent(val);
}
}
ImGui::PopID();
}
if (auto rail = model->GetUser6VRail()) {
DisplayRail(*rail, "6V Rail");
}
if (auto rail = model->GetUser5VRail()) {
DisplayRail(*rail, "5V Rail");
}
if (auto rail = model->GetUser3V3Rail()) {
DisplayRail(*rail, "3.3V Rail");
}
ImGui::PopItemWidth();
}

View File

@@ -0,0 +1,161 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/other/DeviceTree.h"
#include <cinttypes>
#include <imgui.h>
#include "glass/Context.h"
#include "glass/ContextInternal.h"
#include "glass/DataSource.h"
using namespace glass;
void DeviceTreeModel::Update() {
for (auto&& display : m_displays) {
if (display.first) display.first->Update();
}
}
bool DeviceTreeModel::Exists() {
for (auto&& display : m_displays) {
if (display.first && display.first->Exists()) return true;
}
return false;
}
void DeviceTreeModel::Display() {
for (auto&& display : m_displays) {
if (display.second) display.second(display.first);
}
}
void glass::HideDevice(const char* id) { gContext->deviceHidden[id] = true; }
bool glass::BeginDevice(const char* id, ImGuiTreeNodeFlags flags) {
if (gContext->deviceHidden[id]) return false;
PushID(id);
// build label
std::string* name = GetStorage().GetStringRef("name");
char label[128];
std::snprintf(label, sizeof(label), "%s###name",
name->empty() ? id : name->c_str());
bool open = CollapsingHeader(label, flags);
PopupEditName("name", name);
if (!open) PopID();
return open;
}
void glass::EndDevice() { PopID(); }
static bool DeviceBooleanImpl(const char* name, bool readonly, bool* value) {
if (readonly) {
ImGui::LabelText(name, "%s", *value ? "true" : "false");
} else {
static const char* boolOptions[] = {"false", "true"};
int val = *value ? 1 : 0;
if (ImGui::Combo(name, &val, boolOptions, 2)) {
*value = val;
return true;
}
}
return false;
}
static bool DeviceDoubleImpl(const char* name, bool readonly, double* value) {
if (readonly) {
ImGui::LabelText(name, "%.6f", *value);
return false;
} else {
return ImGui::InputDouble(name, value, 0, 0, "%.6f",
ImGuiInputTextFlags_EnterReturnsTrue);
}
}
static bool DeviceEnumImpl(const char* name, bool readonly, int* value,
const char** options, int32_t numOptions) {
if (readonly) {
if (*value < 0 || *value >= numOptions)
ImGui::LabelText(name, "%d (unknown)", *value);
else
ImGui::LabelText(name, "%s", options[*value]);
return false;
} else {
return ImGui::Combo(name, value, options, numOptions);
}
}
static bool DeviceIntImpl(const char* name, bool readonly, int32_t* value) {
if (readonly) {
ImGui::LabelText(name, "%" PRId32, *value);
return false;
} else {
return ImGui::InputScalar(name, ImGuiDataType_S32, value, nullptr, nullptr,
nullptr, ImGuiInputTextFlags_EnterReturnsTrue);
}
}
static bool DeviceLongImpl(const char* name, bool readonly, int64_t* value) {
if (readonly) {
ImGui::LabelText(name, "%" PRId64, *value);
return false;
} else {
return ImGui::InputScalar(name, ImGuiDataType_S64, value, nullptr, nullptr,
nullptr, ImGuiInputTextFlags_EnterReturnsTrue);
}
}
template <typename F, typename... Args>
static inline bool DeviceValueImpl(const char* name, bool readonly,
const DataSource* source, F&& func,
Args... args) {
ImGui::SetNextItemWidth(ImGui::GetWindowWidth() * 0.5f);
if (!source) {
return func(name, readonly, args...);
} else {
ImGui::PushID(name);
bool rv = func("", readonly, args...);
ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x);
ImGui::Selectable(name);
source->EmitDrag();
ImGui::PopID();
return rv;
}
}
bool glass::DeviceBoolean(const char* name, bool readonly, bool* value,
const DataSource* source) {
return DeviceValueImpl(name, readonly, source, DeviceBooleanImpl, value);
}
bool glass::DeviceDouble(const char* name, bool readonly, double* value,
const DataSource* source) {
return DeviceValueImpl(name, readonly, source, DeviceDoubleImpl, value);
}
bool glass::DeviceEnum(const char* name, bool readonly, int* value,
const char** options, int32_t numOptions,
const DataSource* source) {
return DeviceValueImpl(name, readonly, source, DeviceEnumImpl, value, options,
numOptions);
}
bool glass::DeviceInt(const char* name, bool readonly, int32_t* value,
const DataSource* source) {
return DeviceValueImpl(name, readonly, source, DeviceIntImpl, value);
}
bool glass::DeviceLong(const char* name, bool readonly, int64_t* value,
const DataSource* source) {
return DeviceValueImpl(name, readonly, source, DeviceLongImpl, value);
}

View File

@@ -0,0 +1,140 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/other/FMS.h"
#include <imgui.h>
#include <wpi/SmallString.h>
#include "glass/DataSource.h"
using namespace glass;
static const char* stations[] = {"Red 1", "Red 2", "Red 3",
"Blue 1", "Blue 2", "Blue 3"};
void glass::DisplayFMS(FMSModel* model, bool* matchTimeEnabled) {
if (!model->Exists() || model->IsReadOnly()) return DisplayFMSReadOnly(model);
// FMS Attached
if (auto data = model->GetFmsAttachedData()) {
bool val = data->GetValue();
if (ImGui::Checkbox("FMS Attached", &val)) model->SetFmsAttached(val);
data->EmitDrag();
}
// DS Attached
if (auto data = model->GetDsAttachedData()) {
bool val = data->GetValue();
if (ImGui::Checkbox("DS Attached", &val)) model->SetDsAttached(val);
data->EmitDrag();
}
// Alliance Station
if (auto data = model->GetAllianceStationIdData()) {
int val = data->GetValue();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
if (ImGui::Combo("Alliance Station", &val, stations, 6))
model->SetAllianceStationId(val);
data->EmitDrag();
}
// Match Time
if (auto data = model->GetMatchTimeData()) {
if (matchTimeEnabled)
ImGui::Checkbox("Match Time Enabled", matchTimeEnabled);
double val = data->GetValue();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
if (ImGui::InputDouble("Match Time", &val, 0, 0, "%.1f",
ImGuiInputTextFlags_EnterReturnsTrue)) {
model->SetMatchTime(val);
}
data->EmitDrag();
ImGui::SameLine();
if (ImGui::Button("Reset")) {
model->SetMatchTime(0.0);
}
}
// Game Specific Message
// make buffer full 64 width, null terminated, for editability
wpi::SmallString<64> gameSpecificMessage;
model->GetGameSpecificMessage(gameSpecificMessage);
gameSpecificMessage.resize(63);
gameSpecificMessage.push_back('\0');
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
if (ImGui::InputText("Game Specific", gameSpecificMessage.data(),
gameSpecificMessage.size(),
ImGuiInputTextFlags_EnterReturnsTrue)) {
model->SetGameSpecificMessage(gameSpecificMessage.data());
}
}
void glass::DisplayFMSReadOnly(FMSModel* model) {
bool exists = model->Exists();
if (!exists) ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(96, 96, 96, 255));
if (auto data = model->GetEStopData()) {
ImGui::Selectable("E-Stopped: ");
data->EmitDrag();
ImGui::SameLine();
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
}
if (auto data = model->GetEnabledData()) {
ImGui::Selectable("Robot Enabled: ");
data->EmitDrag();
ImGui::SameLine();
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
}
if (auto data = model->GetTestData()) {
ImGui::Selectable("Test Mode: ");
data->EmitDrag();
ImGui::SameLine();
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
}
if (auto data = model->GetAutonomousData()) {
ImGui::Selectable("Autonomous Mode: ");
data->EmitDrag();
ImGui::SameLine();
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
}
if (auto data = model->GetFmsAttachedData()) {
ImGui::Selectable("FMS Attached: ");
data->EmitDrag();
ImGui::SameLine();
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
}
if (auto data = model->GetDsAttachedData()) {
ImGui::Selectable("DS Attached: ");
data->EmitDrag();
ImGui::SameLine();
ImGui::TextUnformatted(exists ? (data->GetValue() ? "Yes" : "No") : "?");
}
if (auto data = model->GetAllianceStationIdData()) {
ImGui::Selectable("Alliance Station: ");
data->EmitDrag();
ImGui::SameLine();
ImGui::TextUnformatted(exists ? stations[static_cast<int>(data->GetValue())]
: "?");
}
if (auto data = model->GetMatchTimeData()) {
ImGui::Selectable("Match Time: ");
data->EmitDrag();
ImGui::SameLine();
if (exists)
ImGui::Text("%.1f", data->GetValue());
else
ImGui::TextUnformatted("?");
}
wpi::SmallString<64> gameSpecificMessage;
model->GetGameSpecificMessage(gameSpecificMessage);
ImGui::Text("Game Specific: %s", exists ? gameSpecificMessage.c_str() : "?");
if (!exists) ImGui::PopStyleColor();
}

View File

@@ -0,0 +1,617 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/other/Field2D.h"
#include <cmath>
#include <memory>
#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui.h>
#include <imgui_internal.h>
#include <portable-file-dialogs.h>
#include <units/angle.h>
#include <units/length.h>
#include <wpi/Path.h>
#include <wpi/SmallString.h>
#include <wpi/StringMap.h>
#include <wpi/json.h>
#include <wpi/raw_istream.h>
#include <wpi/raw_ostream.h>
#include <wpigui.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
using namespace glass;
namespace gui = wpi::gui;
namespace {
// Per-frame field data (not persistent)
struct FieldFrameData {
// in screen coordinates
ImVec2 imageMin;
ImVec2 imageMax;
ImVec2 min;
ImVec2 max;
float scale; // scaling from field units to screen units
};
// Object drag state
struct ObjectDragState {
int object = 0;
int corner = 0;
ImVec2 initialOffset;
double initialAngle = 0;
};
// Per-frame object data (not persistent)
class ObjectFrameData {
public:
explicit ObjectFrameData(FieldObjectModel& model, const FieldFrameData& ffd,
float width, float length);
void SetPosition(double x, double y);
// set and get rotation in radians
void SetRotation(double rot);
double GetRotation() const {
return units::convert<units::degrees, units::radians>(m_rot);
}
void UpdateFrameData();
int IsHovered(const ImVec2& cursor) const;
bool HandleDrag(const ImVec2& cursor, int hitCorner, ObjectDragState* drag);
void Draw(ImDrawList* drawList, const gui::Texture& texture,
int hitCorner) const;
// in window coordinates
ImVec2 m_center;
ImVec2 m_corners[4];
ImVec2 m_arrow[3];
private:
FieldObjectModel& m_model;
const FieldFrameData& m_ffd;
// scaled width/2 and length/2, in screen units
float m_width2;
float m_length2;
float m_hitRadius;
double m_x = 0;
double m_y = 0;
double m_rot = 0;
};
class ObjectGroupInfo {
public:
static constexpr float kDefaultWidth = 0.6858f;
static constexpr float kDefaultLength = 0.8204f;
ObjectGroupInfo();
std::unique_ptr<pfd::open_file> m_fileOpener;
float* m_pWidth;
float* m_pLength;
ObjectDragState m_dragState;
void Reset();
void LoadImage();
const gui::Texture& GetTexture() const { return m_texture; }
private:
bool LoadImageImpl(const char* fn);
std::string* m_pFilename;
gui::Texture m_texture;
};
class FieldInfo {
public:
static constexpr float kDefaultWidth = 15.98f;
static constexpr float kDefaultHeight = 8.21f;
FieldInfo();
std::unique_ptr<pfd::open_file> m_fileOpener;
float* m_pWidth;
float* m_pHeight;
void Reset();
void LoadImage();
void LoadJson(const wpi::Twine& jsonfile);
FieldFrameData GetFrameData(ImVec2 min, ImVec2 max) const;
void Draw(ImDrawList* drawList, const FieldFrameData& frameData) const;
wpi::StringMap<std::unique_ptr<ObjectGroupInfo>> m_objectGroups;
private:
bool LoadImageImpl(const char* fn);
std::string* m_pFilename;
gui::Texture m_texture;
int m_imageWidth;
int m_imageHeight;
int* m_pTop;
int* m_pLeft;
int* m_pBottom;
int* m_pRight;
};
} // namespace
FieldInfo::FieldInfo() {
auto& storage = GetStorage();
m_pFilename = storage.GetStringRef("image");
m_pTop = storage.GetIntRef("top", 0);
m_pLeft = storage.GetIntRef("left", 0);
m_pBottom = storage.GetIntRef("bottom", -1);
m_pRight = storage.GetIntRef("right", -1);
m_pWidth = storage.GetFloatRef("width", kDefaultWidth);
m_pHeight = storage.GetFloatRef("height", kDefaultHeight);
}
void FieldInfo::Reset() {
m_texture = gui::Texture{};
m_pFilename->clear();
m_imageWidth = 0;
m_imageHeight = 0;
*m_pTop = 0;
*m_pLeft = 0;
*m_pBottom = -1;
*m_pRight = -1;
}
void FieldInfo::LoadImage() {
if (m_fileOpener && m_fileOpener->ready(0)) {
auto result = m_fileOpener->result();
if (!result.empty()) {
if (wpi::StringRef(result[0]).endswith(".json")) {
LoadJson(result[0]);
} else {
LoadImageImpl(result[0].c_str());
*m_pTop = 0;
*m_pLeft = 0;
*m_pBottom = -1;
*m_pRight = -1;
}
}
m_fileOpener.reset();
}
if (!m_texture && !m_pFilename->empty()) {
if (!LoadImageImpl(m_pFilename->c_str())) m_pFilename->clear();
}
}
void FieldInfo::LoadJson(const wpi::Twine& jsonfile) {
std::error_code ec;
wpi::raw_fd_istream f(jsonfile, ec);
if (ec) {
wpi::errs() << "GUI: could not open field JSON file\n";
return;
}
// parse file
wpi::json j;
try {
j = wpi::json::parse(f);
} catch (const wpi::json::parse_error& e) {
wpi::errs() << "GUI: JSON: could not parse: " << e.what() << '\n';
}
// top level must be an object
if (!j.is_object()) {
wpi::errs() << "GUI: JSON: does not contain a top object\n";
return;
}
// image filename
std::string image;
try {
image = j.at("field-image").get<std::string>();
} catch (const wpi::json::exception& e) {
wpi::errs() << "GUI: JSON: could not read field-image: " << e.what()
<< '\n';
return;
}
// corners
int top, left, bottom, right;
try {
top = j.at("field-corners").at("top-left").at(1).get<int>();
left = j.at("field-corners").at("top-left").at(0).get<int>();
bottom = j.at("field-corners").at("bottom-right").at(1).get<int>();
right = j.at("field-corners").at("bottom-right").at(0).get<int>();
} catch (const wpi::json::exception& e) {
wpi::errs() << "GUI: JSON: could not read field-corners: " << e.what()
<< '\n';
return;
}
// size
float width;
float height;
try {
width = j.at("field-size").at(0).get<float>();
height = j.at("field-size").at(1).get<float>();
} catch (const wpi::json::exception& e) {
wpi::errs() << "GUI: JSON: could not read field-size: " << e.what() << '\n';
return;
}
// units for size
std::string unit;
try {
unit = j.at("field-unit").get<std::string>();
} catch (const wpi::json::exception& e) {
wpi::errs() << "GUI: JSON: could not read field-unit: " << e.what() << '\n';
return;
}
// convert size units to meters
if (unit == "foot" || unit == "feet") {
width = units::convert<units::feet, units::meters>(width);
height = units::convert<units::feet, units::meters>(height);
}
// the image filename is relative to the json file
wpi::SmallString<128> pathname;
jsonfile.toVector(pathname);
wpi::sys::path::remove_filename(pathname);
wpi::sys::path::append(pathname, image);
// load field image
if (!LoadImageImpl(pathname.c_str())) return;
// save to field info
*m_pFilename = pathname.str();
*m_pTop = top;
*m_pLeft = left;
*m_pBottom = bottom;
*m_pRight = right;
*m_pWidth = width;
*m_pHeight = height;
}
bool FieldInfo::LoadImageImpl(const char* fn) {
wpi::outs() << "GUI: loading field image '" << fn << "'\n";
auto texture = gui::Texture::CreateFromFile(fn);
if (!texture) {
wpi::errs() << "GUI: could not read field image\n";
return false;
}
m_texture = std::move(texture);
m_imageWidth = m_texture.GetWidth();
m_imageHeight = m_texture.GetHeight();
*m_pFilename = fn;
return true;
}
FieldFrameData FieldInfo::GetFrameData(ImVec2 min, ImVec2 max) const {
// fit the image into the window
if (m_texture && m_imageHeight != 0 && m_imageWidth != 0)
gui::MaxFit(&min, &max, m_imageWidth, m_imageHeight);
FieldFrameData ffd;
ffd.imageMin = min;
ffd.imageMax = max;
// size down the box by the image corners (if any)
if (*m_pBottom > 0 && *m_pRight > 0) {
min.x += *m_pLeft * (max.x - min.x) / m_imageWidth;
min.y += *m_pTop * (max.y - min.y) / m_imageHeight;
max.x -= (m_imageWidth - *m_pRight) * (max.x - min.x) / m_imageWidth;
max.y -= (m_imageHeight - *m_pBottom) * (max.y - min.y) / m_imageHeight;
}
// draw the field "active area" as a yellow boundary box
gui::MaxFit(&min, &max, *m_pWidth, *m_pHeight);
ffd.min = min;
ffd.max = max;
ffd.scale = (max.x - min.x) / *m_pWidth;
return ffd;
}
void FieldInfo::Draw(ImDrawList* drawList, const FieldFrameData& ffd) const {
if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) {
drawList->AddImage(m_texture, ffd.imageMin, ffd.imageMax);
}
// draw the field "active area" as a yellow boundary box
drawList->AddRect(ffd.min, ffd.max, IM_COL32(255, 255, 0, 255));
}
ObjectGroupInfo::ObjectGroupInfo() {
auto& storage = GetStorage();
m_pFilename = storage.GetStringRef("image");
m_pWidth = storage.GetFloatRef("width", kDefaultWidth);
m_pLength = storage.GetFloatRef("length", kDefaultLength);
}
void ObjectGroupInfo::Reset() {
m_texture = gui::Texture{};
m_pFilename->clear();
}
void ObjectGroupInfo::LoadImage() {
if (m_fileOpener && m_fileOpener->ready(0)) {
auto result = m_fileOpener->result();
if (!result.empty()) LoadImageImpl(result[0].c_str());
m_fileOpener.reset();
}
if (!m_texture && !m_pFilename->empty()) {
if (!LoadImageImpl(m_pFilename->c_str())) m_pFilename->clear();
}
}
bool ObjectGroupInfo::LoadImageImpl(const char* fn) {
wpi::outs() << "GUI: loading object image '" << fn << "'\n";
auto texture = gui::Texture::CreateFromFile(fn);
if (!texture) {
wpi::errs() << "GUI: could not read object image\n";
return false;
}
m_texture = std::move(texture);
*m_pFilename = fn;
return true;
}
ObjectFrameData::ObjectFrameData(FieldObjectModel& model,
const FieldFrameData& ffd, float width,
float length)
: m_model{model},
m_ffd{ffd},
m_width2(ffd.scale * width / 2),
m_length2(ffd.scale * length / 2),
m_hitRadius((std::min)(m_width2, m_length2) / 2) {
if (auto xData = model.GetXData()) m_x = xData->GetValue();
if (auto yData = model.GetYData()) m_y = yData->GetValue();
if (auto rotationData = model.GetRotationData())
m_rot = rotationData->GetValue();
UpdateFrameData();
}
void ObjectFrameData::SetPosition(double x, double y) {
m_x = x;
m_y = y;
m_model.SetPosition(x, y);
}
void ObjectFrameData::SetRotation(double rot) {
double rotDegrees = units::convert<units::radians, units::degrees>(rot);
// force to -180 to +180 range
rotDegrees = rotDegrees + std::ceil((-rotDegrees - 180) / 360) * 360;
m_rot = rotDegrees;
m_model.SetRotation(rotDegrees);
}
void ObjectFrameData::UpdateFrameData() {
// (0,0) origin is bottom left
ImVec2 center(m_ffd.min.x + m_ffd.scale * m_x,
m_ffd.max.y - m_ffd.scale * m_y);
// build rotated points around center
float length2 = m_length2;
float width2 = m_width2;
double rot = GetRotation();
float cos_a = std::cos(-rot);
float sin_a = std::sin(-rot);
m_corners[0] = center + ImRotate(ImVec2(-length2, -width2), cos_a, sin_a);
m_corners[1] = center + ImRotate(ImVec2(length2, -width2), cos_a, sin_a);
m_corners[2] = center + ImRotate(ImVec2(length2, width2), cos_a, sin_a);
m_corners[3] = center + ImRotate(ImVec2(-length2, width2), cos_a, sin_a);
m_arrow[0] =
center + ImRotate(ImVec2(-length2 / 2, -width2 / 2), cos_a, sin_a);
m_arrow[1] = center + ImRotate(ImVec2(length2 / 2, 0), cos_a, sin_a);
m_arrow[2] =
center + ImRotate(ImVec2(-length2 / 2, width2 / 2), cos_a, sin_a);
m_center = center;
}
int ObjectFrameData::IsHovered(const ImVec2& cursor) const {
// only allow initiation of dragging when invisible button is hovered;
// this prevents the window resize handles from simultaneously activating
// the drag functionality
if (!ImGui::IsItemHovered()) return 0;
float hitRadiusSquared = m_hitRadius * m_hitRadius;
// it's within the hit radius of the center?
if (gui::GetDistSquared(cursor, m_center) < hitRadiusSquared)
return 1;
else if (gui::GetDistSquared(cursor, m_corners[0]) < hitRadiusSquared)
return 2;
else if (gui::GetDistSquared(cursor, m_corners[1]) < hitRadiusSquared)
return 3;
else if (gui::GetDistSquared(cursor, m_corners[2]) < hitRadiusSquared)
return 4;
else if (gui::GetDistSquared(cursor, m_corners[3]) < hitRadiusSquared)
return 5;
else
return 0;
}
bool ObjectFrameData::HandleDrag(const ImVec2& cursor, int hitCorner,
ObjectDragState* drag) {
bool rv = false;
if (hitCorner > 0 && ImGui::IsMouseClicked(0)) {
if (hitCorner == 1) {
drag->corner = hitCorner;
drag->initialOffset = cursor - m_center;
} else {
drag->corner = hitCorner;
ImVec2 off = cursor - m_center;
drag->initialAngle = std::atan2(off.y, off.x) + GetRotation();
}
rv = true;
}
if (drag->corner > 0 && ImGui::IsMouseDown(0)) {
if (drag->corner == 1) {
ImVec2 newPos = cursor - drag->initialOffset;
SetPosition(
(std::clamp(newPos.x, m_ffd.min.x, m_ffd.max.x) - m_ffd.min.x) /
m_ffd.scale,
(m_ffd.max.y - std::clamp(newPos.y, m_ffd.min.y, m_ffd.max.y)) /
m_ffd.scale);
UpdateFrameData();
} else {
ImVec2 off = cursor - m_center;
SetRotation(drag->initialAngle - std::atan2(off.y, off.x));
}
} else {
drag->corner = 0;
}
return rv;
}
void ObjectFrameData::Draw(ImDrawList* drawList, const gui::Texture& texture,
int hitCorner) const {
if (texture) {
drawList->AddImageQuad(texture, m_corners[0], m_corners[1], m_corners[2],
m_corners[3]);
} else {
drawList->AddQuad(m_corners[0], m_corners[1], m_corners[2], m_corners[3],
IM_COL32(255, 0, 0, 255), 4.0);
drawList->AddTriangle(m_arrow[0], m_arrow[1], m_arrow[2],
IM_COL32(0, 255, 0, 255), 4.0);
}
if (hitCorner > 0) {
if (hitCorner == 1) {
drawList->AddCircle(m_center, m_hitRadius, IM_COL32(0, 255, 0, 255));
} else {
drawList->AddCircle(m_corners[hitCorner - 2], m_hitRadius,
IM_COL32(0, 255, 0, 255));
}
}
}
void glass::DisplayField2DSettings(Field2DModel* model) {
auto& storage = GetStorage();
auto field = storage.GetData<FieldInfo>();
if (!field) {
storage.SetData(std::make_shared<FieldInfo>());
field = storage.GetData<FieldInfo>();
}
ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
if (ImGui::CollapsingHeader("Field")) {
ImGui::PushID("Field");
if (ImGui::Button("Choose image...")) {
field->m_fileOpener = std::make_unique<pfd::open_file>(
"Choose field image", "",
std::vector<std::string>{"Image File",
"*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif "
"*.hdr *.pic *.ppm *.pgm",
"PathWeaver JSON File", "*.json"});
}
if (ImGui::Button("Reset image")) {
field->Reset();
}
ImGui::InputFloat("Field Width", field->m_pWidth);
ImGui::InputFloat("Field Height", field->m_pHeight);
// ImGui::InputInt("Field Top", field->m_pTop);
// ImGui::InputInt("Field Left", field->m_pLeft);
// ImGui::InputInt("Field Right", field->m_pRight);
// ImGui::InputInt("Field Bottom", field->m_pBottom);
ImGui::PopID();
}
model->ForEachFieldObjectGroup([&](auto& groupModel, auto name) {
if (!groupModel.Exists()) return;
PushID(name);
auto& objGroupRef = field->m_objectGroups[name];
if (!objGroupRef) objGroupRef = std::make_unique<ObjectGroupInfo>();
auto objGroup = objGroupRef.get();
wpi::SmallString<64> nameBuf = name;
if (ImGui::CollapsingHeader(nameBuf.c_str())) {
if (ImGui::Button("Choose image...")) {
objGroup->m_fileOpener = std::make_unique<pfd::open_file>(
"Choose object image", "",
std::vector<std::string>{
"Image File",
"*.jpg *.jpeg *.png *.bmp *.psd *.tga *.gif "
"*.hdr *.pic *.ppm *.pgm"});
}
if (ImGui::Button("Reset image")) {
objGroup->Reset();
}
ImGui::InputFloat("Width", objGroup->m_pWidth);
ImGui::InputFloat("Length", objGroup->m_pLength);
}
PopID();
});
ImGui::PopItemWidth();
}
void glass::DisplayField2D(Field2DModel* model, const ImVec2& contentSize) {
auto& storage = GetStorage();
auto field = storage.GetData<FieldInfo>();
if (!field) {
storage.SetData(std::make_shared<FieldInfo>());
field = storage.GetData<FieldInfo>();
}
ImVec2 windowPos = ImGui::GetWindowPos();
ImVec2 mousePos = ImGui::GetIO().MousePos;
// for dragging to work, there needs to be a button (otherwise the window is
// dragged)
if (contentSize.x <= 0 || contentSize.y <= 0) return;
ImVec2 cursorPos = windowPos + ImGui::GetCursorPos(); // screen coords
ImGui::InvisibleButton("field", contentSize);
// field
field->LoadImage();
FieldFrameData ffd = field->GetFrameData(cursorPos, cursorPos + contentSize);
auto drawList = ImGui::GetWindowDrawList();
field->Draw(drawList, ffd);
model->ForEachFieldObjectGroup([&](auto& groupModel, auto name) {
if (!groupModel.Exists()) return;
PushID(name);
auto& objGroupRef = field->m_objectGroups[name];
if (!objGroupRef) objGroupRef = std::make_unique<ObjectGroupInfo>();
auto objGroup = objGroupRef.get();
objGroup->LoadImage();
int i = 0;
groupModel.ForEachFieldObject([&](auto& objModel) {
++i;
ObjectFrameData ofd{objModel, ffd, *objGroup->m_pWidth,
*objGroup->m_pLength};
int hitCorner = 0;
if (objGroup->m_dragState.object == 0 ||
objGroup->m_dragState.object == i) {
hitCorner = ofd.IsHovered(mousePos);
if (ofd.HandleDrag(mousePos, hitCorner, &objGroup->m_dragState))
objGroup->m_dragState.object = i;
}
// draw
ofd.Draw(drawList, objGroup->GetTexture(), hitCorner);
});
PopID();
});
}
void Field2DView::Display() {
if (ImGui::BeginPopupContextItem()) {
DisplayField2DSettings(m_model);
ImGui::EndPopup();
}
DisplayField2D(m_model, ImGui::GetWindowContentRegionMax() -
ImGui::GetWindowContentRegionMin());
}

View File

@@ -0,0 +1,932 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/other/Plot.h"
#include <stdint.h>
#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_stdlib.h>
#include <implot.h>
#include <wpigui.h>
#include <wpi/Signal.h>
#include <wpi/SmallString.h>
#include <wpi/SmallVector.h>
#include <wpi/timestamp.h>
#include "glass/Context.h"
#include "glass/DataSource.h"
#include "glass/support/ExtraGuiWidgets.h"
using namespace glass;
namespace {
class PlotView;
struct PlotSeriesRef {
PlotView* view;
size_t plotIndex;
size_t seriesIndex;
};
class PlotSeries {
public:
explicit PlotSeries(wpi::StringRef id);
explicit PlotSeries(DataSource* source, int yAxis = 0);
const std::string& GetId() const { return m_id; }
void CheckSource();
void SetSource(DataSource* source);
DataSource* GetSource() const { return m_source; }
void Clear() { m_size = 0; }
bool ReadIni(wpi::StringRef name, wpi::StringRef value);
void WriteIni(ImGuiTextBuffer* out);
enum Action { kNone, kMoveUp, kMoveDown, kDelete };
Action EmitPlot(PlotView& view, double now, size_t i, size_t plotIndex);
void EmitSettings(size_t i);
void EmitDragDropPayload(PlotView& view, size_t i, size_t plotIndex);
const char* GetName() const;
int GetYAxis() const { return m_yAxis; }
void SetYAxis(int yAxis) { m_yAxis = yAxis; }
private:
bool IsDigital() const {
return m_digital == kDigital ||
(m_digital == kAuto && m_source && m_source->IsDigital());
}
void AppendValue(double value, uint64_t time);
// source linkage
DataSource* m_source = nullptr;
wpi::sig::ScopedConnection m_sourceCreatedConn;
wpi::sig::ScopedConnection m_newValueConn;
std::string m_id;
// user settings
std::string m_name;
int m_yAxis = 0;
ImVec4 m_color = IMPLOT_AUTO_COL;
int m_marker = 0;
float m_weight = IMPLOT_AUTO;
enum Digital { kAuto, kDigital, kAnalog };
int m_digital = 0;
int m_digitalBitHeight = 8;
int m_digitalBitGap = 4;
// value storage
static constexpr int kMaxSize = 2000;
static constexpr double kTimeGap = 0.05;
std::atomic<int> m_size = 0;
std::atomic<int> m_offset = 0;
ImPlotPoint m_data[kMaxSize];
};
class Plot {
public:
Plot();
bool ReadIni(wpi::StringRef name, wpi::StringRef value);
void WriteIni(ImGuiTextBuffer* out);
void Clear();
void DragDropTarget(PlotView& view, size_t i, bool inPlot);
void EmitPlot(PlotView& view, double now, bool paused, size_t i);
void EmitSettings(size_t i);
const std::string& GetName() const { return m_name; }
std::vector<std::unique_ptr<PlotSeries>> m_series;
private:
void EmitSettingsLimits(int axis);
std::string m_name;
bool m_visible = true;
bool m_showPause = true;
unsigned int m_plotFlags = ImPlotFlags_Default;
bool m_lockPrevX = false;
bool m_paused = false;
float m_viewTime = 10;
int m_height = 300;
struct PlotRange {
double min = 0;
double max = 1;
bool lockMin = false;
bool lockMax = false;
bool apply = false;
};
PlotRange m_axisRange[3];
ImPlotRange m_xaxisRange; // read from plot, used for lockPrevX
};
class PlotView : public View {
public:
explicit PlotView(PlotProvider* provider) : m_provider{provider} {}
void Clear();
void Display() override;
void MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex);
void MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
size_t fromSeriesIndex, size_t toPlotIndex,
size_t toSeriesIndex, int yAxis = -1);
PlotProvider* m_provider;
std::vector<std::unique_ptr<Plot>> m_plots;
};
} // namespace
PlotSeries::PlotSeries(wpi::StringRef id) : m_id(id) {
if (DataSource* source = DataSource::Find(id)) {
SetSource(source);
return;
}
CheckSource();
}
PlotSeries::PlotSeries(DataSource* source, int yAxis) : m_yAxis(yAxis) {
SetSource(source);
}
void PlotSeries::CheckSource() {
if (!m_newValueConn.connected() && !m_sourceCreatedConn.connected()) {
m_source = nullptr;
m_sourceCreatedConn = DataSource::sourceCreated.connect_connection(
[this](const char* id, DataSource* source) {
if (m_id == id) {
SetSource(source);
m_sourceCreatedConn.disconnect();
}
});
}
}
void PlotSeries::SetSource(DataSource* source) {
m_source = source;
m_id = source->GetId();
// add initial value
m_data[m_size++] = ImPlotPoint{wpi::Now() * 1.0e-6, source->GetValue()};
m_newValueConn = source->valueChanged.connect_connection(
[this](double value, uint64_t time) { AppendValue(value, time); });
}
void PlotSeries::AppendValue(double value, uint64_t timeUs) {
double time = (timeUs != 0 ? timeUs : wpi::Now()) * 1.0e-6;
if (IsDigital()) {
if (m_size < kMaxSize) {
m_data[m_size] = ImPlotPoint{time, value};
++m_size;
} else {
m_data[m_offset] = ImPlotPoint{time, value};
m_offset = (m_offset + 1) % kMaxSize;
}
} else {
// as an analog graph draws linear lines in between each value,
// insert duplicate value if "long" time between updates so it
// looks appropriately flat
if (m_size < kMaxSize) {
if (m_size > 0) {
if ((time - m_data[m_size - 1].x) > kTimeGap) {
m_data[m_size] = ImPlotPoint{time, m_data[m_size - 1].y};
++m_size;
}
}
m_data[m_size] = ImPlotPoint{time, value};
++m_size;
} else {
if (m_offset == 0) {
if ((time - m_data[kMaxSize - 1].x) > kTimeGap) {
m_data[m_offset] = ImPlotPoint{time, m_data[kMaxSize - 1].y};
++m_offset;
}
} else {
if ((time - m_data[m_offset - 1].x) > kTimeGap) {
m_data[m_offset] = ImPlotPoint{time, m_data[m_offset - 1].y};
m_offset = (m_offset + 1) % kMaxSize;
}
}
m_data[m_offset] = ImPlotPoint{time, value};
m_offset = (m_offset + 1) % kMaxSize;
}
}
}
bool PlotSeries::ReadIni(wpi::StringRef name, wpi::StringRef value) {
if (name == "name") {
m_name = value;
return true;
}
if (name == "yAxis") {
int num;
if (value.getAsInteger(10, num)) return true;
m_yAxis = num;
return true;
} else if (name == "color") {
unsigned int num;
if (value.getAsInteger(10, num)) return true;
m_color = ImColor(num);
return true;
} else if (name == "marker") {
int num;
if (value.getAsInteger(10, num)) return true;
m_marker = num;
return true;
} else if (name == "weight") {
std::sscanf(value.data(), "%f", &m_weight);
return true;
} else if (name == "digital") {
int num;
if (value.getAsInteger(10, num)) return true;
m_digital = num;
return true;
} else if (name == "digitalBitHeight") {
int num;
if (value.getAsInteger(10, num)) return true;
m_digitalBitHeight = num;
return true;
} else if (name == "digitalBitGap") {
int num;
if (value.getAsInteger(10, num)) return true;
m_digitalBitGap = num;
return true;
}
return false;
}
void PlotSeries::WriteIni(ImGuiTextBuffer* out) {
out->appendf(
"name=%s\nyAxis=%d\ncolor=%u\nmarker=%d\nweight=%f\ndigital=%d\n"
"digitalBitHeight=%d\ndigitalBitGap=%d\n",
m_name.c_str(), m_yAxis, static_cast<ImU32>(ImColor(m_color)), m_marker,
m_weight, m_digital, m_digitalBitHeight, m_digitalBitGap);
}
const char* PlotSeries::GetName() const {
if (!m_name.empty()) return m_name.c_str();
if (m_newValueConn.connected()) {
auto sourceName = m_source->GetName();
if (sourceName[0] != '\0') return sourceName;
}
return m_id.c_str();
}
PlotSeries::Action PlotSeries::EmitPlot(PlotView& view, double now, size_t i,
size_t plotIndex) {
CheckSource();
char label[128];
std::snprintf(label, sizeof(label), "%s###name", GetName());
int size = m_size;
int offset = m_offset;
// need to have last value at current time, so need to create fake last value
// we handle the offset logic ourselves to avoid wrap issues with size + 1
struct GetterData {
double now;
ImPlotPoint* data;
int size;
int offset;
};
GetterData getterData = {now, m_data, size, offset};
auto getter = [](void* data, int idx) {
auto d = static_cast<GetterData*>(data);
if (idx == d->size)
return ImPlotPoint{
d->now, d->data[d->offset == 0 ? d->size - 1 : d->offset - 1].y};
if (d->offset + idx < d->size)
return d->data[d->offset + idx];
else
return d->data[d->offset + idx - d->size];
};
if (m_color.w == IMPLOT_AUTO_COL.w) m_color = ImPlot::GetColormapColor(i);
ImPlot::SetNextLineStyle(m_color, m_weight);
if (IsDigital()) {
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight);
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap);
ImPlot::PlotDigital(label, getter, &getterData, size + 1);
ImPlot::PopStyleVar();
ImPlot::PopStyleVar();
} else {
ImPlot::SetPlotYAxis(m_yAxis);
ImPlot::SetNextMarkerStyle(m_marker - 1);
ImPlot::PlotLine(label, getter, &getterData, size + 1);
}
// DND source for PlotSeries
if (ImPlot::BeginLegendDragDropSource(label)) {
EmitDragDropPayload(view, i, plotIndex);
ImPlot::EndLegendDragDropSource();
}
// Edit settings via popup
Action rv = kNone;
if (ImPlot::BeginLegendPopup(label)) {
if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
ImGui::Text("Edit series name:");
ImGui::InputText("##editname", &m_name);
if (ImGui::Button("Move Up")) {
ImGui::CloseCurrentPopup();
rv = kMoveUp;
}
ImGui::SameLine();
if (ImGui::Button("Move Down")) {
ImGui::CloseCurrentPopup();
rv = kMoveDown;
}
ImGui::SameLine();
if (ImGui::Button("Delete")) {
ImGui::CloseCurrentPopup();
rv = kDelete;
}
EmitSettings(i);
ImPlot::EndLegendPopup();
}
return rv;
}
void PlotSeries::EmitDragDropPayload(PlotView& view, size_t i,
size_t plotIndex) {
PlotSeriesRef ref = {&view, plotIndex, i};
ImGui::SetDragDropPayload("PlotSeries", &ref, sizeof(ref));
ImGui::TextUnformatted(GetName());
}
void PlotSeries::EmitSettings(size_t i) {
// Line color
{
ImGui::ColorEdit3("Color", &m_color.x, ImGuiColorEditFlags_NoInputs);
ImGui::SameLine();
if (ImGui::Button("Default")) m_color = ImPlot::GetColormapColor(i);
}
// Line weight
{
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
ImGui::InputFloat("Weight", &m_weight, 0.1f, 1.0f, "%.1f");
}
// Digital
{
static const char* const options[] = {"Auto", "Digital", "Analog"};
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
ImGui::Combo("Digital", &m_digital, options,
sizeof(options) / sizeof(options[0]));
}
if (IsDigital()) {
// Bit Height
{
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
ImGui::InputInt("Bit Height", &m_digitalBitHeight);
}
// Bit Gap
{
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
ImGui::InputInt("Bit Gap", &m_digitalBitGap);
}
} else {
// Y-axis
{
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
static const char* const options[] = {"1", "2", "3"};
ImGui::Combo("Y-Axis", &m_yAxis, options, 3);
}
// Marker
{
static const char* const options[] = {
"None", "Circle", "Square", "Diamond", "Up", "Down",
"Left", "Right", "Cross", "Plus", "Asterisk"};
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
ImGui::Combo("Marker", &m_marker, options,
sizeof(options) / sizeof(options[0]));
}
}
}
Plot::Plot() {
for (int i = 0; i < 3; ++i) {
m_axisRange[i] = PlotRange{};
}
}
bool Plot::ReadIni(wpi::StringRef name, wpi::StringRef value) {
if (name == "name") {
m_name = value;
return true;
} else if (name == "visible") {
int num;
if (value.getAsInteger(10, num)) return true;
m_visible = num != 0;
return true;
} else if (name == "showPause") {
int num;
if (value.getAsInteger(10, num)) return true;
m_showPause = num != 0;
return true;
} else if (name == "lockPrevX") {
int num;
if (value.getAsInteger(10, num)) return true;
m_lockPrevX = num != 0;
return true;
} else if (name == "legend") {
int num;
if (value.getAsInteger(10, num)) return true;
if (num == 0)
m_plotFlags &= ~ImPlotFlags_Legend;
else
m_plotFlags |= ImPlotFlags_Legend;
return true;
} else if (name == "yaxis2") {
int num;
if (value.getAsInteger(10, num)) return true;
if (num == 0)
m_plotFlags &= ~ImPlotFlags_YAxis2;
else
m_plotFlags |= ImPlotFlags_YAxis2;
return true;
} else if (name == "yaxis3") {
int num;
if (value.getAsInteger(10, num)) return true;
if (num == 0)
m_plotFlags &= ~ImPlotFlags_YAxis3;
else
m_plotFlags |= ImPlotFlags_YAxis3;
return true;
} else if (name == "viewTime") {
int num;
if (value.getAsInteger(10, num)) return true;
m_viewTime = num / 1000.0;
return true;
} else if (name == "height") {
int num;
if (value.getAsInteger(10, num)) return true;
m_height = num;
return true;
} else if (name.startswith("y")) {
auto [yAxisStr, yName] = name.split('_');
int yAxis;
if (yAxisStr.substr(1).getAsInteger(10, yAxis)) return false;
if (yAxis < 0 || yAxis > 3) return false;
if (yName == "min") {
int num;
if (value.getAsInteger(10, num)) return true;
m_axisRange[yAxis].min = num / 1000.0;
return true;
} else if (yName == "max") {
int num;
if (value.getAsInteger(10, num)) return true;
m_axisRange[yAxis].max = num / 1000.0;
return true;
} else if (yName == "lockMin") {
int num;
if (value.getAsInteger(10, num)) return true;
m_axisRange[yAxis].lockMin = num != 0;
return true;
} else if (yName == "lockMax") {
int num;
if (value.getAsInteger(10, num)) return true;
m_axisRange[yAxis].lockMax = num != 0;
return true;
}
}
return false;
}
void Plot::WriteIni(ImGuiTextBuffer* out) {
out->appendf(
"name=%s\nvisible=%d\nshowPause=%d\nlockPrevX=%d\nlegend=%d\n"
"yaxis2=%d\nyaxis3=%d\nviewTime=%d\nheight=%d\n",
m_name.c_str(), m_visible ? 1 : 0, m_showPause ? 1 : 0,
m_lockPrevX ? 1 : 0, (m_plotFlags & ImPlotFlags_Legend) ? 1 : 0,
(m_plotFlags & ImPlotFlags_YAxis2) ? 1 : 0,
(m_plotFlags & ImPlotFlags_YAxis3) ? 1 : 0,
static_cast<int>(m_viewTime * 1000), m_height);
for (int i = 0; i < 3; ++i) {
out->appendf("y%d_min=%d\ny%d_max=%d\ny%d_lockMin=%d\ny%d_lockMax=%d\n", i,
static_cast<int>(m_axisRange[i].min * 1000), i,
static_cast<int>(m_axisRange[i].max * 1000), i,
m_axisRange[i].lockMin ? 1 : 0, i,
m_axisRange[i].lockMax ? 1 : 0);
}
}
void Plot::Clear() {
for (auto&& series : m_series) series->Clear();
}
void Plot::DragDropTarget(PlotView& view, size_t i, bool inPlot) {
if (!ImGui::BeginDragDropTarget()) return;
// handle dragging onto a specific Y axis
int yAxis = -1;
if (inPlot) {
for (int y = 0; y < 3; ++y) {
if (ImPlot::IsPlotYAxisHovered(y)) {
yAxis = y;
break;
}
}
}
if (const ImGuiPayload* payload =
ImGui::AcceptDragDropPayload("DataSource")) {
auto source = *static_cast<DataSource**>(payload->Data);
// don't add duplicates unless it's onto a different Y axis
auto it =
std::find_if(m_series.begin(), m_series.end(), [=](const auto& elem) {
return elem->GetId() == source->GetId() &&
(yAxis == -1 || elem->GetYAxis() == yAxis);
});
if (it == m_series.end()) {
m_series.emplace_back(
std::make_unique<PlotSeries>(source, yAxis == -1 ? 0 : yAxis));
}
} else if (const ImGuiPayload* payload =
ImGui::AcceptDragDropPayload("PlotSeries")) {
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
view.MovePlotSeries(ref->view, ref->plotIndex, ref->seriesIndex, i,
m_series.size(), yAxis);
} else if (const ImGuiPayload* payload =
ImGui::AcceptDragDropPayload("Plot")) {
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
view.MovePlot(ref->view, ref->plotIndex, i);
}
}
void Plot::EmitPlot(PlotView& view, double now, bool paused, size_t i) {
if (!m_visible) return;
bool lockX = (i != 0 && m_lockPrevX);
if (!lockX && m_showPause && ImGui::Button(m_paused ? "Resume" : "Pause"))
m_paused = !m_paused;
char label[128];
std::snprintf(label, sizeof(label), "%s##plot", m_name.c_str());
if (lockX) {
ImPlot::SetNextPlotLimitsX(view.m_plots[i - 1]->m_xaxisRange.Min,
view.m_plots[i - 1]->m_xaxisRange.Max,
ImGuiCond_Always);
} else {
// also force-pause plots if overall timing is paused
ImPlot::SetNextPlotLimitsX(
now - m_viewTime, now,
(paused || m_paused) ? ImGuiCond_Once : ImGuiCond_Always);
}
ImPlotAxisFlags yFlags[3] = {ImPlotAxisFlags_Default,
ImPlotAxisFlags_Auxiliary,
ImPlotAxisFlags_Auxiliary};
for (int i = 0; i < 3; ++i) {
ImPlot::SetNextPlotLimitsY(
m_axisRange[i].min, m_axisRange[i].max,
m_axisRange[i].apply ? ImGuiCond_Always : ImGuiCond_Once, i);
m_axisRange[i].apply = false;
if (m_axisRange[i].lockMin) yFlags[i] |= ImPlotAxisFlags_LockMin;
if (m_axisRange[i].lockMax) yFlags[i] |= ImPlotAxisFlags_LockMax;
}
if (ImPlot::BeginPlot(label, nullptr, nullptr, ImVec2(-1, m_height),
m_plotFlags, ImPlotAxisFlags_Default, yFlags[0],
yFlags[1], yFlags[2])) {
for (size_t j = 0; j < m_series.size(); ++j) {
ImGui::PushID(j);
switch (m_series[j]->EmitPlot(view, now, j, i)) {
case PlotSeries::kMoveUp:
if (j > 0) std::swap(m_series[j - 1], m_series[j]);
break;
case PlotSeries::kMoveDown:
if (j < (m_series.size() - 1))
std::swap(m_series[j], m_series[j + 1]);
break;
case PlotSeries::kDelete:
m_series.erase(m_series.begin() + j);
break;
default:
break;
}
ImGui::PopID();
}
DragDropTarget(view, i, true);
m_xaxisRange = ImPlot::GetPlotLimits().X;
ImPlot::EndPlot();
}
}
void Plot::EmitSettingsLimits(int axis) {
ImGui::Indent();
ImGui::PushID(axis);
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
ImGui::InputDouble("Min", &m_axisRange[axis].min, 0, 0, "%.3f");
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
ImGui::InputDouble("Max", &m_axisRange[axis].max, 0, 0, "%.3f");
ImGui::SameLine();
if (ImGui::Button("Apply")) m_axisRange[axis].apply = true;
ImGui::TextUnformatted("Lock Axis");
ImGui::SameLine();
ImGui::Checkbox("Min##minlock", &m_axisRange[axis].lockMin);
ImGui::SameLine();
ImGui::Checkbox("Max##maxlock", &m_axisRange[axis].lockMax);
ImGui::PopID();
ImGui::Unindent();
}
void Plot::EmitSettings(size_t i) {
ImGui::Text("Edit plot name:");
ImGui::InputText("##editname", &m_name);
ImGui::Checkbox("Visible", &m_visible);
ImGui::Checkbox("Show Pause Button", &m_showPause);
ImGui::CheckboxFlags("Show Legend", &m_plotFlags, ImPlotFlags_Legend);
if (i != 0) ImGui::Checkbox("Lock X-axis to previous plot", &m_lockPrevX);
ImGui::TextUnformatted("Primary Y-Axis");
EmitSettingsLimits(0);
ImGui::CheckboxFlags("2nd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis2);
if ((m_plotFlags & ImPlotFlags_YAxis2) != 0) EmitSettingsLimits(1);
ImGui::CheckboxFlags("3rd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis3);
if ((m_plotFlags & ImPlotFlags_YAxis3) != 0) EmitSettingsLimits(2);
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
ImGui::InputFloat("View Time (s)", &m_viewTime, 0.1f, 1.0f, "%.1f");
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
if (ImGui::InputInt("Height", &m_height, 10)) {
if (m_height < 0) m_height = 0;
}
}
void PlotView::Clear() {
for (auto&& plot : m_plots) plot->Clear();
}
void PlotView::Display() {
if (ImGui::BeginPopupContextItem()) {
if (ImGui::Button("Add plot"))
m_plots.emplace_back(std::make_unique<Plot>());
for (size_t i = 0; i < m_plots.size(); ++i) {
auto& plot = m_plots[i];
ImGui::PushID(i);
char name[64];
if (!plot->GetName().empty())
std::snprintf(name, sizeof(name), "%s", plot->GetName().c_str());
else
std::snprintf(name, sizeof(name), "Plot %d", static_cast<int>(i));
char label[90];
std::snprintf(label, sizeof(label), "%s###header%d", name,
static_cast<int>(i));
bool open = ImGui::CollapsingHeader(label);
// DND source and target for Plot
if (ImGui::BeginDragDropSource()) {
PlotSeriesRef ref = {this, i, 0};
ImGui::SetDragDropPayload("Plot", &ref, sizeof(ref));
ImGui::TextUnformatted(name);
ImGui::EndDragDropSource();
}
plot->DragDropTarget(*this, i, false);
if (open) {
if (ImGui::Button("Move Up")) {
if (i > 0) std::swap(m_plots[i - 1], plot);
}
ImGui::SameLine();
if (ImGui::Button("Move Down")) {
if (i < (m_plots.size() - 1)) std::swap(plot, m_plots[i + 1]);
}
ImGui::SameLine();
if (ImGui::Button("Delete")) {
m_plots.erase(m_plots.begin() + i);
ImGui::PopID();
continue;
}
plot->EmitSettings(i);
}
ImGui::PopID();
}
ImGui::EndPopup();
}
if (m_plots.empty()) {
if (ImGui::Button("Add plot"))
m_plots.emplace_back(std::make_unique<Plot>());
// Make "add plot" button a DND target for Plot
if (!ImGui::BeginDragDropTarget()) return;
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("Plot")) {
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
MovePlot(ref->view, ref->plotIndex, 0);
}
}
double now = (wpi::Now() - m_provider->GetStartTime()) * 1.0e-6;
for (size_t i = 0; i < m_plots.size(); ++i) {
ImGui::PushID(i);
m_plots[i]->EmitPlot(*this, now, m_provider->IsPaused(), i);
ImGui::PopID();
}
}
void PlotView::MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex) {
if (fromView == this) {
if (fromIndex == toIndex) return;
auto val = std::move(m_plots[fromIndex]);
m_plots.insert(m_plots.begin() + toIndex, std::move(val));
m_plots.erase(m_plots.begin() + fromIndex + (fromIndex > toIndex ? 1 : 0));
} else {
auto val = std::move(fromView->m_plots[fromIndex]);
m_plots.insert(m_plots.begin() + toIndex, std::move(val));
fromView->m_plots.erase(fromView->m_plots.begin() + fromIndex);
}
}
void PlotView::MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
size_t fromSeriesIndex, size_t toPlotIndex,
size_t toSeriesIndex, int yAxis) {
if (fromView == this && fromPlotIndex == toPlotIndex) {
// need to handle this specially as the index of the old location changes
if (fromSeriesIndex != toSeriesIndex) {
auto& plotSeries = m_plots[fromPlotIndex]->m_series;
auto val = std::move(plotSeries[fromSeriesIndex]);
// only set Y-axis if actually set
if (yAxis != -1) val->SetYAxis(yAxis);
plotSeries.insert(plotSeries.begin() + toSeriesIndex, std::move(val));
plotSeries.erase(plotSeries.begin() + fromSeriesIndex +
(fromSeriesIndex > toSeriesIndex ? 1 : 0));
}
} else {
auto& fromPlot = *fromView->m_plots[fromPlotIndex];
auto& toPlot = *m_plots[toPlotIndex];
// always set Y-axis if moving plots
fromPlot.m_series[fromSeriesIndex]->SetYAxis(yAxis == -1 ? 0 : yAxis);
toPlot.m_series.insert(toPlot.m_series.begin() + toSeriesIndex,
std::move(fromPlot.m_series[fromSeriesIndex]));
fromPlot.m_series.erase(fromPlot.m_series.begin() + fromSeriesIndex);
}
}
PlotProvider::PlotProvider(const wpi::Twine& iniName)
: WindowManager{iniName + "Window"},
m_plotSaver{iniName, this, false},
m_seriesSaver{iniName + "Series", this, true} {}
PlotProvider::~PlotProvider() {}
void PlotProvider::GlobalInit() {
WindowManager::GlobalInit();
wpi::gui::AddInit([this] {
m_plotSaver.Initialize();
m_seriesSaver.Initialize();
});
}
void PlotProvider::ResetTime() {
m_startTime = wpi::Now();
for (auto&& window : m_windows) {
if (auto view = static_cast<PlotView*>(window->GetView())) {
view->Clear();
}
}
}
void PlotProvider::DisplayMenu() {
for (size_t i = 0; i < m_windows.size(); ++i) {
m_windows[i]->DisplayMenuItem();
// provide method to destroy the plot window
if (ImGui::BeginPopupContextItem()) {
if (ImGui::Selectable("Destroy Plot Window")) {
m_windows.erase(m_windows.begin() + i);
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
if (ImGui::MenuItem("New Plot Window")) {
char id[32];
std::snprintf(id, sizeof(id), "Plot <%d>",
static_cast<int>(m_windows.size()));
AddWindow(id, std::make_unique<PlotView>(this));
}
}
void PlotProvider::DisplayWindows() {
// create views if not already created
for (auto&& window : m_windows) {
if (!window->HasView()) window->SetView(std::make_unique<PlotView>(this));
}
WindowManager::DisplayWindows();
}
PlotProvider::IniSaver::IniSaver(const wpi::Twine& typeName,
PlotProvider* provider, bool forSeries)
: IniSaverBase{typeName}, m_provider{provider}, m_forSeries{forSeries} {}
void* PlotProvider::IniSaver::IniReadOpen(const char* name) {
auto [viewId, plotNumStr] = wpi::StringRef{name}.split('#');
wpi::StringRef seriesId;
if (m_forSeries) {
std::tie(plotNumStr, seriesId) = plotNumStr.split('#');
if (seriesId.empty()) return nullptr;
}
unsigned int plotNum;
if (plotNumStr.getAsInteger(10, plotNum)) return nullptr;
// get or create window
auto win = m_provider->GetOrAddWindow(viewId, true);
if (!win) return nullptr;
// get or create view
auto view = static_cast<PlotView*>(win->GetView());
if (!view) {
win->SetView(std::make_unique<PlotView>(m_provider));
view = static_cast<PlotView*>(win->GetView());
}
// get or create plot
if (view->m_plots.size() <= plotNum) view->m_plots.resize(plotNum + 1);
auto& plot = view->m_plots[plotNum];
if (!plot) plot = std::make_unique<Plot>();
// early exit for plot data
if (!m_forSeries) return plot.get();
// get or create series
return plot->m_series.emplace_back(std::make_unique<PlotSeries>(seriesId))
.get();
}
void PlotProvider::IniSaver::IniReadLine(void* entry, const char* lineStr) {
auto [name, value] = wpi::StringRef{lineStr}.split('=');
name = name.trim();
value = value.trim();
if (m_forSeries)
static_cast<PlotSeries*>(entry)->ReadIni(name, value);
else
static_cast<Plot*>(entry)->ReadIni(name, value);
}
void PlotProvider::IniSaver::IniWriteAll(ImGuiTextBuffer* out_buf) {
for (auto&& win : m_provider->m_windows) {
auto view = static_cast<PlotView*>(win->GetView());
auto id = win->GetId();
for (size_t i = 0; i < view->m_plots.size(); ++i) {
if (m_forSeries) {
// Loop over series
for (auto&& series : view->m_plots[i]->m_series) {
out_buf->appendf("[%s][%s#%d#%s]\n", GetTypeName(), id.data(),
static_cast<int>(i), series->GetId().c_str());
series->WriteIni(out_buf);
out_buf->append("\n");
}
} else {
// Just the plot
out_buf->appendf("[%s][%s#%d]\n", GetTypeName(), id.data(),
static_cast<int>(i));
view->m_plots[i]->WriteIni(out_buf);
out_buf->append("\n");
}
}
}
}

View File

@@ -0,0 +1,44 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/other/StringChooser.h"
#include <imgui.h>
using namespace glass;
void glass::DisplayStringChooser(StringChooserModel* model) {
auto& defaultValue = model->GetDefault();
auto& selected = model->GetSelected();
auto& active = model->GetActive();
auto& options = model->GetOptions();
const char* preview =
selected.empty() ? defaultValue.c_str() : selected.c_str();
const char* label;
if (active == preview) {
label = "GOOD##select";
} else {
label = "BAD ##select";
}
if (ImGui::BeginCombo(label, preview)) {
for (auto&& option : options) {
ImGui::PushID(option.c_str());
bool isSelected = (option == selected);
if (ImGui::Selectable(option.c_str(), isSelected)) {
model->SetSelected(option);
}
if (isSelected) ImGui::SetItemDefaultFocus();
ImGui::PopID();
}
ImGui::EndCombo();
}
ImGui::SameLine();
}

View File

@@ -0,0 +1,168 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2017-2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/support/ExtraGuiWidgets.h"
#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui_internal.h>
#include "glass/DataSource.h"
namespace glass {
void DrawLEDSources(const int* values, DataSource** sources, int numValues,
int cols, const ImU32* colors, float size, float spacing,
const LEDConfig& config) {
if (numValues == 0 || cols < 1) return;
if (size == 0) size = ImGui::GetFontSize() / 2.0;
if (spacing == 0) spacing = ImGui::GetFontSize() / 3.0;
int rows = (numValues + cols - 1) / cols;
float inc = size + spacing;
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImVec2 p = ImGui::GetCursorScreenPos();
float sized2 = size / 2;
float ystart, yinc;
if (config.start & 1) {
// lower
ystart = p.y + sized2 + inc * (rows - 1);
yinc = -inc;
} else {
// upper
ystart = p.y + sized2;
yinc = inc;
}
float xstart, xinc;
if (config.start & 2) {
// right
xstart = p.x + sized2 + inc * (cols - 1);
xinc = -inc;
} else {
// left
xstart = p.x + sized2;
xinc = inc;
}
float x = xstart, y = ystart;
int rowcol = 1; // row for row-major, column for column-major
for (int i = 0; i < numValues; ++i) {
if (config.order == LEDConfig::RowMajor) {
if (i >= (rowcol * cols)) {
++rowcol;
if (config.serpentine) {
x -= xinc;
xinc = -xinc;
} else {
x = xstart;
}
y += yinc;
}
} else {
if (i >= (rowcol * rows)) {
++rowcol;
if (config.serpentine) {
y -= yinc;
yinc = -yinc;
} else {
y = ystart;
}
x += xinc;
}
}
if (values[i] > 0)
drawList->AddRectFilled(ImVec2(x, y), ImVec2(x + size, y + size),
colors[values[i] - 1]);
else if (values[i] < 0)
drawList->AddRect(ImVec2(x, y), ImVec2(x + size, y + size),
colors[-values[i] - 1], 0.0f, 0, 1.0);
if (sources) {
ImGui::SetCursorScreenPos(ImVec2(x - sized2, y - sized2));
if (sources[i]) {
ImGui::PushID(i);
ImGui::Selectable("", false, 0, ImVec2(inc, inc));
sources[i]->EmitDrag();
ImGui::PopID();
} else {
ImGui::Dummy(ImVec2(inc, inc));
}
}
if (config.order == LEDConfig::RowMajor) {
x += xinc;
} else {
y += yinc;
}
}
if (!sources) ImGui::Dummy(ImVec2(inc * cols, inc * rows));
}
void DrawLEDs(const int* values, int numValues, int cols, const ImU32* colors,
float size, float spacing, const LEDConfig& config) {
DrawLEDSources(values, nullptr, numValues, cols, colors, size, spacing,
config);
}
bool DeleteButton(ImGuiID id, const ImVec2& pos) {
ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow;
// We intentionally allow interaction when clipped so that a mechanical
// Alt,Right,Validate sequence close a window. (this isn't the regular
// behavior of buttons, but it doesn't affect the user much because navigation
// tends to keep items visible).
const ImRect bb(
pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f);
bool is_clipped = !ImGui::ItemAdd(bb, id);
bool hovered, held;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held);
if (is_clipped) return pressed;
// Render
ImU32 col =
ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered);
ImVec2 center = bb.GetCenter();
if (hovered)
window->DrawList->AddCircleFilled(
center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), col, 12);
ImU32 cross_col = ImGui::GetColorU32(ImGuiCol_Text);
window->DrawList->AddCircle(center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f),
cross_col, 12);
float cross_extent = g.FontSize * 0.5f * 0.5f - 1.0f;
center -= ImVec2(0.5f, 0.5f);
window->DrawList->AddLine(center + ImVec2(+cross_extent, +cross_extent),
center + ImVec2(-cross_extent, -cross_extent),
cross_col, 1.0f);
window->DrawList->AddLine(center + ImVec2(+cross_extent, -cross_extent),
center + ImVec2(-cross_extent, +cross_extent),
cross_col, 1.0f);
return pressed;
}
bool HeaderDeleteButton(const char* label) {
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImGuiContext& g = *GImGui;
ImGuiItemHoveredDataBackup last_item_backup;
ImGuiID id = window->GetID(label);
float button_size = g.FontSize;
float button_x = ImMax(window->DC.LastItemRect.Min.x,
window->DC.LastItemRect.Max.x -
g.Style.FramePadding.x * 2.0f - button_size);
float button_y = window->DC.LastItemRect.Min.y;
bool rv = DeleteButton(
window->GetID(reinterpret_cast<void*>(static_cast<intptr_t>(id) + 1)),
ImVec2(button_x, button_y));
last_item_backup.Restore();
return rv;
}
} // namespace glass

View File

@@ -0,0 +1,64 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/support/IniSaverBase.h"
#include <imgui_internal.h>
using namespace glass;
namespace {
class ImGuiSaver : public IniSaverBackend {
public:
void Register(IniSaverBase* iniSaver) override;
void Unregister(IniSaverBase* iniSaver) override;
};
} // namespace
void ImGuiSaver::Register(IniSaverBase* iniSaver) {
// hook ini handler to save settings
ImGuiSettingsHandler iniHandler;
iniHandler.TypeName = iniSaver->GetTypeName();
iniHandler.TypeHash = ImHashStr(iniHandler.TypeName);
iniHandler.ReadOpenFn = [](ImGuiContext* ctx, ImGuiSettingsHandler* handler,
const char* name) {
return static_cast<IniSaverBase*>(handler->UserData)->IniReadOpen(name);
};
iniHandler.ReadLineFn = [](ImGuiContext* ctx, ImGuiSettingsHandler* handler,
void* entry, const char* line) {
static_cast<IniSaverBase*>(handler->UserData)->IniReadLine(entry, line);
};
iniHandler.WriteAllFn = [](ImGuiContext* ctx, ImGuiSettingsHandler* handler,
ImGuiTextBuffer* out_buf) {
static_cast<IniSaverBase*>(handler->UserData)->IniWriteAll(out_buf);
};
iniHandler.UserData = iniSaver;
ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
}
void ImGuiSaver::Unregister(IniSaverBase* iniSaver) {
if (auto ctx = ImGui::GetCurrentContext()) {
auto& handlers = ctx->SettingsHandlers;
for (auto it = handlers.begin(), end = handlers.end(); it != end; ++it) {
if (it->UserData == iniSaver) {
handlers.erase(it);
return;
}
}
}
}
static ImGuiSaver* GetSaverInstance() {
static ImGuiSaver* inst = new ImGuiSaver;
return inst;
}
IniSaverBase::IniSaverBase(const wpi::Twine& typeName, IniSaverBackend* backend)
: m_typeName(typeName.str()),
m_backend{backend ? backend : GetSaverInstance()} {}
IniSaverBase::~IniSaverBase() { m_backend->Unregister(this); }

View File

@@ -0,0 +1,165 @@
/*----------------------------------------------------------------------------*/
/* Copyright (c) 2020 FIRST. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project. */
/*----------------------------------------------------------------------------*/
#include "glass/support/IniSaverInfo.h"
#include <cstdio>
#include <cstring>
#include <imgui_internal.h>
#include <wpi/SmallString.h>
using namespace glass;
void NameInfo::SetName(const wpi::Twine& name) {
wpi::SmallString<64> nameBuf;
auto nameStr = name.toStringRef(nameBuf);
size_t len = (std::min)(nameStr.size(), sizeof(m_name) - 1);
std::memcpy(m_name, nameStr.data(), len);
m_name[len] = '\0';
}
void NameInfo::GetName(char* buf, size_t size, const char* defaultName) const {
if (m_name[0] != '\0') {
std::snprintf(buf, size, "%s", m_name);
} else {
std::snprintf(buf, size, "%s", defaultName);
}
}
void NameInfo::GetName(char* buf, size_t size, const char* defaultName,
int index) const {
if (m_name[0] != '\0') {
std::snprintf(buf, size, "%s [%d]", m_name, index);
} else {
std::snprintf(buf, size, "%s[%d]", defaultName, index);
}
}
void NameInfo::GetName(char* buf, size_t size, const char* defaultName,
int index, int index2) const {
if (m_name[0] != '\0') {
std::snprintf(buf, size, "%s [%d,%d]", m_name, index, index2);
} else {
std::snprintf(buf, size, "%s[%d,%d]", defaultName, index, index2);
}
}
void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName) const {
if (m_name[0] != '\0') {
std::snprintf(buf, size, "%s###Name%s", m_name, defaultName);
} else {
std::snprintf(buf, size, "%s###Name%s", defaultName, defaultName);
}
}
void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName,
int index) const {
if (m_name[0] != '\0') {
std::snprintf(buf, size, "%s [%d]###Name%d", m_name, index, index);
} else {
std::snprintf(buf, size, "%s[%d]###Name%d", defaultName, index, index);
}
}
void NameInfo::GetLabel(char* buf, size_t size, const char* defaultName,
int index, int index2) const {
if (m_name[0] != '\0') {
std::snprintf(buf, size, "%s [%d,%d]###Name%d", m_name, index, index2,
index);
} else {
std::snprintf(buf, size, "%s[%d,%d]###Name%d", defaultName, index, index2,
index);
}
}
bool NameInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) {
if (name != "name") return false;
size_t len = (std::min)(value.size(), sizeof(m_name) - 1);
std::memcpy(m_name, value.data(), len);
m_name[len] = '\0';
return true;
}
void NameInfo::WriteIni(ImGuiTextBuffer* out) {
out->appendf("name=%s\n", m_name);
}
void NameInfo::PushEditNameId(int index) {
char id[64];
std::snprintf(id, sizeof(id), "Name%d", index);
ImGui::PushID(id);
}
void NameInfo::PushEditNameId(const char* name) {
char id[128];
std::snprintf(id, sizeof(id), "Name%s", name);
ImGui::PushID(id);
}
bool NameInfo::PopupEditName(int index) {
bool rv = false;
char id[64];
std::snprintf(id, sizeof(id), "Name%d", index);
if (ImGui::BeginPopupContextItem(id)) {
ImGui::Text("Edit name:");
if (InputTextName("##edit")) {
rv = true;
}
if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
return rv;
}
bool NameInfo::PopupEditName(const char* name) {
bool rv = false;
char id[128];
std::snprintf(id, sizeof(id), "Name%s", name);
if (ImGui::BeginPopupContextItem(id)) {
ImGui::Text("Edit name:");
if (InputTextName("##edit")) {
rv = true;
}
if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
return rv;
}
bool NameInfo::InputTextName(const char* label_id, ImGuiInputTextFlags flags) {
return ImGui::InputText(label_id, m_name, sizeof(m_name), flags);
}
bool OpenInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) {
if (name != "open") return false;
int num;
if (value.getAsInteger(10, num)) return true;
m_open = num;
return true;
}
void OpenInfo::WriteIni(ImGuiTextBuffer* out) {
out->appendf("open=%d\n", m_open ? 1 : 0);
}
bool NameOpenInfo::ReadIni(wpi::StringRef name, wpi::StringRef value) {
if (NameInfo::ReadIni(name, value)) return true;
if (OpenInfo::ReadIni(name, value)) return true;
return false;
}
void NameOpenInfo::WriteIni(ImGuiTextBuffer* out) {
NameInfo::WriteIni(out);
OpenInfo::WriteIni(out);
}