[glass] Use JSON files for storage instead of imgui ini

Storage is now nested.

Separate "roots" can be configured which save to separate files.
In particular, this is used to save wpigui and ImGui window position
to a -window.json file.

ImGui's ini (for window position) is mapped to JSON.

You can optionally specify a directory to load from on the command line.
If one isn't provided, it uses the global system directory.
Any changes made are automatically saved here.

Workspace | Open: select directory, the current layout is replaced with that
workspace, and future auto-saves also switch to that location. The main
window size/location is not changed, only the contents.

Workspace | Save As: select directory, the current layout is saved there,
and future auto-saves also switch to that location.

Workspace | Reset: window locations are preserved, but all other settings
are reset to default (including e.g. removing plot windows). This will also
end up clearing the current save file. as with load, the main window
size/location is not changed.

Workspace | Save As Global: "save as" to the global system location

Notably, the main window size/location is only loaded at startup, but is
auto-saved as part of the current workspace.
This commit is contained in:
Peter Johnson
2021-11-25 00:51:00 -08:00
parent 0bbf51d566
commit 0587b7043a
70 changed files with 3007 additions and 2358 deletions

View File

@@ -51,13 +51,13 @@ bool glass::BeginDevice(const char* id, ImGuiTreeNodeFlags flags) {
PushID(id);
// build label
std::string* name = GetStorage().GetStringRef("name");
std::string& name = GetStorage().GetString("name");
char label[128];
std::snprintf(label, sizeof(label), "%s###name",
name->empty() ? id : name->c_str());
name.empty() ? id : name.c_str());
bool open = CollapsingHeader(label, flags);
PopupEditName("name", name);
PopupEditName("name", &name);
if (!open) {
PopID();

View File

@@ -32,6 +32,9 @@
#include <wpigui.h>
#include "glass/Context.h"
#include "glass/Storage.h"
#include "glass/support/ColorSetting.h"
#include "glass/support/EnumSetting.h"
using namespace glass;
@@ -114,12 +117,14 @@ struct DisplayOptions {
static constexpr Style kDefaultStyle = kBoxImage;
static constexpr float kDefaultWeight = 4.0f;
static constexpr float kDefaultColorFloat[] = {255, 0, 0, 255};
static constexpr ImU32 kDefaultColor = IM_COL32(255, 0, 0, 255);
static constexpr auto kDefaultWidth = 0.6858_m;
static constexpr auto kDefaultLength = 0.8204_m;
static constexpr bool kDefaultArrows = true;
static constexpr int kDefaultArrowSize = 50;
static constexpr float kDefaultArrowWeight = 4.0f;
static constexpr float kDefaultArrowColorFloat[] = {0, 255, 0, 255};
static constexpr ImU32 kDefaultArrowColor = IM_COL32(0, 255, 0, 255);
static constexpr bool kDefaultSelectable = true;
@@ -180,7 +185,7 @@ class PoseFrameData {
class ObjectInfo {
public:
ObjectInfo();
explicit ObjectInfo(Storage& storage);
DisplayOptions GetDisplayOptions() const;
void DisplaySettings();
@@ -191,26 +196,26 @@ class ObjectInfo {
private:
void Reset();
bool LoadImageImpl(const char* fn);
bool LoadImageImpl(const std::string& fn);
std::unique_ptr<pfd::open_file> m_fileOpener;
// in meters
float* m_pWidth;
float* m_pLength;
float& m_width;
float& m_length;
int* m_pStyle; // DisplayOptions::Style
float* m_pWeight;
int* m_pColor;
EnumSetting m_style; // DisplayOptions::Style
float& m_weight;
ColorSetting m_color;
bool* m_pArrows;
int* m_pArrowSize;
float* m_pArrowWeight;
int* m_pArrowColor;
bool& m_arrows;
int& m_arrowSize;
float& m_arrowWeight;
ColorSetting m_arrowColor;
bool* m_pSelectable;
bool& m_selectable;
std::string* m_pFilename;
std::string& m_filename;
gui::Texture m_texture;
};
@@ -219,7 +224,7 @@ class FieldInfo {
static constexpr auto kDefaultWidth = 15.98_m;
static constexpr auto kDefaultHeight = 8.21_m;
FieldInfo();
explicit FieldInfo(Storage& storage);
void DisplaySettings();
@@ -231,25 +236,25 @@ class FieldInfo {
private:
void Reset();
bool LoadImageImpl(const char* fn);
bool LoadImageImpl(const std::string& fn);
void LoadJson(std::string_view jsonfile);
std::unique_ptr<pfd::open_file> m_fileOpener;
std::string* m_pFilename;
std::string& m_filename;
gui::Texture m_texture;
// in meters
float* m_pWidth;
float* m_pHeight;
float& m_width;
float& m_height;
// in image pixels
int m_imageWidth;
int m_imageHeight;
int* m_pTop;
int* m_pLeft;
int* m_pBottom;
int* m_pRight;
int& m_top;
int& m_left;
int& m_bottom;
int& m_right;
};
} // namespace
@@ -334,16 +339,14 @@ static bool InputPose(frc::Pose2d* pose) {
return changed;
}
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.to<float>());
m_pHeight = storage.GetFloatRef("height", kDefaultHeight.to<float>());
}
FieldInfo::FieldInfo(Storage& storage)
: m_filename{storage.GetString("image")},
m_width{storage.GetFloat("width", kDefaultWidth.to<float>())},
m_height{storage.GetFloat("height", kDefaultHeight.to<float>())},
m_top{storage.GetInt("top", 0)},
m_left{storage.GetInt("left", 0)},
m_bottom{storage.GetInt("bottom", -1)},
m_right{storage.GetInt("right", -1)} {}
void FieldInfo::DisplaySettings() {
if (ImGui::Button("Choose image...")) {
@@ -357,23 +360,23 @@ void FieldInfo::DisplaySettings() {
if (ImGui::Button("Reset image")) {
Reset();
}
InputFloatLength("Field Width", m_pWidth);
InputFloatLength("Field Height", m_pHeight);
// ImGui::InputInt("Field Top", m_pTop);
// ImGui::InputInt("Field Left", m_pLeft);
// ImGui::InputInt("Field Right", m_pRight);
// ImGui::InputInt("Field Bottom", m_pBottom);
InputFloatLength("Field Width", &m_width);
InputFloatLength("Field Height", &m_height);
// ImGui::InputInt("Field Top", &m_top);
// ImGui::InputInt("Field Left", &m_left);
// ImGui::InputInt("Field Right", &m_right);
// ImGui::InputInt("Field Bottom", &m_bottom);
}
void FieldInfo::Reset() {
m_texture = gui::Texture{};
m_pFilename->clear();
m_filename.clear();
m_imageWidth = 0;
m_imageHeight = 0;
*m_pTop = 0;
*m_pLeft = 0;
*m_pBottom = -1;
*m_pRight = -1;
m_top = 0;
m_left = 0;
m_bottom = -1;
m_right = -1;
}
void FieldInfo::LoadImage() {
@@ -384,17 +387,17 @@ void FieldInfo::LoadImage() {
LoadJson(result[0]);
} else {
LoadImageImpl(result[0].c_str());
*m_pTop = 0;
*m_pLeft = 0;
*m_pBottom = -1;
*m_pRight = -1;
m_top = 0;
m_left = 0;
m_bottom = -1;
m_right = -1;
}
}
m_fileOpener.reset();
}
if (!m_texture && !m_pFilename->empty()) {
if (!LoadImageImpl(m_pFilename->c_str())) {
m_pFilename->clear();
if (!m_texture && !m_filename.empty()) {
if (!LoadImageImpl(m_filename)) {
m_filename.clear();
}
}
}
@@ -478,18 +481,18 @@ void FieldInfo::LoadJson(std::string_view jsonfile) {
}
// save to field info
*m_pFilename = pathname;
*m_pTop = top;
*m_pLeft = left;
*m_pBottom = bottom;
*m_pRight = right;
*m_pWidth = width;
*m_pHeight = height;
m_filename = pathname;
m_top = top;
m_left = left;
m_bottom = bottom;
m_right = right;
m_width = width;
m_height = height;
}
bool FieldInfo::LoadImageImpl(const char* fn) {
bool FieldInfo::LoadImageImpl(const std::string& fn) {
fmt::print("GUI: loading field image '{}'\n", fn);
auto texture = gui::Texture::CreateFromFile(fn);
auto texture = gui::Texture::CreateFromFile(fn.c_str());
if (!texture) {
std::puts("GUI: could not read field image");
return false;
@@ -497,7 +500,7 @@ bool FieldInfo::LoadImageImpl(const char* fn) {
m_texture = std::move(texture);
m_imageWidth = m_texture.GetWidth();
m_imageHeight = m_texture.GetHeight();
*m_pFilename = fn;
m_filename = fn;
return true;
}
@@ -512,19 +515,19 @@ FieldFrameData FieldInfo::GetFrameData(ImVec2 min, ImVec2 max) const {
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;
if (m_bottom > 0 && m_right > 0) {
min.x += m_left * (max.x - min.x) / m_imageWidth;
min.y += m_top * (max.y - min.y) / m_imageHeight;
max.x -= (m_imageWidth - m_right) * (max.x - min.x) / m_imageWidth;
max.y -= (m_imageHeight - m_bottom) * (max.y - min.y) / m_imageHeight;
}
// draw the field "active area" as a yellow boundary box
gui::MaxFit(&min, &max, *m_pWidth, *m_pHeight);
gui::MaxFit(&min, &max, m_width, m_height);
ffd.min = min;
ffd.max = max;
ffd.scale = (max.x - min.x) / *m_pWidth;
ffd.scale = (max.x - min.x) / m_width;
return ffd;
}
@@ -537,48 +540,47 @@ void FieldInfo::Draw(ImDrawList* drawList, const FieldFrameData& ffd) const {
drawList->AddRect(ffd.min, ffd.max, IM_COL32(255, 255, 0, 255));
}
ObjectInfo::ObjectInfo() {
auto& storage = GetStorage();
m_pFilename = storage.GetStringRef("image");
m_pWidth =
storage.GetFloatRef("width", DisplayOptions::kDefaultWidth.to<float>());
m_pLength =
storage.GetFloatRef("length", DisplayOptions::kDefaultLength.to<float>());
m_pStyle = storage.GetIntRef("style", DisplayOptions::kDefaultStyle);
m_pWeight = storage.GetFloatRef("weight", DisplayOptions::kDefaultWeight);
m_pColor = storage.GetIntRef("color", DisplayOptions::kDefaultColor);
m_pArrows = storage.GetBoolRef("arrows", DisplayOptions::kDefaultArrows);
m_pArrowSize =
storage.GetIntRef("arrowSize", DisplayOptions::kDefaultArrowSize);
m_pArrowWeight =
storage.GetFloatRef("arrowWeight", DisplayOptions::kDefaultArrowWeight);
m_pArrowColor =
storage.GetIntRef("arrowColor", DisplayOptions::kDefaultArrowColor);
m_pSelectable =
storage.GetBoolRef("selectable", DisplayOptions::kDefaultSelectable);
}
ObjectInfo::ObjectInfo(Storage& storage)
: m_width{storage.GetFloat("width",
DisplayOptions::kDefaultWidth.to<float>())},
m_length{storage.GetFloat("length",
DisplayOptions::kDefaultLength.to<float>())},
m_style{storage.GetString("style"),
DisplayOptions::kDefaultStyle,
{"Box/Image", "Line", "Line (Closed)", "Track"}},
m_weight{storage.GetFloat("weight", DisplayOptions::kDefaultWeight)},
m_color{
storage.GetFloatArray("color", DisplayOptions::kDefaultColorFloat)},
m_arrows{storage.GetBool("arrows", DisplayOptions::kDefaultArrows)},
m_arrowSize{
storage.GetInt("arrowSize", DisplayOptions::kDefaultArrowSize)},
m_arrowWeight{
storage.GetFloat("arrowWeight", DisplayOptions::kDefaultArrowWeight)},
m_arrowColor{storage.GetFloatArray(
"arrowColor", DisplayOptions::kDefaultArrowColorFloat)},
m_selectable{
storage.GetBool("selectable", DisplayOptions::kDefaultSelectable)},
m_filename{storage.GetString("image")} {}
DisplayOptions ObjectInfo::GetDisplayOptions() const {
DisplayOptions rv{m_texture};
rv.style = static_cast<DisplayOptions::Style>(*m_pStyle);
rv.weight = *m_pWeight;
rv.color = *m_pColor;
rv.width = units::meter_t{*m_pWidth};
rv.length = units::meter_t{*m_pLength};
rv.arrows = *m_pArrows;
rv.arrowSize = *m_pArrowSize;
rv.arrowWeight = *m_pArrowWeight;
rv.arrowColor = *m_pArrowColor;
rv.selectable = *m_pSelectable;
rv.style = static_cast<DisplayOptions::Style>(m_style.GetValue());
rv.weight = m_weight;
rv.color = ImGui::ColorConvertFloat4ToU32(m_color.GetColor());
rv.width = units::meter_t{m_width};
rv.length = units::meter_t{m_length};
rv.arrows = m_arrows;
rv.arrowSize = m_arrowSize;
rv.arrowWeight = m_arrowWeight;
rv.arrowColor = ImGui::ColorConvertFloat4ToU32(m_arrowColor.GetColor());
rv.selectable = m_selectable;
return rv;
}
void ObjectInfo::DisplaySettings() {
static const char* styleChoices[] = {"Box/Image", "Line", "Line (Closed)",
"Track"};
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
ImGui::Combo("Style", m_pStyle, styleChoices, IM_ARRAYSIZE(styleChoices));
switch (*m_pStyle) {
m_style.Combo("Style");
switch (m_style.GetValue()) {
case DisplayOptions::kBoxImage:
if (ImGui::Button("Choose image...")) {
m_fileOpener = std::make_unique<pfd::open_file>(
@@ -591,35 +593,27 @@ void ObjectInfo::DisplaySettings() {
if (ImGui::Button("Reset image")) {
Reset();
}
InputFloatLength("Width", m_pWidth);
InputFloatLength("Length", m_pLength);
InputFloatLength("Width", &m_width);
InputFloatLength("Length", &m_length);
break;
case DisplayOptions::kTrack:
InputFloatLength("Width", m_pWidth);
InputFloatLength("Width", &m_width);
break;
default:
break;
}
ImGui::InputFloat("Line Weight", m_pWeight);
ImColor col(*m_pColor);
if (ImGui::ColorEdit3("Line Color", &col.Value.x,
ImGuiColorEditFlags_NoInputs)) {
*m_pColor = col;
}
ImGui::Checkbox("Arrows", m_pArrows);
if (*m_pArrows) {
ImGui::SliderInt("Arrow Size", m_pArrowSize, 0, 100, "%d%%",
ImGui::InputFloat("Line Weight", &m_weight);
m_color.ColorEdit3("Line Color", ImGuiColorEditFlags_NoInputs);
ImGui::Checkbox("Arrows", &m_arrows);
if (m_arrows) {
ImGui::SliderInt("Arrow Size", &m_arrowSize, 0, 100, "%d%%",
ImGuiSliderFlags_AlwaysClamp);
ImGui::InputFloat("Arrow Weight", m_pArrowWeight);
ImColor col(*m_pArrowColor);
if (ImGui::ColorEdit3("Arrow Color", &col.Value.x,
ImGuiColorEditFlags_NoInputs)) {
*m_pArrowColor = col;
}
ImGui::InputFloat("Arrow Weight", &m_arrowWeight);
m_arrowColor.ColorEdit3("Arrow Color", ImGuiColorEditFlags_NoInputs);
}
ImGui::Checkbox("Selectable", m_pSelectable);
ImGui::Checkbox("Selectable", &m_selectable);
}
void ObjectInfo::DrawLine(ImDrawList* drawList,
@@ -629,10 +623,12 @@ void ObjectInfo::DrawLine(ImDrawList* drawList,
}
if (points.size() == 1) {
drawList->AddCircleFilled(points.front(), *m_pWeight, *m_pWeight);
drawList->AddCircleFilled(points.front(), m_weight, m_weight);
return;
}
ImU32 color = ImGui::ColorConvertFloat4ToU32(m_color.GetColor());
// PolyLine doesn't handle acute angles well; workaround from
// https://github.com/ocornut/imgui/issues/3366
size_t i = 0;
@@ -651,18 +647,18 @@ void ObjectInfo::DrawLine(ImDrawList* drawList,
++nlin;
}
drawList->AddPolyline(&points[i], nlin, *m_pColor, false, *m_pWeight);
drawList->AddPolyline(&points[i], nlin, color, false, m_weight);
i += nlin - 1;
}
if (points.size() > 2 && *m_pStyle == DisplayOptions::kLineClosed) {
drawList->AddLine(points.back(), points.front(), *m_pColor, *m_pWeight);
if (points.size() > 2 && m_style.GetValue() == DisplayOptions::kLineClosed) {
drawList->AddLine(points.back(), points.front(), color, m_weight);
}
}
void ObjectInfo::Reset() {
m_texture = gui::Texture{};
m_pFilename->clear();
m_filename.clear();
}
void ObjectInfo::LoadImage() {
@@ -673,22 +669,22 @@ void ObjectInfo::LoadImage() {
}
m_fileOpener.reset();
}
if (!m_texture && !m_pFilename->empty()) {
if (!LoadImageImpl(m_pFilename->c_str())) {
m_pFilename->clear();
if (!m_texture && !m_filename.empty()) {
if (!LoadImageImpl(m_filename)) {
m_filename.clear();
}
}
}
bool ObjectInfo::LoadImageImpl(const char* fn) {
bool ObjectInfo::LoadImageImpl(const std::string& fn) {
fmt::print("GUI: loading object image '{}'\n", fn);
auto texture = gui::Texture::CreateFromFile(fn);
auto texture = gui::Texture::CreateFromFile(fn.c_str());
if (!texture) {
std::fputs("GUI: could not read object image\n", stderr);
return false;
}
m_texture = std::move(texture);
*m_pFilename = fn;
m_filename = fn;
return true;
}
@@ -857,15 +853,16 @@ void glass::DisplayField2DSettings(Field2DModel* model) {
auto& storage = GetStorage();
auto field = storage.GetData<FieldInfo>();
if (!field) {
storage.SetData(std::make_shared<FieldInfo>());
storage.SetData(std::make_shared<FieldInfo>(storage));
field = storage.GetData<FieldInfo>();
}
static const char* unitNames[] = {"meters", "feet", "inches"};
int* pDisplayUnits = GetStorage().GetIntRef("units", kDisplayMeters);
EnumSetting displayUnits{GetStorage().GetString("units"),
kDisplayMeters,
{"meters", "feet", "inches"}};
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
ImGui::Combo("Units", pDisplayUnits, unitNames, IM_ARRAYSIZE(unitNames));
gDisplayUnits = static_cast<DisplayUnits>(*pDisplayUnits);
displayUnits.Combo("Units");
gDisplayUnits = static_cast<DisplayUnits>(displayUnits.GetValue());
ImGui::PushItemWidth(ImGui::GetFontSize() * 4);
if (ImGui::CollapsingHeader("Field")) {
@@ -881,7 +878,7 @@ void glass::DisplayField2DSettings(Field2DModel* model) {
PushID(name);
auto& objRef = field->m_objects[name];
if (!objRef) {
objRef = std::make_unique<ObjectInfo>();
objRef = std::make_unique<ObjectInfo>(GetStorage());
}
auto obj = objRef.get();
@@ -1025,7 +1022,7 @@ void FieldDisplay::DisplayObject(FieldObjectModel& model,
PushID(name);
auto& objRef = m_field->m_objects[name];
if (!objRef) {
objRef = std::make_unique<ObjectInfo>();
objRef = std::make_unique<ObjectInfo>(GetStorage());
}
auto obj = objRef.get();
obj->LoadImage();
@@ -1205,7 +1202,7 @@ void glass::DisplayField2D(Field2DModel* model, const ImVec2& contentSize) {
auto& storage = GetStorage();
auto field = storage.GetData<FieldInfo>();
if (!field) {
storage.SetData(std::make_shared<FieldInfo>());
storage.SetData(std::make_shared<FieldInfo>(storage));
field = storage.GetData<FieldInfo>();
}

View File

@@ -27,6 +27,7 @@
#include <wpigui.h>
#include "glass/Context.h"
#include "glass/Storage.h"
using namespace glass;
@@ -61,7 +62,7 @@ struct FrameData {
class BackgroundInfo {
public:
BackgroundInfo();
explicit BackgroundInfo(Storage& storage);
void DisplaySettings();
@@ -72,11 +73,11 @@ class BackgroundInfo {
private:
void Reset();
bool LoadImageImpl(const char* fn);
bool LoadImageImpl(const std::string& fn);
std::unique_ptr<pfd::open_file> m_fileOpener;
std::string* m_pFilename;
std::string& m_filename;
gui::Texture m_texture;
// in image pixels
@@ -86,10 +87,8 @@ class BackgroundInfo {
} // namespace
BackgroundInfo::BackgroundInfo() {
auto& storage = GetStorage();
m_pFilename = storage.GetStringRef("image");
}
BackgroundInfo::BackgroundInfo(Storage& storage)
: m_filename{storage.GetString("image")} {}
void BackgroundInfo::DisplaySettings() {
if (ImGui::Button("Choose image...")) {
@@ -106,7 +105,7 @@ void BackgroundInfo::DisplaySettings() {
void BackgroundInfo::Reset() {
m_texture = gui::Texture{};
m_pFilename->clear();
m_filename.clear();
m_imageWidth = 0;
m_imageHeight = 0;
}
@@ -119,16 +118,16 @@ void BackgroundInfo::LoadImage() {
}
m_fileOpener.reset();
}
if (!m_texture && !m_pFilename->empty()) {
if (!LoadImageImpl(m_pFilename->c_str())) {
m_pFilename->clear();
if (!m_texture && !m_filename.empty()) {
if (!LoadImageImpl(m_filename)) {
m_filename.clear();
}
}
}
bool BackgroundInfo::LoadImageImpl(const char* fn) {
bool BackgroundInfo::LoadImageImpl(const std::string& fn) {
fmt::print("GUI: loading background image '{}'\n", fn);
auto texture = gui::Texture::CreateFromFile(fn);
auto texture = gui::Texture::CreateFromFile(fn.c_str());
if (!texture) {
std::puts("GUI: could not read background image");
return false;
@@ -136,7 +135,7 @@ bool BackgroundInfo::LoadImageImpl(const char* fn) {
m_texture = std::move(texture);
m_imageWidth = m_texture.GetWidth();
m_imageHeight = m_texture.GetHeight();
*m_pFilename = fn;
m_filename = fn;
return true;
}
@@ -175,7 +174,7 @@ void glass::DisplayMechanism2DSettings(Mechanism2DModel* model) {
auto& storage = GetStorage();
auto bg = storage.GetData<BackgroundInfo>();
if (!bg) {
storage.SetData(std::make_shared<BackgroundInfo>());
storage.SetData(std::make_shared<BackgroundInfo>(storage));
bg = storage.GetData<BackgroundInfo>();
}
bg->DisplaySettings();
@@ -208,7 +207,7 @@ void glass::DisplayMechanism2D(Mechanism2DModel* model,
auto& storage = GetStorage();
auto bg = storage.GetData<BackgroundInfo>();
if (!bg) {
storage.SetData(std::make_shared<BackgroundInfo>());
storage.SetData(std::make_shared<BackgroundInfo>(storage));
bg = storage.GetData<BackgroundInfo>();
}

View File

@@ -19,10 +19,8 @@
#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>
@@ -31,6 +29,9 @@
#include "glass/Context.h"
#include "glass/DataSource.h"
#include "glass/Storage.h"
#include "glass/support/ColorSetting.h"
#include "glass/support/EnumSetting.h"
#include "glass/support/ExtraGuiWidgets.h"
using namespace glass;
@@ -45,9 +46,11 @@ struct PlotSeriesRef {
};
class PlotSeries {
explicit PlotSeries(Storage& storage, int yAxis = 0);
public:
explicit PlotSeries(std::string_view id);
explicit PlotSeries(DataSource* source, int yAxis = 0);
PlotSeries(Storage& storage, std::string_view id);
PlotSeries(Storage& storage, DataSource* source, int yAxis = 0);
const std::string& GetId() const { return m_id; }
@@ -56,9 +59,6 @@ class PlotSeries {
void SetSource(DataSource* source);
DataSource* GetSource() const { return m_source; }
bool ReadIni(std::string_view name, std::string_view 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);
@@ -69,10 +69,12 @@ class PlotSeries {
int GetYAxis() const { return m_yAxis; }
void SetYAxis(int yAxis) { m_yAxis = yAxis; }
void SetColor(const ImVec4& color) { m_color.SetColor(color); }
private:
bool IsDigital() const {
return m_digital == kDigital ||
(m_digital == kAuto && m_source && m_source->IsDigital());
return m_digital.GetValue() == kDigital ||
(m_digital.GetValue() == kAuto && m_source && m_source->IsDigital());
}
void AppendValue(double value, uint64_t time);
@@ -80,19 +82,20 @@ class PlotSeries {
DataSource* m_source = nullptr;
wpi::sig::ScopedConnection m_sourceCreatedConn;
wpi::sig::ScopedConnection m_newValueConn;
std::string m_id;
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;
std::string& m_name;
int& m_yAxis;
static constexpr float kDefaultColor[4] = {0.0, 0.0, 0.0, IMPLOT_AUTO};
ColorSetting m_color;
EnumSetting m_marker;
float& m_weight;
enum Digital { kAuto, kDigital, kAnalog };
int m_digital = 0;
int m_digitalBitHeight = 8;
int m_digitalBitGap = 4;
EnumSetting m_digital;
int& m_digitalBitHeight;
int& m_digitalBitGap;
// value storage
static constexpr int kMaxSize = 2000;
@@ -104,10 +107,7 @@ class PlotSeries {
class Plot {
public:
Plot();
bool ReadIni(std::string_view name, std::string_view value);
void WriteIni(ImGuiTextBuffer* out);
explicit Plot(Storage& storage);
void DragDropTarget(PlotView& view, size_t i, bool inPlot);
void EmitPlot(PlotView& view, double now, bool paused, size_t i);
@@ -116,6 +116,7 @@ class Plot {
const std::string& GetName() const { return m_name; }
std::vector<std::unique_ptr<PlotSeries>> m_series;
std::vector<std::unique_ptr<Storage>>& m_seriesStorage;
// Returns base height; does not include actual plot height if auto-sized.
int GetAutoBaseHeight(bool* isAuto, size_t i);
@@ -129,30 +130,35 @@ class Plot {
private:
void EmitSettingsLimits(int axis);
std::string m_name;
bool m_visible = true;
bool m_showPause = true;
unsigned int m_plotFlags = ImPlotFlags_None;
bool m_lockPrevX = false;
bool m_paused = false;
float m_viewTime = 10;
bool m_autoHeight = true;
int m_height = 300;
std::string& m_name;
bool& m_visible;
bool& m_showPause;
bool& m_lockPrevX;
bool& m_legend;
bool& m_yAxis2;
bool& m_yAxis3;
float& m_viewTime;
bool& m_autoHeight;
int& m_height;
struct PlotRange {
double min = 0;
double max = 1;
bool lockMin = false;
bool lockMax = false;
explicit PlotRange(Storage& storage);
std::string& label;
double& min;
double& max;
bool& lockMin;
bool& lockMax;
bool apply = false;
};
std::string m_axisLabel[3];
PlotRange m_axisRange[3];
std::vector<PlotRange> m_axis;
ImPlotRange m_xaxisRange; // read from plot, used for lockPrevX
};
class PlotView : public View {
public:
explicit PlotView(PlotProvider* provider) : m_provider{provider} {}
PlotView(PlotProvider* provider, Storage& storage);
void Display() override;
@@ -163,12 +169,30 @@ class PlotView : public View {
size_t toSeriesIndex, int yAxis = -1);
PlotProvider* m_provider;
std::vector<std::unique_ptr<Storage>>& m_plotsStorage;
std::vector<std::unique_ptr<Plot>> m_plots;
};
} // namespace
PlotSeries::PlotSeries(std::string_view id) : m_id(id) {
PlotSeries::PlotSeries(Storage& storage, int yAxis)
: m_id{storage.GetString("id")},
m_name{storage.GetString("name")},
m_yAxis{storage.GetInt("yAxis", yAxis)},
m_color{storage.GetFloatArray("color", kDefaultColor)},
m_marker{storage.GetString("marker"),
0,
{"None", "Circle", "Square", "Diamond", "Up", "Down", "Left",
"Right", "Cross", "Plus", "Asterisk"}},
m_weight{storage.GetFloat("weight", IMPLOT_AUTO)},
m_digital{
storage.GetString("digital"), kAuto, {"Auto", "Digital", "Analog"}},
m_digitalBitHeight{storage.GetInt("digitalBitHeight", 8)},
m_digitalBitGap{storage.GetInt("digitalBitGap", 4)} {}
PlotSeries::PlotSeries(Storage& storage, std::string_view id)
: PlotSeries{storage, 0} {
m_id = id;
if (DataSource* source = DataSource::Find(id)) {
SetSource(source);
return;
@@ -176,7 +200,8 @@ PlotSeries::PlotSeries(std::string_view id) : m_id(id) {
CheckSource();
}
PlotSeries::PlotSeries(DataSource* source, int yAxis) : m_yAxis(yAxis) {
PlotSeries::PlotSeries(Storage& storage, DataSource* source, int yAxis)
: PlotSeries{storage, yAxis} {
SetSource(source);
m_id = source->GetId();
}
@@ -245,66 +270,14 @@ void PlotSeries::AppendValue(double value, uint64_t timeUs) {
}
}
bool PlotSeries::ReadIni(std::string_view name, std::string_view value) {
if (name == "name") {
m_name = value;
return true;
}
if (name == "yAxis") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_yAxis = num.value();
}
return true;
} else if (name == "color") {
if (auto num = wpi::parse_integer<unsigned int>(value, 10)) {
m_color = ImColor(num.value());
}
return true;
} else if (name == "marker") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_marker = num.value();
}
return true;
} else if (name == "weight") {
if (auto num = wpi::parse_float<float>(value)) {
m_weight = num.value();
}
return true;
} else if (name == "digital") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_digital = num.value();
}
return true;
} else if (name == "digitalBitHeight") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_digitalBitHeight = num.value();
}
return true;
} else if (name == "digitalBitGap") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_digitalBitGap = num.value();
}
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;
auto& sourceName = m_source->GetName();
if (!sourceName.empty()) {
return sourceName.c_str();
}
}
return m_id.c_str();
@@ -346,10 +319,10 @@ PlotSeries::Action PlotSeries::EmitPlot(PlotView& view, double now, size_t i,
return ImPlotPoint{point->x - d->zeroTime, point->y};
};
if (m_color.w == IMPLOT_AUTO_COL.w) {
m_color = ImPlot::GetColormapColor(i);
if (m_color.GetColorFloat()[3] == IMPLOT_AUTO) {
SetColor(ImPlot::GetColormapColor(i));
}
ImPlot::SetNextLineStyle(m_color, m_weight);
ImPlot::SetNextLineStyle(m_color.GetColor(), m_weight);
if (IsDigital()) {
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight);
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap);
@@ -358,7 +331,7 @@ PlotSeries::Action PlotSeries::EmitPlot(PlotView& view, double now, size_t i,
ImPlot::PopStyleVar();
} else {
ImPlot::SetPlotYAxis(m_yAxis);
ImPlot::SetNextMarkerStyle(m_marker - 1);
ImPlot::SetNextMarkerStyle(m_marker.GetValue() - 1);
ImPlot::PlotLineG(label, getter, &getterData, size + 1);
}
@@ -413,10 +386,10 @@ void PlotSeries::EmitDragDropPayload(PlotView& view, size_t i,
void PlotSeries::EmitSettings(size_t i) {
// Line color
{
ImGui::ColorEdit3("Color", &m_color.x, ImGuiColorEditFlags_NoInputs);
m_color.ColorEdit3("Color", ImGuiColorEditFlags_NoInputs);
ImGui::SameLine();
if (ImGui::Button("Default")) {
m_color = ImPlot::GetColormapColor(i);
SetColor(ImPlot::GetColormapColor(i));
}
}
@@ -428,10 +401,8 @@ void PlotSeries::EmitSettings(size_t i) {
// 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]));
m_digital.Combo("Digital");
}
if (IsDigital()) {
@@ -456,135 +427,44 @@ void PlotSeries::EmitSettings(size_t i) {
// 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]));
m_marker.Combo("Marker");
}
}
}
Plot::Plot() {
for (int i = 0; i < 3; ++i) {
m_axisRange[i] = PlotRange{};
}
}
Plot::PlotRange::PlotRange(Storage& storage)
: label{storage.GetString("label")},
min{storage.GetDouble("min", 0)},
max{storage.GetDouble("max", 1)},
lockMin{storage.GetBool("lockMin", false)},
lockMax{storage.GetBool("lockMax", false)} {}
bool Plot::ReadIni(std::string_view name, std::string_view value) {
if (name == "name") {
m_name = value;
return true;
} else if (name == "visible") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_visible = num.value() != 0;
}
return true;
} else if (name == "showPause") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_showPause = num.value() != 0;
}
return true;
} else if (name == "lockPrevX") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_lockPrevX = num.value() != 0;
}
return true;
} else if (name == "legend") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
if (num.value() == 0) {
m_plotFlags |= ImPlotFlags_NoLegend;
} else {
m_plotFlags &= ~ImPlotFlags_NoLegend;
}
}
return true;
} else if (name == "yaxis2") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
if (num.value() == 0) {
m_plotFlags &= ~ImPlotFlags_YAxis2;
} else {
m_plotFlags |= ImPlotFlags_YAxis2;
}
}
return true;
} else if (name == "yaxis3") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
if (num.value() == 0) {
m_plotFlags &= ~ImPlotFlags_YAxis3;
} else {
m_plotFlags |= ImPlotFlags_YAxis3;
}
}
return true;
} else if (name == "viewTime") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_viewTime = num.value() / 1000.0;
}
return true;
} else if (name == "autoHeight") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_autoHeight = num.value() != 0;
}
return true;
} else if (name == "height") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_height = num.value();
}
return true;
} else if (wpi::starts_with(name, 'y')) {
auto [yAxisStr, yName] = wpi::split(name, '_');
int yAxis =
wpi::parse_integer<int>(wpi::drop_front(yAxisStr), 10).value_or(-1);
if (yAxis < 0 || yAxis > 3) {
return false;
}
if (yName == "min") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_axisRange[yAxis].min = num.value() / 1000.0;
}
return true;
} else if (yName == "max") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_axisRange[yAxis].max = num.value() / 1000.0;
}
return true;
} else if (yName == "lockMin") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_axisRange[yAxis].lockMin = num.value() != 0;
}
return true;
} else if (yName == "lockMax") {
if (auto num = wpi::parse_integer<int>(value, 10)) {
m_axisRange[yAxis].lockMax = num.value() != 0;
}
return true;
} else if (yName == "label") {
m_axisLabel[yAxis] = value;
return true;
Plot::Plot(Storage& storage)
: m_seriesStorage{storage.GetChildArray("series")},
m_name{storage.GetString("name")},
m_visible{storage.GetBool("visible", true)},
m_showPause{storage.GetBool("showPause", true)},
m_lockPrevX{storage.GetBool("lockPrevX", false)},
m_legend{storage.GetBool("legend", true)},
m_yAxis2{storage.GetBool("yaxis2", false)},
m_yAxis3{storage.GetBool("yaxis3", false)},
m_viewTime{storage.GetFloat("viewTime", 10)},
m_autoHeight{storage.GetBool("autoHeight", true)},
m_height{storage.GetInt("height", 300)} {
auto& axesStorage = storage.GetChildArray("axis");
axesStorage.resize(3);
for (auto&& axisStorage : axesStorage) {
if (!axisStorage) {
axisStorage = std::make_unique<Storage>();
}
m_axis.emplace_back(*axisStorage);
}
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\nautoHeight=%d\nheight=%d\n",
m_name.c_str(), m_visible ? 1 : 0, m_showPause ? 1 : 0,
m_lockPrevX ? 1 : 0, (m_plotFlags & ImPlotFlags_NoLegend) ? 0 : 1,
(m_plotFlags & ImPlotFlags_YAxis2) ? 1 : 0,
(m_plotFlags & ImPlotFlags_YAxis3) ? 1 : 0,
static_cast<int>(m_viewTime * 1000), m_autoHeight ? 1 : 0, 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"
"y%d_label=%s\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, i,
m_axisLabel[i].c_str());
// loop over series
for (auto&& v : m_seriesStorage) {
m_series.emplace_back(
std::make_unique<PlotSeries>(*v, v->ReadString("id")));
}
}
@@ -612,8 +492,9 @@ void Plot::DragDropTarget(PlotView& view, size_t i, bool inPlot) {
(yAxis == -1 || elem->GetYAxis() == yAxis);
});
if (it == m_series.end()) {
m_series.emplace_back(
std::make_unique<PlotSeries>(source, yAxis == -1 ? 0 : yAxis));
m_seriesStorage.emplace_back(std::make_unique<Storage>());
m_series.emplace_back(std::make_unique<PlotSeries>(
*m_seriesStorage.back(), source, yAxis == -1 ? 0 : yAxis));
}
} else if (const ImGuiPayload* payload =
ImGui::AcceptDragDropPayload("PlotSeries")) {
@@ -658,38 +539,45 @@ void Plot::EmitPlot(PlotView& view, double now, bool paused, size_t i) {
ImPlotAxisFlags_NoGridLines};
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) {
m_axis[i].min, m_axis[i].max,
m_axis[i].apply ? ImGuiCond_Always : ImGuiCond_Once, i);
m_axis[i].apply = false;
if (m_axis[i].lockMin) {
yFlags[i] |= ImPlotAxisFlags_LockMin;
}
if (m_axisRange[i].lockMax) {
if (m_axis[i].lockMax) {
yFlags[i] |= ImPlotAxisFlags_LockMax;
}
}
ImPlotFlags plotFlags = (m_legend ? 0 : ImPlotFlags_NoLegend) |
(m_yAxis2 ? ImPlotFlags_YAxis2 : 0) |
(m_yAxis3 ? ImPlotFlags_YAxis3 : 0);
if (ImPlot::BeginPlot(
label, nullptr,
m_axisLabel[0].empty() ? nullptr : m_axisLabel[0].c_str(),
ImVec2(-1, m_height), m_plotFlags, ImPlotAxisFlags_None, yFlags[0],
m_axis[0].label.empty() ? nullptr : m_axis[0].label.c_str(),
ImVec2(-1, m_height), plotFlags, ImPlotAxisFlags_None, yFlags[0],
yFlags[1], yFlags[2],
m_axisLabel[1].empty() ? nullptr : m_axisLabel[1].c_str(),
m_axisLabel[2].empty() ? nullptr : m_axisLabel[2].c_str())) {
m_axis[1].label.empty() ? nullptr : m_axis[1].label.c_str(),
m_axis[2].label.empty() ? nullptr : m_axis[2].label.c_str())) {
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_seriesStorage[j - 1], m_seriesStorage[j]);
std::swap(m_series[j - 1], m_series[j]);
}
break;
case PlotSeries::kMoveDown:
if (j < (m_series.size() - 1)) {
std::swap(m_seriesStorage[j], m_seriesStorage[j + 1]);
std::swap(m_series[j], m_series[j + 1]);
}
break;
case PlotSeries::kDelete:
m_seriesStorage.erase(m_seriesStorage.begin() + j);
m_series.erase(m_series.begin() + j);
break;
default:
@@ -708,22 +596,22 @@ void Plot::EmitSettingsLimits(int axis) {
ImGui::PushID(axis);
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10);
ImGui::InputText("Label", &m_axisLabel[axis]);
ImGui::InputText("Label", &m_axis[axis].label);
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
ImGui::InputDouble("Min", &m_axisRange[axis].min, 0, 0, "%.3f");
ImGui::InputDouble("Min", &m_axis[axis].min, 0, 0, "%.3f");
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
ImGui::InputDouble("Max", &m_axisRange[axis].max, 0, 0, "%.3f");
ImGui::InputDouble("Max", &m_axis[axis].max, 0, 0, "%.3f");
ImGui::SameLine();
if (ImGui::Button("Apply")) {
m_axisRange[axis].apply = true;
m_axis[axis].apply = true;
}
ImGui::TextUnformatted("Lock Axis");
ImGui::SameLine();
ImGui::Checkbox("Min##minlock", &m_axisRange[axis].lockMin);
ImGui::Checkbox("Min##minlock", &m_axis[axis].lockMin);
ImGui::SameLine();
ImGui::Checkbox("Max##maxlock", &m_axisRange[axis].lockMax);
ImGui::Checkbox("Max##maxlock", &m_axis[axis].lockMax);
ImGui::PopID();
ImGui::Unindent();
@@ -734,18 +622,18 @@ void Plot::EmitSettings(size_t i) {
ImGui::InputText("##editname", &m_name);
ImGui::Checkbox("Visible", &m_visible);
ImGui::Checkbox("Show Pause Button", &m_showPause);
ImGui::CheckboxFlags("Hide Legend", &m_plotFlags, ImPlotFlags_NoLegend);
ImGui::Checkbox("Show Legend", &m_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) {
ImGui::Checkbox("2nd Y-Axis", &m_yAxis2);
if (m_yAxis2) {
EmitSettingsLimits(1);
}
ImGui::CheckboxFlags("3rd Y-Axis", &m_plotFlags, ImPlotFlags_YAxis3);
if ((m_plotFlags & ImPlotFlags_YAxis3) != 0) {
ImGui::Checkbox("3rd Y-Axis", &m_yAxis3);
if (m_yAxis3) {
EmitSettingsLimits(2);
}
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
@@ -778,10 +666,20 @@ int Plot::GetAutoBaseHeight(bool* isAuto, size_t i) {
return height;
}
PlotView::PlotView(PlotProvider* provider, Storage& storage)
: m_provider{provider}, m_plotsStorage{storage.GetChildArray("plots")} {
// loop over plots
for (auto&& v : m_plotsStorage) {
// create plot
m_plots.emplace_back(std::make_unique<Plot>(*v));
}
}
void PlotView::Display() {
if (ImGui::BeginPopupContextItem()) {
if (ImGui::Button("Add plot")) {
m_plots.emplace_back(std::make_unique<Plot>());
m_plotsStorage.emplace_back(std::make_unique<Storage>());
m_plots.emplace_back(std::make_unique<Plot>(*m_plotsStorage.back()));
}
for (size_t i = 0; i < m_plots.size(); ++i) {
@@ -813,6 +711,7 @@ void PlotView::Display() {
if (open) {
if (ImGui::Button("Move Up")) {
if (i > 0) {
std::swap(m_plotsStorage[i - 1], m_plotsStorage[i]);
std::swap(m_plots[i - 1], plot);
}
}
@@ -820,12 +719,14 @@ void PlotView::Display() {
ImGui::SameLine();
if (ImGui::Button("Move Down")) {
if (i < (m_plots.size() - 1)) {
std::swap(m_plotsStorage[i], m_plotsStorage[i + 1]);
std::swap(plot, m_plots[i + 1]);
}
}
ImGui::SameLine();
if (ImGui::Button("Delete")) {
m_plotsStorage.erase(m_plotsStorage.begin() + i);
m_plots.erase(m_plots.begin() + i);
ImGui::PopID();
continue;
@@ -842,7 +743,8 @@ void PlotView::Display() {
if (m_plots.empty()) {
if (ImGui::Button("Add plot")) {
m_plots.emplace_back(std::make_unique<Plot>());
m_plotsStorage.emplace_back(std::make_unique<Storage>());
m_plots.emplace_back(std::make_unique<Plot>(*m_plotsStorage.back()));
}
// Make "add plot" button a DND target for Plot
@@ -889,10 +791,21 @@ void PlotView::MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex) {
if (fromIndex == toIndex) {
return;
}
auto st = std::move(m_plotsStorage[fromIndex]);
m_plotsStorage.insert(m_plotsStorage.begin() + toIndex, std::move(st));
m_plotsStorage.erase(m_plotsStorage.begin() + fromIndex +
(fromIndex > toIndex ? 1 : 0));
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 st = std::move(fromView->m_plotsStorage[fromIndex]);
m_plotsStorage.insert(m_plotsStorage.begin() + toIndex, std::move(st));
fromView->m_plotsStorage.erase(fromView->m_plotsStorage.begin() +
fromIndex);
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);
@@ -905,6 +818,13 @@ void PlotView::MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
if (fromView == this && fromPlotIndex == toPlotIndex) {
// need to handle this specially as the index of the old location changes
if (fromSeriesIndex != toSeriesIndex) {
auto& seriesStorage = m_plots[fromPlotIndex]->m_seriesStorage;
auto st = std::move(seriesStorage[fromSeriesIndex]);
seriesStorage.insert(seriesStorage.begin() + toSeriesIndex,
std::move(st));
seriesStorage.erase(seriesStorage.begin() + fromSeriesIndex +
(fromSeriesIndex > toSeriesIndex ? 1 : 0));
auto& plotSeries = m_plots[fromPlotIndex]->m_series;
auto val = std::move(plotSeries[fromSeriesIndex]);
// only set Y-axis if actually set
@@ -920,34 +840,53 @@ void PlotView::MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
auto& toPlot = *m_plots[toPlotIndex];
// always set Y-axis if moving plots
fromPlot.m_series[fromSeriesIndex]->SetYAxis(yAxis == -1 ? 0 : yAxis);
toPlot.m_seriesStorage.insert(
toPlot.m_seriesStorage.begin() + toSeriesIndex,
std::move(fromPlot.m_seriesStorage[fromSeriesIndex]));
fromPlot.m_seriesStorage.erase(fromPlot.m_seriesStorage.begin() +
fromSeriesIndex);
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(std::string_view iniName)
: WindowManager{fmt::format("{}Window", iniName)},
m_plotSaver{iniName, this, false},
m_seriesSaver{fmt::format("{}Series", iniName), this, true} {}
PlotProvider::PlotProvider(Storage& storage) : WindowManager{storage} {
storage.SetCustomApply([this] {
// loop over windows
for (auto&& windowkv : m_storage.GetChildren()) {
// get or create window
auto win = GetOrAddWindow(windowkv.key(), true);
if (!win) {
continue;
}
PlotProvider::~PlotProvider() = default;
void PlotProvider::GlobalInit() {
WindowManager::GlobalInit();
wpi::gui::AddInit([this] {
m_plotSaver.Initialize();
m_seriesSaver.Initialize();
// get or create view
auto view = static_cast<PlotView*>(win->GetView());
if (!view) {
win->SetView(std::make_unique<PlotView>(this, windowkv.value()));
view = static_cast<PlotView*>(win->GetView());
}
}
});
storage.SetCustomClear([this] {
EraseWindows();
m_storage.EraseChildren();
});
}
PlotProvider::~PlotProvider() = default;
void PlotProvider::DisplayMenu() {
// use index-based loop due to possible RemoveWindow call
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);
RemoveWindow(i);
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
@@ -971,105 +910,9 @@ void PlotProvider::DisplayMenu() {
break;
}
}
if (auto win = AddWindow(id, std::make_unique<PlotView>(this))) {
if (auto win = AddWindow(
id, std::make_unique<PlotView>(this, m_storage.GetChild(id)))) {
win->SetDefaultSize(700, 400);
}
}
}
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(std::string_view typeName,
PlotProvider* provider, bool forSeries)
: IniSaverBase{typeName}, m_provider{provider}, m_forSeries{forSeries} {}
void* PlotProvider::IniSaver::IniReadOpen(const char* name) {
auto [viewId, plotNumStr] = wpi::split(name, '#');
std::string_view seriesId;
if (m_forSeries) {
std::tie(plotNumStr, seriesId) = wpi::split(plotNumStr, '#');
if (seriesId.empty()) {
return nullptr;
}
}
unsigned int plotNum;
if (auto plotNumOpt = wpi::parse_integer<unsigned int>(plotNumStr, 10)) {
plotNum = plotNumOpt.value();
} else {
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* line) {
auto [name, value] = wpi::split(line, '=');
name = wpi::trim(name);
value = wpi::trim(value);
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");
}
}
}
}