IQ.Pilot Release Commit @ 0b96bd5
This commit is contained in:
212
iqpilot/tools/cabana/ui/app.cc
Normal file
212
iqpilot/tools/cabana/ui/app.cc
Normal file
@@ -0,0 +1,212 @@
|
||||
#include "tools/cabana/ui/app.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_glfw.h"
|
||||
#include "imgui_impl_opengl3.h"
|
||||
#include "imgui_impl_opengl3_loader.h"
|
||||
#include "implot.h"
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/inistate.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/ui/mainwin.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<bool> g_signal_exit{false};
|
||||
std::vector<KeyEvent> g_key_events;
|
||||
void keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) {
|
||||
ImGui_ImplGlfw_KeyCallback(window, key, scancode, action, mods);
|
||||
if (action == GLFW_PRESS) g_key_events.push_back({key, mods});
|
||||
}
|
||||
|
||||
|
||||
|
||||
GLFWwindow *g_focus_lost_window = nullptr;
|
||||
|
||||
|
||||
|
||||
void windowFocusCallback(GLFWwindow *w, int f) {
|
||||
#ifdef __APPLE__
|
||||
ImGui_ImplGlfw_WindowFocusCallback(w, f);
|
||||
#else
|
||||
if (f) {
|
||||
g_focus_lost_window = nullptr;
|
||||
ImGui_ImplGlfw_WindowFocusCallback(w, f);
|
||||
} else {
|
||||
g_focus_lost_window = w;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
bool anyMouseButtonDown(GLFWwindow *w) {
|
||||
for (int b = GLFW_MOUSE_BUTTON_1; b <= GLFW_MOUSE_BUTTON_LAST; ++b) {
|
||||
if (glfwGetMouseButton(w, b) == GLFW_PRESS) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void deliverPendingFocusLoss() {
|
||||
if (g_focus_lost_window == nullptr || anyMouseButtonDown(g_focus_lost_window)) return;
|
||||
ImGui_ImplGlfw_WindowFocusCallback(g_focus_lost_window, GLFW_FALSE);
|
||||
g_focus_lost_window = nullptr;
|
||||
}
|
||||
|
||||
void hookViewportCallbacks() {
|
||||
for (ImGuiViewport *viewport : ImGui::GetPlatformIO().Viewports) {
|
||||
if (viewport->PlatformHandle == nullptr || viewport == ImGui::GetMainViewport()) continue;
|
||||
glfwSetKeyCallback((GLFWwindow *)viewport->PlatformHandle, keyCallback);
|
||||
}
|
||||
}
|
||||
|
||||
void glfwErrorCallback(int error, const char *description) {
|
||||
fprintf(stderr, "GLFW error %d: %s\n", error, description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void renderFrame(GLFWwindow *window, MainWindow *win) {
|
||||
glfwPollEvents();
|
||||
deliverPendingFocusLoss();
|
||||
utils::drainMainThreadQueue();
|
||||
|
||||
int fb_w = 0, fb_h = 0;
|
||||
glfwGetFramebufferSize(window, &fb_w, &fb_h);
|
||||
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplGlfw_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
win->draw();
|
||||
ImGui::Render();
|
||||
|
||||
const ImVec4 &bg = ImGui::GetStyle().Colors[ImGuiCol_WindowBg];
|
||||
glViewport(0, 0, fb_w, fb_h);
|
||||
glClearColor(bg.x, bg.y, bg.z, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
|
||||
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||
GLFWwindow *backup_context = glfwGetCurrentContext();
|
||||
ImGui::UpdatePlatformWindows();
|
||||
hookViewportCallbacks();
|
||||
ImGui::RenderPlatformWindowsDefault();
|
||||
glfwMakeContextCurrent(backup_context);
|
||||
}
|
||||
glfwSwapBuffers(window);
|
||||
}
|
||||
|
||||
class GlfwRuntime {
|
||||
public:
|
||||
GlfwRuntime() {
|
||||
glfwSetErrorCallback(glfwErrorCallback);
|
||||
#ifdef __APPLE__
|
||||
setMacAppName("Cabana");
|
||||
#endif
|
||||
if (!glfwInit()) throw std::runtime_error("glfwInit failed");
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
#ifdef __APPLE__
|
||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
|
||||
#endif
|
||||
window_ = glfwCreateWindow(1600, 900, "Cabana", nullptr, nullptr);
|
||||
if (window_ == nullptr) {
|
||||
glfwTerminate();
|
||||
throw std::runtime_error("glfwCreateWindow failed");
|
||||
}
|
||||
glfwMakeContextCurrent(window_);
|
||||
glfwSwapInterval(1);
|
||||
}
|
||||
|
||||
~GlfwRuntime() {
|
||||
if (window_ != nullptr) glfwDestroyWindow(window_);
|
||||
glfwTerminate();
|
||||
}
|
||||
|
||||
GlfwRuntime(const GlfwRuntime &) = delete;
|
||||
GlfwRuntime &operator=(const GlfwRuntime &) = delete;
|
||||
GLFWwindow *window() const { return window_; }
|
||||
|
||||
private:
|
||||
GLFWwindow *window_ = nullptr;
|
||||
};
|
||||
|
||||
class ImGuiRuntime {
|
||||
public:
|
||||
explicit ImGuiRuntime(GLFWwindow *window) {
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImPlot::CreateContext();
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
||||
io.ConfigViewportsNoDecoration = false;
|
||||
io.IniFilename = nullptr;
|
||||
io.LogFilename = nullptr;
|
||||
if (!ImGui_ImplGlfw_InitForOpenGL(window, true)) {
|
||||
ImPlot::DestroyContext();
|
||||
ImGui::DestroyContext();
|
||||
throw std::runtime_error("ImGui_ImplGlfw_InitForOpenGL failed");
|
||||
}
|
||||
glfwSetKeyCallback(window, keyCallback);
|
||||
glfwSetWindowFocusCallback(window, windowFocusCallback);
|
||||
if (!ImGui_ImplOpenGL3_Init("#version 330")) {
|
||||
ImGui_ImplGlfw_Shutdown();
|
||||
ImPlot::DestroyContext();
|
||||
ImGui::DestroyContext();
|
||||
throw std::runtime_error("ImGui_ImplOpenGL3_Init failed");
|
||||
}
|
||||
}
|
||||
|
||||
~ImGuiRuntime() {
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplGlfw_Shutdown();
|
||||
ImPlot::DestroyContext();
|
||||
ImGui::DestroyContext();
|
||||
}
|
||||
|
||||
ImGuiRuntime(const ImGuiRuntime &) = delete;
|
||||
ImGuiRuntime &operator=(const ImGuiRuntime &) = delete;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
std::vector<KeyEvent> takeKeyEvents() {
|
||||
return std::exchange(g_key_events, {});
|
||||
}
|
||||
|
||||
int run(std::unique_ptr<AbstractStream> stream, StreamLoader stream_loader, const std::string &dbc_file) {
|
||||
try {
|
||||
|
||||
UnixSignalHandler signal_handler([]() { g_signal_exit = true; });
|
||||
|
||||
GlfwRuntime glfw;
|
||||
ImGuiRuntime imgui(glfw.window());
|
||||
loadFonts();
|
||||
applyTheme(settings.theme);
|
||||
inistate::addSettingsHandler();
|
||||
inistate::load();
|
||||
inistate::applyWindowGeometry(glfw.window());
|
||||
|
||||
MainWindow win(glfw.window(), std::move(stream), std::move(stream_loader), dbc_file);
|
||||
while (!win.exited()) {
|
||||
if (g_signal_exit.exchange(false)) {
|
||||
printf("\nexiting...\n");
|
||||
win.close();
|
||||
} else if (glfwWindowShouldClose(glfw.window())) {
|
||||
glfwSetWindowShouldClose(glfw.window(), GLFW_FALSE);
|
||||
win.close();
|
||||
}
|
||||
renderFrame(glfw.window(), &win);
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::exception &e) {
|
||||
fprintf(stderr, "%s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
23
iqpilot/tools/cabana/ui/app.h
Normal file
23
iqpilot/tools/cabana/ui/app.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
|
||||
using StreamLoader = std::function<std::unique_ptr<AbstractStream>()>;
|
||||
|
||||
|
||||
|
||||
int run(std::unique_ptr<AbstractStream> stream, StreamLoader stream_loader, const std::string &dbc_file);
|
||||
|
||||
|
||||
|
||||
struct KeyEvent {
|
||||
int key;
|
||||
int mods;
|
||||
};
|
||||
std::vector<KeyEvent> takeKeyEvents();
|
||||
790
iqpilot/tools/cabana/ui/chart/chart.cc
Normal file
790
iqpilot/tools/cabana/ui/chart/chart.cc
Normal file
@@ -0,0 +1,790 @@
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include "tools/cabana/ui/chart/chart.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
|
||||
#include "tools/cabana/core/settings.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
const int AXIS_X_TOP_MARGIN = 4;
|
||||
const int X_TICK_COUNT = 5;
|
||||
const double MIN_ZOOM_SECONDS = 0.01;
|
||||
const double EPSILON = 1e-6;
|
||||
constexpr ImVec4 LAYOUT_MARGINS{8, 6, 8, 6};
|
||||
static inline bool xLessThan(const ImPlotPoint &p, double x) { return p.x < (x - EPSILON); }
|
||||
static inline bool isNull(const ImPlotPoint &p) { return p.x == 0 && p.y == 0; }
|
||||
|
||||
static std::string formatNumber(double value, int precision) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.*f", precision, value);
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
static int axisPrecision(double range, int tick_count, int min_precision) {
|
||||
return std::max(int(-std::floor(std::log10(range / (tick_count - 1)))), min_precision);
|
||||
}
|
||||
|
||||
static void addTextEllipsis(ImDrawList *dl, ImFont *font, ImU32 col, const ImVec2 &pos, float max_x, const std::string &text) {
|
||||
const float size = ImGui::GetFontSize();
|
||||
ImGui::PushFont(font, 0.0f);
|
||||
ImGui::RenderTextEllipsis(dl, pos, ImVec2(max_x, pos.y + size), max_x, text.c_str(), nullptr, nullptr);
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ChartView::ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent)
|
||||
: x_min_(x_range.first), x_max_(x_range.second), charts_widget_(parent) {
|
||||
series_type_ = (SeriesType)settings.chart_series_type;
|
||||
|
||||
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { signalRemoved(sig); }));
|
||||
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { signalUpdated(sig); }));
|
||||
connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { msgRemoved(id); }));
|
||||
}
|
||||
|
||||
void ChartView::drawMenuActions() {
|
||||
|
||||
const float indent = ImGui::GetFontSize();
|
||||
float label_width = ImGui::CalcTextSize("Manage Signals").x;
|
||||
for (const char *type : SERIES_TYPE_NAMES) label_width = std::max(label_width, ImGui::CalcTextSize(type).x);
|
||||
for (int i = 0; i < (int)std::size(SERIES_TYPE_NAMES); ++i) {
|
||||
if (radioMenuItem(SERIES_TYPE_NAMES[i], i == (int)series_type_, indent + label_width + indent)) {
|
||||
setSeriesType((SeriesType)i);
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::Indent(indent);
|
||||
if (ImGui::MenuItem("Manage Signals")) manageSignals();
|
||||
if (ImGui::MenuItem("Split Chart", nullptr, false, sigs_.size() > 1)) charts_widget_->splitChart(this);
|
||||
ImGui::Unindent(indent);
|
||||
}
|
||||
|
||||
|
||||
void ChartView::createToolButtons() {
|
||||
ImGui::SetCursorScreenPos(layout_.close_btn_rect.Min);
|
||||
bool close_clicked = toolButton("close_btn", icon::X, "Remove Chart");
|
||||
|
||||
ImGui::SetCursorScreenPos(layout_.manage_btn_rect.Min);
|
||||
if (toolButton("manage_btn", icon::LIST, "")) ImGui::OpenPopup("manage_menu");
|
||||
if (ImGui::BeginPopup("manage_menu")) {
|
||||
drawMenuActions();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (close_clicked) charts_widget_->removeChart(this);
|
||||
}
|
||||
|
||||
void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) {
|
||||
if (hasSignal(msg_id, sig)) return;
|
||||
|
||||
sigs_.push_back({.msg_id = msg_id, .sig = sig, .color = uniqueColor(sig->color)});
|
||||
updateSeries(sig);
|
||||
charts_widget_->seriesChanged();
|
||||
}
|
||||
|
||||
bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const {
|
||||
return std::any_of(sigs_.cbegin(), sigs_.cend(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
|
||||
}
|
||||
|
||||
void ChartView::removeIf(std::function<bool(const SigItem &s)> predicate) {
|
||||
int prev_size = sigs_.size();
|
||||
sigs_.erase(std::remove_if(sigs_.begin(), sigs_.end(), predicate), sigs_.end());
|
||||
if (sigs_.empty()) {
|
||||
charts_widget_->removeChart(this);
|
||||
} else if (sigs_.size() != prev_size) {
|
||||
charts_widget_->seriesChanged();
|
||||
updateAxisY();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::signalUpdated(const cabana::Signal *sig) {
|
||||
auto it = std::find_if(sigs_.begin(), sigs_.end(), [sig](auto &s) { return s.sig == sig; });
|
||||
if (it != sigs_.end()) {
|
||||
if (!(it->color == sig->color)) {
|
||||
it->color = uniqueColor(sig->color, sig);
|
||||
}
|
||||
updateSeries(sig);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::manageSignals() {
|
||||
auto dlg = std::make_unique<SignalSelector>("Manage Chart");
|
||||
for (auto &s : sigs_) {
|
||||
dlg->addSelected(s.msg_id, s.sig);
|
||||
}
|
||||
|
||||
charts_widget_->execSignalSelector(std::move(dlg), this, [this](SignalSelector &selector) {
|
||||
const auto &items = selector.selectedItems();
|
||||
for (const auto &s : items) {
|
||||
addSignal(s.msg_id, s.sig);
|
||||
}
|
||||
removeIf([&](auto &s) {
|
||||
return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it.msg_id && s.sig == it.sig; });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ChartView::updateLayout() {
|
||||
const ImVec2 grip = ImGui::CalcTextSize(icon::GRIP_HORIZONTAL);
|
||||
const ImVec2 top_left = layout_.rect.Min + ImVec2(LAYOUT_MARGINS.x, LAYOUT_MARGINS.y);
|
||||
layout_.move_icon_rect = ImRect(top_left, top_left + grip);
|
||||
const ImVec2 pad = ImGui::GetStyle().FramePadding * 2;
|
||||
const ImVec2 close_size = ImGui::CalcTextSize(icon::X) + pad;
|
||||
const ImVec2 manage_size = ImGui::CalcTextSize(icon::LIST) + pad;
|
||||
const ImVec2 close_min(layout_.rect.Max.x - LAYOUT_MARGINS.z - close_size.x, top_left.y);
|
||||
layout_.close_btn_rect = ImRect(close_min, close_min + close_size);
|
||||
const ImVec2 manage_min(close_min.x - manage_size.x - ImGui::GetStyle().ItemSpacing.x, top_left.y);
|
||||
layout_.manage_btn_rect = ImRect(manage_min, manage_min + manage_size);
|
||||
|
||||
ImFont *bold = boldFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float fm_height = ImGui::GetTextLineHeight();
|
||||
const int marker_size = markerSize();
|
||||
const int row_height = std::max<int>(marker_size, fm_height) + fm_height + 3;
|
||||
const int legend_left = layout_.move_icon_rect.Max.x + LAYOUT_MARGINS.x;
|
||||
const int legend_right = std::max<int>(layout_.manage_btn_rect.Min.x - LAYOUT_MARGINS.z, legend_left + 10);
|
||||
|
||||
|
||||
layout_.legend_rects.clear();
|
||||
int x = legend_left, y = top_left.y;
|
||||
for (auto &s : sigs_) {
|
||||
int w = marker_size + 5 + bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x +
|
||||
ImGui::CalcTextSize(msgLabel(s.msg_id).c_str()).x;
|
||||
w = std::min(w, legend_right - legend_left);
|
||||
if (x + w > legend_right && x > legend_left) {
|
||||
x = legend_left;
|
||||
y += row_height;
|
||||
}
|
||||
layout_.legend_rects.emplace_back(ImVec2(x, y), ImVec2(x + w, y + std::max<int>(marker_size, fm_height)));
|
||||
x += w + 12;
|
||||
}
|
||||
|
||||
|
||||
int adjust_top = (y + row_height) - top_left.y;
|
||||
adjust_top = std::max<int>(adjust_top, layout_.manage_btn_rect.Max.y - layout_.rect.Min.y + LAYOUT_MARGINS.y);
|
||||
layout_.header_bottom = layout_.rect.Min.y + adjust_top + LAYOUT_MARGINS.y;
|
||||
}
|
||||
|
||||
void ChartView::updatePlot(double cur, double min, double max) {
|
||||
cur_sec_ = cur;
|
||||
if (min != x_min_ || max != x_max_) {
|
||||
x_min_ = min;
|
||||
x_max_ = max;
|
||||
updateAxisY();
|
||||
if (tooltip_x_ >= 0) {
|
||||
showTip(secondsAtPoint({(float)tooltip_x_, 0}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
|
||||
std::vector<ImPlotPoint> &vals, std::vector<ImPlotPoint> &step_vals) {
|
||||
vals.reserve(vals.size() + events.size());
|
||||
step_vals.reserve(step_vals.size() + events.size() * 2);
|
||||
|
||||
double value = 0;
|
||||
for (const CanEvent *e : events) {
|
||||
if (sig->getValue(e->dat, e->size, &value)) {
|
||||
const double ts = can->toSeconds(e->mono_time);
|
||||
vals.emplace_back(ts, value);
|
||||
if (!step_vals.empty())
|
||||
step_vals.emplace_back(ts, step_vals.back().y);
|
||||
step_vals.emplace_back(ts, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap *msg_new_events) {
|
||||
for (auto &s : sigs_) {
|
||||
if (!sig || s.sig == sig) {
|
||||
if (!msg_new_events) {
|
||||
s.vals.clear();
|
||||
s.step_vals.clear();
|
||||
}
|
||||
auto events = msg_new_events ? msg_new_events : &can->eventsMap();
|
||||
auto it = events->find(s.msg_id);
|
||||
if (it == events->end() || it->second.empty()) continue;
|
||||
|
||||
if (s.vals.empty() || can->toSeconds(it->second.back()->mono_time) > s.vals.back().x) {
|
||||
appendCanEvents(s.sig, it->second, s.vals, s.step_vals);
|
||||
} else {
|
||||
std::vector<ImPlotPoint> vals, step_vals;
|
||||
appendCanEvents(s.sig, it->second, vals, step_vals);
|
||||
if (vals.empty()) continue;
|
||||
s.vals.insert(std::lower_bound(s.vals.begin(), s.vals.end(), vals.front().x, xLessThan),
|
||||
vals.begin(), vals.end());
|
||||
s.step_vals.insert(std::lower_bound(s.step_vals.begin(), s.step_vals.end(), step_vals.front().x, xLessThan),
|
||||
step_vals.begin(), step_vals.end());
|
||||
}
|
||||
|
||||
if (!can->liveStreaming()) {
|
||||
s.segment_tree.build(s.vals.size(), [&vals = s.vals](int i) { return vals[i].y; });
|
||||
}
|
||||
}
|
||||
}
|
||||
updateAxisY();
|
||||
}
|
||||
|
||||
std::pair<ChartView::PointIter, ChartView::PointIter> ChartView::visibleRange(const std::vector<ImPlotPoint> &points) const {
|
||||
auto first = std::lower_bound(points.cbegin(), points.cend(), x_min_, xLessThan);
|
||||
auto last = std::lower_bound(first, points.cend(), x_max_, xLessThan);
|
||||
return {first, last};
|
||||
}
|
||||
|
||||
const ImPlotPoint *ChartView::lastPointBefore(const SigItem &s, double sec) const {
|
||||
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double x) { return p.x > x + EPSILON; });
|
||||
return it != s.vals.crend() && it->x >= x_min_ ? &*it : nullptr;
|
||||
}
|
||||
|
||||
void ChartView::updateAxisY() {
|
||||
if (sigs_.empty()) return;
|
||||
|
||||
double min = std::numeric_limits<double>::max();
|
||||
double max = std::numeric_limits<double>::lowest();
|
||||
std::string unit = sigs_[0].sig->unit;
|
||||
|
||||
for (auto &s : sigs_) {
|
||||
if (!s.visible) continue;
|
||||
|
||||
|
||||
if (unit != s.sig->unit) {
|
||||
unit.clear();
|
||||
}
|
||||
|
||||
auto [first, last] = visibleRange(s.vals);
|
||||
s.min = std::numeric_limits<double>::max();
|
||||
s.max = std::numeric_limits<double>::lowest();
|
||||
if (can->liveStreaming()) {
|
||||
for (auto it = first; it != last; ++it) {
|
||||
if (it->y < s.min) s.min = it->y;
|
||||
if (it->y > s.max) s.max = it->y;
|
||||
}
|
||||
} else {
|
||||
std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last));
|
||||
}
|
||||
min = std::min(min, s.min);
|
||||
max = std::max(max, s.max);
|
||||
}
|
||||
if (min == std::numeric_limits<double>::max()) min = 0;
|
||||
if (max == std::numeric_limits<double>::lowest()) max = 0;
|
||||
|
||||
y_unit_ = unit;
|
||||
|
||||
double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05;
|
||||
auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3);
|
||||
if (min_y != y_min_ || max_y != y_max_) {
|
||||
y_min_ = min_y;
|
||||
y_max_ = max_y;
|
||||
y_tick_count_ = tick_count;
|
||||
y_precision_ = axisPrecision(max_y - min_y, tick_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<double, double, int> ChartView::getNiceAxisNumbers(double min, double max, int tick_count) {
|
||||
double range = niceNumber((max - min), true);
|
||||
double step = niceNumber(range / (tick_count - 1), false);
|
||||
min = std::floor(min / step);
|
||||
max = std::ceil(max / step);
|
||||
tick_count = int(max - min) + 1;
|
||||
return {min * step, max * step, tick_count};
|
||||
}
|
||||
|
||||
int ChartView::xAxisPrecision() const {
|
||||
return axisPrecision(x_max_ - x_min_, X_TICK_COUNT, 2);
|
||||
}
|
||||
|
||||
|
||||
double ChartView::niceNumber(double x, bool ceiling) {
|
||||
double z = std::pow(10, std::floor(std::log10(x)));
|
||||
double q = x / z;
|
||||
if (ceiling) {
|
||||
if (q <= 1.0) q = 1;
|
||||
else if (q <= 2.0) q = 2;
|
||||
else if (q <= 5.0) q = 5;
|
||||
else q = 10;
|
||||
} else {
|
||||
if (q < 1.5) q = 1;
|
||||
else if (q < 3.0) q = 2;
|
||||
else if (q < 7.0) q = 5;
|
||||
else q = 10;
|
||||
}
|
||||
return q * z;
|
||||
}
|
||||
|
||||
void ChartView::drawContextMenu() {
|
||||
if (drawing_ghost_) return;
|
||||
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) &&
|
||||
!ImGui::IsAnyItemActive()) {
|
||||
ImGui::OpenPopup("context_menu");
|
||||
}
|
||||
context_menu_id_ = ImGui::GetID("context_menu");
|
||||
if (ImGui::BeginPopup("context_menu")) {
|
||||
drawMenuActions();
|
||||
|
||||
const float indent = ImGui::GetFontSize();
|
||||
ImGui::Indent(indent);
|
||||
ImGui::Separator();
|
||||
|
||||
if (can->timeRange().has_value()) {
|
||||
const std::string undo_text = std::string(icon::ARROW_COUNTERCLOCKWISE) + " Undo Zoom";
|
||||
const std::string redo_text = std::string(icon::ARROW_CLOCKWISE) + " Redo Zoom";
|
||||
if (ImGui::MenuItem(undo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canUndo())) charts_widget_->zoom_undo_stack_.undo();
|
||||
if (ImGui::MenuItem(redo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canRedo())) charts_widget_->zoom_undo_stack_.redo();
|
||||
ImGui::Separator();
|
||||
}
|
||||
if (ImGui::MenuItem("Close")) charts_widget_->removeChart(this);
|
||||
ImGui::Unindent(indent);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::handleMousePress() {
|
||||
if (drawing_ghost_) return;
|
||||
const ImVec2 pos = ImGui::GetMousePos();
|
||||
|
||||
const bool widget_pressed = ImGui::IsMouseClicked(ImGuiMouseButton_Left) && layout_.rect.Contains(pos) &&
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
|
||||
!layout_.close_btn_rect.Contains(pos) && !layout_.manage_btn_rect.Contains(pos);
|
||||
if (!widget_pressed) return;
|
||||
press_pos_ = pos;
|
||||
if (layout_.move_icon_rect.Contains(pos)) return;
|
||||
|
||||
if (ImGui::GetIO().KeyShift) {
|
||||
|
||||
resume_after_scrub_ = !can->isPaused();
|
||||
if (resume_after_scrub_) {
|
||||
can->pause(true);
|
||||
}
|
||||
mouse_mode_ = MouseMode::Scrub;
|
||||
} else if (layout_.plot_area.Contains(pos)) {
|
||||
mouse_mode_ = MouseMode::Rubber;
|
||||
rubber_rect_ = ImRect();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::handleMouseMove() {
|
||||
if (drawing_ghost_) return;
|
||||
const ImVec2 pos = ImGui::GetMousePos();
|
||||
const ImVec2 delta = ImGui::GetIO().MouseDelta;
|
||||
|
||||
if (delta.x == 0 && delta.y == 0) return;
|
||||
|
||||
if (mouse_mode_ == MouseMode::None && !layout_.rect.Contains(pos)) return;
|
||||
|
||||
if (mouse_mode_ == MouseMode::Scrub && ImGui::GetIO().KeyShift) {
|
||||
if (layout_.plot_area.Contains(pos)) {
|
||||
can->seekTo(std::clamp(secondsAtPoint(pos), can->minSeconds(), can->maxSeconds()));
|
||||
}
|
||||
}
|
||||
|
||||
if (mouse_mode_ == MouseMode::Rubber) {
|
||||
|
||||
float left = std::clamp(std::min(press_pos_.x, pos.x), layout_.plot_area.Min.x, layout_.plot_area.Max.x);
|
||||
float right = std::clamp(std::max(press_pos_.x, pos.x), layout_.plot_area.Min.x, layout_.plot_area.Max.x);
|
||||
rubber_rect_ = ImRect(ImVec2(left, layout_.plot_area.Min.y), ImVec2(right, layout_.plot_area.Max.y));
|
||||
}
|
||||
|
||||
clearTrackPoints();
|
||||
if (mouse_mode_ != MouseMode::Rubber && layout_.plot_area.Contains(pos) && (layout_.plot_hovered || mouse_mode_ != MouseMode::None) &&
|
||||
ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) {
|
||||
charts_widget_->showValueTip(secondsAtPoint(pos));
|
||||
} else if (tip_label_.isVisible()) {
|
||||
charts_widget_->showValueTip(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::handleMouseRelease() {
|
||||
if (drawing_ghost_) return;
|
||||
const bool left_released = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
||||
const bool right_released = ImGui::IsMouseReleased(ImGuiMouseButton_Right) && layout_.rect.Contains(ImGui::GetMousePos());
|
||||
if (!left_released && !right_released) return;
|
||||
if (left_released && mouse_mode_ == MouseMode::Rubber) {
|
||||
mouse_mode_ = MouseMode::None;
|
||||
|
||||
double min = std::clamp(secondsAtPoint(rubber_rect_.Min), can->minSeconds(), can->maxSeconds());
|
||||
double max = std::clamp(secondsAtPoint(rubber_rect_.Max), can->minSeconds(), can->maxSeconds());
|
||||
if (rubber_rect_.GetWidth() <= 0) {
|
||||
|
||||
can->seekTo(std::clamp(secondsAtPoint(press_pos_), can->minSeconds(), can->maxSeconds()));
|
||||
} else if (rubber_rect_.GetWidth() > 10 && (max - min) > MIN_ZOOM_SECONDS) {
|
||||
charts_widget_->zoom_undo_stack_.push(new ZoomCommand({min, max}));
|
||||
}
|
||||
rubber_rect_ = ImRect();
|
||||
} else if (right_released && !ImGui::IsPopupOpen(context_menu_id_, ImGuiPopupFlags_None)) {
|
||||
charts_widget_->zoom_undo_stack_.undo();
|
||||
}
|
||||
|
||||
if (mouse_mode_ == MouseMode::Scrub) {
|
||||
mouse_mode_ = MouseMode::None;
|
||||
if (resume_after_scrub_) {
|
||||
can->pause(false);
|
||||
resume_after_scrub_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::takeSignalsFrom(ChartView *source) {
|
||||
for (auto &s : source->sigs_) {
|
||||
sigs_.push_back(std::move(s));
|
||||
sigs_.back().color = uniqueColor(sigs_.back().color, sigs_.back().sig);
|
||||
}
|
||||
source->sigs_.clear();
|
||||
updateAxisY();
|
||||
charts_widget_->removeChart(source);
|
||||
}
|
||||
|
||||
std::vector<ChartView::SigItem> ChartView::takeExtraSignals() {
|
||||
std::vector<SigItem> extra;
|
||||
for (auto it = sigs_.begin() + 1; it != sigs_.end(); ++it) {
|
||||
it->color = it->sig->color;
|
||||
extra.push_back(std::move(*it));
|
||||
}
|
||||
sigs_.resize(1);
|
||||
updateAxisY();
|
||||
return extra;
|
||||
}
|
||||
|
||||
void ChartView::adoptSignal(SigItem s) {
|
||||
sigs_.push_back(std::move(s));
|
||||
updateAxisY();
|
||||
}
|
||||
|
||||
void ChartView::showTip(double sec) {
|
||||
ImRect tip_area(ImVec2(layout_.rect.Min.x, layout_.plot_area.Min.y), ImVec2(layout_.rect.Max.x, layout_.plot_area.Max.y));
|
||||
ImRect visible_rect = charts_widget_->chartVisibleRect(this);
|
||||
visible_rect.ClipWith(tip_area);
|
||||
if (visible_rect.GetWidth() <= 0 || visible_rect.GetHeight() <= 0) {
|
||||
tip_label_.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
tooltip_x_ = xPos(sec);
|
||||
float x = -1;
|
||||
std::vector<TipLine> text_list;
|
||||
for (auto &s : sigs_) {
|
||||
if (s.visible) {
|
||||
std::string value = "--";
|
||||
if (const ImPlotPoint *pt = lastPointBefore(s, sec)) {
|
||||
value = s.sig->formatValue(pt->y, false);
|
||||
s.track_pt = *pt;
|
||||
x = std::max(x, xPos(pt->x));
|
||||
}
|
||||
std::string name = sigs_.size() > 1 ? s.sig->name + ": " : "";
|
||||
std::string min = s.min == std::numeric_limits<double>::max() ? "--" : utils::toString(s.min);
|
||||
std::string max = s.max == std::numeric_limits<double>::lowest() ? "--" : utils::toString(s.max);
|
||||
text_list.push_back({.has_marker = true, .marker = toImU32(s.color), .name = name, .bold = value, .rest = " (" + min + ", " + max + ")"});
|
||||
}
|
||||
}
|
||||
if (x < 0) {
|
||||
x = tooltip_x_;
|
||||
}
|
||||
ImVec2 pt(x, layout_.plot_area.Min.y);
|
||||
text_list.insert(text_list.begin(), TipLine{.name = formatNumber(secondsAtPoint({x, 0}), 3)});
|
||||
tip_label_.showText(pt, text_list, visible_rect);
|
||||
}
|
||||
|
||||
void ChartView::hideTip() {
|
||||
clearTrackPoints();
|
||||
tooltip_x_ = -1;
|
||||
tip_label_.hide();
|
||||
}
|
||||
|
||||
void ChartView::draw(float width) {
|
||||
ImGui::PushID(this);
|
||||
width = std::max(width, (float)CHART_MIN_WIDTH);
|
||||
layout_.plot_hovered = false;
|
||||
|
||||
const ImVec2 tile_pos = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 tile_size(width, (float)settings.chart_height);
|
||||
layout_.rect = ImRect(tile_pos, tile_pos + tile_size);
|
||||
if (ImGui::BeginChild("chart", tile_size, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
|
||||
updateLayout();
|
||||
paint();
|
||||
drawContextMenu();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
const ImRect visible_rect = charts_widget_->chartVisibleRect(this);
|
||||
if (!drawing_ghost_ && visible_rect.GetWidth() > 0 && visible_rect.GetHeight() > 0) tip_label_.draw();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
void ChartView::drawGhost(float width) {
|
||||
|
||||
drawing_ghost_ = true;
|
||||
const Layout saved = layout_;
|
||||
draw(width);
|
||||
layout_ = saved;
|
||||
drawing_ghost_ = false;
|
||||
}
|
||||
|
||||
void ChartView::paint() {
|
||||
drawStaticLayer();
|
||||
|
||||
if (can_drop_) {
|
||||
ImGui::GetWindowDrawList()->AddRect(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_Header), 0.0f, 0, 4.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::drawStaticLayer() {
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
painter->AddRectFilled(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_ChildBg));
|
||||
ImGui::SetCursorScreenPos(layout_.move_icon_rect.Min);
|
||||
ImGui::InvisibleButton("grip", layout_.move_icon_rect.GetSize());
|
||||
if (ImGui::IsItemActivated()) charts_widget_->startChartDrag(this, ImGui::GetMousePos());
|
||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
painter->AddText(layout_.move_icon_rect.Min, ImGui::GetColorU32(ImGuiCol_Text), icon::GRIP_HORIZONTAL);
|
||||
createToolButtons();
|
||||
drawLegend();
|
||||
drawSignalValue();
|
||||
drawAxes();
|
||||
}
|
||||
|
||||
void ChartView::drawAxes() {
|
||||
ImGui::SetCursorScreenPos(ImVec2(layout_.rect.Min.x, layout_.header_bottom));
|
||||
const float plot_h = std::max(layout_.rect.Max.y - layout_.header_bottom - LAYOUT_MARGINS.w, 10.0f);
|
||||
ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(LAYOUT_MARGINS.x, AXIS_X_TOP_MARGIN));
|
||||
ImPlot::PushStyleColor(ImPlotCol_PlotBg, ImVec4(0, 0, 0, 0));
|
||||
ImPlot::PushStyleColor(ImPlotCol_FrameBg, ImVec4(0, 0, 0, 0));
|
||||
|
||||
|
||||
const bool dark = isDarkTheme();
|
||||
ImVec4 grid_color;
|
||||
if (dark) {
|
||||
grid_color = colorRgb(DarkTheme::light.r, DarkTheme::light.g, DarkTheme::light.b);
|
||||
} else {
|
||||
grid_color = ImGui::GetStyleColorVec4(ImGuiCol_Text);
|
||||
grid_color.w = 50.0f / 255.0f;
|
||||
}
|
||||
ImPlot::PushStyleColor(ImPlotCol_AxisGrid, grid_color);
|
||||
ImPlot::PushStyleColor(ImPlotCol_PlotBorder, grid_color);
|
||||
ImPlot::PushStyleColor(ImPlotCol_AxisTick, ImVec4(0, 0, 0, 0));
|
||||
ImPlot::PushStyleColor(ImPlotCol_AxisText, ImGui::GetStyleColorVec4(ImGuiCol_Text));
|
||||
ImPlot::PushStyleVar(ImPlotStyleVar_MajorTickLen, ImVec2(0, 0));
|
||||
|
||||
ImPlot::PushStyleVar(ImPlotStyleVar_MajorGridSize, dark ? ImVec2(2.0f, 2.0f) : ImVec2(1.0f, 1.0f));
|
||||
const ImPlotFlags flags = ImPlotFlags_NoTitle | ImPlotFlags_NoLegend | ImPlotFlags_NoMenus | ImPlotFlags_NoMouseText |
|
||||
ImPlotFlags_NoBoxSelect | ImPlotFlags_NoInputs | ImPlotFlags_NoFrame;
|
||||
const ImPlotAxisFlags axis_flags = ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_NoSideSwitch | ImPlotAxisFlags_Lock;
|
||||
|
||||
const float x_label_width = ImGui::CalcTextSize(formatNumber(x_max_, xAxisPrecision()).c_str()).x + 5;
|
||||
if (ImPlot::BeginPlot("##plot", ImVec2(layout_.rect.GetWidth() - x_label_width / 2, plot_h), flags)) {
|
||||
ImPlot::SetupAxis(ImAxis_X1, nullptr, axis_flags);
|
||||
ImPlot::SetupAxis(ImAxis_Y1, y_unit_.empty() ? nullptr : y_unit_.c_str(), axis_flags);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, x_min_, x_max_, ImPlotCond_Always);
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, y_min_, y_max_, ImPlotCond_Always);
|
||||
|
||||
ImPlot::SetupAxisFormat(ImAxis_Y1, ("%." + std::to_string(y_precision_) + "f").c_str());
|
||||
ImPlot::SetupAxisTicks(ImAxis_Y1, y_min_, y_max_, y_tick_count_);
|
||||
ImPlot::SetupAxisFormat(ImAxis_X1, ("%." + std::to_string(xAxisPrecision()) + "f").c_str());
|
||||
ImPlot::SetupAxisTicks(ImAxis_X1, x_min_, x_max_, X_TICK_COUNT);
|
||||
ImPlot::SetupFinish();
|
||||
|
||||
layout_.plot_area = ImRect(ImPlot::GetPlotPos(), ImPlot::GetPlotPos() + ImPlot::GetPlotSize());
|
||||
|
||||
layout_.plot_hovered = layout_.plot_area.Contains(ImGui::GetMousePos()) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
||||
drawSeries();
|
||||
handleMousePress();
|
||||
handleMouseMove();
|
||||
handleMouseRelease();
|
||||
drawForeground();
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
ImPlot::PopStyleColor(6);
|
||||
ImPlot::PopStyleVar(3);
|
||||
}
|
||||
|
||||
void ChartView::drawLegend() {
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
const ImU32 title_color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
|
||||
const ImU32 msg_color = withAlpha(title_color, 180);
|
||||
ImFont *bold = boldFont();
|
||||
ImFont *normal = ImGui::GetFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float marker_size = markerSize();
|
||||
|
||||
for (int i = 0; i < sigs_.size() && i < layout_.legend_rects.size(); ++i) {
|
||||
const auto &s = sigs_[i];
|
||||
const ImRect &r = layout_.legend_rects[i];
|
||||
|
||||
ImGui::PushID(i);
|
||||
ImGui::SetCursorScreenPos(r.Min);
|
||||
if (ImGui::InvisibleButton("legend", ImVec2(std::max(r.GetWidth(), 1.0f), std::max(r.GetHeight(), 1.0f))) &&
|
||||
mouse_mode_ == MouseMode::None && sigs_.size() > 1) {
|
||||
sigs_[i].visible = !sigs_[i].visible;
|
||||
updateAxisY();
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
if (series_type_ == SeriesType::Scatter) {
|
||||
painter->AddCircleFilled(r.Min + ImVec2(marker_size / 2.0f, 2.0f + marker_size / 2.0f), marker_size / 2.0f, toImU32(s.color));
|
||||
} else {
|
||||
drawColorMarker(painter, r.Min, toImU32(s.color));
|
||||
}
|
||||
|
||||
float x = r.Min.x + marker_size + 5;
|
||||
const float text_y = r.GetCenter().y - font_size / 2.0f;
|
||||
addTextEllipsis(painter, bold, title_color, ImVec2(x, text_y), r.Max.x, s.sig->name);
|
||||
float name_w = std::min(bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x, r.Max.x - x);
|
||||
x += name_w;
|
||||
const std::string msg = msgLabel(s.msg_id);
|
||||
addTextEllipsis(painter, normal, msg_color, ImVec2(x, text_y), r.Max.x, msg);
|
||||
if (!s.visible) {
|
||||
const float y = r.GetCenter().y;
|
||||
painter->AddLine(ImVec2(r.Min.x + marker_size + 5, y), ImVec2(std::min(x + ImGui::CalcTextSize(msg.c_str()).x, r.Max.x), y), title_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::drawSeries() {
|
||||
for (int i = 0; i < sigs_.size(); ++i) {
|
||||
auto &s = sigs_[i];
|
||||
if (!s.visible) continue;
|
||||
|
||||
|
||||
auto [first, last] = visibleRange(s.vals);
|
||||
int num_points = std::max<int>(last - first, 1);
|
||||
double pixels_per_point = 0;
|
||||
if (first != last) {
|
||||
const ImPlotPoint &right_pt = last == s.vals.cend() ? s.vals.back() : *last;
|
||||
pixels_per_point = (xPos(right_pt.x) - xPos(first->x)) / num_points;
|
||||
}
|
||||
|
||||
const std::string label = "##sig" + std::to_string(i);
|
||||
ImPlotSpec spec;
|
||||
spec.LineColor = toImVec4(s.color);
|
||||
spec.Stride = sizeof(ImPlotPoint);
|
||||
if (series_type_ == SeriesType::Scatter) {
|
||||
float radius = std::clamp(pixels_per_point / 2.0, 2.0, 8.0) / 2.0;
|
||||
spec.Marker = ImPlotMarker_Circle;
|
||||
spec.MarkerSize = radius;
|
||||
if (first != last) ImPlot::PlotScatter(label.c_str(), &first->x, &first->y, last - first, spec);
|
||||
} else {
|
||||
const auto &points = series_type_ == SeriesType::StepLine ? s.step_vals : s.vals;
|
||||
|
||||
auto [begin, end] = visibleRange(points);
|
||||
if (begin != points.cbegin()) --begin;
|
||||
if (end != points.cend()) ++end;
|
||||
if (begin == end) continue;
|
||||
|
||||
spec.LineWeight = 2;
|
||||
ImPlot::PlotLine(label.c_str(), &begin->x, &begin->y, end - begin, spec);
|
||||
|
||||
|
||||
if ((num_points == 1 || pixels_per_point > 20) && first != last) {
|
||||
ImPlotSpec dots;
|
||||
dots.LineColor = toImVec4(s.color);
|
||||
dots.Stride = sizeof(ImPlotPoint);
|
||||
dots.Marker = ImPlotMarker_Circle;
|
||||
dots.MarkerSize = 4;
|
||||
ImPlot::PlotScatter((label + "_pts").c_str(), &first->x, &first->y, last - first, dots);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::drawForeground() {
|
||||
drawTimeline();
|
||||
ImDrawList *painter = ImPlot::GetPlotDrawList();
|
||||
ImPlot::PushPlotClipRect();
|
||||
float track_line_x = -1;
|
||||
for (auto &s : sigs_) {
|
||||
if (!isNull(s.track_pt) && s.visible) {
|
||||
ImVec2 pos(xPos(s.track_pt.x), yPos(s.track_pt.y));
|
||||
painter->AddCircleFilled(pos, 5.5f, toImU32(s.color.darker(125)));
|
||||
track_line_x = std::max(track_line_x, pos.x);
|
||||
}
|
||||
}
|
||||
if (track_line_x > 0) {
|
||||
const ImU32 dark_gray = IM_COL32(0x80, 0x80, 0x80, 0xff);
|
||||
for (float y = layout_.plot_area.Min.y; y < layout_.plot_area.Max.y; y += 8) {
|
||||
painter->AddLine(ImVec2(track_line_x, y), ImVec2(track_line_x, std::min(y + 4, layout_.plot_area.Max.y)), dark_gray, 1.0f);
|
||||
}
|
||||
}
|
||||
ImPlot::PopPlotClipRect();
|
||||
|
||||
drawRubberBandTimeRange();
|
||||
}
|
||||
|
||||
void ChartView::drawRubberBandTimeRange() {
|
||||
if (rubber_rect_.GetWidth() <= 1) return;
|
||||
|
||||
ImDrawList *painter = ImPlot::GetPlotDrawList();
|
||||
|
||||
const ImU32 highlight = withAlpha(ImGui::GetColorU32(ImGuiCol_Header), 255);
|
||||
painter->AddRectFilled(rubber_rect_.Min, rubber_rect_.Max, withAlpha(highlight, 50));
|
||||
painter->AddRect(rubber_rect_.Min, rubber_rect_.Max, highlight);
|
||||
|
||||
|
||||
const ImU32 white = IM_COL32_WHITE;
|
||||
const ImU32 gray = IM_COL32(0xa0, 0xa0, 0xa4, 0xff);
|
||||
painter = ImGui::GetWindowDrawList();
|
||||
painter->PushClipRect(layout_.rect.Min, layout_.rect.Max);
|
||||
for (const auto &pt : {rubber_rect_.GetBL(), rubber_rect_.GetBR()}) {
|
||||
std::string sec = formatNumber(secondsAtPoint(pt), 2);
|
||||
ImVec2 size = ImGui::CalcTextSize(sec.c_str()) + ImVec2(12, AXIS_X_TOP_MARGIN * 2);
|
||||
ImVec2 top_left = pt.x == rubber_rect_.Min.x ? ImVec2(pt.x - size.x, pt.y + 2) : ImVec2(pt.x, pt.y + 2);
|
||||
painter->AddRectFilled(top_left, top_left + size, gray);
|
||||
painter->AddText(top_left + ImVec2(6, AXIS_X_TOP_MARGIN), white, sec.c_str());
|
||||
}
|
||||
painter->PopClipRect();
|
||||
}
|
||||
|
||||
void ChartView::drawTimeline() {
|
||||
ImDrawList *painter = ImPlot::GetPlotDrawList();
|
||||
float x = std::clamp(xPos(cur_sec_), layout_.plot_area.Min.x, layout_.plot_area.Max.x);
|
||||
painter->AddLine(ImVec2(x, layout_.plot_area.Min.y - 1.0f), ImVec2(x, layout_.plot_area.Max.y + 1.0f), ImGui::GetColorU32(ImGuiCol_Text), 1.0f);
|
||||
|
||||
std::string time_str = formatNumber(cur_sec_, 2);
|
||||
ImVec2 time_str_size = ImGui::CalcTextSize(time_str.c_str()) + ImVec2(8, 2);
|
||||
ImVec2 time_str_pos(x - time_str_size.x / 2.0f, layout_.plot_area.Max.y + AXIS_X_TOP_MARGIN);
|
||||
const bool dark = isDarkTheme();
|
||||
painter->AddRectFilled(time_str_pos, time_str_pos + time_str_size, dark ? IM_COL32(0x80, 0x80, 0x80, 0xff) : IM_COL32(0xa0, 0xa0, 0xa4, 0xff), 3.0f);
|
||||
painter->AddText(time_str_pos + ImVec2(4, 1), IM_COL32_WHITE, time_str.c_str());
|
||||
}
|
||||
|
||||
void ChartView::drawSignalValue() {
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
for (int i = 0; i < sigs_.size() && i < layout_.legend_rects.size(); ++i) {
|
||||
const auto &s = sigs_[i];
|
||||
const ImPlotPoint *pt = lastPointBefore(s, cur_sec_);
|
||||
std::string value = pt ? s.sig->formatValue(pt->y) : "--";
|
||||
const ImVec2 value_min = layout_.legend_rects[i].GetBL() - ImVec2(0, 1);
|
||||
ImRect value_rect(value_min, value_min + layout_.legend_rects[i].GetSize());
|
||||
float w = ImGui::CalcTextSize(value.c_str()).x;
|
||||
if (w <= value_rect.GetWidth()) {
|
||||
painter->AddText(ImVec2(value_rect.GetCenter().x - w / 2, value_rect.Min.y), color, value.c_str());
|
||||
} else {
|
||||
addTextEllipsis(painter, ImGui::GetFont(), color, value_rect.Min, value_rect.Max.x, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CabanaColor ChartView::uniqueColor(CabanaColor color, const cabana::Signal *exclude) const {
|
||||
for (auto &s : sigs_) {
|
||||
if (s.sig != exclude && std::abs(color.hsv().hue - s.color.hsv().hue) < 0.1) {
|
||||
|
||||
auto last_color = sigs_.back().color;
|
||||
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||
std::uniform_int_distribution<int> sat(35, 99);
|
||||
std::uniform_int_distribution<int> val(85, 99);
|
||||
color = CabanaColor::fromHsv(std::fmod(last_color.hsv().hue + 60 / 360.0, 1.0),
|
||||
sat(rng) / 100.0,
|
||||
val(rng) / 100.0,
|
||||
color.a / 255.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return color;
|
||||
}
|
||||
142
iqpilot/tools/cabana/ui/chart/chart.h
Normal file
142
iqpilot/tools/cabana/ui/chart/chart.h
Normal file
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "implot.h"
|
||||
|
||||
#include "tools/cabana/ui/chart/tiplabel.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
enum class SeriesType {
|
||||
Line = 0,
|
||||
StepLine,
|
||||
Scatter
|
||||
};
|
||||
inline constexpr const char *SERIES_TYPE_NAMES[] = {"Line", "Step Line", "Scatter"};
|
||||
|
||||
|
||||
inline std::string msgLabel(const MessageId &id) { return " " + msgName(id) + " " + id.toString(); }
|
||||
|
||||
class ChartsWidget;
|
||||
class ChartView {
|
||||
public:
|
||||
struct SigItem {
|
||||
MessageId msg_id;
|
||||
const cabana::Signal *sig = nullptr;
|
||||
CabanaColor color;
|
||||
bool visible = true;
|
||||
std::vector<ImPlotPoint> vals;
|
||||
std::vector<ImPlotPoint> step_vals;
|
||||
ImPlotPoint track_pt{};
|
||||
SegmentTree segment_tree;
|
||||
double min = 0;
|
||||
double max = 0;
|
||||
};
|
||||
|
||||
ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent);
|
||||
void addSignal(const MessageId &msg_id, const cabana::Signal *sig);
|
||||
bool hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const;
|
||||
void updateSeries(const cabana::Signal *sig = nullptr, const MessageEventsMap *msg_new_events = nullptr);
|
||||
void updatePlot(double cur, double min, double max);
|
||||
void setSeriesType(SeriesType type) { series_type_ = type; }
|
||||
void showTip(double sec);
|
||||
void hideTip();
|
||||
void draw(float width);
|
||||
void drawGhost(float width);
|
||||
void removeIf(std::function<bool(const SigItem &)> predicate);
|
||||
void takeSignalsFrom(ChartView *source);
|
||||
|
||||
std::vector<SigItem> takeExtraSignals();
|
||||
void adoptSignal(SigItem s);
|
||||
void setDropHighlight(bool highlight) { can_drop_ = highlight; }
|
||||
const std::vector<SigItem> &signals() const { return sigs_; }
|
||||
const ImRect &rect() const { return layout_.rect; }
|
||||
bool plotHovered() const { return layout_.plot_hovered; }
|
||||
double secondsAtPoint(const ImVec2 &pt) const {
|
||||
return x_min_ + (pt.x - layout_.plot_area.Min.x) * (x_max_ - x_min_) / std::max(layout_.plot_area.GetWidth(), 1.0f);
|
||||
}
|
||||
|
||||
private:
|
||||
using PointIter = std::vector<ImPlotPoint>::const_iterator;
|
||||
|
||||
void signalUpdated(const cabana::Signal *sig);
|
||||
void manageSignals();
|
||||
void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id.address == id.address && !dbc()->msg(id); }); }
|
||||
void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); }
|
||||
|
||||
void appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
|
||||
std::vector<ImPlotPoint> &vals, std::vector<ImPlotPoint> &step_vals);
|
||||
void createToolButtons();
|
||||
void drawContextMenu();
|
||||
void handleMousePress();
|
||||
void handleMouseMove();
|
||||
void handleMouseRelease();
|
||||
void updateLayout();
|
||||
void updateAxisY();
|
||||
void paint();
|
||||
void drawStaticLayer();
|
||||
void drawAxes();
|
||||
void drawLegend();
|
||||
void drawSeries();
|
||||
void drawForeground();
|
||||
void drawSignalValue();
|
||||
void drawTimeline();
|
||||
void drawRubberBandTimeRange();
|
||||
void drawMenuActions();
|
||||
int xAxisPrecision() const;
|
||||
std::tuple<double, double, int> getNiceAxisNumbers(double min, double max, int tick_count);
|
||||
double niceNumber(double x, bool ceiling);
|
||||
CabanaColor uniqueColor(CabanaColor color, const cabana::Signal *exclude = nullptr) const;
|
||||
|
||||
const ImPlotPoint *lastPointBefore(const SigItem &s, double sec) const;
|
||||
|
||||
std::pair<PointIter, PointIter> visibleRange(const std::vector<ImPlotPoint> &points) const;
|
||||
inline void clearTrackPoints() { for (auto &s : sigs_) s.track_pt = {}; }
|
||||
inline float xPos(double sec) const { return layout_.plot_area.Min.x + (sec - x_min_) / (x_max_ - x_min_) * layout_.plot_area.GetWidth(); }
|
||||
inline float yPos(double val) const { return layout_.plot_area.Max.y - (val - y_min_) / (y_max_ - y_min_) * layout_.plot_area.GetHeight(); }
|
||||
|
||||
|
||||
struct Layout {
|
||||
ImRect rect;
|
||||
ImRect plot_area;
|
||||
ImRect move_icon_rect;
|
||||
ImRect close_btn_rect;
|
||||
ImRect manage_btn_rect;
|
||||
std::vector<ImRect> legend_rects;
|
||||
float header_bottom = 0;
|
||||
bool plot_hovered = false;
|
||||
} layout_;
|
||||
|
||||
double x_min_;
|
||||
double x_max_;
|
||||
double y_min_ = 0;
|
||||
double y_max_ = 1;
|
||||
int y_tick_count_ = 3;
|
||||
int y_precision_ = 0;
|
||||
std::string y_unit_;
|
||||
|
||||
enum class MouseMode { None, Rubber, Scrub };
|
||||
MouseMode mouse_mode_ = MouseMode::None;
|
||||
ImVec2 press_pos_;
|
||||
ImRect rubber_rect_;
|
||||
bool resume_after_scrub_ = false;
|
||||
bool drawing_ghost_ = false;
|
||||
ImGuiID context_menu_id_ = 0;
|
||||
|
||||
TipLabel tip_label_;
|
||||
std::vector<SigItem> sigs_;
|
||||
double cur_sec_ = 0;
|
||||
SeriesType series_type_ = SeriesType::Line;
|
||||
bool can_drop_ = false;
|
||||
double tooltip_x_ = -1;
|
||||
ChartsWidget *charts_widget_;
|
||||
Connections connections_;
|
||||
};
|
||||
674
iqpilot/tools/cabana/ui/chart/chartswidget.cc
Normal file
674
iqpilot/tools/cabana/ui/chart/chartswidget.cc
Normal file
@@ -0,0 +1,674 @@
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||
|
||||
#include "tools/cabana/ui/threadpool.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <future>
|
||||
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/chart/chart.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
const int MAX_COLUMN_COUNT = 4;
|
||||
const int CHART_SPACING = 4;
|
||||
const int START_DRAG_DISTANCE = 10;
|
||||
const float LAYOUT_HORIZONTAL_SPACING = 6.0f;
|
||||
const float MIN_RANGE_SLIDER_WIDTH = 40.0f;
|
||||
|
||||
bool LogSlider::draw(const char *label, float width) {
|
||||
return fusionSliderInt(label, &pos_, min_, max_, width);
|
||||
}
|
||||
|
||||
ChartsWidget::ChartsWidget() {
|
||||
range_slider_.setRange(1, settings.max_cached_minutes * 60);
|
||||
|
||||
tabbar_.setAutoHide(true);
|
||||
tabbar_.setUsesScrollButtons(true);
|
||||
tabbar_.setTabsClosable(true);
|
||||
|
||||
column_count_ = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT);
|
||||
max_chart_range_ = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60);
|
||||
display_range_ = std::make_pair(can->minSeconds(), can->minSeconds() + max_chart_range_);
|
||||
range_slider_.setValue(max_chart_range_);
|
||||
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { removeAll(); }));
|
||||
connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &events) { eventsMerged(events); }));
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *, bool) { updateState(); }));
|
||||
connections_.push_back(can->seeking.connect([this](double) { updateState(); }));
|
||||
connections_.push_back(can->timeRangeChanged.connect([this](const auto &) { updateState(); }));
|
||||
connections_.push_back(settings.changed.connect([this]() { settingChanged(); }));
|
||||
connections_.push_back(seriesChanged.connect([this]() { updateTabBar(); }));
|
||||
connections_.push_back(tabbar_.tabCloseRequested.connect([this](int index) { removeTab(index); }));
|
||||
connections_.push_back(tabbar_.tabContextMenu.connect([this](int index) {
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::MenuItem("Close Other Tabs")) {
|
||||
tabbar_.moveTab(index, 0);
|
||||
tabbar_.setCurrentIndex(0);
|
||||
while (tabbar_.count() > 1) removeTab(1);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}));
|
||||
connections_.push_back(tabbar_.currentChanged.connect([this](int index) {
|
||||
if (index != -1) updateLayout();
|
||||
}));
|
||||
|
||||
setIsDocked(true);
|
||||
newTab();
|
||||
}
|
||||
|
||||
ChartsWidget::~ChartsWidget() = default;
|
||||
|
||||
std::string ChartsWidget::whatsThis() const {
|
||||
return R"(
|
||||
<b>Chart View</b><br />
|
||||
<b>Click</b>: Click to seek to a corresponding time.<br />
|
||||
<b>Drag</b>: Zoom into the chart.<br />
|
||||
<b>Shift + Drag</b>: Scrub through the chart to view values.<br />
|
||||
<b>Right Mouse</b>: Open the context menu.<br />
|
||||
)";
|
||||
}
|
||||
|
||||
void ChartsWidget::newTab() {
|
||||
static int tab_unique_id = 0;
|
||||
int idx = tabbar_.addTab("");
|
||||
tabbar_.setTabData(idx, tab_unique_id++);
|
||||
tabbar_.setCurrentIndex(idx);
|
||||
updateTabBar();
|
||||
}
|
||||
|
||||
void ChartsWidget::removeTab(int index) {
|
||||
int id = tabbar_.tabData(index);
|
||||
for (auto &c : std::vector<ChartView *>(tab_charts_[id])) {
|
||||
removeChart(c);
|
||||
}
|
||||
tab_charts_.erase(id);
|
||||
tabbar_.removeTab(index);
|
||||
updateTabBar();
|
||||
}
|
||||
|
||||
void ChartsWidget::updateTabBar() {
|
||||
for (int i = 0; i < tabbar_.count(); ++i) {
|
||||
const auto &charts_in_tab = tab_charts_[tabbar_.tabData(i)];
|
||||
tabbar_.setTabText(i, "Tab " + std::to_string(i + 1) + " (" + std::to_string((int)charts_in_tab.size()) + ")");
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) {
|
||||
std::vector<std::future<void>> futures;
|
||||
for (auto &c : charts_) {
|
||||
futures.push_back(ThreadPool::instance().run([c = c.get(), &new_events]() { c->updateSeries(nullptr, &new_events); }));
|
||||
}
|
||||
for (auto &f : futures) f.get();
|
||||
}
|
||||
|
||||
void ChartsWidget::zoomReset() {
|
||||
can->setTimeRange(std::nullopt);
|
||||
zoom_undo_stack_.clear();
|
||||
}
|
||||
|
||||
ImRect ChartsWidget::chartVisibleRect(ChartView *chart) {
|
||||
ImRect r = chart->rect();
|
||||
r.ClipWith(charts_scroll_viewport_);
|
||||
return r;
|
||||
}
|
||||
|
||||
void ChartsWidget::showValueTip(double sec) {
|
||||
if (chartDragActive()) sec = -1;
|
||||
showTip(sec);
|
||||
if (sec < 0 && !value_tip_visible_) return;
|
||||
|
||||
value_tip_visible_ = sec >= 0;
|
||||
for (auto c : currentCharts()) {
|
||||
value_tip_visible_ ? c->showTip(sec) : c->hideTip();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::updateState() {
|
||||
if (charts_.empty()) return;
|
||||
|
||||
const auto &time_range = can->timeRange();
|
||||
const double cur_sec = can->currentSec();
|
||||
if (!time_range.has_value()) {
|
||||
double pos = (cur_sec - display_range_.first) / std::max<float>(1.0, max_chart_range_);
|
||||
if (pos < 0 || pos > 0.8) {
|
||||
display_range_.first = std::max(can->minSeconds(), cur_sec - max_chart_range_ * 0.1);
|
||||
}
|
||||
double max_sec = std::min(display_range_.first + max_chart_range_, can->maxSeconds());
|
||||
display_range_.first = std::max(can->minSeconds(), max_sec - max_chart_range_);
|
||||
display_range_.second = display_range_.first + max_chart_range_;
|
||||
}
|
||||
|
||||
const auto &range = time_range ? *time_range : display_range_;
|
||||
for (auto &c : charts_) {
|
||||
c->updatePlot(cur_sec, range.first, range.second);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::setMaxChartRange(int value) {
|
||||
max_chart_range_ = settings.chart_range = value;
|
||||
updateState();
|
||||
}
|
||||
|
||||
void ChartsWidget::setIsDocked(bool docked) {
|
||||
is_docked_ = docked;
|
||||
if (!docked) float_window_init_ = true;
|
||||
}
|
||||
|
||||
void ChartsWidget::drawToolBar() {
|
||||
beginToolbar();
|
||||
float slider_width = 150.0f;
|
||||
const bool is_zoomed = can->timeRange().has_value();
|
||||
|
||||
|
||||
std::vector<ToolbarItem> items;
|
||||
items.push_back({toolbarButtonWidth(icon::PLUS_SQUARE), [this]() {
|
||||
if (toolButton("new_plot_btn", icon::PLUS_SQUARE, "New Chart")) newChart();
|
||||
}});
|
||||
items.push_back({toolbarButtonWidth(icon::WINDOW_STACK), [this]() {
|
||||
if (toolButton("new_tab_btn", icon::WINDOW_STACK, "New Tab")) newTab();
|
||||
}});
|
||||
const std::string title_label = "Charts: " + std::to_string(charts_.size());
|
||||
items.push_back({ImGui::CalcTextSize(title_label.c_str()).x + LAYOUT_HORIZONTAL_SPACING, [&title_label]() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(title_label.c_str());
|
||||
ImGui::SameLine(0.0f, LAYOUT_HORIZONTAL_SPACING);
|
||||
ImGui::Dummy(ImVec2(0.0f, 0.0f));
|
||||
}});
|
||||
|
||||
const int type_count = (int)std::size(SERIES_TYPE_NAMES);
|
||||
const std::string chart_type_text = std::string("Type: ") + SERIES_TYPE_NAMES[std::clamp(settings.chart_series_type, 0, type_count - 1)];
|
||||
items.push_back({menuButtonWidth(chart_type_text), [this, &chart_type_text]() {
|
||||
menuButton("chart_type", chart_type_text, "chart_type_menu");
|
||||
if (ImGui::BeginPopup("chart_type_menu")) {
|
||||
for (int i = 0; i < type_count; ++i) {
|
||||
if (ImGui::MenuItem(SERIES_TYPE_NAMES[i])) {
|
||||
settings.chart_series_type = i;
|
||||
settingChanged();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}});
|
||||
|
||||
const std::string columns_action_text = "Columns: " + std::to_string(column_count_);
|
||||
if (columns_action_visible_) {
|
||||
items.push_back({menuButtonWidth(columns_action_text), [this, &columns_action_text]() {
|
||||
menuButton("columns", columns_action_text, "columns_menu");
|
||||
if (ImGui::BeginPopup("columns_menu")) {
|
||||
for (int i = 0; i < MAX_COLUMN_COUNT; ++i) {
|
||||
if (ImGui::MenuItem(std::to_string(i + 1).c_str())) setColumnCount(i + 1);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
|
||||
const size_t spacer_index = items.size();
|
||||
size_t slider_index = (size_t)-1;
|
||||
const std::string range_lb = is_zoomed ? std::string() : utils::formatSeconds(max_chart_range_);
|
||||
std::string reset_zoom_text;
|
||||
if (!is_zoomed) {
|
||||
items.push_back({ImGui::CalcTextSize(range_lb.c_str()).x, [&range_lb]() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(range_lb.c_str());
|
||||
}});
|
||||
slider_index = items.size();
|
||||
items.push_back({slider_width, [this, &slider_width]() {
|
||||
if (range_slider_.draw("##range_slider", slider_width)) setMaxChartRange(range_slider_.value());
|
||||
ImGui::SetItemTooltip("Set the chart range");
|
||||
}});
|
||||
} else {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.2f-%.2f", can->timeRange()->first, can->timeRange()->second);
|
||||
reset_zoom_text = buf;
|
||||
items.push_back({toolbarButtonWidth(icon::ARROW_COUNTERCLOCKWISE), [this]() {
|
||||
ImGui::BeginDisabled(!zoom_undo_stack_.canUndo());
|
||||
if (toolButton("undo_zoom", icon::ARROW_COUNTERCLOCKWISE, "Undo Zoom")) zoom_undo_stack_.undo();
|
||||
ImGui::EndDisabled();
|
||||
}});
|
||||
items.push_back({toolbarButtonWidth(icon::ARROW_CLOCKWISE), [this]() {
|
||||
ImGui::BeginDisabled(!zoom_undo_stack_.canRedo());
|
||||
if (toolButton("redo_zoom", icon::ARROW_CLOCKWISE, "Redo Zoom")) zoom_undo_stack_.redo();
|
||||
ImGui::EndDisabled();
|
||||
}});
|
||||
items.push_back({toolbarButtonWidth(std::string(icon::ZOOM_OUT) + " " + reset_zoom_text), [this, &reset_zoom_text]() {
|
||||
if (toolButton("reset_zoom_btn", icon::ZOOM_OUT, "Reset Zoom", reset_zoom_text.c_str())) zoomReset();
|
||||
}});
|
||||
}
|
||||
items.push_back({toolbarButtonWidth(icon::X_SQUARE), [this]() {
|
||||
ImGui::BeginDisabled(charts_.empty());
|
||||
if (toolButton("remove_all_btn", icon::X_SQUARE, "Remove all charts")) removeAll();
|
||||
ImGui::EndDisabled();
|
||||
}});
|
||||
const char *dock_btn_icon = is_docked_ ? icon::ARROW_UP_RIGHT_SQUARE : icon::ARROW_DOWN_LEFT_SQUARE;
|
||||
items.push_back({toolbarButtonWidth(dock_btn_icon), [this, dock_btn_icon]() {
|
||||
if (toolButton("dock_btn", dock_btn_icon, is_docked_ ? "Float the charts window" : "Dock the charts window")) toggleChartsDocking();
|
||||
}});
|
||||
|
||||
|
||||
if (slider_index != (size_t)-1) {
|
||||
const float shrink = std::min(slider_width - MIN_RANGE_SLIDER_WIDTH, toolbarWidth(items, spacer_index) - ImGui::GetContentRegionAvail().x);
|
||||
if (shrink > 0.0f) {
|
||||
slider_width -= shrink;
|
||||
items[slider_index].width = slider_width;
|
||||
}
|
||||
}
|
||||
drawToolbar(items, spacer_index);
|
||||
endToolbar();
|
||||
}
|
||||
|
||||
void ChartsWidget::settingChanged() {
|
||||
if (range_slider_.maximum() != settings.max_cached_minutes * 60) {
|
||||
range_slider_.setRange(1, settings.max_cached_minutes * 60);
|
||||
}
|
||||
for (auto &c : charts_) {
|
||||
c->setSeriesType((SeriesType)settings.chart_series_type);
|
||||
}
|
||||
}
|
||||
|
||||
ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) {
|
||||
for (auto &c : charts_)
|
||||
if (c->hasSignal(id, sig)) return c.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ChartView *ChartsWidget::createChart(int pos) {
|
||||
auto chart = std::make_unique<ChartView>(can->timeRange().value_or(display_range_), this);
|
||||
ChartView *ptr = chart.get();
|
||||
pos = std::clamp(pos, 0, (int)charts_.size());
|
||||
charts_.insert(charts_.begin() + pos, std::move(chart));
|
||||
currentCharts().insert(currentCharts().begin() + pos, ptr);
|
||||
updateLayout();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) {
|
||||
ChartView *chart = findChart(id, sig);
|
||||
if (show && !chart) {
|
||||
chart = merge && currentCharts().size() > 0 ? currentCharts().front() : createChart();
|
||||
chart->addSignal(id, sig);
|
||||
updateState();
|
||||
} else if (!show && chart) {
|
||||
chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; });
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::splitChart(ChartView *src_chart) {
|
||||
if (src_chart->signals().size() > 1) {
|
||||
auto it = std::find_if(charts_.begin(), charts_.end(), [src_chart](auto &c) { return c.get() == src_chart; });
|
||||
const int pos = it - charts_.begin() + 1;
|
||||
for (auto &s : src_chart->takeExtraSignals()) {
|
||||
createChart(pos)->adoptSignal(std::move(s));
|
||||
}
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ChartsWidget::serializeChartIds() const {
|
||||
std::vector<std::string> chart_ids;
|
||||
for (auto &c : charts_) {
|
||||
std::string ids;
|
||||
for (const auto &s : c->signals()) {
|
||||
if (!ids.empty()) ids += ',';
|
||||
ids += s.msg_id.toString() + "|" + s.sig->name;
|
||||
}
|
||||
chart_ids.push_back(ids);
|
||||
}
|
||||
std::reverse(chart_ids.begin(), chart_ids.end());
|
||||
return chart_ids;
|
||||
}
|
||||
|
||||
void ChartsWidget::restoreChartsFromIds(const std::vector<std::string> &chart_ids) {
|
||||
for (const auto &chart_id : chart_ids) {
|
||||
int index = 0;
|
||||
for (const auto &part : utils::split(chart_id, ',')) {
|
||||
const size_t sep = part.find('|');
|
||||
if (sep == std::string::npos) continue;
|
||||
MessageId msg_id = MessageId::fromString(part.substr(0, sep));
|
||||
if (auto *msg = dbc()->msg(msg_id))
|
||||
if (auto *sig = msg->sig(part.substr(sep + 1)))
|
||||
showChart(msg_id, sig, true, index++ > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::setColumnCount(int n) {
|
||||
n = std::clamp(n, 1, MAX_COLUMN_COUNT);
|
||||
if (column_count_ != n) {
|
||||
column_count_ = settings.chart_column_count = n;
|
||||
updateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::updateLayout() {
|
||||
|
||||
const float container_width = charts_container_.geometry().GetWidth();
|
||||
if (container_width <= 0) return;
|
||||
|
||||
int n = MAX_COLUMN_COUNT;
|
||||
for (; n > 1; --n) {
|
||||
if ((n * CHART_MIN_WIDTH + (n - 1) * CHART_SPACING) < container_width) break;
|
||||
}
|
||||
|
||||
columns_action_visible_ = n > 1;
|
||||
current_column_count_ = std::min(column_count_, n);
|
||||
}
|
||||
|
||||
void ChartsWidget::startChartDrag(ChartView *chart, const ImVec2 &global_pos) {
|
||||
stopAutoScroll();
|
||||
drag_ = {.source = chart, .press_pos = global_pos};
|
||||
showValueTip(-1);
|
||||
|
||||
drag_preview_size_ = ImVec2(CHART_MIN_WIDTH, (float)settings.chart_height);
|
||||
}
|
||||
|
||||
void ChartsWidget::dragChartMove(const ImVec2 &global_pos) {
|
||||
if (!drag_.active) {
|
||||
ImVec2 d = global_pos - drag_.press_pos;
|
||||
if (std::abs(d.x) + std::abs(d.y) < START_DRAG_DISTANCE) return;
|
||||
drag_.active = true;
|
||||
drag_preview_visible_ = true;
|
||||
}
|
||||
drag_preview_pos_ = global_pos + ImVec2(5, 5);
|
||||
|
||||
|
||||
int tab = tabbar_.tabAt(global_pos);
|
||||
if (tab >= 0 && tab != tabbar_.currentIndex()) {
|
||||
tabbar_.setCurrentIndex(tab);
|
||||
}
|
||||
|
||||
ChartView *target = nullptr;
|
||||
for (auto c : currentCharts()) {
|
||||
if (c != drag_.source && c->rect().Contains(global_pos)) {
|
||||
target = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (std::exchange(drop_target_, target) != target) {
|
||||
for (auto &c : charts_) c->setDropHighlight(c.get() == target);
|
||||
}
|
||||
bool in_viewport = charts_scroll_viewport_.Contains(global_pos);
|
||||
bool on_background = !target && in_viewport && !charts_container_.childAt(global_pos);
|
||||
charts_container_.setDropIndicator(on_background ? global_pos : ImVec2());
|
||||
|
||||
if (in_viewport) {
|
||||
startAutoScroll(global_pos);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::cancelChartDrag() {
|
||||
drag_ = {};
|
||||
stopAutoScroll();
|
||||
drag_preview_visible_ = false;
|
||||
charts_container_.setDropIndicator({});
|
||||
if (auto target = std::exchange(drop_target_, nullptr)) target->setDropHighlight(false);
|
||||
}
|
||||
|
||||
void ChartsWidget::dragChartRelease(const ImVec2 &global_pos) {
|
||||
ChartView *source = drag_.source;
|
||||
bool active = drag_.active;
|
||||
ChartView *target = drop_target_;
|
||||
cancelChartDrag();
|
||||
if (!active) return;
|
||||
|
||||
bool in_viewport = charts_scroll_viewport_.Contains(global_pos);
|
||||
if (target) {
|
||||
|
||||
target->takeSignalsFrom(source);
|
||||
} else if (in_viewport && !charts_container_.childAt(global_pos)) {
|
||||
|
||||
auto w = charts_container_.getDropAfter(global_pos);
|
||||
if (w != source) {
|
||||
for (auto &[_, list] : tab_charts_) {
|
||||
list.erase(std::remove(list.begin(), list.end(), source), list.end());
|
||||
}
|
||||
auto &cur = currentCharts();
|
||||
int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0;
|
||||
cur.insert(cur.begin() + to, source);
|
||||
updateLayout();
|
||||
updateTabBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::drawDragPreview() {
|
||||
if (!drag_preview_visible_ || !drag_.source) return;
|
||||
|
||||
|
||||
ImGui::SetNextWindowPos(drag_preview_pos_);
|
||||
ImGui::SetNextWindowSize(drag_preview_size_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
|
||||
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking;
|
||||
if (ImGui::Begin("##chart_drag_ghost", nullptr, flags)) {
|
||||
drag_.source->drawGhost(drag_preview_size_.x);
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(3);
|
||||
}
|
||||
|
||||
void ChartsWidget::startAutoScroll(const ImVec2 &global_pos) {
|
||||
auto_scroll_pos_ = global_pos;
|
||||
if (!auto_scroll_timer_active_) auto_scroll_timer_next_ = ImGui::GetTime() + 0.05;
|
||||
auto_scroll_timer_active_ = true;
|
||||
}
|
||||
|
||||
void ChartsWidget::stopAutoScroll() {
|
||||
auto_scroll_timer_active_ = false;
|
||||
auto_scroll_count_ = 0;
|
||||
}
|
||||
|
||||
void ChartsWidget::doAutoScroll() {
|
||||
if (!charts_scroll_) return;
|
||||
const int page_step = charts_scroll_viewport_.GetHeight();
|
||||
if (auto_scroll_count_ < page_step) {
|
||||
++auto_scroll_count_;
|
||||
}
|
||||
|
||||
int value = charts_scroll_->Scroll.y;
|
||||
ImVec2 pos = auto_scroll_pos_;
|
||||
ImRect area = charts_scroll_viewport_;
|
||||
|
||||
int new_value = value;
|
||||
if (pos.y - area.Min.y < settings.chart_height / 2) {
|
||||
new_value = value - auto_scroll_count_;
|
||||
} else if (area.Max.y - pos.y < settings.chart_height / 2) {
|
||||
new_value = value + auto_scroll_count_;
|
||||
}
|
||||
new_value = std::clamp<int>(new_value, 0, charts_scroll_->ScrollMax.y);
|
||||
if (new_value != value) ImGui::SetScrollY(charts_scroll_, new_value);
|
||||
if (value == new_value) {
|
||||
stopAutoScroll();
|
||||
} else if (chartDragActive()) {
|
||||
|
||||
dragChartMove(auto_scroll_pos_);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::newChart() {
|
||||
execSignalSelector(std::make_unique<SignalSelector>("New Chart"), nullptr, [this](SignalSelector &dlg) {
|
||||
const auto &items = dlg.selectedItems();
|
||||
if (!items.empty()) {
|
||||
auto c = createChart();
|
||||
for (const auto &it : items) {
|
||||
c->addSignal(it.msg_id, it.sig);
|
||||
}
|
||||
updateState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ChartsWidget::execSignalSelector(std::unique_ptr<SignalSelector> dlg, ChartView *owner, std::function<void(SignalSelector &)> accepted) {
|
||||
signal_selector_ = std::move(dlg);
|
||||
signal_selector_owner_ = owner;
|
||||
signal_selector_accepted_ = std::move(accepted);
|
||||
signal_selector_->open();
|
||||
}
|
||||
|
||||
void ChartsWidget::removeChart(ChartView *chart) {
|
||||
if (drag_.source == chart) cancelChartDrag();
|
||||
if (drop_target_ == chart) drop_target_ = nullptr;
|
||||
if (signal_selector_owner_ == chart) {
|
||||
signal_selector_owner_ = nullptr;
|
||||
signal_selector_accepted_ = nullptr;
|
||||
}
|
||||
auto it = std::find_if(charts_.begin(), charts_.end(), [chart](auto &c) { return c.get() == chart; });
|
||||
if (it != charts_.end()) {
|
||||
deleted_charts_.push_back(std::move(*it));
|
||||
charts_.erase(it);
|
||||
}
|
||||
for (auto &[_, list] : tab_charts_) {
|
||||
list.erase(std::remove(list.begin(), list.end(), chart), list.end());
|
||||
}
|
||||
updateLayout();
|
||||
seriesChanged();
|
||||
}
|
||||
|
||||
void ChartsWidget::removeAll() {
|
||||
while (tabbar_.count() > 1) {
|
||||
tabbar_.removeTab(1);
|
||||
}
|
||||
std::vector<ChartView *> all;
|
||||
for (auto &c : charts_) all.push_back(c.get());
|
||||
for (auto c : all) removeChart(c);
|
||||
tab_charts_.clear();
|
||||
zoomReset();
|
||||
}
|
||||
|
||||
void ChartsWidget::handleEvents() {
|
||||
|
||||
if (ImGui::IsMouseClicked(3) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
|
||||
zoom_undo_stack_.undo();
|
||||
}
|
||||
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) {
|
||||
if (chartDragActive()) cancelChartDrag();
|
||||
showValueTip(-1);
|
||||
}
|
||||
|
||||
|
||||
if (chartDragActive()) {
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
dragChartMove(ImGui::GetMousePos());
|
||||
} else {
|
||||
dragChartRelease(ImGui::GetMousePos());
|
||||
}
|
||||
}
|
||||
|
||||
if (!value_tip_visible_) return;
|
||||
|
||||
|
||||
const ImVec2 delta = ImGui::GetIO().MouseDelta;
|
||||
if (!any_plot_hovered_ &&
|
||||
(delta.x != 0 || delta.y != 0 || !ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))) {
|
||||
showValueTip(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::draw() {
|
||||
deleted_charts_.clear();
|
||||
|
||||
|
||||
if (float_window_init_ && !is_docked_) {
|
||||
float_window_init_ = false;
|
||||
const ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||
const ImVec2 size(viewport->WorkSize.x * 0.6f, viewport->WorkSize.y * 0.6f);
|
||||
ImGui::SetWindowSize(size);
|
||||
ImGui::SetWindowPos(viewport->WorkPos + (viewport->WorkSize - size) * 0.5f);
|
||||
}
|
||||
ImGui::PushID(this);
|
||||
if (auto_scroll_timer_active_ && ImGui::GetTime() >= auto_scroll_timer_next_) {
|
||||
auto_scroll_timer_next_ = ImGui::GetTime() + 0.05;
|
||||
doAutoScroll();
|
||||
}
|
||||
|
||||
|
||||
handleEvents();
|
||||
|
||||
drawToolBar();
|
||||
tabbar_.draw();
|
||||
|
||||
any_plot_hovered_ = false;
|
||||
if (ImGui::BeginChild("charts_scroll", ImVec2(0, 0), ImGuiChildFlags_None, 0)) {
|
||||
charts_scroll_ = ImGui::GetCurrentWindow();
|
||||
charts_scroll_viewport_ = charts_scroll_->InnerRect;
|
||||
charts_container_.draw();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
drawDragPreview();
|
||||
|
||||
if (signal_selector_ && !signal_selector_->draw()) {
|
||||
auto dlg = std::move(signal_selector_);
|
||||
auto accepted = std::move(signal_selector_accepted_);
|
||||
signal_selector_owner_ = nullptr;
|
||||
if (dlg->accepted() && accepted) accepted(*dlg);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
void ChartsContainer::draw() {
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
geometry_ = ImRect(start, start + ImVec2(window->InnerRect.GetWidth(), 0));
|
||||
charts_widget_->updateLayout();
|
||||
|
||||
const int n = std::max(charts_widget_->current_column_count_, 1);
|
||||
const float spacing = CHART_SPACING;
|
||||
const float width = (geometry_.GetWidth() - (n - 1) * spacing) / n;
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos() + ImVec2(0, CHART_SPACING);
|
||||
auto current_charts = charts_widget_->currentCharts();
|
||||
float bottom = origin.y;
|
||||
const bool aligned = ImPlot::BeginAlignedPlots("charts_align", true);
|
||||
for (int i = 0; i < current_charts.size(); ++i) {
|
||||
ImVec2 pos = origin + ImVec2((i % n) * (width + spacing), (i / n) * (settings.chart_height + spacing));
|
||||
ImGui::SetCursorScreenPos(pos);
|
||||
current_charts[i]->draw(width);
|
||||
bottom = std::max(bottom, pos.y + settings.chart_height);
|
||||
if (current_charts[i]->plotHovered()) charts_widget_->any_plot_hovered_ = true;
|
||||
}
|
||||
if (aligned) ImPlot::EndAlignedPlots();
|
||||
ImGui::SetCursorScreenPos(ImVec2(origin.x, bottom));
|
||||
ImGui::Dummy(ImVec2(geometry_.GetWidth(), CHART_SPACING));
|
||||
geometry_.Max.y = bottom + CHART_SPACING;
|
||||
drawDropIndicator();
|
||||
}
|
||||
|
||||
void ChartsContainer::drawDropIndicator() {
|
||||
if (!(drop_indicator_pos_.x == 0 && drop_indicator_pos_.y == 0) && !childAt(drop_indicator_pos_)) {
|
||||
ImRect r = geometry_;
|
||||
r.Max.y = r.Min.y + CHART_SPACING;
|
||||
if (auto insert_after = getDropAfter(drop_indicator_pos_)) {
|
||||
float h = r.GetHeight();
|
||||
r.Min.y = insert_after->rect().Max.y;
|
||||
r.Max.y = r.Min.y + h;
|
||||
}
|
||||
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_Header));
|
||||
}
|
||||
}
|
||||
|
||||
ChartView *ChartsContainer::getDropAfter(const ImVec2 &pos) const {
|
||||
const auto &charts = charts_widget_->currentCharts();
|
||||
auto it = std::find_if(charts.crbegin(), charts.crend(), [&pos](auto c) {
|
||||
const ImRect &area = c->rect();
|
||||
return pos.x >= area.Min.x && pos.x <= area.Max.x && pos.y >= area.Max.y;
|
||||
});
|
||||
return it == charts.crend() ? nullptr : *it;
|
||||
}
|
||||
|
||||
ChartView *ChartsContainer::childAt(const ImVec2 &pos) const {
|
||||
for (auto c : charts_widget_->currentCharts()) {
|
||||
if (c->rect().Contains(pos)) return c;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
168
iqpilot/tools/cabana/ui/chart/chartswidget.h
Normal file
168
iqpilot/tools/cabana/ui/chart/chartswidget.h
Normal file
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
#include "tools/cabana/ui/chart/signalselector.h"
|
||||
#include "tools/cabana/ui/widgets/tabbar.h"
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
const int CHART_MIN_WIDTH = 300;
|
||||
|
||||
|
||||
class LogSlider {
|
||||
public:
|
||||
LogSlider(double factor) : scale_(factor) {}
|
||||
|
||||
void setRange(double min, double max) {
|
||||
scale_.setRange(min, max);
|
||||
min_ = min;
|
||||
max_ = max;
|
||||
setValue(pos_);
|
||||
}
|
||||
int value() const { return scale_.value(pos_, minimum(), maximum()); }
|
||||
void setValue(int v) { pos_ = scale_.position(v, minimum(), maximum()); }
|
||||
int minimum() const { return min_; }
|
||||
int maximum() const { return max_; }
|
||||
bool draw(const char *label, float width);
|
||||
|
||||
private:
|
||||
LogScale scale_;
|
||||
int min_ = 0;
|
||||
int max_ = 1;
|
||||
int pos_ = 0;
|
||||
};
|
||||
|
||||
class ChartView;
|
||||
class ChartsWidget;
|
||||
|
||||
class ChartsContainer {
|
||||
public:
|
||||
ChartsContainer(ChartsWidget *parent) : charts_widget_(parent) {}
|
||||
void setDropIndicator(const ImVec2 &pt) { drop_indicator_pos_ = pt; }
|
||||
void draw();
|
||||
ChartView *getDropAfter(const ImVec2 &pos) const;
|
||||
ChartView *childAt(const ImVec2 &pos) const;
|
||||
const ImRect &geometry() const { return geometry_; }
|
||||
|
||||
private:
|
||||
void drawDropIndicator();
|
||||
|
||||
ImRect geometry_;
|
||||
ChartsWidget *charts_widget_;
|
||||
ImVec2 drop_indicator_pos_;
|
||||
};
|
||||
|
||||
class ChartsWidget {
|
||||
public:
|
||||
ChartsWidget();
|
||||
~ChartsWidget();
|
||||
void draw();
|
||||
void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge);
|
||||
inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; }
|
||||
std::vector<std::string> serializeChartIds() const;
|
||||
void restoreChartsFromIds(const std::vector<std::string> &chart_ids);
|
||||
std::string whatsThis() const;
|
||||
|
||||
void setColumnCount(int n);
|
||||
void removeAll();
|
||||
void setIsDocked(bool dock);
|
||||
|
||||
Observable<> toggleChartsDocking;
|
||||
Observable<> seriesChanged;
|
||||
Observable<double> showTip;
|
||||
|
||||
private:
|
||||
void handleEvents();
|
||||
void newChart();
|
||||
ChartView *createChart(int pos = 0);
|
||||
void removeChart(ChartView *chart);
|
||||
void splitChart(ChartView *chart);
|
||||
ImRect chartVisibleRect(ChartView *chart);
|
||||
void eventsMerged(const MessageEventsMap &new_events);
|
||||
void updateState();
|
||||
void zoomReset();
|
||||
void startChartDrag(ChartView *chart, const ImVec2 &global_pos);
|
||||
void dragChartMove(const ImVec2 &global_pos);
|
||||
void dragChartRelease(const ImVec2 &global_pos);
|
||||
void cancelChartDrag();
|
||||
bool chartDragActive() const { return drag_.source != nullptr; }
|
||||
void startAutoScroll(const ImVec2 &global_pos);
|
||||
void stopAutoScroll();
|
||||
void doAutoScroll();
|
||||
void drawToolBar();
|
||||
void updateTabBar();
|
||||
void setMaxChartRange(int value);
|
||||
void updateLayout();
|
||||
void settingChanged();
|
||||
void showValueTip(double sec);
|
||||
void newTab();
|
||||
void removeTab(int index);
|
||||
inline std::vector<ChartView *> ¤tCharts() { return tab_charts_[tabbar_.tabData(tabbar_.currentIndex())]; }
|
||||
ChartView *findChart(const MessageId &id, const cabana::Signal *sig);
|
||||
|
||||
void execSignalSelector(std::unique_ptr<SignalSelector> dlg, ChartView *owner, std::function<void(SignalSelector &)> accepted);
|
||||
void drawDragPreview();
|
||||
|
||||
LogSlider range_slider_{1000};
|
||||
bool is_docked_ = true;
|
||||
bool float_window_init_ = false;
|
||||
|
||||
UndoStack zoom_undo_stack_;
|
||||
|
||||
std::vector<std::unique_ptr<ChartView>> charts_;
|
||||
std::unordered_map<int, std::vector<ChartView *>> tab_charts_;
|
||||
TabBar tabbar_;
|
||||
ChartsContainer charts_container_{this};
|
||||
ImGuiWindow *charts_scroll_ = nullptr;
|
||||
ImRect charts_scroll_viewport_;
|
||||
int max_chart_range_ = 0;
|
||||
std::pair<double, double> display_range_;
|
||||
bool columns_action_visible_ = false;
|
||||
int column_count_ = 1;
|
||||
int current_column_count_ = 0;
|
||||
struct ChartDrag {
|
||||
ChartView *source = nullptr;
|
||||
ImVec2 press_pos;
|
||||
bool active = false;
|
||||
} drag_;
|
||||
|
||||
ImVec2 drag_preview_pos_;
|
||||
ImVec2 drag_preview_size_;
|
||||
bool drag_preview_visible_ = false;
|
||||
ChartView *drop_target_ = nullptr;
|
||||
int auto_scroll_count_ = 0;
|
||||
ImVec2 auto_scroll_pos_;
|
||||
bool auto_scroll_timer_active_ = false;
|
||||
double auto_scroll_timer_next_ = 0;
|
||||
bool value_tip_visible_ = false;
|
||||
bool any_plot_hovered_ = false;
|
||||
std::vector<std::unique_ptr<ChartView>> deleted_charts_;
|
||||
std::unique_ptr<SignalSelector> signal_selector_;
|
||||
ChartView *signal_selector_owner_ = nullptr;
|
||||
std::function<void(SignalSelector &)> signal_selector_accepted_;
|
||||
Connections connections_;
|
||||
friend class ChartView;
|
||||
friend class ChartsContainer;
|
||||
};
|
||||
|
||||
class ZoomCommand : public UndoCommand {
|
||||
public:
|
||||
ZoomCommand(std::pair<double, double> range) : range(range) {
|
||||
prev_range = can->timeRange();
|
||||
}
|
||||
void undo() override { can->setTimeRange(prev_range); }
|
||||
void redo() override { can->setTimeRange(range); }
|
||||
std::optional<std::pair<double, double>> prev_range, range;
|
||||
};
|
||||
155
iqpilot/tools/cabana/ui/chart/signalselector.cc
Normal file
155
iqpilot/tools/cabana/ui/chart/signalselector.cc
Normal file
@@ -0,0 +1,155 @@
|
||||
#include "tools/cabana/ui/chart/signalselector.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/ui/chart/chart.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
SignalSelector::SignalSelector(std::string title) : title_(std::move(title)) {
|
||||
for (const auto &[id, _] : can->lastMessages()) {
|
||||
if (auto m = dbc()->msg(id)) {
|
||||
msgs_combo_.push_back({m->name + " (" + id.toString() + ")", id});
|
||||
}
|
||||
}
|
||||
std::sort(msgs_combo_.begin(), msgs_combo_.end(), [](auto &a, auto &b) { return a.text < b.text; });
|
||||
}
|
||||
|
||||
bool SignalSelector::draw() {
|
||||
if (!open_) return false;
|
||||
const std::string popup_id = title_ + "###SignalSelector";
|
||||
if (!show_) {
|
||||
ImGui::OpenPopup(popup_id.c_str());
|
||||
show_ = true;
|
||||
}
|
||||
setNextDialogWindow(ImVec2(700.0f, 450.0f));
|
||||
if (!ImGui::BeginPopupModal(popup_id.c_str(), nullptr, ImGuiWindowFlags_NoSavedSettings)) {
|
||||
open_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const float btn_w = ImGui::GetFrameHeight() + 8.0f;
|
||||
const float column_w = (ImGui::GetContentRegionAvail().x - btn_w - ImGui::GetStyle().ItemSpacing.x * 2) / 2;
|
||||
|
||||
const float lists_h = ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing() * 3;
|
||||
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted("Available Signals");
|
||||
|
||||
const char *preview = msgs_combo_index_ >= 0 ? msgs_combo_[msgs_combo_index_].text.c_str() : "Select a msg...";
|
||||
ImGui::SetNextItemWidth(column_w);
|
||||
if (ImGui::BeginCombo("##msgs_combo", preview)) {
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
msgs_combo_filter_.clear();
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
inputText("##msgs_filter", &msgs_combo_filter_, "Select a msg...");
|
||||
for (int i = 0; i < (int)msgs_combo_.size(); ++i) {
|
||||
if (!msgs_combo_filter_.empty() && !utils::containsCI(msgs_combo_[i].text, msgs_combo_filter_)) continue;
|
||||
if (ImGui::Selectable(msgs_combo_[i].text.c_str(), i == msgs_combo_index_)) {
|
||||
msgs_combo_index_ = i;
|
||||
updateAvailableList(i);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
bool add_dbl = false;
|
||||
drawList("##available_list", available_list_, &available_row_, false, &add_dbl, ImVec2(column_w, lists_h));
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::Dummy(ImVec2(btn_w, (lists_h + ImGui::GetFrameHeightWithSpacing() * 2) / 2 - ImGui::GetFrameHeight()));
|
||||
ImGui::BeginDisabled(available_row_ == -1);
|
||||
bool add_clicked = ImGui::Button(icon::CHEVRON_RIGHT, ImVec2(btn_w, 0));
|
||||
ImGui::EndDisabled();
|
||||
ImGui::BeginDisabled(selected_row_ == -1);
|
||||
bool remove_clicked = ImGui::Button(icon::CHEVRON_LEFT, ImVec2(btn_w, 0));
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted("Selected Signals");
|
||||
bool remove_dbl = false;
|
||||
drawList("##selected_list", selected_list_, &selected_row_, true, &remove_dbl, ImVec2(column_w, lists_h + ImGui::GetFrameHeightWithSpacing()));
|
||||
bool rejected = false;
|
||||
dialogButtons("OK", &accepted_, &rejected);
|
||||
const bool done = accepted_ || rejected;
|
||||
ImGui::EndGroup();
|
||||
|
||||
if ((add_dbl || add_clicked) && available_row_ >= 0 && available_row_ < (int)available_list_.size()) {
|
||||
add(available_row_);
|
||||
} else if ((remove_dbl || remove_clicked) && selected_row_ >= 0 && selected_row_ < (int)selected_list_.size()) {
|
||||
remove(selected_row_);
|
||||
}
|
||||
|
||||
if (done) {
|
||||
open_ = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
return open_;
|
||||
}
|
||||
|
||||
void SignalSelector::drawList(const char *id, std::vector<ListItem> &list, int *current_row, bool show_msg_name, bool *double_clicked, const ImVec2 &size) {
|
||||
if (!ImGui::BeginListBox(id, size)) return;
|
||||
for (int i = 0; i < (int)list.size(); ++i) {
|
||||
const auto &item = list[i];
|
||||
ImGui::PushID(i);
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
if (ImGui::Selectable("##item", i == *current_row)) *current_row = i;
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||
*current_row = i;
|
||||
*double_clicked = true;
|
||||
}
|
||||
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
float x = pos.x + 5;
|
||||
drawColorMarker(dl, ImVec2(x, pos.y), toImU32(item.sig->color));
|
||||
x += markerSize() + 4;
|
||||
dl->AddText(ImVec2(x, pos.y), ImGui::GetColorU32(ImGuiCol_Text), item.sig->name.c_str());
|
||||
if (show_msg_name) {
|
||||
x += ImGui::CalcTextSize(item.sig->name.c_str()).x;
|
||||
dl->AddText(ImVec2(x, pos.y), ImGui::GetColorU32(ImGuiCol_TextDisabled), msgLabel(item.msg_id).c_str());
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndListBox();
|
||||
}
|
||||
|
||||
void SignalSelector::add(int row) {
|
||||
const auto &item = available_list_[row];
|
||||
selected_list_.emplace_back(item.msg_id, item.sig);
|
||||
available_list_.erase(available_list_.begin() + row);
|
||||
available_row_ = -1;
|
||||
}
|
||||
|
||||
void SignalSelector::remove(int row) {
|
||||
const auto &item = selected_list_[row];
|
||||
if (msgs_combo_index_ >= 0 && item.msg_id == msgs_combo_[msgs_combo_index_].id) {
|
||||
available_list_.emplace_back(item.msg_id, item.sig);
|
||||
}
|
||||
selected_list_.erase(selected_list_.begin() + row);
|
||||
selected_row_ = -1;
|
||||
}
|
||||
|
||||
void SignalSelector::updateAvailableList(int index) {
|
||||
if (index == -1) return;
|
||||
available_list_.clear();
|
||||
available_row_ = -1;
|
||||
MessageId msg_id = msgs_combo_[index].id;
|
||||
for (auto s : dbc()->msg(msg_id)->getSignals()) {
|
||||
bool is_selected = std::any_of(selected_list_.begin(), selected_list_.end(),
|
||||
[sig = s, &msg_id](auto &it) { return it.msg_id == msg_id && it.sig == sig; });
|
||||
if (!is_selected) {
|
||||
available_list_.emplace_back(msg_id, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
iqpilot/tools/cabana/ui/chart/signalselector.h
Normal file
46
iqpilot/tools/cabana/ui/chart/signalselector.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
|
||||
|
||||
class SignalSelector {
|
||||
public:
|
||||
struct ListItem {
|
||||
ListItem(const MessageId &msg_id, const cabana::Signal *sig) : msg_id(msg_id), sig(sig) {}
|
||||
MessageId msg_id;
|
||||
const cabana::Signal *sig;
|
||||
};
|
||||
|
||||
SignalSelector(std::string title);
|
||||
const std::vector<ListItem> &selectedItems() const { return selected_list_; }
|
||||
inline void addSelected(const MessageId &id, const cabana::Signal *sig) { selected_list_.emplace_back(id, sig); }
|
||||
void open() { open_ = true; show_ = false; accepted_ = false; }
|
||||
bool draw();
|
||||
bool accepted() const { return accepted_; }
|
||||
|
||||
private:
|
||||
void updateAvailableList(int index);
|
||||
void add(int row);
|
||||
void remove(int row);
|
||||
void drawList(const char *id, std::vector<ListItem> &list, int *current_row, bool show_msg_name, bool *double_clicked, const ImVec2 &size);
|
||||
|
||||
struct ComboItem {
|
||||
std::string text;
|
||||
MessageId id;
|
||||
};
|
||||
std::string title_;
|
||||
std::vector<ComboItem> msgs_combo_;
|
||||
int msgs_combo_index_ = -1;
|
||||
std::string msgs_combo_filter_;
|
||||
std::vector<ListItem> available_list_;
|
||||
std::vector<ListItem> selected_list_;
|
||||
int available_row_ = -1;
|
||||
int selected_row_ = -1;
|
||||
bool accepted_ = false;
|
||||
bool open_ = false;
|
||||
bool show_ = false;
|
||||
};
|
||||
177
iqpilot/tools/cabana/ui/chart/sparkline.cc
Normal file
177
iqpilot/tools/cabana/ui/chart/sparkline.cc
Normal file
@@ -0,0 +1,177 @@
|
||||
#include "tools/cabana/ui/chart/sparkline.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, ImVec2 sz,
|
||||
double window_end) {
|
||||
if (first == last || sz.x <= 0 || sz.y <= 0) {
|
||||
render_points_.clear();
|
||||
size = {};
|
||||
return;
|
||||
}
|
||||
|
||||
points_.clear();
|
||||
min_val = std::numeric_limits<double>::max();
|
||||
max_val = std::numeric_limits<double>::lowest();
|
||||
points_.reserve(std::distance(first, last));
|
||||
|
||||
|
||||
|
||||
|
||||
const double window_start = window_end - range;
|
||||
double value = 0.0;
|
||||
for (auto it = first; it != last; ++it) {
|
||||
if (sig->getValue((*it)->dat, (*it)->size, &value)) {
|
||||
double x = can->toSeconds((*it)->mono_time) - window_start;
|
||||
|
||||
|
||||
if (x >= 0.0) {
|
||||
min_val = std::min(min_val, value);
|
||||
max_val = std::max(max_val, value);
|
||||
}
|
||||
points_.push_back({x, value});
|
||||
}
|
||||
}
|
||||
if (min_val > max_val) {
|
||||
for (const auto &p : points_) {
|
||||
min_val = std::min(min_val, p.y);
|
||||
max_val = std::max(max_val, p.y);
|
||||
}
|
||||
}
|
||||
|
||||
if (points_.empty()) {
|
||||
render_points_.clear();
|
||||
size = {};
|
||||
return;
|
||||
}
|
||||
|
||||
freq_ = points_.size() / std::max(points_.back().x - points_.front().x, 1.0);
|
||||
render(sig->color, range, sz, window_end);
|
||||
}
|
||||
|
||||
void Sparkline::render(const CabanaColor &color, int range, ImVec2 sz, double window_end) {
|
||||
bool is_flat_line = min_val == max_val;
|
||||
if (is_flat_line) {
|
||||
min_val -= 1.0;
|
||||
max_val += 1.0;
|
||||
}
|
||||
|
||||
const double xscale = (sz.x - 1) / (double)range;
|
||||
const double yscale = (sz.y - 3) / (max_val - min_val);
|
||||
const double span = points_.back().x - points_.front().x;
|
||||
bool draw_individual_points = (span * xscale / points_.size()) > 8.0;
|
||||
|
||||
|
||||
render_points_.reserve(points_.size());
|
||||
render_points_.clear();
|
||||
if (draw_individual_points) {
|
||||
for (const auto &p : points_) {
|
||||
render_points_.emplace_back(p.x * xscale, 1.0 + (max_val - p.y) * yscale);
|
||||
}
|
||||
} else if (is_flat_line) {
|
||||
double y = sz.y / 2.0;
|
||||
render_points_.emplace_back(points_.front().x * xscale, y);
|
||||
render_points_.emplace_back(points_.back().x * xscale, y);
|
||||
} else {
|
||||
double prev_y = points_.front().y;
|
||||
render_points_.emplace_back(points_.front().x * xscale, 1.0 + (max_val - prev_y) * yscale);
|
||||
bool in_flat = false;
|
||||
|
||||
for (size_t i = 1; i < points_.size(); ++i) {
|
||||
const auto &p = points_[i];
|
||||
double y = p.y;
|
||||
if (std::abs(y - prev_y) < 1e-6) {
|
||||
in_flat = true;
|
||||
} else {
|
||||
if (in_flat) render_points_.emplace_back(points_[i - 1].x * xscale, 1.0 + (max_val - prev_y) * yscale);
|
||||
render_points_.emplace_back(p.x * xscale, 1.0 + (max_val - y) * yscale);
|
||||
in_flat = false;
|
||||
}
|
||||
prev_y = y;
|
||||
}
|
||||
if (in_flat) render_points_.emplace_back(points_.back().x * xscale, 1.0 + (max_val - prev_y) * yscale);
|
||||
}
|
||||
|
||||
size = sz;
|
||||
CabanaColor line_color = color;
|
||||
if (!isDarkTheme()) {
|
||||
auto [h, s, v] = color.hsv();
|
||||
line_color = CabanaColor::fromHsv(h, std::min(1.0f, s * 2.0f), v * 0.7f, color.a / 255.0f);
|
||||
}
|
||||
color_ = toImU32(line_color);
|
||||
draw_individual_points_ = draw_individual_points;
|
||||
window_end_ = window_end;
|
||||
xscale_ = xscale;
|
||||
}
|
||||
|
||||
void Sparkline::draw(ImDrawList *draw_list, ImVec2 pos) const {
|
||||
if (render_points_.empty()) return;
|
||||
|
||||
|
||||
|
||||
|
||||
const float shift = std::clamp((float)((can->currentSec() - window_end_) * xscale_), 0.0f, size.x);
|
||||
|
||||
|
||||
const float px = 1.0f / std::max(1.0f, ImGui::GetIO().DisplayFramebufferScale.x);
|
||||
auto snap = [&](float x) { return std::floor(x / px) * px; };
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ImVec2 offset(pos.x - shift, pos.y);
|
||||
const double k = offset.x - (window_end_ * xscale_ - (size.x - 1));
|
||||
offset.x += snap(k) - k;
|
||||
auto point_at = [&](const ImVec2 &p) { return ImVec2(offset.x + p.x, offset.y + p.y); };
|
||||
|
||||
draw_list->PushClipRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), true);
|
||||
|
||||
|
||||
auto draw_point = [&](const ImVec2 &p) { draw_list->AddRectFilled(ImVec2(p.x - 1.5f, p.y - 1.5f), ImVec2(p.x + 1.5f, p.y + 1.5f), color_); };
|
||||
|
||||
if (draw_individual_points_) {
|
||||
for (const auto &p : render_points_) {
|
||||
draw_list->PathLineTo(point_at(p));
|
||||
draw_point(point_at(p));
|
||||
}
|
||||
draw_list->PathStroke(color_, ImDrawFlags_None, 1.5f);
|
||||
} else {
|
||||
|
||||
|
||||
std::vector<ImVec2> pts;
|
||||
pts.reserve(render_points_.size());
|
||||
float col = -1e9f;
|
||||
for (const auto &p : render_points_) {
|
||||
ImVec2 sp = point_at(p);
|
||||
float c = snap(sp.x);
|
||||
if (c != col) {
|
||||
pts.push_back(sp);
|
||||
col = c;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
auto steep = [&](size_t i) { return std::abs(pts[i + 1].y - pts[i].y) > 2.0f * std::abs(pts[i + 1].x - pts[i].x) + px; };
|
||||
const ImDrawListFlags saved = draw_list->Flags;
|
||||
size_t i = 0;
|
||||
while (i + 1 < pts.size()) {
|
||||
const bool is_steep = steep(i);
|
||||
size_t j = i + 1;
|
||||
while (j + 1 < pts.size() && steep(j) == is_steep) ++j;
|
||||
draw_list->Flags = is_steep ? (saved & ~ImDrawListFlags_AntiAliasedLines) : saved;
|
||||
for (size_t n = i; n <= j; ++n) draw_list->PathLineTo(pts[n]);
|
||||
draw_list->PathStroke(color_, ImDrawFlags_None, 1.0f);
|
||||
i = j;
|
||||
}
|
||||
draw_list->Flags = saved;
|
||||
draw_point(point_at(render_points_.back()));
|
||||
}
|
||||
draw_list->PopClipRect();
|
||||
}
|
||||
34
iqpilot/tools/cabana/ui/chart/sparkline.h
Normal file
34
iqpilot/tools/cabana/ui/chart/sparkline.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/dbc/dbc.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
class Sparkline {
|
||||
public:
|
||||
void update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, ImVec2 sz, double window_end);
|
||||
inline double freq() const { return freq_; }
|
||||
bool isEmpty() const { return render_points_.empty(); }
|
||||
|
||||
void draw(ImDrawList *draw_list, ImVec2 pos) const;
|
||||
|
||||
ImVec2 size = {};
|
||||
double min_val = 0;
|
||||
double max_val = 0;
|
||||
|
||||
private:
|
||||
struct Point {
|
||||
double x, y;
|
||||
};
|
||||
void render(const CabanaColor &color, int range, ImVec2 sz, double window_end);
|
||||
|
||||
std::vector<Point> points_;
|
||||
std::vector<ImVec2> render_points_;
|
||||
ImU32 color_ = 0;
|
||||
double window_end_ = 0;
|
||||
double xscale_ = 0;
|
||||
bool draw_individual_points_ = false;
|
||||
double freq_ = 0;
|
||||
};
|
||||
69
iqpilot/tools/cabana/ui/chart/tiplabel.cc
Normal file
69
iqpilot/tools/cabana/ui/chart/tiplabel.cc
Normal file
@@ -0,0 +1,69 @@
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include "tools/cabana/ui/chart/tiplabel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
ImVec2 TipLabel::layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const {
|
||||
ImFont *bold = boldFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float line_height = ImGui::GetTextLineHeight();
|
||||
ImVec2 size(0, 0);
|
||||
float y = origin.y;
|
||||
for (const auto &line : text_) {
|
||||
float x = origin.x;
|
||||
if (line.has_marker) {
|
||||
if (p) drawColorMarker(p, ImVec2(x, y), line.marker);
|
||||
x += markerSize() + 4;
|
||||
}
|
||||
if (p) p->AddText(ImVec2(x, y), fg, line.name.c_str());
|
||||
x += ImGui::CalcTextSize(line.name.c_str()).x;
|
||||
if (!line.bold.empty()) {
|
||||
if (p) p->AddText(bold, font_size, ImVec2(x, y), fg, line.bold.c_str());
|
||||
x += bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, line.bold.c_str()).x;
|
||||
}
|
||||
if (p) p->AddText(ImVec2(x, y), fg, line.rest.c_str());
|
||||
x += ImGui::CalcTextSize(line.rest.c_str()).x;
|
||||
size.x = std::max(size.x, x - origin.x);
|
||||
y += line_height;
|
||||
}
|
||||
size.y = y - origin.y;
|
||||
return size;
|
||||
}
|
||||
|
||||
ImVec2 TipLabel::sizeHint() const {
|
||||
return layoutLines(nullptr, ImVec2(0, 0), 0) + ImVec2(MARGIN * 2, MARGIN * 2);
|
||||
}
|
||||
|
||||
void TipLabel::showText(const ImVec2 &pt, const std::vector<TipLine> &text, const ImRect &rect) {
|
||||
text_ = text;
|
||||
if (!text_.empty()) {
|
||||
ImVec2 extra(1, 1);
|
||||
size_ = sizeHint() + extra;
|
||||
ImVec2 tip_pos(pt.x + 8, rect.Min.y + 2);
|
||||
if (tip_pos.x + size_.x >= rect.Max.x) {
|
||||
tip_pos.x = pt.x - size_.x - 8;
|
||||
}
|
||||
if (rect.Contains(ImRect(tip_pos, tip_pos + size_))) {
|
||||
pos_ = tip_pos;
|
||||
visible_ = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
visible_ = false;
|
||||
}
|
||||
|
||||
void TipLabel::draw() {
|
||||
if (!visible_) return;
|
||||
|
||||
ImDrawList *p = ImGui::GetForegroundDrawList();
|
||||
const bool dark = isDarkTheme();
|
||||
const ImU32 bg = dark ? ImGui::GetColorU32(ImGuiCol_PopupBg) : ImGui::GetColorU32(ImGuiCol_ChildBg);
|
||||
const ImU32 fg = dark ? ImGui::GetColorU32(ImGuiCol_Text) : IM_COL32(0x40, 0x40, 0x44, 0xff);
|
||||
|
||||
p->AddRectFilled(pos_, pos_ + size_, bg);
|
||||
p->AddRect(pos_, pos_ + size_, ImGui::GetColorU32(ImGuiCol_Border));
|
||||
layoutLines(p, pos_ + ImVec2(MARGIN, MARGIN), fg);
|
||||
}
|
||||
35
iqpilot/tools/cabana/ui/chart/tiplabel.h
Normal file
35
iqpilot/tools/cabana/ui/chart/tiplabel.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
|
||||
struct TipLine {
|
||||
bool has_marker = false;
|
||||
ImU32 marker = 0;
|
||||
std::string name;
|
||||
std::string bold;
|
||||
std::string rest;
|
||||
};
|
||||
|
||||
class TipLabel {
|
||||
public:
|
||||
void showText(const ImVec2 &pt, const std::vector<TipLine> &text, const ImRect &rect);
|
||||
void hide() { visible_ = false; }
|
||||
bool isVisible() const { return visible_; }
|
||||
void draw();
|
||||
|
||||
private:
|
||||
|
||||
ImVec2 layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const;
|
||||
ImVec2 sizeHint() const;
|
||||
|
||||
static constexpr float MARGIN = 2.0f;
|
||||
std::vector<TipLine> text_;
|
||||
ImVec2 pos_;
|
||||
ImVec2 size_;
|
||||
bool visible_ = false;
|
||||
};
|
||||
222
iqpilot/tools/cabana/ui/dialogs/filedialog.cc
Normal file
222
iqpilot/tools/cabana/ui/dialogs/filedialog.cc
Normal file
@@ -0,0 +1,222 @@
|
||||
#include "tools/cabana/ui/dialogs/filedialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <system_error>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace FileDialog {
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
|
||||
|
||||
bool naturalLess(const std::string &a, const std::string &b) {
|
||||
auto skip = [](const std::string &s, size_t &i) {
|
||||
while (i < s.size() && !isalnum(static_cast<unsigned char>(s[i]))) ++i;
|
||||
};
|
||||
size_t i = 0, j = 0;
|
||||
for (;;) {
|
||||
skip(a, i);
|
||||
skip(b, j);
|
||||
if (i >= a.size() || j >= b.size()) break;
|
||||
if (isdigit(static_cast<unsigned char>(a[i])) && isdigit(static_cast<unsigned char>(b[j]))) {
|
||||
size_t ie = i, je = j;
|
||||
while (ie < a.size() && isdigit(static_cast<unsigned char>(a[ie]))) ++ie;
|
||||
while (je < b.size() && isdigit(static_cast<unsigned char>(b[je]))) ++je;
|
||||
const unsigned long long na = std::stoull(a.substr(i, ie - i)), nb = std::stoull(b.substr(j, je - j));
|
||||
if (na != nb) return na < nb;
|
||||
i = ie;
|
||||
j = je;
|
||||
} else {
|
||||
const int ca = tolower(static_cast<unsigned char>(a[i])), cb = tolower(static_cast<unsigned char>(b[j]));
|
||||
if (ca != cb) return ca < cb;
|
||||
++i;
|
||||
++j;
|
||||
}
|
||||
}
|
||||
const bool a_done = i >= a.size(), b_done = j >= b.size();
|
||||
if (a_done != b_done) return a_done;
|
||||
return a < b;
|
||||
}
|
||||
|
||||
enum class Mode { OpenFile, SaveFile, Directory };
|
||||
|
||||
struct State {
|
||||
bool active = false;
|
||||
Mode mode = Mode::OpenFile;
|
||||
std::string title;
|
||||
std::string extension;
|
||||
fs::path dir;
|
||||
std::string dir_input;
|
||||
std::string filename;
|
||||
std::vector<fs::directory_entry> entries;
|
||||
Callback callback;
|
||||
};
|
||||
|
||||
State g_state;
|
||||
PopupOwner g_owner;
|
||||
|
||||
void listDir() {
|
||||
State &s = g_state;
|
||||
s.entries.clear();
|
||||
std::error_code ec;
|
||||
for (const auto &entry : fs::directory_iterator(s.dir, ec)) {
|
||||
const std::string name = entry.path().filename().string();
|
||||
if (name.empty() || name[0] == '.') continue;
|
||||
const bool is_dir = entry.is_directory(ec);
|
||||
if (!is_dir && s.mode == Mode::Directory) continue;
|
||||
if (!is_dir && !s.extension.empty() && entry.path().extension() != s.extension) continue;
|
||||
s.entries.push_back(entry);
|
||||
}
|
||||
std::sort(s.entries.begin(), s.entries.end(), [](const auto &a, const auto &b) {
|
||||
std::error_code sort_ec;
|
||||
const bool da = a.is_directory(sort_ec), db = b.is_directory(sort_ec);
|
||||
return da != db ? da : naturalLess(a.path().filename().string(), b.path().filename().string());
|
||||
});
|
||||
s.dir_input = s.dir.string();
|
||||
}
|
||||
|
||||
void setDir(const fs::path &dir) {
|
||||
std::error_code ec;
|
||||
fs::path d = fs::is_directory(dir, ec) ? fs::absolute(dir, ec) : fs::current_path(ec);
|
||||
g_state.dir = d.lexically_normal();
|
||||
listDir();
|
||||
}
|
||||
|
||||
void start(Mode mode, const std::string &title, const fs::path &dir, const std::string &filename,
|
||||
const std::string &extension, Callback cb) {
|
||||
State &s = g_state;
|
||||
s = State{};
|
||||
s.active = true;
|
||||
s.mode = mode;
|
||||
s.title = title;
|
||||
s.extension = extension;
|
||||
s.filename = filename;
|
||||
s.callback = std::move(cb);
|
||||
g_owner.reset();
|
||||
setDir(dir);
|
||||
}
|
||||
|
||||
void finish(const std::string &path) {
|
||||
Callback cb = std::move(g_state.callback);
|
||||
g_state = State{};
|
||||
g_owner.reset();
|
||||
if (cb) cb(path);
|
||||
}
|
||||
|
||||
void accept(const fs::path &path) {
|
||||
if (g_state.mode == Mode::SaveFile) {
|
||||
std::error_code ec;
|
||||
if (fs::exists(path, ec)) {
|
||||
const std::string name = path.filename().string();
|
||||
MessageBox::question(g_state.title, name + " already exists.\nDo you want to replace it?", [path](bool ok) {
|
||||
if (ok) finish(path.string());
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
finish(path.string());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void getOpenFileName(const std::string &title, const std::string &dir, const std::string &extension, Callback cb) {
|
||||
start(Mode::OpenFile, title, dir, "", extension, std::move(cb));
|
||||
}
|
||||
|
||||
void getSaveFileName(const std::string &title, const std::string &default_path, const std::string &extension, Callback cb) {
|
||||
const fs::path p(default_path);
|
||||
start(Mode::SaveFile, title, p.parent_path(), p.filename().string(), extension, std::move(cb));
|
||||
}
|
||||
|
||||
void getExistingDirectory(const std::string &title, const std::string &dir, Callback cb) {
|
||||
start(Mode::Directory, title, dir, "", "", std::move(cb));
|
||||
}
|
||||
|
||||
void draw() {
|
||||
State &s = g_state;
|
||||
if (!s.active) return;
|
||||
const std::string popup_id = s.title + "###FileDialog";
|
||||
if (!beginDialog(popup_id.c_str(), &g_owner, ImVec2(640.0f, 480.0f), 0)) return;
|
||||
|
||||
if (ImGui::Button("Up")) setDir(s.dir.parent_path());
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (inputText("##dir", &s.dir_input, "", ImGuiInputTextFlags_EnterReturnsTrue)) setDir(s.dir_input);
|
||||
|
||||
const float footer = ImGui::GetFrameHeightWithSpacing() * (s.mode == Mode::Directory ? 1.0f : 2.0f) + ImGui::GetStyle().ItemSpacing.y;
|
||||
bool ok = false, cancel = false;
|
||||
fs::path result, pending_dir;
|
||||
ImGui::BeginChild("entries", ImVec2(0, -footer), ImGuiChildFlags_Borders);
|
||||
std::error_code dir_ec;
|
||||
for (size_t i = 0; i < s.entries.size(); ++i) {
|
||||
const auto &entry = s.entries[i];
|
||||
const bool is_dir = entry.is_directory(dir_ec);
|
||||
const std::string name = entry.path().filename().string();
|
||||
const std::string label = (is_dir ? std::string(icon::FOLDER) : std::string(icon::FILE_EARMARK)) + " " + name;
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
const bool selected = !is_dir && name == s.filename;
|
||||
if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
const bool double_clicked = ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
|
||||
if (is_dir) {
|
||||
if (double_clicked) {
|
||||
pending_dir = entry.path();
|
||||
} else if (s.mode == Mode::Directory) {
|
||||
s.filename = name;
|
||||
}
|
||||
} else {
|
||||
s.filename = name;
|
||||
if (double_clicked && s.mode == Mode::OpenFile) {
|
||||
result = entry.path();
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
if (ok || !pending_dir.empty()) break;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
if (!pending_dir.empty()) setDir(pending_dir);
|
||||
|
||||
if (s.mode != Mode::Directory) {
|
||||
ImGui::SetNextItemWidth(-90.0f);
|
||||
if (inputText("##name", &s.filename, "File name", ImGuiInputTextFlags_EnterReturnsTrue)) ok = true;
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", s.extension.empty() ? "*" : ("*" + s.extension).c_str());
|
||||
}
|
||||
const char *accept_label = s.mode == Mode::SaveFile ? "Save" : (s.mode == Mode::Directory ? "Choose" : "Open");
|
||||
dialogButtons(accept_label, &ok, &cancel);
|
||||
|
||||
if (ok && result.empty()) {
|
||||
if (s.mode == Mode::Directory) {
|
||||
result = s.filename.empty() ? s.dir : s.dir / s.filename;
|
||||
} else if (!s.filename.empty()) {
|
||||
result = fs::path(s.filename).is_absolute() ? fs::path(s.filename) : s.dir / s.filename;
|
||||
if (s.mode == Mode::SaveFile && !s.extension.empty() && result.extension().empty()) result += s.extension;
|
||||
if (s.mode == Mode::OpenFile && !fs::is_regular_file(result, dir_ec)) ok = false;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok || cancel) ImGui::CloseCurrentPopup();
|
||||
|
||||
MessageBox::draw();
|
||||
if (!s.active) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
if (cancel) {
|
||||
finish("");
|
||||
} else if (ok) {
|
||||
accept(result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
19
iqpilot/tools/cabana/ui/dialogs/filedialog.h
Normal file
19
iqpilot/tools/cabana/ui/dialogs/filedialog.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace FileDialog {
|
||||
|
||||
using Callback = std::function<void(const std::string &path)>;
|
||||
|
||||
void getOpenFileName(const std::string &title, const std::string &dir, const std::string &extension, Callback cb);
|
||||
void getSaveFileName(const std::string &title, const std::string &default_path, const std::string &extension, Callback cb);
|
||||
void getExistingDirectory(const std::string &title, const std::string &dir, Callback cb);
|
||||
|
||||
void draw();
|
||||
|
||||
}
|
||||
87
iqpilot/tools/cabana/ui/dialogs/messagebox.cc
Normal file
87
iqpilot/tools/cabana/ui/dialogs/messagebox.cc
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||
|
||||
#include <deque>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
namespace MessageBox {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Box {
|
||||
std::string title;
|
||||
std::string text;
|
||||
std::string detailed_text;
|
||||
bool has_cancel = false;
|
||||
std::function<void(bool)> on_result;
|
||||
};
|
||||
|
||||
std::deque<Box> g_queue;
|
||||
bool g_show_details = false;
|
||||
PopupOwner g_owner;
|
||||
|
||||
void push(Box box) { g_queue.push_back(std::move(box)); }
|
||||
|
||||
std::function<void(bool)> wrap(std::function<void()> on_close) {
|
||||
if (!on_close) return nullptr;
|
||||
return [on_close = std::move(on_close)](bool) { on_close(); };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void information(const std::string &title, const std::string &text, std::function<void()> on_close) {
|
||||
push({.title = title, .text = text, .on_result = wrap(std::move(on_close))});
|
||||
}
|
||||
|
||||
void warning(const std::string &title, const std::string &text, const std::string &detailed_text,
|
||||
std::function<void()> on_close) {
|
||||
push({.title = title, .text = text, .detailed_text = detailed_text, .on_result = wrap(std::move(on_close))});
|
||||
}
|
||||
|
||||
void question(const std::string &title, const std::string &text, std::function<void(bool)> on_result) {
|
||||
push({.title = title, .text = text, .has_cancel = true, .on_result = std::move(on_result)});
|
||||
}
|
||||
|
||||
void draw() {
|
||||
if (g_queue.empty()) return;
|
||||
Box &box = g_queue.front();
|
||||
const std::string popup_id = box.title + "###MessageBox";
|
||||
const bool first = g_owner.popup_id == 0;
|
||||
if (!g_owner.begin(popup_id.c_str())) return;
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float min_width = ImGui::CalcTextSize(box.title.c_str()).x + style.FramePadding.x * 2 + style.WindowPadding.x * 2;
|
||||
ImGui::SetNextWindowSizeConstraints(ImVec2(min_width, 0.0f), ImVec2(FLT_MAX, FLT_MAX));
|
||||
setNextDialogWindow(ImVec2(0.0f, 0.0f));
|
||||
if (!ImGui::BeginPopupModal(popup_id.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) return;
|
||||
if (first) g_show_details = false;
|
||||
bool result = false, done = false;
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 480.0f);
|
||||
ImGui::TextUnformatted(box.text.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
if (g_show_details) {
|
||||
ImGui::InputTextMultiline("##details", box.detailed_text.data(), box.detailed_text.size() + 1,
|
||||
ImVec2(480.0f, 160.0f), ImGuiInputTextFlags_ReadOnly);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (!box.detailed_text.empty()) {
|
||||
|
||||
if (ImGui::Button(g_show_details ? "Hide Details..." : "Show Details...")) g_show_details = !g_show_details;
|
||||
ImGui::SameLine();
|
||||
}
|
||||
dialogButtons("OK", &result, &done, true, box.has_cancel ? "Cancel" : nullptr);
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false)) result = true;
|
||||
if (result) done = true;
|
||||
if (done) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
if (done) {
|
||||
g_owner.reset();
|
||||
Box finished = std::move(g_queue.front());
|
||||
g_queue.pop_front();
|
||||
if (finished.on_result) finished.on_result(result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
19
iqpilot/tools/cabana/ui/dialogs/messagebox.h
Normal file
19
iqpilot/tools/cabana/ui/dialogs/messagebox.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
|
||||
|
||||
namespace MessageBox {
|
||||
|
||||
|
||||
void information(const std::string &title, const std::string &text, std::function<void()> on_close = nullptr);
|
||||
void warning(const std::string &title, const std::string &text, const std::string &detailed_text = "",
|
||||
std::function<void()> on_close = nullptr);
|
||||
|
||||
void question(const std::string &title, const std::string &text, std::function<void(bool ok)> on_result);
|
||||
|
||||
void draw();
|
||||
|
||||
}
|
||||
124
iqpilot/tools/cabana/ui/dialogs/routesdialog.cc
Normal file
124
iqpilot/tools/cabana/ui/dialogs/routesdialog.cc
Normal file
@@ -0,0 +1,124 @@
|
||||
#include "tools/cabana/ui/dialogs/routesdialog.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
const char *PERIOD_NAMES[] = {"Last week", "Last 2 weeks", "Last month", "Last 6 months", "Preserved"};
|
||||
const int PERIOD_DAYS[] = {7, 14, 30, 180, -1};
|
||||
}
|
||||
|
||||
void RoutesDialog::open(std::function<void(bool, const std::string &)> on_done) {
|
||||
on_done_ = std::move(on_done);
|
||||
open_ = true;
|
||||
popup_.reset();
|
||||
s_ = State{};
|
||||
alive_ = std::make_shared<bool>(true);
|
||||
|
||||
routes::fetchDevices([this, alive = std::weak_ptr<bool>(alive_)](std::vector<routes::DeviceInfo> devices, bool success, int error_code) {
|
||||
utils::runOnMainThread(utils::guarded(alive.lock(), [this, devices = std::move(devices), success, error_code]() {
|
||||
setDeviceList(devices, success, error_code);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
void RoutesDialog::setDeviceList(const std::vector<routes::DeviceInfo> &devices, bool success, int error_code) {
|
||||
if (success) {
|
||||
s_.devices.clear();
|
||||
for (const auto &device : devices) s_.devices.push_back(device.dongle_id);
|
||||
s_.devices_loaded = true;
|
||||
s_.device_index = 0;
|
||||
fetchRoutes();
|
||||
} else {
|
||||
|
||||
MessageBox::warning("Error", error_code == 401 ? "Unauthorized. Authenticate with iqpilot/tools/lib/auth.py" : "Network error", "",
|
||||
utils::guarded(alive_, [this]() { finish(false); }));
|
||||
}
|
||||
}
|
||||
|
||||
void RoutesDialog::fetchRoutes() {
|
||||
if (!s_.devices_loaded || s_.devices.empty()) return;
|
||||
|
||||
s_.routes.clear();
|
||||
s_.route_index = -1;
|
||||
s_.empty_text = "Loading...";
|
||||
|
||||
const int request_id = ++s_.fetch_id;
|
||||
auto on_routes = [this, alive = std::weak_ptr<bool>(alive_), request_id](std::vector<routes::RouteInfo> list, bool success, int) {
|
||||
utils::runOnMainThread(utils::guarded(alive.lock(), [this, list = std::move(list), success, request_id]() {
|
||||
if (s_.fetch_id == request_id) setRouteList(list, success);
|
||||
}));
|
||||
};
|
||||
routes::fetchRoutes(s_.devices[s_.device_index], PERIOD_DAYS[s_.period_index], std::move(on_routes));
|
||||
}
|
||||
|
||||
void RoutesDialog::setRouteList(const std::vector<routes::RouteInfo> &list, bool success) {
|
||||
if (success) {
|
||||
for (const auto &route : list) {
|
||||
const int mins = static_cast<int>((route.end_ms - route.start_ms) / 60000);
|
||||
s_.routes.push_back({routes::formatUnixMs(route.start_ms) + " " + std::to_string(mins) + "min", route.name});
|
||||
}
|
||||
if (!s_.routes.empty()) s_.route_index = 0;
|
||||
} else {
|
||||
MessageBox::warning("Error", "Failed to fetch routes. Check your network connection.", "",
|
||||
utils::guarded(alive_, [this]() { finish(false); }));
|
||||
}
|
||||
s_.empty_text = "No items";
|
||||
}
|
||||
|
||||
void RoutesDialog::finish(bool accepted) {
|
||||
alive_.reset();
|
||||
open_ = false;
|
||||
auto on_done = std::move(on_done_);
|
||||
if (on_done) on_done(accepted, accepted && s_.route_index >= 0 ? s_.routes[s_.route_index].name : "");
|
||||
}
|
||||
|
||||
void RoutesDialog::draw() {
|
||||
if (!open_) return;
|
||||
if (!beginDialog("Remote routes", &popup_, ImVec2(480.0f, 420.0f))) return;
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Device");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (s_.devices_loaded) {
|
||||
if (comboBox("##device", &s_.device_index, s_.devices)) fetchRoutes();
|
||||
} else {
|
||||
int idx = 0;
|
||||
ImGui::BeginDisabled();
|
||||
comboBox("##device", &idx, {"Loading..."});
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::Combo("##period", &s_.period_index, PERIOD_NAMES, IM_ARRAYSIZE(PERIOD_NAMES))) fetchRoutes();
|
||||
|
||||
bool accepted = false, rejected = false;
|
||||
const float footer = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
||||
ImGui::BeginChild("routes", ImVec2(0, -footer), ImGuiChildFlags_Borders);
|
||||
if (s_.routes.empty()) {
|
||||
const ImVec2 size = ImGui::CalcTextSize(s_.empty_text.c_str());
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
ImGui::SetCursorPos(ImVec2((avail.x - size.x) * 0.5f, (avail.y - size.y) * 0.5f));
|
||||
ImGui::TextUnformatted(s_.empty_text.c_str());
|
||||
}
|
||||
for (int i = 0; i < static_cast<int>(s_.routes.size()); ++i) {
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable(s_.routes[i].label.c_str(), s_.route_index == i, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
s_.route_index = i;
|
||||
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) accepted = true;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
dialogButtons("OK", &accepted, &rejected);
|
||||
MessageBox::draw();
|
||||
if (accepted || rejected || !open_) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
if (accepted || rejected) finish(accepted);
|
||||
}
|
||||
45
iqpilot/tools/cabana/ui/dialogs/routesdialog.h
Normal file
45
iqpilot/tools/cabana/ui/dialogs/routesdialog.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/routes.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
|
||||
class RoutesDialog {
|
||||
public:
|
||||
void open(std::function<void(bool accepted, const std::string &route)> on_done);
|
||||
void draw();
|
||||
|
||||
private:
|
||||
void setDeviceList(const std::vector<routes::DeviceInfo> &devices, bool success, int error_code);
|
||||
void setRouteList(const std::vector<routes::RouteInfo> &list, bool success);
|
||||
void fetchRoutes();
|
||||
void finish(bool accepted);
|
||||
|
||||
struct RouteItem {
|
||||
std::string label;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct State {
|
||||
bool devices_loaded = false;
|
||||
std::vector<std::string> devices;
|
||||
int device_index = 0;
|
||||
int period_index = 0;
|
||||
std::vector<RouteItem> routes;
|
||||
int route_index = -1;
|
||||
std::string empty_text = "No items";
|
||||
int fetch_id = 0;
|
||||
};
|
||||
|
||||
bool open_ = false;
|
||||
PopupOwner popup_;
|
||||
State s_;
|
||||
std::function<void(bool, const std::string &)> on_done_;
|
||||
|
||||
std::shared_ptr<bool> alive_;
|
||||
};
|
||||
108
iqpilot/tools/cabana/ui/dialogs/settingsdialog.cc
Normal file
108
iqpilot/tools/cabana/ui/dialogs/settingsdialog.cc
Normal file
@@ -0,0 +1,108 @@
|
||||
#include "tools/cabana/ui/dialogs/settingsdialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/dialogs/filedialog.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const int MIN_CACHE_MINUTES = 30;
|
||||
const int MAX_CACHE_MINUTES = 120;
|
||||
|
||||
|
||||
enum FormLabel { THEME, CACHED_MINUTES, DRAG_DIRECTION, CHART_HEIGHT, FORM_LABEL_COUNT };
|
||||
const char *FORM_LABELS[FORM_LABEL_COUNT] = {"Color Theme", "Max Cached Minutes", "Drag Direction", "Chart Height"};
|
||||
|
||||
float formLabelWidth() {
|
||||
float w = 0.0f;
|
||||
for (const char *label : FORM_LABELS) w = std::max(w, ImGui::CalcTextSize(label).x);
|
||||
return w + ImGui::GetStyle().ItemSpacing.x * 2;
|
||||
}
|
||||
|
||||
void formRow(FormLabel label, float label_width) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(FORM_LABELS[label]);
|
||||
ImGui::SameLine(label_width);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SettingsDialog::open() {
|
||||
theme_ = settings.theme;
|
||||
cached_minutes_ = settings.max_cached_minutes;
|
||||
drag_direction_ = settings.drag_direction;
|
||||
chart_height_ = settings.chart_height;
|
||||
log_livestream_ = settings.log_livestream;
|
||||
log_path_ = settings.log_path;
|
||||
open_ = true;
|
||||
popup_.reset();
|
||||
}
|
||||
|
||||
void SettingsDialog::draw() {
|
||||
if (!open_) return;
|
||||
if (!beginDialog("Settings", &popup_, ImVec2(400.0f, 0.0f))) return;
|
||||
const float label_width = formLabelWidth();
|
||||
|
||||
ImGui::SeparatorText("General");
|
||||
static const char *themes[] = {"Light", "Dark"};
|
||||
formRow(THEME, label_width);
|
||||
int theme_index = theme_ - LIGHT_THEME;
|
||||
if (ImGui::Combo("##theme", &theme_index, themes, IM_ARRAYSIZE(themes))) theme_ = theme_index + LIGHT_THEME;
|
||||
formRow(CACHED_MINUTES, label_width);
|
||||
|
||||
if (ImGui::InputInt("##cached_minutes", &cached_minutes_, 1, 10)) {
|
||||
cached_minutes_ = std::clamp(cached_minutes_, MIN_CACHE_MINUTES, MAX_CACHE_MINUTES);
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("New Signal Settings");
|
||||
static const char *directions[] = {"MSB First", "LSB First", "Always Little Endian", "Always Big Endian"};
|
||||
formRow(DRAG_DIRECTION, label_width);
|
||||
ImGui::Combo("##drag_direction", &drag_direction_, directions, IM_ARRAYSIZE(directions));
|
||||
|
||||
ImGui::SeparatorText("Chart");
|
||||
formRow(CHART_HEIGHT, label_width);
|
||||
if (ImGui::InputInt("##chart_height", &chart_height_, 10, 10)) chart_height_ = std::clamp(chart_height_, 100, 500);
|
||||
|
||||
checkBox("Enable live stream logging", &log_livestream_);
|
||||
ImGui::BeginDisabled(!log_livestream_);
|
||||
ImGui::SetNextItemWidth(-90.0f);
|
||||
inputText("##log_path", &log_path_, "", ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Browse...")) {
|
||||
FileDialog::getExistingDirectory("Log File Location", utils::homePath(), [this](const std::string &fn) {
|
||||
if (!fn.empty()) log_path_ = fn;
|
||||
});
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::Separator();
|
||||
bool accepted = false, done = false;
|
||||
dialogButtons("OK", &accepted, &done);
|
||||
if (accepted) {
|
||||
save();
|
||||
done = true;
|
||||
}
|
||||
FileDialog::draw();
|
||||
if (done) {
|
||||
open_ = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void SettingsDialog::save() {
|
||||
if (std::exchange(settings.theme, theme_) != settings.theme) applyTheme(settings.theme);
|
||||
settings.max_cached_minutes = cached_minutes_;
|
||||
settings.chart_height = chart_height_;
|
||||
settings.log_livestream = log_livestream_;
|
||||
settings.log_path = log_path_;
|
||||
settings.drag_direction = (Settings::DragDirection)drag_direction_;
|
||||
settings.changed();
|
||||
}
|
||||
24
iqpilot/tools/cabana/ui/dialogs/settingsdialog.h
Normal file
24
iqpilot/tools/cabana/ui/dialogs/settingsdialog.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "tools/cabana/core/settings.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
class SettingsDialog {
|
||||
public:
|
||||
void open();
|
||||
void draw();
|
||||
|
||||
private:
|
||||
void save();
|
||||
|
||||
bool open_ = false;
|
||||
PopupOwner popup_;
|
||||
int theme_ = 0;
|
||||
int cached_minutes_ = 0;
|
||||
int drag_direction_ = 0;
|
||||
int chart_height_ = 0;
|
||||
bool log_livestream_ = false;
|
||||
std::string log_path_;
|
||||
};
|
||||
323
iqpilot/tools/cabana/ui/dialogs/streamselector.cc
Normal file
323
iqpilot/tools/cabana/ui/dialogs/streamselector.cc
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "tools/cabana/ui/dialogs/streamselector.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/streams/devicestream.h"
|
||||
#include "tools/cabana/streams/replaystream.h"
|
||||
#include "tools/cabana/ui/dialogs/filedialog.h"
|
||||
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
void OpenReplayWidget::draw() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Route");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-250.0f);
|
||||
inputText("##route", &route_, "Enter route name or browse for local/remote route");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Remote route...")) {
|
||||
routes_dialog_.open(utils::guarded(alive_, [this](bool accepted, const std::string &route) {
|
||||
if (accepted) route_ = route;
|
||||
}));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Local route...")) {
|
||||
FileDialog::getExistingDirectory("Open Local Route", settings.last_route_dir, utils::guarded(alive_, [this](const std::string &dir) {
|
||||
if (!dir.empty()) {
|
||||
route_ = dir;
|
||||
settings.last_route_dir = std::filesystem::absolute(dir).parent_path().string();
|
||||
}
|
||||
}));
|
||||
}
|
||||
checkBox("Road camera", &cameras_[0]);
|
||||
ImGui::SameLine();
|
||||
checkBox("Driver camera", &cameras_[1]);
|
||||
ImGui::SameLine();
|
||||
checkBox("Wide road camera", &cameras_[2]);
|
||||
}
|
||||
|
||||
void OpenReplayWidget::drawPopups() {
|
||||
routes_dialog_.draw();
|
||||
}
|
||||
|
||||
std::unique_ptr<AbstractStream> OpenReplayWidget::open() {
|
||||
std::string route = route_;
|
||||
std::string data_dir;
|
||||
if (auto idx = route.rfind('/'); idx != std::string::npos && util::file_exists(route)) {
|
||||
data_dir = route.substr(0, idx + 1);
|
||||
route = route.substr(idx + 1);
|
||||
}
|
||||
|
||||
bool is_valid_format = Route::parseRoute(route).str.size() > 0;
|
||||
if (!is_valid_format) {
|
||||
MessageBox::warning("Warning", "Invalid route format: '" + route + "'");
|
||||
} else {
|
||||
auto replay_stream = std::make_unique<ReplayStream>();
|
||||
Connection err = replay_stream->error.connect([](const std::string &msg) {
|
||||
MessageBox::warning("Error", msg);
|
||||
});
|
||||
uint32_t flags = REPLAY_FLAG_NONE;
|
||||
if (cameras_[1]) flags |= REPLAY_FLAG_DCAM;
|
||||
if (cameras_[2]) flags |= REPLAY_FLAG_ECAM;
|
||||
if (flags == REPLAY_FLAG_NONE && !cameras_[0]) flags = REPLAY_FLAG_NO_VIPC;
|
||||
|
||||
if (replay_stream->loadRoute(route, data_dir, flags)) {
|
||||
return replay_stream;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
namespace {
|
||||
const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U};
|
||||
const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U};
|
||||
}
|
||||
|
||||
OpenPandaWidget::OpenPandaWidget() {
|
||||
if (can && dynamic_cast<PandaStream *>(can) != nullptr) {
|
||||
already_connected_ = true;
|
||||
return;
|
||||
}
|
||||
refreshSerials();
|
||||
buildConfigForm();
|
||||
}
|
||||
|
||||
void OpenPandaWidget::refreshSerials() {
|
||||
serials_ = Panda::list();
|
||||
serial_index_ = 0;
|
||||
}
|
||||
|
||||
void OpenPandaWidget::buildConfigForm() {
|
||||
std::string serial = serial_index_ < static_cast<int>(serials_.size()) ? serials_[serial_index_] : "";
|
||||
has_fd_ = false;
|
||||
has_panda_ = !serial.empty();
|
||||
if (has_panda_) {
|
||||
try {
|
||||
Panda panda(serial);
|
||||
has_fd_ = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2);
|
||||
} catch (const std::exception &e) {
|
||||
fprintf(stderr, "failed to open panda %s\n", serial.c_str());
|
||||
has_panda_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_panda_) {
|
||||
config.serial = serial;
|
||||
config.bus_config.resize(3);
|
||||
can_speed_index_.assign(3, 0);
|
||||
data_speed_index_.assign(3, 0);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < static_cast<int>(std::size(speeds)); j++) {
|
||||
if (speeds[j] == config.bus_config[i].can_speed_kbps) can_speed_index_[i] = j;
|
||||
}
|
||||
for (int j = 0; j < static_cast<int>(std::size(data_speeds)); j++) {
|
||||
if (data_speeds[j] == config.bus_config[i].data_speed_kbps) data_speed_index_[i] = j;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
config.serial = "";
|
||||
}
|
||||
}
|
||||
|
||||
void OpenPandaWidget::draw() {
|
||||
if (already_connected_) {
|
||||
ImGui::Text("Already connected to %s.", can->routeName().c_str());
|
||||
ImGui::TextUnformatted("Close the current connection via [File menu -> Close Stream] before connecting to another Panda.");
|
||||
return;
|
||||
}
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Serial");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-100.0f);
|
||||
if (comboBox("##serial", &serial_index_, serials_)) buildConfigForm();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Refresh")) {
|
||||
refreshSerials();
|
||||
buildConfigForm();
|
||||
}
|
||||
|
||||
if (!has_panda_) {
|
||||
ImGui::TextUnformatted("No panda found");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < static_cast<int>(config.bus_config.size()); i++) {
|
||||
ImGui::PushID(i);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("Bus %d:", i);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("CAN Speed (kbps):");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(90.0f);
|
||||
if (comboBox("##can_speed", &can_speed_index_[i], speeds, (int)std::size(speeds))) {
|
||||
config.bus_config[i].can_speed_kbps = speeds[can_speed_index_[i]];
|
||||
}
|
||||
if (has_fd_) {
|
||||
ImGui::SameLine();
|
||||
checkBox("CAN-FD", &config.bus_config[i].can_fd);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("Data Speed (kbps):");
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!config.bus_config[i].can_fd);
|
||||
ImGui::SetNextItemWidth(90.0f);
|
||||
if (comboBox("##data_speed", &data_speed_index_[i], data_speeds, (int)std::size(data_speeds))) {
|
||||
config.bus_config[i].data_speed_kbps = data_speeds[data_speed_index_[i]];
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<AbstractStream> OpenPandaWidget::open() {
|
||||
try {
|
||||
return std::make_unique<PandaStream>(config);
|
||||
} catch (std::exception &e) {
|
||||
MessageBox::warning("Warning", std::string("Failed to connect to panda: '") + e.what() + "'");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void OpenDeviceWidget::draw() {
|
||||
ImGui::RadioButton("MSGQ", &mode_, 0);
|
||||
ImGui::RadioButton("ZMQ", &mode_, 1);
|
||||
ImGui::RadioButton("Bridge", &mode_, 2);
|
||||
|
||||
const float label_width = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x +
|
||||
std::max(ImGui::CalcTextSize("MSGQ").x, ImGui::CalcTextSize("ZMQ").x) +
|
||||
ImGui::GetStyle().ItemInnerSpacing.x;
|
||||
ImGui::SameLine(label_width);
|
||||
ImGui::BeginDisabled(mode_ == 0);
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
validatedText("##ip", &ip_address_, validateIpAddress, "Enter device Ip Address", ipValidator);
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
std::unique_ptr<AbstractStream> OpenDeviceWidget::open() {
|
||||
std::string ip = ip_address_.empty() ? "127.0.0.1" : ip_address_;
|
||||
const DeviceStream::Mode modes[] = {DeviceStream::Mode::Msgq, DeviceStream::Mode::Zmq, DeviceStream::Mode::Bridge};
|
||||
return std::make_unique<DeviceStream>(modes[mode_], ip);
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
OpenSocketCanWidget::OpenSocketCanWidget() {
|
||||
refreshDevices();
|
||||
}
|
||||
|
||||
void OpenSocketCanWidget::refreshDevices() {
|
||||
devices_.clear();
|
||||
|
||||
std::error_code ec;
|
||||
for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) {
|
||||
std::ifstream type_file(entry.path() / "type");
|
||||
int type = 0;
|
||||
if (type_file >> type && type == 280) {
|
||||
devices_.push_back(entry.path().filename().string());
|
||||
}
|
||||
}
|
||||
device_index_ = 0;
|
||||
config.device = devices_.empty() ? "" : devices_[0];
|
||||
}
|
||||
|
||||
void OpenSocketCanWidget::draw() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Device");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(300.0f);
|
||||
if (comboBox("##device", &device_index_, devices_)) config.device = devices_[device_index_];
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Refresh", ImVec2(100.0f, 0.0f))) refreshDevices();
|
||||
}
|
||||
|
||||
std::unique_ptr<AbstractStream> OpenSocketCanWidget::open() {
|
||||
try {
|
||||
return std::make_unique<SocketCanStream>(config);
|
||||
} catch (std::exception &e) {
|
||||
MessageBox::warning("Warning", std::string("Failed to connect to SocketCAN device: '") + e.what() + "'");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void StreamSelector::open(Callback on_done) {
|
||||
on_done_ = std::move(on_done);
|
||||
open_ = true;
|
||||
popup_.reset();
|
||||
first_frame_ = true;
|
||||
dbc_file_.clear();
|
||||
widgets_.clear();
|
||||
widgets_.push_back(std::make_unique<OpenReplayWidget>());
|
||||
widgets_.push_back(std::make_unique<OpenPandaWidget>());
|
||||
#ifdef __linux__
|
||||
if (SocketCanStream::available()) {
|
||||
widgets_.push_back(std::make_unique<OpenSocketCanWidget>());
|
||||
}
|
||||
#endif
|
||||
widgets_.push_back(std::make_unique<OpenDeviceWidget>());
|
||||
}
|
||||
|
||||
void StreamSelector::draw() {
|
||||
if (!open_) return;
|
||||
if (!beginDialog("Open stream", &popup_, ImVec2(640.0f, 0.0f))) return;
|
||||
|
||||
AbstractOpenStreamWidget *current = nullptr;
|
||||
if (ImGui::BeginTabBar("streams")) {
|
||||
for (auto &w : widgets_) {
|
||||
|
||||
ImGuiTabItemFlags tab_flags = (first_frame_ && w == widgets_.front()) ? ImGuiTabItemFlags_SetSelected : 0;
|
||||
if (ImGui::BeginTabItem(w->title(), nullptr, tab_flags)) {
|
||||
current = w.get();
|
||||
ImGui::BeginChild("tab", ImVec2(0, 130.0f));
|
||||
w->draw();
|
||||
ImGui::EndChild();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
first_frame_ = false;
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("dbc File");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-90.0f);
|
||||
inputText("##dbc", &dbc_file_, "Choose a dbc file to open", ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Browse...")) {
|
||||
FileDialog::getOpenFileName("Open File", settings.last_dir, ".dbc", [this](const std::string &fn) {
|
||||
if (!fn.empty()) {
|
||||
dbc_file_ = fn;
|
||||
settings.last_dir = std::filesystem::absolute(fn).parent_path().string();
|
||||
}
|
||||
});
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
bool accepted = false, rejected = false;
|
||||
std::unique_ptr<AbstractStream> stream;
|
||||
bool open_clicked = false;
|
||||
dialogButtons("Open", &open_clicked, &rejected, current != nullptr && current->openEnabled());
|
||||
if (open_clicked) {
|
||||
if (stream = current->open(); stream) accepted = true;
|
||||
}
|
||||
|
||||
|
||||
if (current) current->drawPopups();
|
||||
FileDialog::draw();
|
||||
MessageBox::draw();
|
||||
|
||||
if (accepted || rejected) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
if (accepted || rejected) {
|
||||
open_ = false;
|
||||
widgets_.clear();
|
||||
auto on_done = std::move(on_done_);
|
||||
if (on_done) on_done(std::move(stream), dbc_file_);
|
||||
}
|
||||
}
|
||||
106
iqpilot/tools/cabana/ui/dialogs/streamselector.h
Normal file
106
iqpilot/tools/cabana/ui/dialogs/streamselector.h
Normal file
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/streams/pandastream.h"
|
||||
#ifdef __linux__
|
||||
#include "tools/cabana/streams/socketcanstream.h"
|
||||
#endif
|
||||
#include "tools/cabana/ui/dialogs/routesdialog.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
class AbstractOpenStreamWidget {
|
||||
public:
|
||||
virtual ~AbstractOpenStreamWidget() = default;
|
||||
virtual const char *title() const = 0;
|
||||
virtual void draw() = 0;
|
||||
|
||||
|
||||
virtual void drawPopups() {}
|
||||
virtual std::unique_ptr<AbstractStream> open() = 0;
|
||||
virtual bool openEnabled() const { return true; }
|
||||
};
|
||||
|
||||
class OpenReplayWidget : public AbstractOpenStreamWidget {
|
||||
public:
|
||||
const char *title() const override { return "Replay"; }
|
||||
void draw() override;
|
||||
void drawPopups() override;
|
||||
std::unique_ptr<AbstractStream> open() override;
|
||||
|
||||
private:
|
||||
std::string route_;
|
||||
bool cameras_[3] = {true, false, false};
|
||||
RoutesDialog routes_dialog_;
|
||||
|
||||
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
|
||||
};
|
||||
|
||||
class OpenPandaWidget : public AbstractOpenStreamWidget {
|
||||
public:
|
||||
OpenPandaWidget();
|
||||
const char *title() const override { return "Panda"; }
|
||||
void draw() override;
|
||||
std::unique_ptr<AbstractStream> open() override;
|
||||
bool openEnabled() const override { return !already_connected_; }
|
||||
|
||||
private:
|
||||
void refreshSerials();
|
||||
void buildConfigForm();
|
||||
|
||||
bool already_connected_ = false;
|
||||
std::vector<std::string> serials_;
|
||||
int serial_index_ = 0;
|
||||
bool has_panda_ = false;
|
||||
bool has_fd_ = false;
|
||||
std::vector<int> can_speed_index_, data_speed_index_;
|
||||
PandaStreamConfig config = {};
|
||||
};
|
||||
|
||||
class OpenDeviceWidget : public AbstractOpenStreamWidget {
|
||||
public:
|
||||
const char *title() const override { return "Device"; }
|
||||
void draw() override;
|
||||
std::unique_ptr<AbstractStream> open() override;
|
||||
|
||||
private:
|
||||
int mode_ = 1;
|
||||
std::string ip_address_;
|
||||
};
|
||||
|
||||
#ifdef __linux__
|
||||
class OpenSocketCanWidget : public AbstractOpenStreamWidget {
|
||||
public:
|
||||
OpenSocketCanWidget();
|
||||
const char *title() const override { return "SocketCAN"; }
|
||||
void draw() override;
|
||||
std::unique_ptr<AbstractStream> open() override;
|
||||
|
||||
private:
|
||||
void refreshDevices();
|
||||
|
||||
std::vector<std::string> devices_;
|
||||
int device_index_ = 0;
|
||||
SocketCanStreamConfig config = {};
|
||||
};
|
||||
#endif
|
||||
|
||||
class StreamSelector {
|
||||
public:
|
||||
using Callback = std::function<void(std::unique_ptr<AbstractStream> stream, const std::string &dbc_file)>;
|
||||
|
||||
void open(Callback on_done);
|
||||
void draw();
|
||||
|
||||
private:
|
||||
bool open_ = false;
|
||||
PopupOwner popup_;
|
||||
bool first_frame_ = false;
|
||||
std::string dbc_file_;
|
||||
std::vector<std::unique_ptr<AbstractOpenStreamWidget>> widgets_;
|
||||
Callback on_done_;
|
||||
};
|
||||
196
iqpilot/tools/cabana/ui/helpoverlay.cc
Normal file
196
iqpilot/tools/cabana/ui/helpoverlay.cc
Normal file
@@ -0,0 +1,196 @@
|
||||
#include "tools/cabana/ui/helpoverlay.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
namespace {
|
||||
struct HelpRun {
|
||||
std::string text;
|
||||
bool bold = false;
|
||||
bool chip = false;
|
||||
bool swatch = false;
|
||||
ImU32 color = 0;
|
||||
};
|
||||
|
||||
ImU32 helpColor(const std::string &name) {
|
||||
if (name == "gray") return IM_COL32(128, 128, 128, 255);
|
||||
if (name == "blue") return IM_COL32(0, 0, 255, 255);
|
||||
if (name == "red") return IM_COL32(255, 0, 0, 255);
|
||||
unsigned rgb = 0;
|
||||
if (name.size() == 7 && name[0] == '#' && sscanf(name.c_str() + 1, "%6x", &rgb) == 1) {
|
||||
return IM_COL32((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff, 255);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<std::vector<HelpRun>> parseHelpHtml(const std::string &raw) {
|
||||
std::vector<std::vector<HelpRun>> lines(1);
|
||||
HelpRun style;
|
||||
std::vector<HelpRun> span_stack;
|
||||
bool prev_space = true;
|
||||
std::string pending;
|
||||
auto flush = [&]() {
|
||||
if (!pending.empty()) {
|
||||
HelpRun run = style;
|
||||
run.text = pending;
|
||||
lines.back().push_back(run);
|
||||
pending.clear();
|
||||
}
|
||||
};
|
||||
auto push_swatch = [&](ImU32 color) {
|
||||
flush();
|
||||
HelpRun run = style;
|
||||
run.swatch = true;
|
||||
if (color) run.color = color;
|
||||
lines.back().push_back(run);
|
||||
prev_space = false;
|
||||
};
|
||||
for (size_t i = 0; i < raw.size(); ++i) {
|
||||
const char c = raw[i];
|
||||
if (c == '<') {
|
||||
const size_t close = raw.find('>', i);
|
||||
if (close == std::string::npos) break;
|
||||
const std::string tag = raw.substr(i + 1, close - i - 1);
|
||||
i = close;
|
||||
if (tag.compare(0, 3, "!--") == 0) continue;
|
||||
flush();
|
||||
if (tag == "b") {
|
||||
style.bold = true;
|
||||
} else if (tag == "/b") {
|
||||
style.bold = false;
|
||||
} else if (tag.compare(0, 2, "br") == 0) {
|
||||
lines.emplace_back();
|
||||
prev_space = true;
|
||||
} else if (tag.compare(0, 4, "span") == 0) {
|
||||
span_stack.push_back(style);
|
||||
const size_t st = tag.find("style=\"");
|
||||
if (st != std::string::npos) {
|
||||
const std::string css = tag.substr(st + 7, tag.find('"', st + 7) - st - 7);
|
||||
size_t pos = 0;
|
||||
while (pos < css.size()) {
|
||||
const size_t semi = css.find(';', pos);
|
||||
const std::string decl = css.substr(pos, semi == std::string::npos ? std::string::npos : semi - pos);
|
||||
const size_t colon = decl.find(':');
|
||||
if (colon != std::string::npos) {
|
||||
const std::string key = decl.substr(0, colon), value = decl.substr(colon + 1);
|
||||
if (key == "color") style.color = helpColor(value);
|
||||
if (key == "background-color") style.chip = true;
|
||||
}
|
||||
if (semi == std::string::npos) break;
|
||||
pos = semi + 1;
|
||||
}
|
||||
}
|
||||
} else if (tag == "/span") {
|
||||
if (!span_stack.empty()) {
|
||||
style = span_stack.back();
|
||||
span_stack.pop_back();
|
||||
}
|
||||
}
|
||||
} else if (c == '&') {
|
||||
static const std::pair<const char *, const char *> entities[] = {{" ", " "}};
|
||||
bool matched = false;
|
||||
for (const auto &[name, text] : entities) {
|
||||
if (raw.compare(i, strlen(name), name) == 0) {
|
||||
pending += text;
|
||||
prev_space = false;
|
||||
i += strlen(name) - 1;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched && raw.compare(i, 7, "■") == 0) {
|
||||
push_swatch(0);
|
||||
i += 6;
|
||||
matched = true;
|
||||
}
|
||||
if (!matched) {
|
||||
pending += c;
|
||||
prev_space = false;
|
||||
}
|
||||
} else if (isspace(static_cast<unsigned char>(c))) {
|
||||
if (!prev_space) pending += ' ';
|
||||
prev_space = true;
|
||||
} else if (c == '#' && i + 6 < raw.size() && helpColor(raw.substr(i, 7)) != 0) {
|
||||
push_swatch(helpColor(raw.substr(i, 7)));
|
||||
i += 6;
|
||||
} else {
|
||||
pending += c;
|
||||
prev_space = false;
|
||||
}
|
||||
}
|
||||
flush();
|
||||
for (auto &line : lines) {
|
||||
if (!line.empty() && !line.back().text.empty() && line.back().text.back() == ' ') line.back().text.pop_back();
|
||||
if (!line.empty() && !line.front().text.empty() && line.front().text.front() == ' ') line.front().text.erase(0, 1);
|
||||
}
|
||||
while (!lines.empty() && lines.back().empty()) lines.pop_back();
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
void HelpOverlay::toggle() {
|
||||
visible_ = !visible_;
|
||||
opened_frame_ = ImGui::GetFrameCount();
|
||||
}
|
||||
|
||||
void HelpOverlay::add(const std::string &text, const ImRect &rect) {
|
||||
if (visible_) texts_.emplace_back(text, rect);
|
||||
}
|
||||
|
||||
void HelpOverlay::draw() {
|
||||
if (!visible_) return;
|
||||
const ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||
ImDrawList *dl = ImGui::GetForegroundDrawList();
|
||||
const ImRect work_rect(viewport->WorkPos, ImVec2(viewport->WorkPos.x + viewport->WorkSize.x, viewport->WorkPos.y + viewport->WorkSize.y));
|
||||
dl->AddRectFilled(viewport->Pos, ImVec2(viewport->Pos.x + viewport->Size.x, viewport->Pos.y + viewport->Size.y), IM_COL32(0, 0, 0, 50));
|
||||
ImFont *font = ImGui::GetFont();
|
||||
ImFont *bold_font = boldFont() ? boldFont() : font;
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float line_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
auto run_width = [&](const HelpRun &r) {
|
||||
if (r.swatch) return font_size;
|
||||
return (r.bold ? bold_font : font)->CalcTextSizeA(font_size, FLT_MAX, 0.0f, r.text.c_str()).x;
|
||||
};
|
||||
for (const auto &[raw, rect] : texts_) {
|
||||
if (raw.empty()) continue;
|
||||
const auto lines = parseHelpHtml(raw);
|
||||
float width = 0;
|
||||
for (const auto &line : lines) {
|
||||
float w = 0;
|
||||
for (const auto &r : line) w += run_width(r);
|
||||
width = std::max(width, w);
|
||||
}
|
||||
const ImVec2 size(width, lines.size() * line_h);
|
||||
const ImVec2 center((rect.Min.x + rect.Max.x) * 0.5f, (rect.Min.y + rect.Max.y) * 0.5f);
|
||||
if (!work_rect.Contains(center)) continue;
|
||||
const ImVec2 min(center.x - size.x * 0.5f - 8.0f, center.y - size.y * 0.5f - 8.0f);
|
||||
const ImVec2 max(center.x + size.x * 0.5f + 8.0f, center.y + size.y * 0.5f + 8.0f);
|
||||
|
||||
const ImU32 tooltip_base = isDarkTheme() ? ImGui::GetColorU32(ImGuiCol_PopupBg) : IM_COL32(255, 255, 220, 255);
|
||||
dl->AddRectFilled(min, max, tooltip_base);
|
||||
float y = min.y + 8.0f;
|
||||
for (const auto &line : lines) {
|
||||
float x = min.x + 8.0f;
|
||||
for (const auto &r : line) {
|
||||
const float w = run_width(r);
|
||||
const ImU32 color = r.color ? r.color : ImGui::GetColorU32(ImGuiCol_Text);
|
||||
if (r.swatch) {
|
||||
dl->AddRectFilled(ImVec2(x + 2, y + 3), ImVec2(x + font_size - 2, y + font_size - 1), color);
|
||||
} else {
|
||||
if (r.chip) dl->AddRectFilled(ImVec2(x, y), ImVec2(x + w, y + font_size), IM_COL32(211, 211, 211, 255));
|
||||
dl->AddText(r.bold ? bold_font : font, font_size, ImVec2(x, y), color, r.text.c_str());
|
||||
}
|
||||
x += w;
|
||||
}
|
||||
y += line_h;
|
||||
}
|
||||
}
|
||||
texts_.clear();
|
||||
|
||||
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && ImGui::GetFrameCount() != opened_frame_) visible_ = false;
|
||||
}
|
||||
24
iqpilot/tools/cabana/ui/helpoverlay.h
Normal file
24
iqpilot/tools/cabana/ui/helpoverlay.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui_internal.h"
|
||||
|
||||
|
||||
|
||||
|
||||
class HelpOverlay {
|
||||
public:
|
||||
void toggle();
|
||||
bool visible() const { return visible_; }
|
||||
|
||||
void add(const std::string &text, const ImRect &rect);
|
||||
void draw();
|
||||
|
||||
private:
|
||||
std::vector<std::pair<std::string, ImRect>> texts_;
|
||||
bool visible_ = false;
|
||||
int opened_frame_ = -1;
|
||||
};
|
||||
40
iqpilot/tools/cabana/ui/icons.h
Normal file
40
iqpilot/tools/cabana/ui/icons.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
namespace icon {
|
||||
constexpr const char ARROW_CLOCKWISE[] = "\xef\x84\x96";
|
||||
constexpr const char ARROW_COUNTERCLOCKWISE[] = "\xef\x84\x97";
|
||||
constexpr const char ARROW_DOWN_LEFT_SQUARE[] = "\xef\x84\x9d";
|
||||
constexpr const char ARROW_UP_RIGHT_SQUARE[] = "\xef\x85\x83";
|
||||
constexpr const char CHEVRON_LEFT[] = "\xef\x8a\x84";
|
||||
constexpr const char CHEVRON_RIGHT[] = "\xef\x8a\x85";
|
||||
constexpr const char DASH[] = "\xef\x8b\xaa";
|
||||
constexpr const char DASH_SQUARE[] = "\xef\x8b\xa9";
|
||||
constexpr const char EXCLAMATION_TRIANGLE[] = "\xef\x8c\xbb";
|
||||
constexpr const char FAST_FORWARD[] = "\xef\x9f\xb4";
|
||||
constexpr const char FILETYPE_CSV[] = "\xef\x9d\x83";
|
||||
constexpr const char FOLDER[] = "\xef\x8f\x99";
|
||||
constexpr const char FILE_EARMARK[] = "\xef\x8e\x92";
|
||||
constexpr const char FILE_EARMARK_RULED[] = "\xef\x8e\x85";
|
||||
constexpr const char PLUS_SQUARE[] = "\xef\x93\xbd";
|
||||
constexpr const char GRAPH_UP[] = "\xef\x8f\xb2";
|
||||
constexpr const char GRIP_HORIZONTAL[] = "\xef\x8f\xbd";
|
||||
constexpr const char INFO_CIRCLE[] = "\xef\x90\xb1";
|
||||
constexpr const char LIST[] = "\xef\x91\xb9";
|
||||
constexpr const char PAUSE[] = "\xef\x93\x84";
|
||||
constexpr const char PENCIL[] = "\xef\x93\x8b";
|
||||
constexpr const char PLAY[] = "\xef\x93\xb5";
|
||||
constexpr const char PLUS[] = "\xef\x93\xbe";
|
||||
constexpr const char RAQUO[] = "\xc2\xbb";
|
||||
constexpr const char REPEAT[] = "\xef\xa0\x93";
|
||||
constexpr const char REPEAT_1[] = "\xef\xa0\x92";
|
||||
constexpr const char REWIND[] = "\xef\xa0\x99";
|
||||
constexpr const char SKIP_END[] = "\xef\x95\x98";
|
||||
constexpr const char STOPWATCH[] = "\xef\x96\x97";
|
||||
constexpr const char THREE_DOTS[] = "\xef\x97\x94";
|
||||
constexpr const char WINDOW_STACK[] = "\xef\x9b\x92";
|
||||
constexpr const char X[] = "\xef\x98\xaa";
|
||||
constexpr const char X_LG[] = "\xef\x99\x99";
|
||||
constexpr const char X_SQUARE[] = "\xef\x98\xa9";
|
||||
constexpr const char ZOOM_OUT[] = "\xef\x98\xad";
|
||||
}
|
||||
87
iqpilot/tools/cabana/ui/inistate.cc
Normal file
87
iqpilot/tools/cabana/ui/inistate.cc
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "tools/cabana/ui/inistate.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
namespace inistate {
|
||||
|
||||
MainWindowState main_window;
|
||||
|
||||
namespace {
|
||||
|
||||
void *readOpen(ImGuiContext *, ImGuiSettingsHandler *, const char *name) {
|
||||
return strcmp(name, "MainWindow") == 0 ? (void *)&main_window : nullptr;
|
||||
}
|
||||
|
||||
void readLine(ImGuiContext *, ImGuiSettingsHandler *, void *entry, const char *line) {
|
||||
auto *state = (MainWindowState *)entry;
|
||||
int x = 0, y = 0, flag = 0;
|
||||
float ratio = 0.0f;
|
||||
if (sscanf(line, "Pos=%d,%d", &x, &y) == 2) {
|
||||
state->pos[0] = x;
|
||||
state->pos[1] = y;
|
||||
} else if (sscanf(line, "Size=%d,%d", &x, &y) == 2) {
|
||||
state->size[0] = x;
|
||||
state->size[1] = y;
|
||||
state->has_geometry = true;
|
||||
} else if (sscanf(line, "Maximized=%d", &flag) == 1) {
|
||||
state->maximized = flag != 0;
|
||||
} else if (sscanf(line, "VideoSplitterRatio=%f", &ratio) == 1) {
|
||||
state->video_splitter_ratio = ratio;
|
||||
} else if (sscanf(line, "MessagesVisible=%d", &flag) == 1) {
|
||||
state->messages_visible = flag != 0;
|
||||
} else if (sscanf(line, "VideoVisible=%d", &flag) == 1) {
|
||||
state->video_visible = flag != 0;
|
||||
}
|
||||
}
|
||||
|
||||
void writeAll(ImGuiContext *, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buf) {
|
||||
buf->appendf("[%s][MainWindow]\n", handler->TypeName);
|
||||
if (main_window.has_geometry) {
|
||||
buf->appendf("Pos=%d,%d\n", main_window.pos[0], main_window.pos[1]);
|
||||
buf->appendf("Size=%d,%d\n", main_window.size[0], main_window.size[1]);
|
||||
}
|
||||
buf->appendf("Maximized=%d\n", main_window.maximized ? 1 : 0);
|
||||
buf->appendf("VideoSplitterRatio=%.4f\n", main_window.video_splitter_ratio);
|
||||
buf->appendf("MessagesVisible=%d\n", main_window.messages_visible ? 1 : 0);
|
||||
buf->appendf("VideoVisible=%d\n", main_window.video_visible ? 1 : 0);
|
||||
buf->append("\n");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void addSettingsHandler() {
|
||||
ImGuiSettingsHandler handler;
|
||||
handler.TypeName = "Cabana";
|
||||
handler.TypeHash = ImHashStr("Cabana");
|
||||
handler.ReadOpenFn = readOpen;
|
||||
handler.ReadLineFn = readLine;
|
||||
handler.WriteAllFn = writeAll;
|
||||
ImGui::AddSettingsHandler(&handler);
|
||||
}
|
||||
|
||||
void load() {
|
||||
if (!settings.ui_state.empty())
|
||||
ImGui::LoadIniSettingsFromMemory(settings.ui_state.data(), settings.ui_state.size());
|
||||
}
|
||||
|
||||
void applyWindowGeometry(GLFWwindow *window) {
|
||||
if (main_window.has_geometry && main_window.size[0] > 0 && main_window.size[1] > 0) {
|
||||
glfwSetWindowPos(window, main_window.pos[0], main_window.pos[1]);
|
||||
glfwSetWindowSize(window, main_window.size[0], main_window.size[1]);
|
||||
}
|
||||
if (main_window.maximized) glfwMaximizeWindow(window);
|
||||
}
|
||||
|
||||
std::string save() {
|
||||
return std::string(ImGui::SaveIniSettingsToMemory());
|
||||
}
|
||||
|
||||
}
|
||||
25
iqpilot/tools/cabana/ui/inistate.h
Normal file
25
iqpilot/tools/cabana/ui/inistate.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
struct GLFWwindow;
|
||||
|
||||
namespace inistate {
|
||||
|
||||
struct MainWindowState {
|
||||
int pos[2] = {0, 0};
|
||||
int size[2] = {0, 0};
|
||||
bool maximized = false;
|
||||
bool has_geometry = false;
|
||||
float video_splitter_ratio = -1.0f;
|
||||
bool messages_visible = true;
|
||||
bool video_visible = true;
|
||||
};
|
||||
|
||||
extern MainWindowState main_window;
|
||||
|
||||
void addSettingsHandler();
|
||||
void load();
|
||||
void applyWindowGeometry(GLFWwindow *window);
|
||||
std::string save();
|
||||
|
||||
}
|
||||
206
iqpilot/tools/cabana/ui/main.cc
Normal file
206
iqpilot/tools/cabana/ui/main.cc
Normal file
@@ -0,0 +1,206 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "tools/cabana/streams/devicestream.h"
|
||||
#include "tools/cabana/streams/pandastream.h"
|
||||
#include "tools/cabana/streams/replaystream.h"
|
||||
#ifdef __linux__
|
||||
#include "tools/cabana/streams/socketcanstream.h"
|
||||
#endif
|
||||
#include "tools/cabana/ui/app.h"
|
||||
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
#ifdef __GLIBC__
|
||||
#include <malloc.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
struct CabanaArgs {
|
||||
bool demo = false;
|
||||
bool auto_source = false;
|
||||
bool qcam = false;
|
||||
bool wide_road = false;
|
||||
bool cabin = false;
|
||||
bool msgq = false;
|
||||
bool panda = false;
|
||||
bool no_vipc = false;
|
||||
bool no_cache = false;
|
||||
std::string panda_serial;
|
||||
std::string socketcan;
|
||||
std::string zmq;
|
||||
std::string bridge;
|
||||
std::string data_dir;
|
||||
std::string dbc;
|
||||
std::string route;
|
||||
};
|
||||
|
||||
void printUsage(const char *argv0) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s [options] [route]\n"
|
||||
"\n"
|
||||
" route local path or Konn3kt route to replay\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" --help show this help\n"
|
||||
" --demo use a demo route instead of providing your own\n"
|
||||
" --auto Auto load the route from the best available source (no video):\n"
|
||||
" internal, openpilotci, comma_api, car_segments, testing_closet\n"
|
||||
" --qcam load qcamera\n"
|
||||
" --wide-road load wide road camera (alias: --ecam)\n"
|
||||
" --cabin load cabin camera (alias: --dcam)\n"
|
||||
" --msgq read can messages from the msgq\n"
|
||||
" --panda read can messages from panda\n"
|
||||
" --panda-serial <serial> read can messages from panda with given serial\n"
|
||||
#ifdef __linux__
|
||||
" --socketcan <device> read can messages from given SocketCAN device\n"
|
||||
#endif
|
||||
" --zmq <ip-address> read can messages from zmq at the specified ip-address\n"
|
||||
" --bridge <ip-address> bridge remote ZMQ into local msgq\n"
|
||||
" --data_dir <dir> local directory with routes\n"
|
||||
" --no-vipc do not output video\n"
|
||||
" --no-cache turn off the local route file cache\n"
|
||||
" --dbc <file> dbc file to open\n",
|
||||
argv0);
|
||||
}
|
||||
|
||||
bool takeValue(int argc, char *argv[], int &i, std::string &out) {
|
||||
if (i + 1 >= argc) {
|
||||
fprintf(stderr, "error: %s requires a value\n", argv[i]);
|
||||
return false;
|
||||
}
|
||||
out = argv[++i];
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<int> parseArgs(int argc, char *argv[], CabanaArgs &args) {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const char *a = argv[i];
|
||||
if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) {
|
||||
printUsage(argv[0]);
|
||||
return 0;
|
||||
} else if (std::strcmp(a, "--demo") == 0) {
|
||||
args.demo = true;
|
||||
} else if (std::strcmp(a, "--auto") == 0) {
|
||||
args.auto_source = true;
|
||||
} else if (std::strcmp(a, "--qcam") == 0) {
|
||||
args.qcam = true;
|
||||
} else if (std::strcmp(a, "--wide-road") == 0 || std::strcmp(a, "--ecam") == 0) {
|
||||
args.wide_road = true;
|
||||
} else if (std::strcmp(a, "--cabin") == 0 || std::strcmp(a, "--dcam") == 0) {
|
||||
args.cabin = true;
|
||||
} else if (std::strcmp(a, "--msgq") == 0) {
|
||||
args.msgq = true;
|
||||
} else if (std::strcmp(a, "--panda") == 0) {
|
||||
args.panda = true;
|
||||
} else if (std::strcmp(a, "--panda-serial") == 0) {
|
||||
if (!takeValue(argc, argv, i, args.panda_serial)) return 1;
|
||||
args.panda = true;
|
||||
} else if (std::strcmp(a, "--socketcan") == 0) {
|
||||
if (!takeValue(argc, argv, i, args.socketcan)) return 1;
|
||||
#ifndef __linux__
|
||||
fprintf(stderr, "error: --socketcan is only supported on Linux\n");
|
||||
return 1;
|
||||
#endif
|
||||
} else if (std::strcmp(a, "--zmq") == 0) {
|
||||
if (!takeValue(argc, argv, i, args.zmq)) return 1;
|
||||
} else if (std::strcmp(a, "--bridge") == 0) {
|
||||
if (!takeValue(argc, argv, i, args.bridge)) return 1;
|
||||
} else if (std::strcmp(a, "--data_dir") == 0) {
|
||||
if (!takeValue(argc, argv, i, args.data_dir)) return 1;
|
||||
} else if (std::strcmp(a, "--no-vipc") == 0) {
|
||||
args.no_vipc = true;
|
||||
} else if (std::strcmp(a, "--no-cache") == 0) {
|
||||
args.no_cache = true;
|
||||
} else if (std::strcmp(a, "--dbc") == 0) {
|
||||
if (!takeValue(argc, argv, i, args.dbc)) return 1;
|
||||
} else if (a[0] == '-') {
|
||||
fprintf(stderr, "error: unknown option %s\n", a);
|
||||
printUsage(argv[0]);
|
||||
return 1;
|
||||
} else if (args.route.empty()) {
|
||||
args.route = a;
|
||||
} else {
|
||||
fprintf(stderr, "error: unexpected argument %s\n", a);
|
||||
printUsage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
#ifdef __GLIBC__
|
||||
|
||||
|
||||
mallopt(M_ARENA_MAX, 1);
|
||||
#endif
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::current_path(executableDir(), ec);
|
||||
|
||||
CabanaArgs args;
|
||||
if (auto code = parseArgs(argc, argv, args)) return *code;
|
||||
|
||||
std::unique_ptr<AbstractStream> stream;
|
||||
StreamLoader stream_loader;
|
||||
|
||||
if (args.msgq) {
|
||||
stream = std::make_unique<DeviceStream>();
|
||||
} else if (!args.zmq.empty()) {
|
||||
stream = std::make_unique<DeviceStream>(DeviceStream::Mode::Zmq, args.zmq);
|
||||
} else if (!args.bridge.empty()) {
|
||||
stream = std::make_unique<DeviceStream>(DeviceStream::Mode::Bridge, args.bridge);
|
||||
} else if (args.panda) {
|
||||
try {
|
||||
stream = std::make_unique<PandaStream>(PandaStreamConfig{.serial = args.panda_serial});
|
||||
} catch (std::exception &e) {
|
||||
fprintf(stderr, "%s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
#ifdef __linux__
|
||||
} else if (!args.socketcan.empty()) {
|
||||
if (!SocketCanStream::available()) {
|
||||
fprintf(stderr, "error: SocketCAN is not available on this system\n");
|
||||
return 1;
|
||||
}
|
||||
stream = std::make_unique<SocketCanStream>(SocketCanStreamConfig{.device = args.socketcan});
|
||||
#endif
|
||||
} else {
|
||||
uint32_t replay_flags = REPLAY_FLAG_NONE;
|
||||
if (args.wide_road) replay_flags |= REPLAY_FLAG_ECAM;
|
||||
if (args.qcam) replay_flags |= REPLAY_FLAG_QCAMERA;
|
||||
if (args.cabin) replay_flags |= REPLAY_FLAG_DCAM;
|
||||
if (args.no_vipc) replay_flags |= REPLAY_FLAG_NO_VIPC;
|
||||
if (args.no_cache) replay_flags |= REPLAY_FLAG_NO_FILE_CACHE;
|
||||
|
||||
std::string route;
|
||||
if (!args.route.empty()) {
|
||||
route = args.route;
|
||||
} else if (args.demo) {
|
||||
route = DEMO_ROUTE;
|
||||
}
|
||||
if (!route.empty()) {
|
||||
stream_loader = [route, data_dir = args.data_dir, replay_flags, auto_source = args.auto_source]() -> std::unique_ptr<AbstractStream> {
|
||||
auto replay_stream = std::make_unique<ReplayStream>();
|
||||
Connection err = replay_stream->error.connect([](const std::string &msg) {
|
||||
fprintf(stderr, "%s\n", msg.c_str());
|
||||
utils::runOnMainThread([msg]() { MessageBox::warning("Error", msg); });
|
||||
});
|
||||
if (!replay_stream->loadRoute(route, data_dir, replay_flags, auto_source)) {
|
||||
return nullptr;
|
||||
}
|
||||
return replay_stream;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return run(std::move(stream), std::move(stream_loader), args.dbc);
|
||||
}
|
||||
929
iqpilot/tools/cabana/ui/mainwin.cc
Normal file
929
iqpilot/tools/cabana/ui/mainwin.cc
Normal file
@@ -0,0 +1,929 @@
|
||||
#include "tools/cabana/ui/mainwin.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#include "json11/json11.hpp"
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/app.h"
|
||||
#include "tools/cabana/ui/dialogs/filedialog.h"
|
||||
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||
#include "tools/cabana/ui/inistate.h"
|
||||
#include "tools/cabana/ui/threadpool.h"
|
||||
#include "tools/cabana/ui/tools/findsignal.h"
|
||||
#include "tools/cabana/ui/tools/findsimilarbits.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/export.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
#include "tools/replay/util.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *VIDEO_PANEL = "###VideoPanel";
|
||||
constexpr const char *CENTER_PANEL = "###CenterWidget";
|
||||
constexpr const char *CHARTS_WINDOW = "Charts###ChartsWindow";
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(GLFWwindow *window, std::unique_ptr<AbstractStream> stream, StreamLoader stream_loader,
|
||||
const std::string &dbc_file) : window_(window) {
|
||||
can = &dummy_;
|
||||
video_splitter_ratio_ = inistate::main_window.video_splitter_ratio;
|
||||
messages_visible_ = inistate::main_window.messages_visible;
|
||||
video_visible_ = inistate::main_window.video_visible;
|
||||
loadFingerprints();
|
||||
std::error_code ec;
|
||||
for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH, ec)) {
|
||||
if (entry.is_regular_file() && entry.path().extension() == ".dbc") {
|
||||
opendbc_names_.push_back(entry.path().filename().string());
|
||||
}
|
||||
}
|
||||
std::sort(opendbc_names_.begin(), opendbc_names_.end());
|
||||
|
||||
|
||||
installDownloadProgressHandler([this](uint64_t cur, uint64_t total, bool success) {
|
||||
utils::runOnMainThread([this, cur, total, success]() { updateDownloadProgress(cur, total, success); });
|
||||
});
|
||||
installMessageHandler([this](ReplyMsgType type, const std::string &msg) {
|
||||
fprintf(stderr, "%s\n", msg.c_str());
|
||||
utils::runOnMainThread([this, msg]() { showStatusMessage(msg, 2000); });
|
||||
});
|
||||
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { dbcFileChanged(); }));
|
||||
connections_.push_back(UndoStack::instance()->cleanChanged.connect([this](bool clean) {
|
||||
window_modified_ = !clean;
|
||||
updateWindowTitle();
|
||||
}));
|
||||
|
||||
startup_stream_ = std::move(stream);
|
||||
startup_loader_ = std::move(stream_loader);
|
||||
nextFrame([this, dbc_file]() {
|
||||
if (startup_loader_) {
|
||||
loadStartupStream(dbc_file);
|
||||
} else {
|
||||
startup_stream_ ? openStream(std::move(startup_stream_), dbc_file) : selectAndOpenStream();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::loadFingerprints() {
|
||||
std::ifstream json_file((executableDir() / "dbc/car_fingerprint_to_dbc.json"));
|
||||
if (!json_file) return;
|
||||
const std::string contents{std::istreambuf_iterator<char>(json_file), std::istreambuf_iterator<char>()};
|
||||
std::string err;
|
||||
auto doc = json11::Json::parse(contents, err);
|
||||
if (!err.empty() || !doc.is_object()) return;
|
||||
for (const auto &kv : doc.object_items()) {
|
||||
if (kv.second.is_string()) {
|
||||
fingerprint_to_dbc_.emplace(kv.first, kv.second.string_value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::drawFileMenu() {
|
||||
const bool has_stream = hasStream();
|
||||
if (ImGui::MenuItem("Open Stream...")) selectAndOpenStream();
|
||||
if (ImGui::MenuItem("Close stream", nullptr, false, has_stream)) closeStream();
|
||||
if (ImGui::MenuItem("Export to CSV...", nullptr, false, has_stream)) exportToCSV();
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem("New DBC File", "Ctrl+N")) newFile();
|
||||
if (ImGui::MenuItem("Open DBC File...", "Ctrl+O")) openFile();
|
||||
|
||||
if (ImGui::BeginMenu("Manage DBC Files", has_stream)) {
|
||||
drawManageDBCsMenu();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu("Open Recent")) {
|
||||
drawRecentFilesMenu();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::BeginMenu("Load DBC from IQ.Pilot iqdbc")) {
|
||||
for (const auto &name : opendbc_names_) {
|
||||
if (ImGui::MenuItem(name.c_str())) loadDBCFromOpendbc(name);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::MenuItem("Load DBC From Clipboard")) loadFromClipboard();
|
||||
|
||||
ImGui::Separator();
|
||||
const int cnt = dbc()->nonEmptyDBCCount();
|
||||
const std::string save_text = cnt > 1 ? "Save " + std::to_string(cnt) + " DBCs..." : "Save DBC...";
|
||||
if (ImGui::MenuItem(save_text.c_str(), "Ctrl+S", false, cnt > 0)) save();
|
||||
if (ImGui::MenuItem("Save DBC As...", "Ctrl+Shift+S", false, cnt == 1)) saveAs();
|
||||
|
||||
if (ImGui::MenuItem("Copy DBC To Clipboard", nullptr, false, cnt == 1)) saveToClipboard();
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Settings...")) openSettings();
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Exit", "Ctrl+Q")) close();
|
||||
}
|
||||
|
||||
void MainWindow::drawMenuBar() {
|
||||
if (!ImGui::BeginMainMenuBar()) return;
|
||||
if (ImGui::BeginMenu("File")) {
|
||||
drawFileMenu();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Edit")) {
|
||||
auto stack = UndoStack::instance();
|
||||
const std::string undo_text = stack->canUndo() ? "Undo " + stack->undoText() : "Undo";
|
||||
const std::string redo_text = stack->canRedo() ? "Redo " + stack->redoText() : "Redo";
|
||||
if (ImGui::MenuItem(undo_text.c_str(), "Ctrl+Z", false, stack->canUndo())) stack->undo();
|
||||
if (ImGui::MenuItem(redo_text.c_str(), "Ctrl+Shift+Z", false, stack->canRedo())) stack->redo();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("View")) {
|
||||
if (ImGui::MenuItem("Full Screen", "Ctrl+F11")) toggleFullScreen();
|
||||
ImGui::Separator();
|
||||
ImGui::MenuItem(messages_widget_ ? messages_widget_->title().c_str() : "MESSAGES", nullptr, &messages_visible_);
|
||||
ImGui::MenuItem(video_dock_title_.empty() ? "##video_dock" : video_dock_title_.c_str(), nullptr, &video_visible_);
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Reset Window Layout")) {
|
||||
messages_visible_ = video_visible_ = true;
|
||||
reset_layout_ = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Tools", hasStream())) {
|
||||
if (ImGui::MenuItem("Find Similar Bits")) findSimilarBits();
|
||||
if (ImGui::MenuItem("Find Signal")) findSignal();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui::BeginMenu("Help")) {
|
||||
if (ImGui::MenuItem("Help", "F1")) toggleHelp();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
|
||||
void MainWindow::createDockWidgets() {
|
||||
widget_connections_.clear();
|
||||
messages_widget_ = std::make_unique<MessagesWidget>();
|
||||
widget_connections_.push_back(messages_widget_->msgSelectionChanged.connect([this](const MessageId &id) { center_widget_.setMessage(id); }));
|
||||
|
||||
charts_widget_ = std::make_unique<ChartsWidget>();
|
||||
center_widget_.setChartsWidget(charts_widget_.get());
|
||||
video_widget_ = std::make_unique<VideoWidget>();
|
||||
widget_connections_.push_back(charts_widget_->toggleChartsDocking.connect([this]() { toggleChartsDocking(); }));
|
||||
widget_connections_.push_back(charts_widget_->showTip.connect([this](double sec) { video_widget_->showThumbnail(sec); }));
|
||||
}
|
||||
|
||||
void MainWindow::showStatusMessage(const std::string &msg, int timeout_ms) {
|
||||
status_bar_.message = msg;
|
||||
status_bar_.message_until = timeout_ms > 0 ? ImGui::GetTime() + timeout_ms / 1000.0 : 0;
|
||||
}
|
||||
|
||||
void MainWindow::updateWindowTitle() {
|
||||
std::string title;
|
||||
for (auto f : dbc()->allDBCFiles()) {
|
||||
if (!title.empty()) title += " | ";
|
||||
title += "(" + toString(dbc()->sources(f)) + ") " + f->name();
|
||||
}
|
||||
if (window_modified_) title += "*";
|
||||
if (!title.empty()) title += " \xe2\x80\x94 ";
|
||||
title += "Cabana";
|
||||
glfwSetWindowTitle(window_, title.c_str());
|
||||
}
|
||||
|
||||
void MainWindow::dbcFileChanged() {
|
||||
UndoStack::instance()->clear();
|
||||
updateWindowTitle();
|
||||
nextFrame([this]() { restoreSessionState(); });
|
||||
}
|
||||
|
||||
void MainWindow::selectAndOpenStream() {
|
||||
stream_selector_.open([this](std::unique_ptr<AbstractStream> stream, const std::string &dbc_file) {
|
||||
if (stream) {
|
||||
openStream(std::move(stream), dbc_file);
|
||||
} else if (!stream_) {
|
||||
openStream(std::make_unique<DummyStream>());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void MainWindow::loadStartupStream(const std::string &dbc_file) {
|
||||
wait_dlg_.text = "Loading route...";
|
||||
wait_dlg_.value = 0;
|
||||
wait_dlg_.open = true;
|
||||
wait_dlg_.show_at = ImGui::GetTime() + 4.0;
|
||||
ThreadPool::instance().run([this, dbc_file, loader = std::move(startup_loader_)]() {
|
||||
AbstractStream *loaded = nullptr;
|
||||
std::string error;
|
||||
try {
|
||||
loaded = loader().release();
|
||||
} catch (const std::exception &e) {
|
||||
|
||||
error = e.what();
|
||||
}
|
||||
utils::runOnMainThread([this, dbc_file, loaded, error]() {
|
||||
wait_dlg_.open = false;
|
||||
std::unique_ptr<AbstractStream> stream(loaded);
|
||||
if (!error.empty()) {
|
||||
fprintf(stderr, "%s\n", error.c_str());
|
||||
MessageBox::warning("Failed to load route", error);
|
||||
}
|
||||
stream ? openStream(std::move(stream), dbc_file) : openStream(std::make_unique<DummyStream>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::closeStream() {
|
||||
openStream(std::make_unique<DummyStream>());
|
||||
if (dbc()->nonEmptyDBCCount() > 0) {
|
||||
dbc()->fileChanged();
|
||||
}
|
||||
showStatusMessage("stream closed");
|
||||
}
|
||||
|
||||
void MainWindow::exportToCSV() {
|
||||
std::string dir = settings.last_dir + "/" + can->routeName() + ".csv";
|
||||
FileDialog::getSaveFileName("Export stream to CSV file", dir, ".csv", [](const std::string &fn) {
|
||||
if (!fn.empty()) {
|
||||
utils::exportToCSV(fn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::newFile(SourceSet s) {
|
||||
closeFile(s, [s]() { dbc()->open(s, std::string(""), std::string("")); });
|
||||
}
|
||||
|
||||
void MainWindow::openFile(SourceSet s) {
|
||||
remindSaveChanges([this, s]() {
|
||||
FileDialog::getOpenFileName("Open File", settings.last_dir, ".dbc", [this, s](const std::string &fn) {
|
||||
if (!fn.empty()) {
|
||||
loadFile(fn, s);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::loadFile(const std::string &fn, SourceSet s, std::function<void()> then) {
|
||||
if (!fn.empty()) {
|
||||
closeFile(s, [this, fn, s, then]() {
|
||||
std::string error;
|
||||
if (dbc()->open(s, fn, &error)) {
|
||||
updateRecentFiles(fn);
|
||||
showStatusMessage("DBC File " + fn + " loaded", 2000);
|
||||
if (then) then();
|
||||
} else {
|
||||
MessageBox::warning("Failed to load DBC file", "Failed to parse DBC file " + fn, error, then);
|
||||
}
|
||||
});
|
||||
} else if (then) {
|
||||
then();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::loadDBCFromOpendbc(const std::string &name) {
|
||||
loadFile(std::string(OPENDBC_FILE_PATH) + "/" + name);
|
||||
}
|
||||
|
||||
void MainWindow::loadFromClipboard(SourceSet s, bool close_all) {
|
||||
std::string text;
|
||||
if (!utils::getClipboardText(&text)) {
|
||||
MessageBox::warning("Load From Clipboard", "No clipboard tool found. Install xclip (X11) or wl-clipboard (Wayland).");
|
||||
return;
|
||||
}
|
||||
if (text.empty()) {
|
||||
MessageBox::warning("Load From Clipboard", "Clipboard is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
closeFile(s, [s, text]() {
|
||||
std::string error;
|
||||
bool ret = dbc()->open(s, std::string(""), text, &error);
|
||||
if (ret && dbc()->nonEmptyDBCCount() > 0) {
|
||||
MessageBox::information("Load From Clipboard", "DBC Successfully Loaded!");
|
||||
} else {
|
||||
MessageBox::warning("Failed to load DBC from clipboard", "Make sure that you paste the text with correct format.", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() {
|
||||
installDownloadProgressHandler(nullptr);
|
||||
installMessageHandler(nullptr);
|
||||
releaseStream();
|
||||
can = nullptr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void MainWindow::releaseStream() {
|
||||
tool_dialogs_.clear();
|
||||
wait_dlg_.connection.disconnect();
|
||||
wait_dlg_.open = false;
|
||||
widget_connections_.clear();
|
||||
charts_widget_.reset();
|
||||
video_widget_.reset();
|
||||
center_widget_.clear();
|
||||
messages_widget_.reset();
|
||||
stream_connections_.clear();
|
||||
stream_.reset();
|
||||
can = &dummy_;
|
||||
}
|
||||
|
||||
void MainWindow::openStream(std::unique_ptr<AbstractStream> stream, const std::string &dbc_file) {
|
||||
releaseStream();
|
||||
startStream(std::move(stream), dbc_file);
|
||||
}
|
||||
|
||||
void MainWindow::startStream(std::unique_ptr<AbstractStream> stream, const std::string &dbc_file) {
|
||||
stream_ = std::move(stream);
|
||||
can = stream_.get();
|
||||
stream_connections_.push_back(can->error.connect([](const std::string &msg) {
|
||||
MessageBox::warning("Error", msg);
|
||||
}));
|
||||
can->start();
|
||||
|
||||
loadFile(dbc_file, SOURCE_ALL, [this]() {
|
||||
showStatusMessage("Stream [" + can->routeName() + "] started", 2000);
|
||||
createDockWidgets();
|
||||
|
||||
video_dock_title_ = can->routeName();
|
||||
|
||||
if (!dbc()->nonEmptyDBCCount()) {
|
||||
newFile();
|
||||
}
|
||||
|
||||
stream_connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &) { eventsMerged(); }));
|
||||
|
||||
if (hasStream()) {
|
||||
wait_dlg_.text = can->liveStreaming() ? "Waiting for the live stream to start..." : "Loading segment data...";
|
||||
wait_dlg_.value = 0;
|
||||
wait_dlg_.open = true;
|
||||
wait_dlg_.show_at = ImGui::GetTime() + 4.0;
|
||||
wait_dlg_.connection = can->eventsMerged.connect([this](const MessageEventsMap &) {
|
||||
wait_dlg_.open = false;
|
||||
wait_dlg_.connection.disconnect();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::eventsMerged() {
|
||||
const std::string fingerprint = can->carFingerprint();
|
||||
if (!can->liveStreaming() && std::exchange(car_fingerprint_, fingerprint) != fingerprint) {
|
||||
video_dock_title_ = "ROUTE: " + can->routeName() + " FINGERPRINT: " + (car_fingerprint_.empty() ? "Unknown Car" : car_fingerprint_);
|
||||
|
||||
auto it = fingerprint_to_dbc_.find(car_fingerprint_);
|
||||
if (!dbc()->nonEmptyDBCCount() && it != fingerprint_to_dbc_.end()) {
|
||||
nextFrame([this, dbc_name = it->second]() { loadDBCFromOpendbc(dbc_name + ".dbc"); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::saveFiles(bool as, std::function<void()> then) {
|
||||
const std::vector<DBCFile *> files = dbc()->nonEmptyDBCFiles();
|
||||
auto next = std::make_shared<std::function<void(size_t)>>();
|
||||
*next = [this, as, files, next, then](size_t i) {
|
||||
if (i >= files.size()) {
|
||||
if (then) then();
|
||||
return;
|
||||
}
|
||||
auto cb = [next, i]() { (*next)(i + 1); };
|
||||
as ? saveFileAs(files[i], cb) : saveFile(files[i], cb);
|
||||
};
|
||||
(*next)(0);
|
||||
}
|
||||
|
||||
void MainWindow::save(std::function<void()> then) {
|
||||
saveFiles(false, std::move(then));
|
||||
}
|
||||
|
||||
void MainWindow::saveAs(std::function<void()> then) {
|
||||
saveFiles(true, std::move(then));
|
||||
}
|
||||
|
||||
void MainWindow::closeFile(SourceSet s, std::function<void()> then) {
|
||||
remindSaveChanges([s, then]() {
|
||||
if (s == SOURCE_ALL) {
|
||||
dbc()->closeAll();
|
||||
} else {
|
||||
dbc()->close(s);
|
||||
}
|
||||
if (then) then();
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::closeFile(DBCFile *dbc_file) {
|
||||
assert(dbc_file != nullptr);
|
||||
remindSaveChanges([this, dbc_file]() {
|
||||
dbc()->close(dbc_file);
|
||||
|
||||
if (dbc()->dbcCount() == 0) {
|
||||
newFile();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::saveFile(DBCFile *dbc_file, std::function<void()> then) {
|
||||
assert(dbc_file != nullptr);
|
||||
if (!dbc_file->filename.empty()) {
|
||||
dbc_file->save();
|
||||
UndoStack::instance()->setClean();
|
||||
showStatusMessage("File saved", 2000);
|
||||
if (then) then();
|
||||
} else if (!dbc_file->isEmpty()) {
|
||||
saveFileAs(dbc_file, then);
|
||||
} else if (then) {
|
||||
then();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::saveFileAs(DBCFile *dbc_file, std::function<void()> then) {
|
||||
std::string title = "Save File (bus: " + toString(dbc()->sources(dbc_file)) + ")";
|
||||
std::string default_path = (std::filesystem::path(settings.last_dir) / "untitled.dbc").string();
|
||||
FileDialog::getSaveFileName(title, default_path, ".dbc", [this, dbc_file, then](const std::string &fn) {
|
||||
if (!fn.empty()) {
|
||||
dbc_file->saveAs(fn);
|
||||
UndoStack::instance()->setClean();
|
||||
showStatusMessage("File saved as " + fn, 2000);
|
||||
updateRecentFiles(fn);
|
||||
}
|
||||
if (then) then();
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::saveToClipboard() {
|
||||
|
||||
for (auto dbc_file : dbc()->nonEmptyDBCFiles()) {
|
||||
saveFileToClipboard(dbc_file);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::saveFileToClipboard(DBCFile *dbc_file) {
|
||||
assert(dbc_file != nullptr);
|
||||
copyToClipboard(dbc_file->generateDBC());
|
||||
}
|
||||
|
||||
void MainWindow::copyToClipboard(const std::string &text) {
|
||||
if (utils::setClipboardText(text)) {
|
||||
MessageBox::information("Copy To Clipboard", "DBC Successfully copied!");
|
||||
} else {
|
||||
MessageBox::warning("Copy To Clipboard", "Failed to copy DBC to clipboard. Install xclip (X11) or wl-clipboard (Wayland).");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::drawManageDBCsMenu() {
|
||||
for (int source : can->sources) {
|
||||
if (source >= 64) continue;
|
||||
|
||||
SourceSet ss = {source, uint8_t(source + 128), uint8_t(source + 192)};
|
||||
|
||||
auto dbc_file = dbc()->findDBCFile(source);
|
||||
const std::string title = "Bus " + std::to_string(source) + " (" + (dbc_file ? dbc_file->name() : "No DBCs loaded") + ")";
|
||||
ImGui::PushID(source);
|
||||
if (ImGui::BeginMenu(title.c_str())) {
|
||||
if (ImGui::MenuItem("New DBC File...")) newFile(ss);
|
||||
if (ImGui::MenuItem("Open DBC File...")) openFile(ss);
|
||||
if (ImGui::MenuItem("Load DBC From Clipboard...")) loadFromClipboard(ss, false);
|
||||
|
||||
|
||||
if (dbc_file) {
|
||||
ImGui::Separator();
|
||||
ImGui::MenuItem((dbc_file->name() + " (" + toString(dbc()->sources(dbc_file)) + ")").c_str(), nullptr, false, false);
|
||||
if (ImGui::MenuItem("Save...")) saveFile(dbc_file);
|
||||
if (ImGui::MenuItem("Save As...")) saveFileAs(dbc_file);
|
||||
if (ImGui::MenuItem("Copy to Clipboard...")) saveFileToClipboard(dbc_file);
|
||||
if (ImGui::MenuItem("Remove from this bus...")) closeFile(ss, {});
|
||||
if (ImGui::MenuItem("Remove from all buses...")) closeFile(dbc_file);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::updateRecentFiles(const std::string &fn) {
|
||||
settings.recent_files.erase(std::remove(settings.recent_files.begin(), settings.recent_files.end(), fn), settings.recent_files.end());
|
||||
settings.recent_files.insert(settings.recent_files.begin(), fn);
|
||||
while (settings.recent_files.size() > MAX_RECENT_FILES) {
|
||||
settings.recent_files.pop_back();
|
||||
}
|
||||
settings.last_dir = std::filesystem::absolute(fn).parent_path().string();
|
||||
}
|
||||
|
||||
void MainWindow::drawRecentFilesMenu() {
|
||||
int num_recent_files = std::min<int>(settings.recent_files.size(), MAX_RECENT_FILES);
|
||||
if (!num_recent_files) {
|
||||
ImGui::MenuItem("No Recent Files", nullptr, false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_recent_files; ++i) {
|
||||
std::string text = std::to_string(i + 1) + " " + std::filesystem::path(settings.recent_files[i]).filename().string();
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::MenuItem(text.c_str())) loadFile(settings.recent_files[i]);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::remindSaveChanges(std::function<void()> then) {
|
||||
if (UndoStack::instance()->isClean()) {
|
||||
UndoStack::instance()->clear();
|
||||
if (then) then();
|
||||
return;
|
||||
}
|
||||
std::string text = "You have unsaved changes. Press ok to save them, cancel to discard.";
|
||||
MessageBox::question("Unsaved Changes", text, [this, then](bool ok) {
|
||||
if (ok) {
|
||||
save([this, then]() { remindSaveChanges(then); });
|
||||
} else {
|
||||
UndoStack::instance()->clear();
|
||||
if (then) then();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool success) {
|
||||
const double fraction = total > 0 ? cur / (double)total : 0.0;
|
||||
if (wait_dlg_.open) wait_dlg_.value = (int)(fraction * 100);
|
||||
if (success && cur < total) {
|
||||
status_bar_.progress_value = fraction;
|
||||
status_bar_.progress_text = "Downloading " + std::to_string((int)(fraction * 100)) + "% (" + formattedDataSize(total) + ")";
|
||||
status_bar_.progress_visible = true;
|
||||
} else {
|
||||
status_bar_.progress_visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::toggleChartsDocking() {
|
||||
charts_floating_ = !charts_floating_;
|
||||
charts_widget_->setIsDocked(!charts_floating_);
|
||||
}
|
||||
|
||||
void MainWindow::close() {
|
||||
if (closing_) return;
|
||||
closing_ = true;
|
||||
remindSaveChanges([this]() { finishClose(); });
|
||||
}
|
||||
|
||||
void MainWindow::finishClose() {
|
||||
|
||||
auto &state = inistate::main_window;
|
||||
state.maximized = glfwGetWindowAttrib(window_, GLFW_MAXIMIZED);
|
||||
if (full_screen_) {
|
||||
#ifndef __APPLE__
|
||||
|
||||
state.pos[0] = windowed_rect_[0]; state.pos[1] = windowed_rect_[1];
|
||||
state.size[0] = windowed_rect_[2]; state.size[1] = windowed_rect_[3];
|
||||
#endif
|
||||
} else if (!state.maximized) {
|
||||
glfwGetWindowPos(window_, &state.pos[0], &state.pos[1]);
|
||||
glfwGetWindowSize(window_, &state.size[0], &state.size[1]);
|
||||
}
|
||||
state.has_geometry = state.size[0] > 0 && state.size[1] > 0;
|
||||
state.video_splitter_ratio = video_splitter_ratio_;
|
||||
state.messages_visible = messages_visible_;
|
||||
state.video_visible = video_visible_;
|
||||
settings.ui_state = inistate::save();
|
||||
|
||||
saveSessionState();
|
||||
settings.save();
|
||||
exited_ = true;
|
||||
}
|
||||
|
||||
void MainWindow::openSettings() {
|
||||
settings_dialog_.open();
|
||||
}
|
||||
|
||||
void MainWindow::findSimilarBits() {
|
||||
auto dlg = std::make_unique<FindSimilarBitsDlg>();
|
||||
dlg->connections_.push_back(dlg->openMessage.connect([this](const MessageId &id) { messages_widget_->selectMessage(id); }));
|
||||
tool_dialogs_.push_back(std::move(dlg));
|
||||
}
|
||||
|
||||
void MainWindow::findSignal() {
|
||||
auto dlg = std::make_unique<FindSignalDlg>();
|
||||
dlg->connections_.push_back(dlg->openMessage.connect([this](const MessageId &id) { messages_widget_->selectMessage(id); }));
|
||||
tool_dialogs_.push_back(std::move(dlg));
|
||||
}
|
||||
|
||||
void MainWindow::toggleHelp() {
|
||||
help_overlay_.toggle();
|
||||
}
|
||||
|
||||
void MainWindow::toggleFullScreen() {
|
||||
#ifdef __APPLE__
|
||||
toggleNativeFullScreen(window_);
|
||||
#else
|
||||
full_screen_ = !full_screen_;
|
||||
if (full_screen_) {
|
||||
glfwGetWindowPos(window_, &windowed_rect_[0], &windowed_rect_[1]);
|
||||
glfwGetWindowSize(window_, &windowed_rect_[2], &windowed_rect_[3]);
|
||||
GLFWmonitor *monitor = glfwGetPrimaryMonitor();
|
||||
const GLFWvidmode *mode = glfwGetVideoMode(monitor);
|
||||
glfwSetWindowMonitor(window_, monitor, 0, 0, mode->width, mode->height, mode->refreshRate);
|
||||
} else {
|
||||
glfwSetWindowMonitor(window_, nullptr, windowed_rect_[0], windowed_rect_[1], windowed_rect_[2], windowed_rect_[3], 0);
|
||||
glfwMaximizeWindow(window_);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::saveSessionState() {
|
||||
settings.recent_dbc_file = "";
|
||||
settings.active_msg_id = "";
|
||||
settings.selected_msg_ids.clear();
|
||||
settings.active_charts.clear();
|
||||
|
||||
const auto files = dbc()->nonEmptyDBCFiles();
|
||||
if (!files.empty()) settings.recent_dbc_file = files.front()->filename;
|
||||
|
||||
if (auto *detail = center_widget_.getDetailWidget()) {
|
||||
auto [active_id, ids] = detail->serializeMessageIds();
|
||||
settings.active_msg_id = active_id;
|
||||
settings.selected_msg_ids = ids;
|
||||
}
|
||||
if (charts_widget_) {
|
||||
settings.active_charts = charts_widget_->serializeChartIds();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::restoreSessionState() {
|
||||
if (settings.recent_dbc_file.empty() || dbc()->nonEmptyDBCCount() == 0) return;
|
||||
|
||||
if (dbc()->nonEmptyDBCFiles().front()->filename != settings.recent_dbc_file) return;
|
||||
|
||||
if (!settings.selected_msg_ids.empty()) {
|
||||
center_widget_.ensureDetailWidget()->restoreTabs(settings.active_msg_id, settings.selected_msg_ids);
|
||||
}
|
||||
|
||||
if (charts_widget_ != nullptr && !settings.active_charts.empty()) {
|
||||
charts_widget_->restoreChartsFromIds(settings.active_charts);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::handleShortcuts() {
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
for (const KeyEvent &e : takeKeyEvents()) {
|
||||
const bool ctrl = e.mods & (GLFW_MOD_CONTROL | GLFW_MOD_SUPER);
|
||||
const bool shift = e.mods & GLFW_MOD_SHIFT;
|
||||
|
||||
if (e.key == GLFW_KEY_SPACE && !ctrl && can && !io.WantTextInput) can->pause(!can->isPaused());
|
||||
if (e.key == GLFW_KEY_F1) toggleHelp();
|
||||
if (e.key == GLFW_KEY_F11 && ctrl) toggleFullScreen();
|
||||
|
||||
if (e.key == GLFW_KEY_ESCAPE && full_screen_ && !io.WantTextInput &&
|
||||
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) {
|
||||
toggleFullScreen();
|
||||
}
|
||||
if (!ctrl) continue;
|
||||
if (e.key == GLFW_KEY_N) newFile();
|
||||
if (e.key == GLFW_KEY_O) openFile();
|
||||
if (e.key == GLFW_KEY_S) {
|
||||
if (shift) {
|
||||
if (dbc()->nonEmptyDBCCount() == 1) saveAs();
|
||||
} else if (dbc()->nonEmptyDBCCount() > 0) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key == GLFW_KEY_Z && !io.WantTextInput) shift ? UndoStack::instance()->redo() : UndoStack::instance()->undo();
|
||||
if (e.key == GLFW_KEY_Q) close();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::drawStatusBar() {
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyle().Colors[ImGuiCol_MenuBarBg]);
|
||||
ImGui::BeginChild("status_bar", ImVec2(0, ImGui::GetFrameHeight()), ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar);
|
||||
|
||||
|
||||
const float width = ImGui::GetContentRegionAvail().x;
|
||||
const float pad = ImGui::GetStyle().WindowPadding.x;
|
||||
ImGui::SetCursorPosX(pad);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
|
||||
auto &bar = status_bar_;
|
||||
if (!bar.message.empty() && (bar.message_until == 0 || ImGui::GetTime() < bar.message_until)) {
|
||||
ImGui::TextUnformatted(bar.message.c_str());
|
||||
} else {
|
||||
bar.message.clear();
|
||||
ImGui::TextUnformatted("For Help, Press F1");
|
||||
}
|
||||
if (bar.progress_visible) {
|
||||
ImGui::SameLine(width - pad - 300.0f);
|
||||
ImGui::ProgressBar(bar.progress_value, ImVec2(300.0f, 16.0f), bar.progress_text.c_str());
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
void MainWindow::drawWaitDialog() {
|
||||
const char *id = "###WaitDialog";
|
||||
if (wait_dlg_.open && !ImGui::IsPopupOpen(id) && ImGui::GetTime() >= wait_dlg_.show_at) ImGui::OpenPopup(id);
|
||||
if (!ImGui::IsPopupOpen(id)) return;
|
||||
ImGui::SetNextWindowSize(ImVec2(400.0f, 0.0f), ImGuiCond_Always);
|
||||
setNextDialogWindow(ImVec2(0.0f, 0.0f));
|
||||
if (ImGui::BeginPopupModal(id, nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::TextUnformatted(wait_dlg_.text.c_str());
|
||||
|
||||
ImGui::ProgressBar(wait_dlg_.value / 100.0f, ImVec2(-1.0f, 0.0f), wait_dlg_.value == 0 ? "" : (const char *)nullptr);
|
||||
bool abort = false, rejected = false;
|
||||
dialogButtons("Abort", &abort, &rejected, true, nullptr);
|
||||
if (abort || rejected) {
|
||||
wait_dlg_.open = false;
|
||||
close();
|
||||
}
|
||||
if (!wait_dlg_.open) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::drawDockspace() {
|
||||
const ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||
ImGui::SetNextWindowSize(viewport->WorkSize);
|
||||
ImGui::SetNextWindowViewport(viewport->ID);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus |
|
||||
ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoBackground |
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
||||
ImGui::Begin("##host", nullptr, flags);
|
||||
ImGui::PopStyleVar(3);
|
||||
|
||||
|
||||
|
||||
const float status_height = full_screen_ ? 0.0f : ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y;
|
||||
const ImVec2 dock_size(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - status_height);
|
||||
const ImGuiID dock_id = ImGui::GetID("cabana_dockspace");
|
||||
if (reset_layout_ || ImGui::DockBuilderGetNode(dock_id) == nullptr) {
|
||||
|
||||
ImGui::DockBuilderRemoveNode(dock_id);
|
||||
ImGui::DockBuilderAddNode(dock_id, ImGuiDockNodeFlags_DockSpace);
|
||||
ImGui::DockBuilderSetNodeSize(dock_id, dock_size);
|
||||
ImGuiID center = dock_id, left = 0, right = 0;
|
||||
ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.28f, &left, ¢er);
|
||||
ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.4f, &right, ¢er);
|
||||
ImGui::DockBuilderDockWindow(MESSAGES_PANEL_ID, left);
|
||||
ImGui::DockBuilderDockWindow(VIDEO_PANEL, right);
|
||||
ImGui::DockBuilderDockWindow(CENTER_PANEL, center);
|
||||
ImGui::DockBuilderGetNode(center)->LocalFlags |= ImGuiDockNodeFlags_NoTabBar;
|
||||
ImGui::DockBuilderFinish(dock_id);
|
||||
reset_layout_ = false;
|
||||
}
|
||||
|
||||
const float min_panel_width = SignalView::minimumWidth() + (ImGui::GetStyle().WindowPadding.x + ImGui::GetStyle().WindowBorderSize) * 2;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(min_panel_width, ImGui::GetStyle().WindowMinSize.y));
|
||||
ImGui::DockSpace(dock_id, dock_size);
|
||||
ImGui::PopStyleVar();
|
||||
if (!full_screen_) drawStatusBar();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
bool floatingOut() { return ImGui::GetWindowViewport() != ImGui::GetMainViewport(); }
|
||||
|
||||
|
||||
|
||||
void setNextPanelClass() {
|
||||
ImGuiWindowClass window_class;
|
||||
window_class.ViewportFlagsOverrideSet = ImGuiViewportFlags_NoAutoMerge;
|
||||
window_class.DockNodeFlagsOverrideSet = ImGuiDockNodeFlags_NoWindowMenuButton;
|
||||
ImGui::SetNextWindowClass(&window_class);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::drawMessagesPanel() {
|
||||
const std::string name = messages_widget_->title() + MESSAGES_PANEL_ID;
|
||||
setNextPanelClass();
|
||||
if (ImGui::Begin(name.c_str(), &messages_visible_)) {
|
||||
help_overlay_.add(messages_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect());
|
||||
messages_widget_->draw();
|
||||
}
|
||||
const bool floating = floatingOut();
|
||||
ImGui::End();
|
||||
if (!messages_visible_ && floating) messages_visible_ = reset_layout_ = true;
|
||||
}
|
||||
|
||||
void MainWindow::drawVideoPanel() {
|
||||
const std::string name = video_dock_title_ + VIDEO_PANEL;
|
||||
setNextPanelClass();
|
||||
const bool video_open = ImGui::Begin(name.c_str(), &video_visible_);
|
||||
const bool floating = floatingOut();
|
||||
if (!video_open) {
|
||||
video_widget_->setVisible(false);
|
||||
} else {
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const bool live = can->liveStreaming();
|
||||
|
||||
const float video_padding = ImGui::GetStyle().WindowPadding.y * 2.0f;
|
||||
const float default_h = video_widget_->defaultHeight(avail.x) + video_padding;
|
||||
const float video_hint = video_splitter_ratio_ >= 0.0f ? avail.y * video_splitter_ratio_ : default_h;
|
||||
float video_h = charts_floating_ ? avail.y : std::clamp(video_hint, 0.0f, avail.y - 1.0f);
|
||||
if (live) video_h = default_h;
|
||||
|
||||
if (!charts_floating_ && !live) {
|
||||
const float min_h = std::min(video_widget_->sizeHintHeight() + video_padding, avail.y - 1.0f);
|
||||
video_h = video_h < min_h / 2 ? 0.0f : std::max(video_h, min_h);
|
||||
}
|
||||
if (video_h > 0.0f) {
|
||||
ImGui::BeginChild("video", ImVec2(0, video_h), ImGuiChildFlags_Borders);
|
||||
help_overlay_.add(video_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect());
|
||||
video_widget_->draw();
|
||||
ImGui::EndChild();
|
||||
} else {
|
||||
video_widget_->setVisible(false);
|
||||
}
|
||||
if (!charts_floating_) {
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f));
|
||||
ImGui::InvisibleButton("##splitter", ImVec2(-1.0f, ImGui::GetStyle().WindowPadding.x));
|
||||
if (ImGui::IsItemActive() && !live) {
|
||||
|
||||
const float top = ImGui::GetWindowPos().y + ImGui::GetCursorStartPos().y;
|
||||
video_splitter_ratio_ = std::clamp((ImGui::GetMousePos().y - top) / avail.y, 0.0f, 1.0f);
|
||||
}
|
||||
if (ImGui::IsItemHovered() && !live) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
|
||||
|
||||
ImGui::BeginChild("charts", ImVec2(0, 0), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::PopStyleVar();
|
||||
help_overlay_.add(charts_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect());
|
||||
charts_widget_->draw();
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
if (!video_visible_ && floating) video_visible_ = reset_layout_ = true;
|
||||
}
|
||||
|
||||
void MainWindow::draw() {
|
||||
#ifdef __APPLE__
|
||||
full_screen_ = isNativeFullScreen(window_);
|
||||
#endif
|
||||
auto pending = std::move(next_frame_);
|
||||
next_frame_.clear();
|
||||
for (auto &fn : pending) fn();
|
||||
|
||||
if (ImGui::GetTopMostPopupModal() == nullptr) {
|
||||
handleShortcuts();
|
||||
} else {
|
||||
takeKeyEvents();
|
||||
}
|
||||
if (!full_screen_) drawMenuBar();
|
||||
drawDockspace();
|
||||
|
||||
|
||||
if (ImGui::Begin(CENTER_PANEL, nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
|
||||
center_widget_.draw();
|
||||
if (auto *detail = center_widget_.getDetailWidget(); detail && help_overlay_.visible()) {
|
||||
for (const auto &[text, rect] : detail->helpRects()) help_overlay_.add(text, rect);
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
if (messages_widget_ && messages_visible_) drawMessagesPanel();
|
||||
if (video_widget_ && !video_visible_) video_widget_->setVisible(false);
|
||||
if (video_widget_ && video_visible_) drawVideoPanel();
|
||||
if (charts_widget_ && charts_floating_) {
|
||||
bool open = true;
|
||||
ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize, ImGuiCond_Appearing);
|
||||
setNextWindowFloatsOut();
|
||||
if (ImGui::Begin(CHARTS_WINDOW, &open, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) charts_widget_->draw();
|
||||
ImGui::End();
|
||||
if (!open) toggleChartsDocking();
|
||||
}
|
||||
for (auto it = tool_dialogs_.begin(); it != tool_dialogs_.end();) {
|
||||
it = (*it)->draw() ? it + 1 : tool_dialogs_.erase(it);
|
||||
}
|
||||
|
||||
stream_selector_.draw();
|
||||
settings_dialog_.draw();
|
||||
drawWaitDialog();
|
||||
FileDialog::draw();
|
||||
MessageBox::draw();
|
||||
help_overlay_.draw();
|
||||
|
||||
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
|
||||
ImGuiWindow *top = topPopupWindow();
|
||||
if (top != nullptr && !(top->Flags & ImGuiWindowFlags_Modal)) ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true);
|
||||
}
|
||||
}
|
||||
138
iqpilot/tools/cabana/ui/mainwin.h
Normal file
138
iqpilot/tools/cabana/ui/mainwin.h
Normal file
@@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/ui/app.h"
|
||||
#include "tools/cabana/ui/dialogs/settingsdialog.h"
|
||||
#include "tools/cabana/ui/dialogs/streamselector.h"
|
||||
#include "tools/cabana/ui/helpoverlay.h"
|
||||
#include "tools/cabana/ui/tools/tooldialog.h"
|
||||
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||
#include "tools/cabana/ui/widgets/detailwidget.h"
|
||||
#include "tools/cabana/ui/widgets/messageswidget.h"
|
||||
#include "tools/cabana/ui/widgets/videowidget.h"
|
||||
|
||||
struct GLFWwindow;
|
||||
|
||||
class MainWindow {
|
||||
public:
|
||||
MainWindow(GLFWwindow *window, std::unique_ptr<AbstractStream> stream, StreamLoader stream_loader, const std::string &dbc_file);
|
||||
~MainWindow();
|
||||
void draw();
|
||||
void toggleChartsDocking();
|
||||
void close();
|
||||
bool exited() const { return exited_; }
|
||||
void showStatusMessage(const std::string &msg, int timeout_ms = 0);
|
||||
void loadFile(const std::string &fn, SourceSet s = SOURCE_ALL, std::function<void()> then = {});
|
||||
|
||||
void selectAndOpenStream();
|
||||
void openStream(std::unique_ptr<AbstractStream> stream, const std::string &dbc_file = {});
|
||||
void closeStream();
|
||||
void exportToCSV();
|
||||
|
||||
void newFile(SourceSet s = SOURCE_ALL);
|
||||
void openFile(SourceSet s = SOURCE_ALL);
|
||||
void loadDBCFromOpendbc(const std::string &name);
|
||||
void save(std::function<void()> then = {});
|
||||
void saveAs(std::function<void()> then = {});
|
||||
void saveToClipboard();
|
||||
|
||||
private:
|
||||
bool hasStream() const { return dynamic_cast<const DummyStream *>(can) == nullptr; }
|
||||
void releaseStream();
|
||||
void startStream(std::unique_ptr<AbstractStream> stream, const std::string &dbc_file);
|
||||
void loadStartupStream(const std::string &dbc_file);
|
||||
void remindSaveChanges(std::function<void()> then);
|
||||
void closeFile(SourceSet s, std::function<void()> then);
|
||||
void closeFile(DBCFile *dbc_file);
|
||||
void saveFiles(bool as, std::function<void()> then);
|
||||
void saveFile(DBCFile *dbc_file, std::function<void()> then = {});
|
||||
void saveFileAs(DBCFile *dbc_file, std::function<void()> then = {});
|
||||
void saveFileToClipboard(DBCFile *dbc_file);
|
||||
void copyToClipboard(const std::string &text);
|
||||
void loadFingerprints();
|
||||
void loadFromClipboard(SourceSet s = SOURCE_ALL, bool close_all = true);
|
||||
void updateRecentFiles(const std::string &fn);
|
||||
void dbcFileChanged();
|
||||
void updateDownloadProgress(uint64_t cur, uint64_t total, bool success);
|
||||
void openSettings();
|
||||
void findSimilarBits();
|
||||
void findSignal();
|
||||
void toggleHelp();
|
||||
void toggleFullScreen();
|
||||
void updateWindowTitle();
|
||||
void eventsMerged();
|
||||
void saveSessionState();
|
||||
void restoreSessionState();
|
||||
void finishClose();
|
||||
void nextFrame(std::function<void()> fn) { next_frame_.push_back(std::move(fn)); }
|
||||
void createDockWidgets();
|
||||
|
||||
void handleShortcuts();
|
||||
void drawMenuBar();
|
||||
void drawFileMenu();
|
||||
void drawManageDBCsMenu();
|
||||
void drawRecentFilesMenu();
|
||||
void drawDockspace();
|
||||
void drawMessagesPanel();
|
||||
void drawVideoPanel();
|
||||
void drawStatusBar();
|
||||
void drawWaitDialog();
|
||||
|
||||
GLFWwindow *window_;
|
||||
std::unique_ptr<AbstractStream> startup_stream_;
|
||||
StreamLoader startup_loader_;
|
||||
std::unique_ptr<AbstractStream> stream_;
|
||||
DummyStream dummy_;
|
||||
std::unique_ptr<MessagesWidget> messages_widget_;
|
||||
CenterWidget center_widget_;
|
||||
std::unique_ptr<VideoWidget> video_widget_;
|
||||
std::unique_ptr<ChartsWidget> charts_widget_;
|
||||
StreamSelector stream_selector_;
|
||||
SettingsDialog settings_dialog_;
|
||||
HelpOverlay help_overlay_;
|
||||
std::unordered_map<std::string, std::string> fingerprint_to_dbc_;
|
||||
std::vector<std::string> opendbc_names_;
|
||||
enum { MAX_RECENT_FILES = 15 };
|
||||
std::string car_fingerprint_;
|
||||
std::string video_dock_title_;
|
||||
bool messages_visible_ = true;
|
||||
bool video_visible_ = true;
|
||||
bool reset_layout_ = false;
|
||||
bool full_screen_ = false;
|
||||
#ifndef __APPLE__
|
||||
int windowed_rect_[4] = {0, 0, 1600, 900};
|
||||
#endif
|
||||
bool charts_floating_ = false;
|
||||
float video_splitter_ratio_ = -1.0f;
|
||||
std::vector<std::unique_ptr<ToolDialog>> tool_dialogs_;
|
||||
bool closing_ = false;
|
||||
bool exited_ = false;
|
||||
bool window_modified_ = false;
|
||||
struct StatusBar {
|
||||
std::string message;
|
||||
double message_until = 0;
|
||||
bool progress_visible = false;
|
||||
float progress_value = 0;
|
||||
std::string progress_text;
|
||||
} status_bar_;
|
||||
|
||||
struct WaitDialog {
|
||||
bool open = false;
|
||||
double show_at = 0;
|
||||
std::string text;
|
||||
int value = 0;
|
||||
Connection connection;
|
||||
} wait_dlg_;
|
||||
std::vector<std::function<void()>> next_frame_;
|
||||
Connections connections_;
|
||||
Connections stream_connections_;
|
||||
Connections widget_connections_;
|
||||
};
|
||||
280
iqpilot/tools/cabana/ui/style.cc
Normal file
280
iqpilot/tools/cabana/ui/style.cc
Normal file
@@ -0,0 +1,280 @@
|
||||
#include "tools/cabana/ui/app.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
|
||||
#include "implot.h"
|
||||
#include "tools/cabana/core/settings.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
bool g_dark = false;
|
||||
ImFont *g_ui_font = nullptr;
|
||||
ImFont *g_bold_font = nullptr;
|
||||
ImFont *g_mono_font = nullptr;
|
||||
ImFont *g_large_font = nullptr;
|
||||
|
||||
void addIconFont(float size, ImFont *base) {
|
||||
ImFontConfig cfg;
|
||||
cfg.MergeMode = base != nullptr;
|
||||
cfg.GlyphMinAdvanceX = size;
|
||||
if (base != nullptr) {
|
||||
ImFontBaked *baked = base->GetFontBaked(size);
|
||||
const float center = baked != nullptr ? (baked->Ascent + baked->Descent) * 0.5f : size * 0.5f;
|
||||
cfg.GlyphOffset.y = std::round(size * 0.5f - center);
|
||||
}
|
||||
static const ImWchar ranges[] = {0xF000, 0xF8FF, 0};
|
||||
ImGui::GetIO().Fonts->AddFontFromFileTTF(BOOTSTRAP_ICONS_TTF, size, &cfg, ranges);
|
||||
}
|
||||
|
||||
ImFont *addFont(const fs::path &path, float size) {
|
||||
ImFontConfig cfg;
|
||||
cfg.OversampleH = 2;
|
||||
cfg.OversampleV = 2;
|
||||
ImFont *font = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size, &cfg);
|
||||
if (font != nullptr) addIconFont(size, font);
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
void loadFonts() {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
const fs::path fonts = fs::path(CABANA_FONTS_DIR);
|
||||
g_ui_font = addFont(fonts / "Inter-Regular.ttf", 16.0f);
|
||||
g_bold_font = addFont(fonts / "Inter-SemiBold.ttf", 16.0f);
|
||||
g_mono_font = addFont(fonts / "JetBrainsMono-Medium.ttf", 15.0f);
|
||||
g_large_font = addFont(fonts / "Inter-Bold.ttf", 50.0f);
|
||||
if (g_ui_font != nullptr) io.FontDefault = g_ui_font;
|
||||
if (g_bold_font == nullptr) g_bold_font = g_ui_font;
|
||||
if (g_mono_font == nullptr) g_mono_font = g_ui_font;
|
||||
if (g_large_font == nullptr) g_large_font = g_bold_font;
|
||||
}
|
||||
|
||||
void applyTheme(int theme) {
|
||||
const bool dark = theme == DARK_THEME;
|
||||
g_dark = dark;
|
||||
if (dark) {
|
||||
ImGui::StyleColorsDark();
|
||||
ImPlot::StyleColorsDark();
|
||||
} else {
|
||||
ImGui::StyleColorsLight();
|
||||
ImPlot::StyleColorsLight();
|
||||
}
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowRounding = 0.0f;
|
||||
style.ChildRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.FrameRounding = 2.0f;
|
||||
style.GrabRounding = 2.0f;
|
||||
style.ScrollbarRounding = 2.0f;
|
||||
style.TabRounding = 2.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
style.TabBorderSize = 1.0f;
|
||||
style.WindowPadding = ImVec2(8.0f, 7.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 3.0f);
|
||||
style.ItemSpacing = ImVec2(8.0f, 5.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 13.0f;
|
||||
|
||||
auto c = [](const CabanaColor &col, float a = 1.0f) { return colorRgb(col.r, col.g, col.b, a); };
|
||||
ImVec4 *colors = style.Colors;
|
||||
if (dark) {
|
||||
|
||||
|
||||
const ImVec4 highlight = c(DarkTheme::highlight);
|
||||
const ImVec4 outline = colorRgb(0x5a, 0x5d, 0x60);
|
||||
colors[ImGuiCol_WindowBg] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_ChildBg] = c(DarkTheme::base);
|
||||
colors[ImGuiCol_PopupBg] = c(DarkTheme::base);
|
||||
colors[ImGuiCol_MenuBarBg] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_DockingEmptyBg] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_Text] = colorRgb(0xdc, 0xdc, 0xdc);
|
||||
colors[ImGuiCol_TextDisabled] = colorRgb(0x8c, 0x8c, 0x8c);
|
||||
colors[ImGuiCol_Border] = outline;
|
||||
colors[ImGuiCol_BorderShadow] = colorRgb(0, 0, 0, 0.0f);
|
||||
colors[ImGuiCol_FrameBg] = colorRgb(0x2e, 0x30, 0x32);
|
||||
colors[ImGuiCol_FrameBgHovered] = colorRgb(0x3a, 0x3d, 0x40);
|
||||
colors[ImGuiCol_FrameBgActive] = colorRgb(0x45, 0x48, 0x4b);
|
||||
colors[ImGuiCol_Button] = c(DarkTheme::button);
|
||||
colors[ImGuiCol_ButtonHovered] = colorRgb(0x52, 0x56, 0x59);
|
||||
colors[ImGuiCol_ButtonActive] = colorRgb(0x2b, 0x2d, 0x30);
|
||||
colors[ImGuiCol_Header] = highlight;
|
||||
colors[ImGuiCol_HeaderHovered] = c(DarkTheme::highlight, 0.8f);
|
||||
colors[ImGuiCol_HeaderActive] = highlight;
|
||||
colors[ImGuiCol_CheckMark] = c(DarkTheme::bright_text);
|
||||
colors[ImGuiCol_SliderGrab] = colorRgb(0x8f, 0x92, 0x95);
|
||||
colors[ImGuiCol_SliderGrabActive] = colorRgb(0xa8, 0xab, 0xae);
|
||||
colors[ImGuiCol_ScrollbarBg] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_ScrollbarGrab] = colorRgb(0x70, 0x73, 0x76);
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = colorRgb(0x85, 0x88, 0x8b);
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = c(DarkTheme::light);
|
||||
colors[ImGuiCol_Separator] = outline;
|
||||
colors[ImGuiCol_SeparatorHovered] = c(DarkTheme::highlight, 0.6f);
|
||||
colors[ImGuiCol_SeparatorActive] = highlight;
|
||||
colors[ImGuiCol_ResizeGrip] = colorRgb(0, 0, 0, 0.0f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = c(DarkTheme::highlight, 0.6f);
|
||||
colors[ImGuiCol_ResizeGripActive] = highlight;
|
||||
colors[ImGuiCol_Tab] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_TabHovered] = colorRgb(0x4b, 0x4e, 0x52);
|
||||
colors[ImGuiCol_TabSelected] = c(DarkTheme::base);
|
||||
colors[ImGuiCol_TabSelectedOverline] = highlight;
|
||||
colors[ImGuiCol_TabDimmed] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_TabDimmedSelected] = c(DarkTheme::base);
|
||||
colors[ImGuiCol_TabDimmedSelectedOverline] = colorRgb(0, 0, 0, 0.0f);
|
||||
colors[ImGuiCol_TitleBg] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_TitleBgActive] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_TitleBgCollapsed] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_TableHeaderBg] = c(DarkTheme::window);
|
||||
colors[ImGuiCol_TableBorderStrong] = outline;
|
||||
colors[ImGuiCol_TableBorderLight] = colorRgb(0x23, 0x26, 0x28);
|
||||
colors[ImGuiCol_TableRowBg] = colorRgb(0, 0, 0, 0.0f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = colorRgb(0xff, 0xff, 0xff, 0.06f);
|
||||
colors[ImGuiCol_TextSelectedBg] = c(DarkTheme::highlight, 0.6f);
|
||||
colors[ImGuiCol_DockingPreview] = c(DarkTheme::highlight, 0.5f);
|
||||
colors[ImGuiCol_NavCursor] = highlight;
|
||||
colors[ImGuiCol_PlotLines] = c(DarkTheme::text);
|
||||
colors[ImGuiCol_PlotHistogram] = highlight;
|
||||
colors[ImGuiCol_DragDropTarget] = highlight;
|
||||
} else {
|
||||
const ImVec4 window = colorRgb(0xef, 0xef, 0xef);
|
||||
const ImVec4 base = colorRgb(0xff, 0xff, 0xff);
|
||||
const ImVec4 outline = colorRgb(0xb9, 0xb9, 0xb9);
|
||||
const ImVec4 highlight = colorRgb(0x30, 0x8c, 0xc6);
|
||||
colors[ImGuiCol_WindowBg] = window;
|
||||
colors[ImGuiCol_ChildBg] = base;
|
||||
colors[ImGuiCol_PopupBg] = colorRgb(0xfb, 0xfb, 0xfb);
|
||||
colors[ImGuiCol_MenuBarBg] = window;
|
||||
colors[ImGuiCol_DockingEmptyBg] = window;
|
||||
colors[ImGuiCol_Text] = colorRgb(0x00, 0x00, 0x00);
|
||||
colors[ImGuiCol_TextDisabled] = colorRgb(0xbe, 0xbe, 0xbe);
|
||||
colors[ImGuiCol_Border] = outline;
|
||||
colors[ImGuiCol_BorderShadow] = colorRgb(0, 0, 0, 0.0f);
|
||||
colors[ImGuiCol_FrameBg] = base;
|
||||
colors[ImGuiCol_FrameBgHovered] = colorRgb(0xf7, 0xf7, 0xf7);
|
||||
colors[ImGuiCol_FrameBgActive] = colorRgb(0xef, 0xef, 0xef);
|
||||
colors[ImGuiCol_Button] = colorRgb(0xf3, 0xf3, 0xf3);
|
||||
colors[ImGuiCol_ButtonHovered] = colorRgb(0xf9, 0xf9, 0xf9);
|
||||
colors[ImGuiCol_ButtonActive] = colorRgb(0xdc, 0xdc, 0xdc);
|
||||
colors[ImGuiCol_Header] = highlight;
|
||||
colors[ImGuiCol_HeaderHovered] = colorRgb(0x30, 0x8c, 0xc6, 0.8f);
|
||||
colors[ImGuiCol_HeaderActive] = highlight;
|
||||
colors[ImGuiCol_CheckMark] = colorRgb(0x3b, 0x3b, 0x3b);
|
||||
colors[ImGuiCol_SliderGrab] = colorRgb(0xd8, 0xd8, 0xd8);
|
||||
colors[ImGuiCol_SliderGrabActive] = colorRgb(0xc4, 0xc4, 0xc4);
|
||||
colors[ImGuiCol_ScrollbarBg] = window;
|
||||
colors[ImGuiCol_ScrollbarGrab] = colorRgb(0xc8, 0xc8, 0xc8);
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = colorRgb(0xb4, 0xb4, 0xb4);
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = colorRgb(0xa0, 0xa0, 0xa0);
|
||||
colors[ImGuiCol_Separator] = outline;
|
||||
colors[ImGuiCol_SeparatorHovered] = colorRgb(0x30, 0x8c, 0xc6, 0.6f);
|
||||
colors[ImGuiCol_SeparatorActive] = highlight;
|
||||
colors[ImGuiCol_ResizeGrip] = colorRgb(0, 0, 0, 0.0f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = colorRgb(0x30, 0x8c, 0xc6, 0.6f);
|
||||
colors[ImGuiCol_ResizeGripActive] = highlight;
|
||||
colors[ImGuiCol_Tab] = colorRgb(0xe2, 0xe2, 0xe2);
|
||||
colors[ImGuiCol_TabHovered] = colorRgb(0xf5, 0xf5, 0xf5);
|
||||
colors[ImGuiCol_TabSelected] = base;
|
||||
colors[ImGuiCol_TabSelectedOverline] = highlight;
|
||||
colors[ImGuiCol_TabDimmed] = colorRgb(0xe2, 0xe2, 0xe2);
|
||||
colors[ImGuiCol_TabDimmedSelected] = base;
|
||||
colors[ImGuiCol_TabDimmedSelectedOverline] = colorRgb(0, 0, 0, 0.0f);
|
||||
colors[ImGuiCol_TitleBg] = window;
|
||||
colors[ImGuiCol_TitleBgActive] = window;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = window;
|
||||
colors[ImGuiCol_TableHeaderBg] = colorRgb(0xf2, 0xf2, 0xf2);
|
||||
colors[ImGuiCol_TableBorderStrong] = outline;
|
||||
colors[ImGuiCol_TableBorderLight] = colorRgb(0xd8, 0xd8, 0xd8);
|
||||
colors[ImGuiCol_TableRowBg] = colorRgb(0, 0, 0, 0.0f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = colorRgb(0, 0, 0, 0.03f);
|
||||
colors[ImGuiCol_TextSelectedBg] = colorRgb(0x30, 0x8c, 0xc6, 0.35f);
|
||||
colors[ImGuiCol_DockingPreview] = colorRgb(0x30, 0x8c, 0xc6, 0.5f);
|
||||
colors[ImGuiCol_NavCursor] = highlight;
|
||||
colors[ImGuiCol_PlotLines] = colorRgb(0x3b, 0x3b, 0x3b);
|
||||
colors[ImGuiCol_PlotHistogram] = highlight;
|
||||
colors[ImGuiCol_DragDropTarget] = highlight;
|
||||
}
|
||||
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0, 0, 0, 0);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
bool isDarkTheme() { return g_dark; }
|
||||
|
||||
ImU32 highlightedTextColor() {
|
||||
return g_dark ? IM_COL32(DarkTheme::window_text.r, DarkTheme::window_text.g, DarkTheme::window_text.b, 255)
|
||||
: IM_COL32(255, 255, 255, 255);
|
||||
}
|
||||
|
||||
ImU32 paletteBrightText() {
|
||||
return g_dark ? IM_COL32(DarkTheme::bright_text.r, DarkTheme::bright_text.g, DarkTheme::bright_text.b, 255)
|
||||
: IM_COL32(255, 255, 255, 255);
|
||||
}
|
||||
|
||||
void drawSliderHandle(ImDrawList *p, const ImRect &r) {
|
||||
const bool dark = isDarkTheme();
|
||||
const ImU32 top = dark ? IM_COL32(0x3e, 0x41, 0x43, 255) : IM_COL32(255, 255, 255, 255);
|
||||
const ImU32 bottom = dark ? IM_COL32(0x39, 0x3c, 0x3e, 255) : IM_COL32(0xf0, 0xf0, 0xf0, 255);
|
||||
|
||||
const ImU32 outline_top = dark ? IM_COL32(0xa3, 0xa3, 0xa3, 255) : IM_COL32(0xab, 0xab, 0xab, 255);
|
||||
const ImU32 outline_bottom = dark ? IM_COL32(0x9c, 0x9c, 0x9c, 255) : IM_COL32(0xa4, 0xa4, 0xa4, 255);
|
||||
p->AddRectFilled(r.Min, r.Max, top, 2.0f);
|
||||
p->AddRectFilled(ImVec2(r.Min.x, r.GetCenter().y), r.Max, bottom, 2.0f, ImDrawFlags_RoundCornersBottom);
|
||||
p->AddRect(r.Min, r.Max, outline_bottom, 2.0f, 0, 1.0f);
|
||||
|
||||
const float c = 2.0f;
|
||||
p->AddRectFilled(ImVec2(r.Min.x + c, r.Min.y), ImVec2(r.Max.x - c, r.Min.y + 1.0f), outline_top);
|
||||
p->AddRectFilled(ImVec2(r.Min.x, r.Min.y + c), ImVec2(r.Min.x + 1.0f, r.Max.y - c), outline_top);
|
||||
p->AddRectFilled(ImVec2(r.Min.x + c, r.Max.y - 1.0f), ImVec2(r.Max.x - c, r.Max.y), outline_bottom);
|
||||
p->AddRectFilled(ImVec2(r.Max.x - 1.0f, r.Min.y + c), ImVec2(r.Max.x, r.Max.y - c), outline_bottom);
|
||||
}
|
||||
|
||||
bool fusionSliderInt(const char *label, int *v, int min, int max, float width) {
|
||||
|
||||
const ImU32 groove_col = isDarkTheme() ? IM_COL32(0x2a, 0x2c, 0x2e, 255) : IM_COL32(0xc4, 0xc4, 0xc4, 255);
|
||||
const ImU32 fill_col = ImGui::GetColorU32(ImGuiCol_Header);
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32_BLACK_TRANS);
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, IM_COL32_BLACK_TRANS);
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, IM_COL32_BLACK_TRANS);
|
||||
ImGui::PushStyleColor(ImGuiCol_SliderGrab, IM_COL32_BLACK_TRANS);
|
||||
ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, IM_COL32_BLACK_TRANS);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
ImGui::SetNextItemWidth(width);
|
||||
bool changed = ImGui::SliderInt(label, v, min, max, "", ImGuiSliderFlags_NoInput);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(5);
|
||||
|
||||
const ImVec2 bb_min = ImGui::GetItemRectMin(), bb_max = ImGui::GetItemRectMax();
|
||||
const float cy = (bb_min.y + bb_max.y) * 0.5f;
|
||||
const float groove_h = SLIDER_THICKNESS * 0.5f;
|
||||
const float handle_h = std::min(SLIDER_THICKNESS, bb_max.y - bb_min.y);
|
||||
const float x0 = bb_min.x + SLIDER_LENGTH * 0.5f, x1 = bb_max.x - SLIDER_LENGTH * 0.5f;
|
||||
const float t = max > min ? (float)(*v - min) / (float)(max - min) : 0.0f;
|
||||
const float hx = x0 + (x1 - x0) * t;
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
const float groove_y0 = cy - groove_h * 0.5f, groove_y1 = cy + groove_h * 0.5f;
|
||||
dl->AddRectFilled(ImVec2(bb_min.x, groove_y0), ImVec2(bb_max.x, groove_y1), groove_col, groove_h * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(bb_min.x, groove_y0), ImVec2(hx, groove_y1), fill_col, groove_h * 0.5f);
|
||||
drawSliderHandle(dl, ImRect(ImVec2(hx - SLIDER_LENGTH * 0.5f, cy - handle_h * 0.5f),
|
||||
ImVec2(hx + SLIDER_LENGTH * 0.5f, cy + handle_h * 0.5f)));
|
||||
return changed;
|
||||
}
|
||||
|
||||
ImFont *boldFont() { return g_bold_font; }
|
||||
ImFont *monoFont() { return g_mono_font; }
|
||||
|
||||
void pushMonoFont(float size) {
|
||||
if (!g_mono_font) return;
|
||||
size > 0.0f ? ImGui::PushFont(g_mono_font, size) : ImGui::PushFont(g_mono_font);
|
||||
}
|
||||
void popMonoFont() { if (g_mono_font) ImGui::PopFont(); }
|
||||
void pushBoldFont() { if (g_bold_font) ImGui::PushFont(g_bold_font); }
|
||||
void popBoldFont() { if (g_bold_font) ImGui::PopFont(); }
|
||||
void pushLargeFont() { if (g_large_font) ImGui::PushFont(g_large_font); }
|
||||
void popLargeFont() { if (g_large_font) ImGui::PopFont(); }
|
||||
81
iqpilot/tools/cabana/ui/threadpool.h
Normal file
81
iqpilot/tools/cabana/ui/threadpool.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
|
||||
|
||||
class ThreadPool {
|
||||
public:
|
||||
static ThreadPool &instance() {
|
||||
static ThreadPool pool(std::clamp(std::thread::hardware_concurrency(), 2u, 4u));
|
||||
return pool;
|
||||
}
|
||||
|
||||
std::future<void> run(std::function<void()> fn) {
|
||||
auto task = std::make_shared<std::packaged_task<void()>>(std::move(fn));
|
||||
std::future<void> future = task->get_future();
|
||||
{
|
||||
std::lock_guard lk(mutex_);
|
||||
tasks_.push([task]() { (*task)(); });
|
||||
}
|
||||
cv_.notify_one();
|
||||
return future;
|
||||
}
|
||||
|
||||
~ThreadPool() {
|
||||
{
|
||||
std::lock_guard lk(mutex_);
|
||||
stop_ = true;
|
||||
}
|
||||
cv_.notify_all();
|
||||
for (auto &t : threads_) t.join();
|
||||
}
|
||||
|
||||
private:
|
||||
explicit ThreadPool(unsigned n) {
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
threads_.emplace_back([this]() {
|
||||
for (;;) {
|
||||
std::function<void()> task;
|
||||
{
|
||||
std::unique_lock lk(mutex_);
|
||||
cv_.wait(lk, [this]() { return stop_ || !tasks_.empty(); });
|
||||
if (stop_ && tasks_.empty()) return;
|
||||
task = std::move(tasks_.front());
|
||||
tasks_.pop();
|
||||
}
|
||||
task();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::thread> threads_;
|
||||
std::queue<std::function<void()>> tasks_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
bool stop_ = false;
|
||||
};
|
||||
|
||||
|
||||
|
||||
inline void parallelFor(size_t n, const std::function<void(size_t begin, size_t end)> &fn) {
|
||||
const size_t chunks = std::clamp<size_t>(std::thread::hardware_concurrency(), 2, 4) + 1;
|
||||
const size_t chunk = (n + chunks - 1) / chunks;
|
||||
if (chunk == 0) return;
|
||||
std::vector<std::future<void>> futures;
|
||||
size_t begin = chunk;
|
||||
for (; begin < n; begin += chunk) {
|
||||
futures.push_back(ThreadPool::instance().run([&fn, begin, end = std::min(begin + chunk, n)]() { fn(begin, end); }));
|
||||
}
|
||||
fn(0, std::min(chunk, n));
|
||||
for (auto &f : futures) f.get();
|
||||
}
|
||||
325
iqpilot/tools/cabana/ui/tools/findsignal.cc
Normal file
325
iqpilot/tools/cabana/ui/tools/findsignal.cc
Normal file
@@ -0,0 +1,325 @@
|
||||
#include "tools/cabana/ui/tools/findsignal.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/ui/threadpool.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
constexpr int MAX_ROWS = 300;
|
||||
}
|
||||
|
||||
void SignalSearch::search(const std::function<bool(double)> &cmp) {
|
||||
const auto prev_sigs = !histories.empty() ? histories.back() : initial_signals;
|
||||
filtered_signals.clear();
|
||||
filtered_signals.reserve(prev_sigs.size());
|
||||
|
||||
std::mutex lock;
|
||||
parallelFor(prev_sigs.size(), [&](size_t begin, size_t end) {
|
||||
for (size_t i = begin; i < end; ++i) {
|
||||
const auto &s = prev_sigs[i];
|
||||
const auto &events = can->events(s.id);
|
||||
auto first = std::upper_bound(events.cbegin(), events.cend(), s.mono_time, CompareCanEvent());
|
||||
auto last = events.cend();
|
||||
if (last_time < std::numeric_limits<uint64_t>::max()) {
|
||||
last = std::upper_bound(events.cbegin(), events.cend(), last_time, CompareCanEvent());
|
||||
}
|
||||
|
||||
auto it = std::find_if(first, last, [&](const CanEvent *e) { return cmp(get_raw_value(e->dat, e->size, s.sig)); });
|
||||
if (it != last) {
|
||||
auto values = s.values;
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "(%.3f, %g)", can->toSeconds((*it)->mono_time), get_raw_value((*it)->dat, (*it)->size, s.sig));
|
||||
values.push_back(buf);
|
||||
std::lock_guard lk(lock);
|
||||
filtered_signals.push_back({.id = s.id, .mono_time = (*it)->mono_time, .sig = s.sig, .values = values});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
histories.push_back(filtered_signals);
|
||||
}
|
||||
|
||||
void SignalSearch::undo() {
|
||||
if (!histories.empty()) {
|
||||
histories.pop_back();
|
||||
filtered_signals.clear();
|
||||
if (!histories.empty()) filtered_signals = histories.back();
|
||||
}
|
||||
}
|
||||
|
||||
void SignalSearch::reset() {
|
||||
histories.clear();
|
||||
filtered_signals.clear();
|
||||
initial_signals.clear();
|
||||
}
|
||||
|
||||
FindSignalDlg::FindSignalDlg() {
|
||||
setTitle("Find Signal");
|
||||
}
|
||||
|
||||
FindSignalDlg::~FindSignalDlg() {
|
||||
if (search_future_.valid()) search_future_.wait();
|
||||
}
|
||||
|
||||
bool FindSignalDlg::draw() {
|
||||
if (search_future_.valid() && search_future_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
|
||||
search_future_.get();
|
||||
searched_ = true;
|
||||
}
|
||||
searching_ = search_future_.valid();
|
||||
if (begin(ImVec2(900, 650))) {
|
||||
float group_w = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) / 2;
|
||||
ImGui::BeginChild("Messages", ImVec2(group_w, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY);
|
||||
drawMessageGroup();
|
||||
ImGui::EndChild();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginChild("Signal", ImVec2(group_w, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY);
|
||||
drawPropertiesGroup();
|
||||
ImGui::EndChild();
|
||||
float footer = searched_ ? ImGui::GetTextLineHeightWithSpacing() : 0;
|
||||
ImGui::BeginChild("Find signal", ImVec2(0, -footer), ImGuiChildFlags_Borders);
|
||||
drawFindGroup();
|
||||
ImGui::EndChild();
|
||||
if (searched_) {
|
||||
ImGui::Text("%zu matches. right click on an item to create signal. double click to open message",
|
||||
search_.filtered_signals.size());
|
||||
}
|
||||
}
|
||||
return end();
|
||||
}
|
||||
|
||||
void FindSignalDlg::drawMessageGroup() {
|
||||
ImGui::BeginDisabled(searching_ || !search_.histories.empty());
|
||||
ImGui::TextUnformatted("Messages");
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Bus");
|
||||
ImGui::SameLine(80);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
inputText("##bus", &bus_, "comma-separated values. Leave blank for all");
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Address");
|
||||
ImGui::SameLine(80);
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
inputText("##address", &address_, "comma-separated hex values. Leave blank for all");
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Time");
|
||||
ImGui::SameLine(80);
|
||||
ImGui::SetNextItemWidth(70);
|
||||
validatedText("##first_time", &first_time_, validateDouble);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("-");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(70);
|
||||
validatedText("##last_time", &last_time_, validateDouble);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("seconds");
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
void FindSignalDlg::drawPropertiesGroup() {
|
||||
ImGui::BeginDisabled(searching_ || !search_.histories.empty());
|
||||
ImGui::TextUnformatted("Signal");
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Size");
|
||||
ImGui::SameLine(80);
|
||||
ImGui::SetNextItemWidth(70);
|
||||
if (ImGui::InputInt("##min_size", &min_size_, 1, 10)) min_size_ = std::clamp(min_size_, 1, 64);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("-");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(70);
|
||||
if (ImGui::InputInt("##max_size", &max_size_, 1, 10)) max_size_ = std::clamp(max_size_, 1, 64);
|
||||
ImGui::SameLine();
|
||||
checkBox("Little endian", &little_endian_);
|
||||
ImGui::SameLine();
|
||||
checkBox("Signed", &is_signed_);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Factor");
|
||||
ImGui::SameLine(80);
|
||||
ImGui::SetNextItemWidth(100);
|
||||
validatedText("##factor", &factor_, validateDouble);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Offset");
|
||||
ImGui::SameLine(80);
|
||||
ImGui::SetNextItemWidth(100);
|
||||
validatedText("##offset", &offset_, validateDouble);
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
void FindSignalDlg::drawFindGroup() {
|
||||
static const char *compare_items[] = {"=", ">", ">=", "!=", "<", "<=", "between"};
|
||||
const int compare_count = IM_ARRAYSIZE(compare_items);
|
||||
ImGui::TextUnformatted("Find signal");
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Value");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(90);
|
||||
ImGui::Combo("##compare", &compare_, compare_items, compare_count);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80);
|
||||
if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere();
|
||||
validatedText("##value1", &value1_, validateDouble);
|
||||
if (compare_ == compare_count - 1) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("-");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80);
|
||||
validatedText("##value2", &value2_, validateDouble);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
const bool first = !searching_ && search_.histories.empty();
|
||||
ImGui::BeginDisabled(searching_ || search_.histories.size() <= 1);
|
||||
if (ImGui::Button("Undo prev find")) {
|
||||
search_.undo();
|
||||
searched_ = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(searching_ || (search_.filtered_signals.empty() && !first));
|
||||
if (ImGui::Button(searching_ ? "Finding ...." : (first ? "Find" : "Find Next"))) search();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(searching_ || first);
|
||||
if (ImGui::Button("Reset")) {
|
||||
search_.reset();
|
||||
searched_ = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if (searching_) {
|
||||
ImGui::BeginChild("view", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
||||
ImGui::EndChild();
|
||||
} else {
|
||||
drawTable();
|
||||
}
|
||||
}
|
||||
|
||||
void FindSignalDlg::drawTable() {
|
||||
static const char *titles[] = {"Id", "Start Bit, size", "(time, value)"};
|
||||
const int columns = IM_ARRAYSIZE(titles);
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings;
|
||||
if (!ImGui::BeginTable("view", columns + 1, flags, ImVec2(0, 0))) return;
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
const int rows = std::min<int>(search_.filtered_signals.size(), MAX_ROWS);
|
||||
|
||||
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed | (rows ? 0 : ImGuiTableColumnFlags_Disabled), 40.0f);
|
||||
for (int c = 0; c < columns; ++c) {
|
||||
auto column_flags = c == columns - 1 ? ImGuiTableColumnFlags_WidthStretch : ImGuiTableColumnFlags_WidthFixed;
|
||||
ImGui::TableSetupColumn(titles[c], column_flags, c == 0 ? 80.0f : 120.0f);
|
||||
}
|
||||
tableHeadersRow();
|
||||
for (int row = 0; row < rows; ++row) {
|
||||
const auto &s = search_.filtered_signals[row];
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::PushID(row);
|
||||
if (ImGui::Selectable(std::to_string(row + 1).c_str(), false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) openMessage(s.id);
|
||||
}
|
||||
drawContextMenu(row);
|
||||
ImGui::PopID();
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::TextUnformatted(s.id.toString().c_str());
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::Text("%d, %d", s.sig.start_bit, s.sig.size);
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
std::string values;
|
||||
for (size_t i = 0; i < s.values.size(); ++i) {
|
||||
if (i) values += " ";
|
||||
values += s.values[i];
|
||||
}
|
||||
ImGui::TextUnformatted(values.c_str());
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
void FindSignalDlg::search() {
|
||||
if (search_.histories.empty()) {
|
||||
setInitialSignals();
|
||||
}
|
||||
auto v1 = utils::toDouble(value1_);
|
||||
auto v2 = utils::toDouble(value2_);
|
||||
std::function<bool(double)> cmp = nullptr;
|
||||
switch (compare_) {
|
||||
case 0: cmp = [v1](double v) { return v == v1;}; break;
|
||||
case 1: cmp = [v1](double v) { return v > v1;}; break;
|
||||
case 2: cmp = [v1](double v) { return v >= v1;}; break;
|
||||
case 3: cmp = [v1](double v) { return v != v1;}; break;
|
||||
case 4: cmp = [v1](double v) { return v < v1;}; break;
|
||||
case 5: cmp = [v1](double v) { return v <= v1;}; break;
|
||||
case 6: cmp = [v1, v2](double v) { return v >= v1 && v <= v2;}; break;
|
||||
}
|
||||
searched_ = false;
|
||||
|
||||
search_future_ = std::async(std::launch::async, [this, cmp = std::move(cmp)]() { search_.search(cmp); });
|
||||
searching_ = true;
|
||||
}
|
||||
|
||||
void FindSignalDlg::setInitialSignals() {
|
||||
std::set<unsigned short> buses;
|
||||
for (auto bus : utils::split(utils::trimmed(bus_), ',')) {
|
||||
bus = utils::trimmed(bus);
|
||||
if (!bus.empty()) buses.insert((unsigned short)utils::toULong(bus));
|
||||
}
|
||||
|
||||
std::set<uint32_t> addresses;
|
||||
for (auto addr : utils::split(utils::trimmed(address_), ',')) {
|
||||
addr = utils::trimmed(addr);
|
||||
if (!addr.empty()) addresses.insert(utils::toULong(addr, 16));
|
||||
}
|
||||
|
||||
cabana::Signal sig{};
|
||||
sig.is_little_endian = little_endian_;
|
||||
sig.is_signed = is_signed_;
|
||||
sig.factor = utils::toDouble(factor_);
|
||||
sig.offset = utils::toDouble(offset_);
|
||||
|
||||
double first_time_val = utils::toDouble(first_time_);
|
||||
double last_time_val = utils::toDouble(last_time_);
|
||||
auto [first_sec, last_sec] = std::minmax(first_time_val, last_time_val);
|
||||
uint64_t first_time = can->toMonoTime(first_sec);
|
||||
search_.last_time = std::numeric_limits<uint64_t>::max();
|
||||
if (last_sec > 0) {
|
||||
search_.last_time = can->toMonoTime(last_sec);
|
||||
}
|
||||
search_.initial_signals.clear();
|
||||
|
||||
for (const auto &[id, m] : can->lastMessages()) {
|
||||
if ((buses.empty() || buses.count(id.source)) && (addresses.empty() || addresses.count(id.address))) {
|
||||
const auto &events = can->events(id);
|
||||
auto e = std::lower_bound(events.cbegin(), events.cend(), first_time, CompareCanEvent());
|
||||
if (e != events.cend()) {
|
||||
const int total_size = m.dat.size() * 8;
|
||||
for (int size = min_size_; size <= max_size_; ++size) {
|
||||
for (int start = 0; start <= total_size - size; ++start) {
|
||||
SignalSearch::SearchSignal s{.id = id, .mono_time = first_time, .sig = sig};
|
||||
s.sig.start_bit = start;
|
||||
s.sig.size = size;
|
||||
updateMsbLsb(s.sig);
|
||||
s.value = get_raw_value((*e)->dat, (*e)->size, s.sig);
|
||||
search_.initial_signals.push_back(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FindSignalDlg::drawContextMenu(int row) {
|
||||
if (ImGui::BeginPopupContextItem("menu")) {
|
||||
if (ImGui::MenuItem("Create Signal")) {
|
||||
auto &s = search_.filtered_signals[row];
|
||||
UndoStack::instance()->push(new AddSigCommand(s.id, s.sig));
|
||||
openMessage(s.id);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
59
iqpilot/tools/cabana/ui/tools/findsignal.h
Normal file
59
iqpilot/tools/cabana/ui/tools/findsignal.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/tools/tooldialog.h"
|
||||
|
||||
struct SignalSearch {
|
||||
struct SearchSignal {
|
||||
MessageId id = {};
|
||||
uint64_t mono_time = 0;
|
||||
cabana::Signal sig = {};
|
||||
double value = 0.;
|
||||
std::vector<std::string> values;
|
||||
};
|
||||
|
||||
void search(const std::function<bool(double)> &cmp);
|
||||
void reset();
|
||||
void undo();
|
||||
|
||||
std::vector<SearchSignal> filtered_signals;
|
||||
std::vector<SearchSignal> initial_signals;
|
||||
std::vector<std::vector<SearchSignal>> histories;
|
||||
uint64_t last_time = std::numeric_limits<uint64_t>::max();
|
||||
};
|
||||
|
||||
class FindSignalDlg : public ToolDialog {
|
||||
public:
|
||||
FindSignalDlg();
|
||||
~FindSignalDlg() override;
|
||||
bool draw() override;
|
||||
|
||||
Observable<const MessageId &> openMessage;
|
||||
|
||||
private:
|
||||
void search();
|
||||
void setInitialSignals();
|
||||
void drawContextMenu(int row);
|
||||
void drawMessageGroup();
|
||||
void drawPropertiesGroup();
|
||||
void drawFindGroup();
|
||||
void drawTable();
|
||||
|
||||
std::string value1_, value2_, factor_ = "1.0", offset_ = "0.0";
|
||||
std::string bus_, address_, first_time_ = "0", last_time_ = "MAX";
|
||||
int compare_ = 0;
|
||||
int min_size_ = 8, max_size_ = 8;
|
||||
bool little_endian_ = true, is_signed_ = false;
|
||||
bool searched_ = false;
|
||||
SignalSearch search_;
|
||||
std::future<void> search_future_;
|
||||
bool searching_ = false;
|
||||
};
|
||||
178
iqpilot/tools/cabana/ui/tools/findsimilarbits.cc
Normal file
178
iqpilot/tools/cabana/ui/tools/findsimilarbits.cc
Normal file
@@ -0,0 +1,178 @@
|
||||
#include "tools/cabana/ui/tools/findsimilarbits.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
FindSimilarBitsDlg::FindSimilarBitsDlg() {
|
||||
setTitle("Find similar bits");
|
||||
|
||||
for (int bus : can->sources) {
|
||||
bus_items_.push_back(bus);
|
||||
}
|
||||
updateMessages();
|
||||
}
|
||||
|
||||
void FindSimilarBitsDlg::updateMessages() {
|
||||
msg_items_.clear();
|
||||
msg_names_.clear();
|
||||
for (auto &[address, msg] : dbc()->getMessages(busAt(src_bus_))) {
|
||||
msg_items_.push_back({msg.name, address});
|
||||
}
|
||||
std::sort(msg_items_.begin(), msg_items_.end(), [](auto &l, auto &r) { return l.first < r.first; });
|
||||
for (auto &[name, _] : msg_items_) msg_names_.push_back(name);
|
||||
msg_index_ = 0;
|
||||
}
|
||||
|
||||
bool FindSimilarBitsDlg::draw() {
|
||||
if (begin(ImVec2(700, 500))) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Find From:");
|
||||
ImGui::SameLine(90);
|
||||
ImGui::TextUnformatted("Bus");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(60);
|
||||
if (comboBox("##src_bus", &src_bus_, bus_items_.data(), (int)bus_items_.size())) updateMessages();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(200);
|
||||
comboBox("##msg", &msg_index_, msg_names_);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("Byte Index");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80);
|
||||
if (ImGui::InputInt("##byte_idx", &byte_idx_, 1, 10)) byte_idx_ = std::clamp(byte_idx_, 0, 63);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("Bit Index");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80);
|
||||
if (ImGui::InputInt("##bit_idx", &bit_idx_, 1, 10)) bit_idx_ = std::clamp(bit_idx_, 0, 7);
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Find In:");
|
||||
ImGui::SameLine(90);
|
||||
ImGui::TextUnformatted("Bus");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(60);
|
||||
comboBox("##find_bus", &find_bus_, bus_items_.data(), (int)bus_items_.size());
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("Equal");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(60);
|
||||
ImGui::Combo("##equal", &equal_, "Yes\0No\0");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("Min msg count");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80);
|
||||
if (ImGui::InputInt("##min_msgs", &min_msgs_, 1, 10)) min_msgs_ = std::max(min_msgs_, 0);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Find")) find();
|
||||
|
||||
drawTable();
|
||||
}
|
||||
return end();
|
||||
}
|
||||
|
||||
void FindSimilarBitsDlg::drawTable() {
|
||||
|
||||
if (!table_has_columns_) {
|
||||
ImGui::BeginChild("table", ImVec2(0, 0), ImGuiChildFlags_Borders);
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings;
|
||||
if (!ImGui::BeginTable("table", 7, flags, ImVec2(0, 0))) return;
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
static const char *headers[] = {"address", "byte idx", "bit idx", "mismatches", "total msgs", "% mismatched"};
|
||||
|
||||
const float padding = ImGui::GetStyle().CellPadding.x * 2;
|
||||
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 40.0f - padding);
|
||||
for (int c = 0; c < 6; ++c) {
|
||||
ImGui::TableSetupColumn(headers[c], c == 5 ? ImGuiTableColumnFlags_WidthStretch : ImGuiTableColumnFlags_WidthFixed,
|
||||
100.0f - padding);
|
||||
}
|
||||
tableHeadersRow();
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin((int)table_.size());
|
||||
while (clipper.Step()) {
|
||||
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) {
|
||||
auto &m = table_[i];
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable(std::to_string(i + 1).c_str(), false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||
openMessage(MessageId{.source = busAt(find_bus_), .address = m.address});
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::Text("%x", m.address);
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::Text("%u", m.byte_idx);
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::Text("%u", m.bit_idx);
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
ImGui::Text("%u", m.mismatches);
|
||||
ImGui::TableSetColumnIndex(5);
|
||||
ImGui::Text("%u", m.total);
|
||||
ImGui::TableSetColumnIndex(6);
|
||||
ImGui::Text("%.2f", m.perc);
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
void FindSimilarBitsDlg::find() {
|
||||
const uint32_t selected_address = msg_index_ < (int)msg_items_.size() ? msg_items_[msg_index_].second : 0;
|
||||
table_ = calcBits(busAt(src_bus_), selected_address, byte_idx_, bit_idx_, busAt(find_bus_), equal_ == 0, min_msgs_);
|
||||
table_has_columns_ = true;
|
||||
}
|
||||
|
||||
std::vector<FindSimilarBitsDlg::Mismatch> FindSimilarBitsDlg::calcBits(uint8_t bus, uint32_t selected_address, int byte_idx,
|
||||
int bit_idx, uint8_t find_bus, bool equal, int min_msgs_cnt) {
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> mismatches;
|
||||
std::unordered_map<uint32_t, uint32_t> msg_count;
|
||||
const auto &events = can->allEvents();
|
||||
int bit_to_find = -1;
|
||||
for (const CanEvent *e : events) {
|
||||
if (e->src == bus) {
|
||||
if (e->address == selected_address && e->size > byte_idx) {
|
||||
bit_to_find = ((e->dat[byte_idx] >> (7 - bit_idx)) & 1) != 0;
|
||||
}
|
||||
}
|
||||
if (e->src == find_bus) {
|
||||
++msg_count[e->address];
|
||||
if (bit_to_find == -1) continue;
|
||||
|
||||
auto &mismatched = mismatches[e->address];
|
||||
if (mismatched.size() < e->size * 8) {
|
||||
mismatched.resize(e->size * 8);
|
||||
}
|
||||
for (int i = 0; i < e->size; ++i) {
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
int bit = ((e->dat[i] >> (7 - j)) & 1) != 0;
|
||||
mismatched[i * 8 + j] += equal ? (bit != bit_to_find) : (bit == bit_to_find);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Mismatch> result;
|
||||
result.reserve(mismatches.size());
|
||||
for (auto it = mismatches.begin(); it != mismatches.end(); ++it) {
|
||||
if (auto cnt = msg_count[it->first]; cnt > (uint32_t)min_msgs_cnt) {
|
||||
auto &mismatched = it->second;
|
||||
for (int i = 0; i < (int)mismatched.size(); ++i) {
|
||||
if (float perc = (mismatched[i] / (double)cnt) * 100; perc < 50) {
|
||||
result.push_back({it->first, (uint32_t)i / 8, (uint32_t)i % 8, mismatched[i], cnt, perc});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(result.begin(), result.end(), [](auto &l, auto &r) { return l.perc < r.perc; });
|
||||
return result;
|
||||
}
|
||||
40
iqpilot/tools/cabana/ui/tools/findsimilarbits.h
Normal file
40
iqpilot/tools/cabana/ui/tools/findsimilarbits.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/ui/tools/tooldialog.h"
|
||||
|
||||
class FindSimilarBitsDlg : public ToolDialog {
|
||||
public:
|
||||
FindSimilarBitsDlg();
|
||||
bool draw() override;
|
||||
|
||||
Observable<const MessageId &> openMessage;
|
||||
|
||||
private:
|
||||
struct Mismatch {
|
||||
uint32_t address, byte_idx, bit_idx, mismatches, total;
|
||||
float perc;
|
||||
};
|
||||
std::vector<Mismatch> calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, uint8_t find_bus,
|
||||
bool equal, int min_msgs_cnt);
|
||||
uint8_t busAt(int index) const { return index < (int)bus_items_.size() ? bus_items_[index] : 0; }
|
||||
void updateMessages();
|
||||
void find();
|
||||
void drawTable();
|
||||
|
||||
std::vector<Mismatch> table_;
|
||||
bool table_has_columns_ = false;
|
||||
std::vector<int> bus_items_;
|
||||
int src_bus_ = 0, find_bus_ = 0;
|
||||
std::vector<std::pair<std::string, uint32_t>> msg_items_;
|
||||
std::vector<std::string> msg_names_;
|
||||
int msg_index_ = 0;
|
||||
int equal_ = 0;
|
||||
int byte_idx_ = 0, bit_idx_ = 0;
|
||||
int min_msgs_ = 100;
|
||||
};
|
||||
50
iqpilot/tools/cabana/ui/tools/routeinfo.cc
Normal file
50
iqpilot/tools/cabana/ui/tools/routeinfo.cc
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "tools/cabana/ui/tools/routeinfo.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/streams/replaystream.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
RouteInfoDlg::RouteInfoDlg() {
|
||||
replay_ = dynamic_cast<ReplayStream *>(can)->getReplay();
|
||||
setTitle("Route: " + replay_->route().name());
|
||||
}
|
||||
|
||||
bool RouteInfoDlg::draw() {
|
||||
static const char *headers[] = {"", "rlog", "road", "wide road", "driver", "qlog", "qcam"};
|
||||
auto yn = [](const std::string &s) { return s.empty() ? "--" : "Yes"; };
|
||||
const auto &segments = replay_->route().segments();
|
||||
|
||||
float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
float min_h = row_h * (std::min((int)segments.size(), 13) + 1) + ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y * 2;
|
||||
if (begin(ImVec2(520, min_h))) {
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit;
|
||||
if (ImGui::BeginTable("table", 7, flags, ImVec2(0, 0))) {
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
for (int c = 0; c < 7; ++c) ImGui::TableSetupColumn(headers[c]);
|
||||
tableHeadersRow();
|
||||
int row = 0;
|
||||
for (const auto &[seg_num, seg] : segments) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::PushID(row);
|
||||
if (ImGui::Selectable(std::to_string(seg_num).c_str(), false, ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
can->seekTo(row * 60.0);
|
||||
}
|
||||
ImGui::SetItemTooltip("Click on a row to seek to the corresponding segment.");
|
||||
ImGui::PopID();
|
||||
const char *cells[] = {yn(seg.rlog), yn(seg.road_cam), yn(seg.wide_road_cam),
|
||||
yn(seg.driver_cam), yn(seg.qlog), yn(seg.qcamera)};
|
||||
for (int c = 1; c < 7; ++c) {
|
||||
ImGui::TableSetColumnIndex(c);
|
||||
ImGui::TextUnformatted(cells[c - 1]);
|
||||
}
|
||||
++row;
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
return end();
|
||||
}
|
||||
14
iqpilot/tools/cabana/ui/tools/routeinfo.h
Normal file
14
iqpilot/tools/cabana/ui/tools/routeinfo.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "tools/cabana/ui/tools/tooldialog.h"
|
||||
|
||||
class Replay;
|
||||
|
||||
class RouteInfoDlg : public ToolDialog {
|
||||
public:
|
||||
RouteInfoDlg();
|
||||
bool draw() override;
|
||||
|
||||
private:
|
||||
Replay *replay_ = nullptr;
|
||||
};
|
||||
52
iqpilot/tools/cabana/ui/tools/tooldialog.h
Normal file
52
iqpilot/tools/cabana/ui/tools/tooldialog.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
|
||||
class ToolDialog {
|
||||
public:
|
||||
virtual ~ToolDialog() = default;
|
||||
virtual bool draw() = 0;
|
||||
|
||||
Connections connections_;
|
||||
|
||||
protected:
|
||||
void setTitle(const std::string &name) {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "###tooldialog%p", (void *)this);
|
||||
title_ = name + buf;
|
||||
}
|
||||
|
||||
|
||||
bool begin(const ImVec2 &size) {
|
||||
if (!open_) return false;
|
||||
ImGui::SetNextWindowSize(size, ImGuiCond_Appearing);
|
||||
setNextWindowFloatsOut();
|
||||
began_ = true;
|
||||
return visible_ = ImGui::Begin(title_.c_str(), &open_, ImGuiWindowFlags_NoSavedSettings);
|
||||
}
|
||||
|
||||
bool end() {
|
||||
if (!began_) return false;
|
||||
|
||||
if (visible_ && ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape, false) &&
|
||||
!ImGui::IsPopupOpen(nullptr, ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) {
|
||||
open_ = false;
|
||||
}
|
||||
ImGui::End();
|
||||
began_ = false;
|
||||
return open_;
|
||||
}
|
||||
|
||||
std::string title_;
|
||||
bool open_ = true;
|
||||
|
||||
private:
|
||||
bool began_ = false, visible_ = false;
|
||||
};
|
||||
466
iqpilot/tools/cabana/ui/util.cc
Normal file
466
iqpilot/tools/cabana/ui/util.cc
Normal file
@@ -0,0 +1,466 @@
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#ifdef __APPLE__
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <GLFW/glfw3.h>
|
||||
extern "C" {
|
||||
struct objc_object;
|
||||
struct objc_selector;
|
||||
objc_object *glfwGetCocoaWindow(GLFWwindow *window);
|
||||
objc_selector *sel_registerName(const char *name);
|
||||
void objc_msgSend(void);
|
||||
}
|
||||
#endif
|
||||
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
|
||||
int inputCallback(ImGuiInputTextCallbackData *data) {
|
||||
auto *ctx = static_cast<InputContext *>(data->UserData);
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackCharFilter) {
|
||||
return ctx->validator ? ctx->validator(data) : 0;
|
||||
}
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit) {
|
||||
if (ctx->validate(std::string(data->Buf, data->BufTextLen)) == ValidState::Invalid) {
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
data->InsertChars(0, ctx->last_valid->c_str());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
||||
ctx->str->resize(data->BufTextLen);
|
||||
data->Buf = ctx->str->data();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool validatedInput(const char *label, std::string *s, ImGuiInputTextCallback validator, const char *hint,
|
||||
ImGuiInputTextFlags flags) {
|
||||
InputContext ctx{s, validator};
|
||||
flags |= ImGuiInputTextFlags_CallbackResize;
|
||||
if (validator) flags |= ImGuiInputTextFlags_CallbackCharFilter;
|
||||
return ImGui::InputTextWithHint(label, hint, s->data(), s->capacity() + 1, flags, inputCallback, &ctx);
|
||||
}
|
||||
|
||||
bool inputTextMultiline(const char *label, std::string *s, const ImVec2 &size, ImGuiInputTextFlags flags) {
|
||||
InputContext ctx{s, nullptr};
|
||||
return ImGui::InputTextMultiline(label, s->data(), s->capacity() + 1, size, flags | ImGuiInputTextFlags_CallbackResize,
|
||||
inputCallback, &ctx);
|
||||
}
|
||||
|
||||
bool clearableInput(const char *label, std::string *s, const char *hint, ImGuiInputTextCallback validator) {
|
||||
bool changed = validatedInput(label, s, validator, hint);
|
||||
if (!s->empty()) {
|
||||
ImGui::SameLine(0.0f, 0.0f);
|
||||
ImGui::PushID(label);
|
||||
if (toolButton("clear", icon::X)) {
|
||||
s->clear();
|
||||
changed = true;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool comboBox(const char *label, int *index, const std::vector<std::string> &items) {
|
||||
bool changed = false;
|
||||
const int count = (int)items.size();
|
||||
if (ImGui::BeginCombo(label, *index >= 0 && *index < count ? items[*index].c_str() : "")) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable(items[i].c_str(), i == *index) && *index != i) {
|
||||
*index = i;
|
||||
changed = true;
|
||||
}
|
||||
if (i == *index) ImGui::SetItemDefaultFocus();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool validatedText(const char *label, std::string *s, ValidState (*validate)(const std::string &),
|
||||
const char *hint, ImGuiInputTextCallback filter) {
|
||||
const std::string last_valid = *s;
|
||||
InputContext ctx{s, filter, validate, &last_valid};
|
||||
ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize | ImGuiInputTextFlags_CallbackEdit;
|
||||
if (filter) flags |= ImGuiInputTextFlags_CallbackCharFilter;
|
||||
ImGui::InputTextWithHint(label, hint, s->data(), s->capacity() + 1, flags, inputCallback, &ctx);
|
||||
return *s != last_valid;
|
||||
}
|
||||
|
||||
int nameValidator(ImGuiInputTextCallbackData *data) {
|
||||
|
||||
if (data->EventChar == ' ') {
|
||||
data->EventChar = '_';
|
||||
return 0;
|
||||
}
|
||||
return (data->EventChar < 128 && (std::isalnum((int)data->EventChar) || data->EventChar == '_')) ? 0 : 1;
|
||||
}
|
||||
|
||||
int nodeValidator(ImGuiInputTextCallbackData *data) {
|
||||
|
||||
return (data->EventChar < 128 && (std::isalnum((int)data->EventChar) || data->EventChar == '_' || data->EventChar == ',')) ? 0 : 1;
|
||||
}
|
||||
|
||||
int doubleValidator(ImGuiInputTextCallbackData *data) {
|
||||
|
||||
const ImWchar c = data->EventChar;
|
||||
return (c < 128 && (std::isdigit((int)c) || c == '+' || c == '-' || c == '.' || c == 'e' || c == 'E')) ? 0 : 1;
|
||||
}
|
||||
|
||||
int ipValidator(ImGuiInputTextCallbackData *data) {
|
||||
|
||||
const ImWchar c = data->EventChar;
|
||||
return ((c >= '0' && c <= '9') || c == '.') ? 0 : 1;
|
||||
}
|
||||
|
||||
int nonWhitespaceValidator(ImGuiInputTextCallbackData *data) {
|
||||
|
||||
return (data->EventChar < 128 && std::isspace((int)data->EventChar)) ? 1 : 0;
|
||||
}
|
||||
|
||||
bool toolButton(const char *id, const char *icon, const char *tooltip, const char *text) {
|
||||
std::string label = text && *text ? std::string(icon) + " " + text + "###" + id : std::string(icon) + "###" + id;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
bool clicked = ImGui::Button(label.c_str());
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
if (tooltip && *tooltip) ImGui::SetItemTooltip("%s", tooltip);
|
||||
return clicked;
|
||||
}
|
||||
|
||||
void disabledItemTooltip(const char *text) {
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", text);
|
||||
}
|
||||
|
||||
bool radioMenuItem(const char *label, bool checked, float width) {
|
||||
const float indent = ImGui::GetFontSize();
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
const bool clicked = ImGui::Selectable((std::string("##") + label).c_str(), false, ImGuiSelectableFlags_None,
|
||||
ImVec2(ImMax(width, ImGui::GetContentRegionAvail().x), 0.0f));
|
||||
const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
if (checked) ImGui::RenderBullet(painter, ImVec2(pos.x + indent / 2, pos.y + ImGui::GetTextLineHeight() / 2), color);
|
||||
painter->AddText(ImVec2(pos.x + indent, pos.y), color, label);
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool PopupOwner::begin(const char *id) {
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindowRead();
|
||||
if (popup_id == 0) {
|
||||
|
||||
|
||||
ImGuiWindow *modal = ImGui::GetTopMostPopupModal();
|
||||
if (modal != nullptr && modal != window) return false;
|
||||
ImGui::OpenPopup(id);
|
||||
popup_id = window->GetID(id);
|
||||
owner_id = window->ID;
|
||||
} else if (owner_id != window->ID) {
|
||||
return false;
|
||||
} else if (!ImGui::IsPopupOpen(popup_id, ImGuiPopupFlags_AnyPopupLevel)) {
|
||||
|
||||
ImGui::OpenPopup(id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ImGuiWindow *topPopupWindow() {
|
||||
ImGuiContext &g = *GImGui;
|
||||
return g.OpenPopupStack.Size > 0 ? g.OpenPopupStack.back().Window : nullptr;
|
||||
}
|
||||
|
||||
bool dialogEscapePressed() {
|
||||
return ImGui::IsKeyPressed(ImGuiKey_Escape, false) && topPopupWindow() == ImGui::GetCurrentWindow();
|
||||
}
|
||||
|
||||
bool dialogButtons(const char *accept_label, bool *accepted, bool *rejected, bool accept_enabled,
|
||||
const char *reject_label) {
|
||||
const float button_width = 80.0f;
|
||||
const int count = reject_label ? 2 : 1;
|
||||
const float total = button_width * count + ImGui::GetStyle().ItemSpacing.x * (count - 1);
|
||||
const float avail = ImGui::GetContentRegionAvail().x;
|
||||
if (avail > total) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - total);
|
||||
bool pressed = false;
|
||||
if (reject_label) {
|
||||
if (ImGui::Button(reject_label, ImVec2(button_width, 0.0f))) {
|
||||
if (rejected) *rejected = true;
|
||||
pressed = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::BeginDisabled(!accept_enabled);
|
||||
if (ImGui::Button(accept_label, ImVec2(button_width, 0.0f))) {
|
||||
if (accepted) *accepted = true;
|
||||
pressed = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (rejected && dialogEscapePressed()) {
|
||||
*rejected = true;
|
||||
pressed = true;
|
||||
}
|
||||
return pressed;
|
||||
}
|
||||
|
||||
int tableHeadersRow() {
|
||||
int clicked = -1;
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
|
||||
for (int c = 0, count = ImGui::TableGetColumnCount(); c < count; ++c) {
|
||||
if (!ImGui::TableSetColumnIndex(c)) continue;
|
||||
const char *name = ImGui::TableGetColumnName(c);
|
||||
if (!name) name = "";
|
||||
const float offset = (ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(name).x) * 0.5f;
|
||||
if (offset > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offset);
|
||||
ImGui::PushID(c);
|
||||
ImGui::TableHeader(name);
|
||||
|
||||
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) clicked = c;
|
||||
ImGui::PopID();
|
||||
}
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool viewSelectable(const char *label, bool selected, ImGuiSelectableFlags flags, const ImVec2 &size) {
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, selected ? ImGui::GetColorU32(ImGuiCol_Header) : IM_COL32(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImGui::GetColorU32(ImGuiCol_Header));
|
||||
const bool clicked = ImGui::Selectable(label, selected, flags, size);
|
||||
ImGui::PopStyleColor(2);
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool checkBox(const char *label, bool *v) {
|
||||
const float box = 16.0f;
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
if (window->SkipItems) return false;
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const ImGuiID id = window->GetID(label);
|
||||
const ImVec2 label_size = ImGui::CalcTextSize(label, nullptr, true);
|
||||
const float frame_h = ImGui::GetFrameHeight();
|
||||
const ImVec2 pos = window->DC.CursorPos;
|
||||
const ImRect total_bb(pos, ImVec2(pos.x + box + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), pos.y + frame_h));
|
||||
ImGui::ItemSize(total_bb, style.FramePadding.y);
|
||||
if (!ImGui::ItemAdd(total_bb, id)) return false;
|
||||
bool hovered, held;
|
||||
const bool pressed = ImGui::ButtonBehavior(total_bb, id, &hovered, &held);
|
||||
if (pressed) {
|
||||
*v = !*v;
|
||||
ImGui::MarkItemEdited(id);
|
||||
}
|
||||
const float y = pos.y + IM_TRUNC((frame_h - box) * 0.5f);
|
||||
const ImRect check_bb(ImVec2(pos.x, y), ImVec2(pos.x + box, y + box));
|
||||
ImGui::RenderNavCursor(total_bb, id);
|
||||
const ImU32 bg = ImGui::GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
|
||||
ImGui::RenderFrame(check_bb.Min, check_bb.Max, bg, true, style.FrameRounding);
|
||||
if (*v) {
|
||||
const float pad = ImMax(1.0f, IM_TRUNC(box / 6.0f));
|
||||
ImGui::RenderCheckMark(window->DrawList, ImVec2(check_bb.Min.x + pad, check_bb.Min.y + pad), ImGui::GetColorU32(ImGuiCol_CheckMark), box - pad * 2.0f);
|
||||
}
|
||||
if (label_size.x > 0.0f) ImGui::RenderText(ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, pos.y + style.FramePadding.y), label);
|
||||
return pressed;
|
||||
}
|
||||
|
||||
void alignRight(float width) {
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - width));
|
||||
}
|
||||
|
||||
void drawText(ImDrawList *dl, const ImRect &rect, const char *text, ImU32 col, ImFont *font, float font_size, const ImVec2 &align) {
|
||||
if (font == nullptr) font = ImGui::GetFont();
|
||||
if (font_size <= 0.0f) font_size = ImGui::GetFontSize();
|
||||
const ImVec2 size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text);
|
||||
const ImVec2 pos(rect.Min.x + (rect.GetWidth() - size.x) * align.x, rect.Min.y + (rect.GetHeight() - size.y) * align.y);
|
||||
dl->AddText(font, font_size, pos, col, text);
|
||||
}
|
||||
|
||||
void drawElidedText(ImDrawList *dl, const ImRect &rect, const std::string &text, ImU32 col, bool align_right) {
|
||||
const ImVec2 size = ImGui::CalcTextSize(text.c_str());
|
||||
const float y = rect.Min.y + std::max(0.0f, (rect.GetHeight() - size.y) * 0.5f);
|
||||
if (size.x <= rect.GetWidth()) {
|
||||
dl->AddText(ImVec2(align_right ? rect.Max.x - size.x : rect.Min.x, y), col, text.c_str());
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, col);
|
||||
ImGui::RenderTextEllipsis(dl, ImVec2(rect.Min.x, y), ImVec2(rect.Max.x, y + size.y), rect.Max.x, text.c_str(), nullptr, &size);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
float markerSize() { return ImGui::GetTextLineHeight() - 4; }
|
||||
|
||||
void drawColorMarker(ImDrawList *dl, const ImVec2 &pos, ImU32 col) {
|
||||
const float size = markerSize();
|
||||
dl->AddRectFilled(ImVec2(pos.x, pos.y + 2), ImVec2(pos.x + size, pos.y + 2 + size), col);
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
void setMacAppName(const char *name) {
|
||||
auto info = (CFMutableDictionaryRef)CFBundleGetInfoDictionary(CFBundleGetMainBundle());
|
||||
if (info == nullptr) return;
|
||||
CFStringRef value = CFStringCreateWithCString(kCFAllocatorDefault, name, kCFStringEncodingUTF8);
|
||||
CFDictionarySetValue(info, CFSTR("CFBundleName"), value);
|
||||
CFRelease(value);
|
||||
}
|
||||
|
||||
bool isNativeFullScreen(GLFWwindow *window) {
|
||||
constexpr unsigned long NS_WINDOW_STYLE_MASK_FULL_SCREEN = 1ul << 14;
|
||||
objc_object *ns_window = glfwGetCocoaWindow(window);
|
||||
if (ns_window == nullptr) return false;
|
||||
auto styleMask = (unsigned long (*)(objc_object *, objc_selector *))objc_msgSend;
|
||||
return (styleMask(ns_window, sel_registerName("styleMask")) & NS_WINDOW_STYLE_MASK_FULL_SCREEN) != 0;
|
||||
}
|
||||
|
||||
void toggleNativeFullScreen(GLFWwindow *window) {
|
||||
auto toggle = (void (*)(objc_object *, objc_selector *, objc_object *))objc_msgSend;
|
||||
toggle(glfwGetCocoaWindow(window), sel_registerName("toggleFullScreen:"), nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
void setNextWindowFloatsOut() {
|
||||
ImGuiWindowClass window_class;
|
||||
window_class.ViewportFlagsOverrideSet = ImGuiViewportFlags_NoAutoMerge;
|
||||
ImGui::SetNextWindowClass(&window_class);
|
||||
}
|
||||
|
||||
void setNextDialogWindow(const ImVec2 &size) {
|
||||
if (size.x > 0.0f || size.y > 0.0f) ImGui::SetNextWindowSize(size, ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
setNextWindowFloatsOut();
|
||||
}
|
||||
|
||||
bool beginDialog(const char *id, PopupOwner *owner, const ImVec2 &size, ImGuiWindowFlags flags) {
|
||||
if (!owner->begin(id)) return false;
|
||||
setNextDialogWindow(size);
|
||||
return ImGui::BeginPopupModal(id, nullptr, flags | ImGuiWindowFlags_NoSavedSettings);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void beginToolbar() {
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(TOOLBAR_ITEM_SPACING, ImGui::GetStyle().ItemSpacing.y));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(TOOLBAR_BUTTON_PADDING, ImGui::GetStyle().FramePadding.y));
|
||||
}
|
||||
|
||||
void endToolbar() { ImGui::PopStyleVar(2); }
|
||||
|
||||
float toolbarButtonWidth(const std::string &label) {
|
||||
return ImGui::CalcTextSize(label.c_str(), nullptr, true).x + ImGui::GetStyle().FramePadding.x * 2;
|
||||
}
|
||||
|
||||
static float toolbarGroupWidth(const std::vector<ToolbarItem> &items, size_t begin, size_t end) {
|
||||
float w = 0;
|
||||
for (size_t i = begin; i < end; ++i) w += items[i].width + (i > begin ? ImGui::GetStyle().ItemSpacing.x : 0);
|
||||
return w;
|
||||
}
|
||||
|
||||
float toolbarWidth(const std::vector<ToolbarItem> &items, size_t spacer_index) {
|
||||
spacer_index = std::min(spacer_index, items.size());
|
||||
float w = toolbarGroupWidth(items, 0, spacer_index) + toolbarGroupWidth(items, spacer_index, items.size());
|
||||
if (spacer_index > 0 && spacer_index < items.size()) w += ImGui::GetStyle().ItemSpacing.x;
|
||||
return w;
|
||||
}
|
||||
|
||||
void drawToolbar(const std::vector<ToolbarItem> &items, size_t spacer_index) {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
spacer_index = std::min(spacer_index, items.size());
|
||||
const float right_width = toolbarGroupWidth(items, spacer_index, items.size());
|
||||
const float start_x = ImGui::GetCursorPosX();
|
||||
const float avail = ImGui::GetContentRegionAvail().x;
|
||||
const float right_edge = start_x + avail;
|
||||
const float extension_width = toolbarButtonWidth(icon::RAQUO);
|
||||
|
||||
|
||||
|
||||
const bool fits = toolbarWidth(items, spacer_index) <= avail;
|
||||
size_t visible = items.size();
|
||||
if (!fits) {
|
||||
const float usable = avail - (extension_width + style.ItemSpacing.x);
|
||||
float used = 0;
|
||||
for (visible = 0; visible < items.size(); ++visible) {
|
||||
const float w = items[visible].width + (visible ? style.ItemSpacing.x : 0);
|
||||
if (used + w > usable) break;
|
||||
used += w;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < visible; ++i) {
|
||||
if (i == 0) ImGui::SetCursorPosX(start_x);
|
||||
else if (fits && i == spacer_index) ImGui::SameLine(right_edge - right_width);
|
||||
else ImGui::SameLine();
|
||||
items[i].draw();
|
||||
}
|
||||
|
||||
if (visible < items.size()) {
|
||||
|
||||
const float extension_x = std::max(start_x, right_edge - extension_width);
|
||||
visible == 0 ? ImGui::SetCursorPosX(extension_x) : ImGui::SameLine(extension_x);
|
||||
if (ImGui::Button((std::string(icon::RAQUO) + "###toolbar_extension").c_str(), ImVec2(extension_width, 0)))
|
||||
ImGui::OpenPopup("toolbar_extension_menu");
|
||||
ImGui::SetItemTooltip("More");
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMax().x, ImGui::GetItemRectMax().y), ImGuiCond_Always, ImVec2(1, 0));
|
||||
if (ImGui::BeginPopup("toolbar_extension_menu")) {
|
||||
for (size_t i = visible; i < items.size(); ++i) {
|
||||
if (!items[i].in_menu) continue;
|
||||
if (items[i].menu_label.empty()) {
|
||||
items[i].draw();
|
||||
} else if (ImGui::MenuItem(items[i].menu_label.c_str(), nullptr, false, items[i].enabled)) {
|
||||
items[i].trigger();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const float MENU_ARROW_SIZE = 6.0f;
|
||||
const float MENU_ARROW_SPACING = 5.0f;
|
||||
|
||||
float menuButtonWidth(const std::string &text, bool bold) {
|
||||
if (bold) pushBoldFont();
|
||||
const float w = ImGui::CalcTextSize(text.c_str(), nullptr, true).x + MENU_ARROW_SPACING + MENU_ARROW_SIZE +
|
||||
ImGui::GetStyle().FramePadding.x * 2;
|
||||
if (bold) popBoldFont();
|
||||
return w;
|
||||
}
|
||||
|
||||
bool menuButton(const char *id, const std::string &text, const char *popup_id, bool bold, float width) {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const bool popup_open = ImGui::IsPopupOpen(popup_id);
|
||||
if (width <= 0.0f) width = menuButtonWidth(text, bold);
|
||||
|
||||
|
||||
|
||||
if (bold) pushBoldFont();
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, popup_open ? style.Colors[ImGuiCol_ButtonActive] : ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.5f));
|
||||
const bool clicked = ImGui::ButtonEx((text + "###" + id).c_str(), ImVec2(width, 0.0f), ImGuiButtonFlags_PressedOnClick);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
const float text_width = ImGui::CalcTextSize(text.c_str(), nullptr, true).x;
|
||||
const float ascent = ImGui::GetFontBaked()->Ascent;
|
||||
if (bold) popBoldFont();
|
||||
|
||||
const ImVec2 min = ImGui::GetItemRectMin();
|
||||
const float x = min.x + style.FramePadding.x + text_width + MENU_ARROW_SPACING;
|
||||
const float baseline = min.y + style.FramePadding.y + ascent;
|
||||
ImGui::GetWindowDrawList()->AddTriangleFilled(ImVec2(x, baseline - MENU_ARROW_SIZE * 0.5f),
|
||||
ImVec2(x + MENU_ARROW_SIZE, baseline - MENU_ARROW_SIZE * 0.5f),
|
||||
ImVec2(x + MENU_ARROW_SIZE * 0.5f, baseline),
|
||||
ImGui::GetColorU32(ImGuiCol_TextDisabled));
|
||||
if (clicked && !popup_open) ImGui::OpenPopup(popup_id);
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(min.x, ImGui::GetItemRectMax().y), ImGuiCond_Always);
|
||||
return clicked;
|
||||
}
|
||||
201
iqpilot/tools/cabana/ui/util.h
Normal file
201
iqpilot/tools/cabana/ui/util.h
Normal file
@@ -0,0 +1,201 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
#include "tools/cabana/core/color.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
struct GLFWwindow;
|
||||
|
||||
inline ImVec4 colorRgb(int r, int g, int b, float alpha = 1.0f) {
|
||||
return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, alpha);
|
||||
}
|
||||
|
||||
inline ImU32 toImU32(const CabanaColor &c) { return IM_COL32(c.r, c.g, c.b, c.a); }
|
||||
inline ImVec4 toImVec4(const CabanaColor &c) { return ImVec4(c.r / 255.0f, c.g / 255.0f, c.b / 255.0f, c.a / 255.0f); }
|
||||
inline ImU32 withAlpha(ImU32 c, int alpha) { return (c & ~IM_COL32_A_MASK) | ((ImU32)alpha << IM_COL32_A_SHIFT); }
|
||||
|
||||
|
||||
constexpr const char *MESSAGES_PANEL_ID = "###MessagesPanel";
|
||||
|
||||
struct InputContext {
|
||||
std::string *str;
|
||||
ImGuiInputTextCallback validator;
|
||||
ValidState (*validate)(const std::string &) = nullptr;
|
||||
const std::string *last_valid = nullptr;
|
||||
};
|
||||
|
||||
int inputCallback(ImGuiInputTextCallbackData *data);
|
||||
|
||||
|
||||
bool validatedInput(const char *label, std::string *s, ImGuiInputTextCallback validator, const char *hint = "",
|
||||
ImGuiInputTextFlags flags = 0);
|
||||
|
||||
inline bool inputText(const char *label, std::string *s, const char *hint = "", ImGuiInputTextFlags flags = 0) {
|
||||
return validatedInput(label, s, nullptr, hint, flags);
|
||||
}
|
||||
|
||||
bool inputTextMultiline(const char *label, std::string *s, const ImVec2 &size, ImGuiInputTextFlags flags = 0);
|
||||
|
||||
|
||||
bool clearableInput(const char *label, std::string *s, const char *hint = "", ImGuiInputTextCallback validator = nullptr);
|
||||
|
||||
bool comboBox(const char *label, int *index, const std::vector<std::string> &items);
|
||||
|
||||
|
||||
template <typename T>
|
||||
inline bool comboBox(const char *label, int *index, const T *values, int count) {
|
||||
bool changed = false;
|
||||
const std::string preview = *index >= 0 && *index < count ? std::to_string(values[*index]) : "";
|
||||
if (ImGui::BeginCombo(label, preview.c_str())) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable(std::to_string(values[i]).c_str(), i == *index) && *index != i) {
|
||||
*index = i;
|
||||
changed = true;
|
||||
}
|
||||
if (i == *index) ImGui::SetItemDefaultFocus();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
|
||||
bool validatedText(const char *label, std::string *s, ValidState (*validate)(const std::string &),
|
||||
const char *hint = "", ImGuiInputTextCallback filter = nullptr);
|
||||
|
||||
|
||||
int nameValidator(ImGuiInputTextCallbackData *data);
|
||||
int nodeValidator(ImGuiInputTextCallbackData *data);
|
||||
int doubleValidator(ImGuiInputTextCallbackData *data);
|
||||
int ipValidator(ImGuiInputTextCallbackData *data);
|
||||
int nonWhitespaceValidator(ImGuiInputTextCallbackData *data);
|
||||
|
||||
|
||||
bool toolButton(const char *id, const char *icon, const char *tooltip = nullptr, const char *text = nullptr);
|
||||
|
||||
|
||||
void disabledItemTooltip(const char *text);
|
||||
|
||||
|
||||
|
||||
bool radioMenuItem(const char *label, bool checked, float width = 0.0f);
|
||||
|
||||
|
||||
|
||||
|
||||
struct PopupOwner {
|
||||
ImGuiID popup_id = 0, owner_id = 0;
|
||||
|
||||
|
||||
bool begin(const char *id);
|
||||
|
||||
void reset() { popup_id = owner_id = 0; }
|
||||
};
|
||||
|
||||
|
||||
bool dialogEscapePressed();
|
||||
|
||||
|
||||
ImGuiWindow *topPopupWindow();
|
||||
|
||||
|
||||
bool dialogButtons(const char *accept_label, bool *accepted, bool *rejected, bool accept_enabled = true,
|
||||
const char *reject_label = "Cancel");
|
||||
|
||||
|
||||
int tableHeadersRow();
|
||||
|
||||
|
||||
|
||||
|
||||
bool viewSelectable(const char *label, bool selected, ImGuiSelectableFlags flags, const ImVec2 &size);
|
||||
|
||||
|
||||
|
||||
bool checkBox(const char *label, bool *v);
|
||||
|
||||
|
||||
void alignRight(float width);
|
||||
|
||||
|
||||
void drawText(ImDrawList *dl, const ImRect &rect, const char *text, ImU32 col, ImFont *font = nullptr,
|
||||
float font_size = 0.0f, const ImVec2 &align = ImVec2(0.5f, 0.5f));
|
||||
|
||||
void drawElidedText(ImDrawList *dl, const ImRect &rect, const std::string &text, ImU32 col, bool align_right = false);
|
||||
|
||||
float markerSize();
|
||||
void drawColorMarker(ImDrawList *dl, const ImVec2 &pos, ImU32 col);
|
||||
|
||||
void loadFonts();
|
||||
void applyTheme(int theme);
|
||||
bool isDarkTheme();
|
||||
|
||||
ImU32 highlightedTextColor();
|
||||
ImU32 paletteBrightText();
|
||||
|
||||
|
||||
void setNextWindowFloatsOut();
|
||||
#ifdef __APPLE__
|
||||
|
||||
|
||||
void setMacAppName(const char *name);
|
||||
|
||||
bool isNativeFullScreen(GLFWwindow *window);
|
||||
void toggleNativeFullScreen(GLFWwindow *window);
|
||||
#endif
|
||||
|
||||
|
||||
void setNextDialogWindow(const ImVec2 &size);
|
||||
|
||||
bool beginDialog(const char *id, PopupOwner *owner, const ImVec2 &size, ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize);
|
||||
|
||||
const float TOOLBAR_ITEM_SPACING = 1.0f;
|
||||
const float TOOLBAR_BUTTON_PADDING = 4.0f;
|
||||
const float SLIDER_LENGTH = 13.0f;
|
||||
const float SLIDER_THICKNESS = 13.0f;
|
||||
|
||||
|
||||
|
||||
struct ToolbarItem {
|
||||
float width;
|
||||
std::function<void()> draw;
|
||||
std::string menu_label;
|
||||
std::function<void()> trigger;
|
||||
bool enabled = true;
|
||||
bool in_menu = true;
|
||||
};
|
||||
void beginToolbar();
|
||||
void endToolbar();
|
||||
float toolbarButtonWidth(const std::string &label);
|
||||
|
||||
float toolbarWidth(const std::vector<ToolbarItem> &items, size_t spacer_index);
|
||||
|
||||
void drawToolbar(const std::vector<ToolbarItem> &items, size_t spacer_index);
|
||||
|
||||
|
||||
|
||||
float menuButtonWidth(const std::string &text, bool bold = false);
|
||||
bool menuButton(const char *id, const std::string &text, const char *popup_id, bool bold = false, float width = 0.0f);
|
||||
|
||||
|
||||
void drawSliderHandle(ImDrawList *p, const ImRect &r);
|
||||
|
||||
|
||||
bool fusionSliderInt(const char *label, int *v, int min, int max, float width);
|
||||
|
||||
ImFont *boldFont();
|
||||
ImFont *monoFont();
|
||||
void pushMonoFont(float size = 0.0f);
|
||||
void popMonoFont();
|
||||
void pushBoldFont();
|
||||
void popBoldFont();
|
||||
void pushLargeFont();
|
||||
void popLargeFont();
|
||||
548
iqpilot/tools/cabana/ui/widgets/binaryview.cc
Normal file
548
iqpilot/tools/cabana/ui/widgets/binaryview.cc
Normal file
@@ -0,0 +1,548 @@
|
||||
#include "tools/cabana/ui/widgets/binaryview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iterator>
|
||||
#include <utility>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const int CELL_HEIGHT = 36;
|
||||
const float SMALL_FONT_SIZE = 10.0f;
|
||||
const int VERTICAL_HEADER_WIDTH = 30;
|
||||
inline int get_bit_pos(const BinaryIndex &index) { return flipBitPos(index.row * 8 + index.column); }
|
||||
|
||||
inline ImU32 paletteHighlight() { return ImGui::GetColorU32(ImGuiCol_Header); }
|
||||
inline ImU32 paletteBase() { return ImGui::GetColorU32(ImGuiCol_ChildBg); }
|
||||
inline ImU32 paletteText(bool active) { return ImGui::GetColorU32(active ? ImGuiCol_Text : ImGuiCol_TextDisabled); }
|
||||
const ImU32 DARK_GRAY = IM_COL32(128, 128, 128, 255);
|
||||
|
||||
|
||||
|
||||
void drawBoldText(ImDrawList *p, const ImRect &r, const char *text, ImU32 col, ImFont *font, float font_size) {
|
||||
drawText(p, r, text, col, font, font_size);
|
||||
drawText(p, ImRect(ImVec2(r.Min.x + 0.6f, r.Min.y), ImVec2(r.Max.x + 0.6f, r.Max.y)), text, col, font, font_size);
|
||||
}
|
||||
|
||||
|
||||
void fillDense7Pattern(ImDrawList *p, const ImRect &r, ImU32 col) {
|
||||
p->PushClipRect(r.Min, r.Max, true);
|
||||
for (float y = r.Min.y; y < r.Max.y; y += 4.0f) {
|
||||
for (float x = r.Min.x + (static_cast<int>((y - r.Min.y) / 4.0f) % 2) * 2.0f; x < r.Max.x; x += 4.0f) {
|
||||
p->AddRectFilled(ImVec2(x, y), ImVec2(x + 1.0f, y + 1.0f), col);
|
||||
}
|
||||
}
|
||||
p->PopClipRect();
|
||||
}
|
||||
|
||||
|
||||
void fillBDiagPattern(ImDrawList *p, const ImRect &r, ImU32 col) {
|
||||
p->PushClipRect(r.Min, r.Max, true);
|
||||
const float h = r.GetHeight();
|
||||
for (float x = r.Min.x - h; x < r.Max.x; x += 8.0f) {
|
||||
p->AddLine(ImVec2(x, r.Max.y), ImVec2(x + h, r.Min.y), col, 1.0f);
|
||||
}
|
||||
p->PopClipRect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BinaryView::BinaryView() {
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); }));
|
||||
}
|
||||
|
||||
std::string BinaryView::whatsThis() const {
|
||||
return R"(
|
||||
<b>Binary View</b><br/>
|
||||
<span style="color:gray">Shortcuts</span><br />
|
||||
Delete Signal:
|
||||
<span style="background-color:lightGray;color:gray"> x </span>,
|
||||
<span style="background-color:lightGray;color:gray"> Backspace </span>,
|
||||
<span style="background-color:lightGray;color:gray"> Delete </span><br />
|
||||
Change endianness: <span style="background-color:lightGray;color:gray"> e </span><br />
|
||||
Change signedness: <span style="background-color:lightGray;color:gray"> s </span><br />
|
||||
Open chart:
|
||||
<span style="background-color:lightGray;color:gray"> c </span>,
|
||||
<span style="background-color:lightGray;color:gray"> p </span>,
|
||||
<span style="background-color:lightGray;color:gray"> g </span>
|
||||
)";
|
||||
}
|
||||
|
||||
void BinaryView::addShortcuts() {
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
if (io.WantTextInput || io.KeyCtrl || io.KeySuper) return;
|
||||
if (ImGui::GetTopMostPopupModal() != nullptr) return;
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_X, false) || ImGui::IsKeyPressed(ImGuiKey_Backspace, false) || ImGui::IsKeyPressed(ImGuiKey_Delete, false)) {
|
||||
if (hovered_sig_ != nullptr) {
|
||||
UndoStack::instance()->push(new RemoveSigCommand(msg_id_, hovered_sig_));
|
||||
hovered_sig_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_E, false)) {
|
||||
if (hovered_sig_ != nullptr) {
|
||||
cabana::Signal s = *hovered_sig_;
|
||||
s.is_little_endian = !s.is_little_endian;
|
||||
editSignal(hovered_sig_, s);
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_S, false)) {
|
||||
if (hovered_sig_ != nullptr) {
|
||||
cabana::Signal s = *hovered_sig_;
|
||||
s.is_signed = !s.is_signed;
|
||||
editSignal(hovered_sig_, s);
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_P, false) || ImGui::IsKeyPressed(ImGuiKey_G, false) || ImGui::IsKeyPressed(ImGuiKey_C, false)) {
|
||||
if (hovered_sig_ != nullptr) {
|
||||
showChart(msg_id_, hovered_sig_, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImVec2 BinaryView::minimumSizeHint() const {
|
||||
|
||||
pushMonoFont();
|
||||
const float min_section_size = ImGui::CalcTextSize("W").x + 8.0f;
|
||||
popMonoFont();
|
||||
return {(min_section_size + 1) * 9 + VERTICAL_HEADER_WIDTH + 2,
|
||||
static_cast<float>(CELL_HEIGHT * std::min(row_count_, 10) + 2)};
|
||||
}
|
||||
|
||||
void BinaryView::highlight(const cabana::Signal *sig) {
|
||||
if (sig != hovered_sig_) {
|
||||
hovered_sig_ = sig;
|
||||
signalHovered(hovered_sig_);
|
||||
}
|
||||
}
|
||||
|
||||
void BinaryView::setSelection() {
|
||||
auto index = indexAt(last_mouse_pos_);
|
||||
if (!anchor_index_.isValid() || !index.isValid())
|
||||
return;
|
||||
|
||||
std::set<BinaryIndex> selection;
|
||||
auto [start, size, is_lb] = getSelection(index);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
int pos = is_lb ? flipBitPos(start + i) : flipBitPos(start) + i;
|
||||
selection.insert({pos / 8, pos % 8});
|
||||
}
|
||||
selection_ = std::move(selection);
|
||||
}
|
||||
|
||||
void BinaryView::handleMousePress(const ImVec2 &pos) {
|
||||
resize_sig_ = nullptr;
|
||||
if (auto index = indexAt(last_mouse_pos_ = pos); index.isValid() && index.column != HEX_COLUMN) {
|
||||
anchor_index_ = index;
|
||||
auto item = &cellAt(anchor_index_);
|
||||
int bit_pos = get_bit_pos(anchor_index_);
|
||||
for (auto s : item->sigs) {
|
||||
if (bit_pos == s->lsb || bit_pos == s->msb) {
|
||||
int idx = flipBitPos(bit_pos == s->lsb ? s->msb : s->lsb);
|
||||
anchor_index_ = {idx / 8, idx % 8};
|
||||
resize_sig_ = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BinaryView::highlightPosition(const ImVec2 &pos) {
|
||||
if (auto index = indexAt(pos); index.isValid()) {
|
||||
auto item = &cellAt(index);
|
||||
const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back();
|
||||
highlight(sig);
|
||||
}
|
||||
}
|
||||
|
||||
void BinaryView::handleMouseMove(const ImVec2 &pos) {
|
||||
highlightPosition(last_mouse_pos_ = pos);
|
||||
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left) && indexAt(pos).column != HEX_COLUMN) setSelection();
|
||||
}
|
||||
|
||||
void BinaryView::handleMouseRelease(const ImVec2 &pos) {
|
||||
auto release_index = indexAt(pos);
|
||||
if (release_index.isValid() && anchor_index_.isValid()) {
|
||||
if (hasSelection()) {
|
||||
auto sig = resize_sig_ ? *resize_sig_ : cabana::Signal{};
|
||||
std::tie(sig.start_bit, sig.size, sig.is_little_endian) = getSelection(release_index);
|
||||
resize_sig_ ? editSignal(resize_sig_, sig)
|
||||
: UndoStack::instance()->push(new AddSigCommand(msg_id_, sig));
|
||||
} else {
|
||||
auto item = &cellAt(anchor_index_);
|
||||
if (item->sigs.size() > 0)
|
||||
signalClicked(item->sigs.back());
|
||||
}
|
||||
}
|
||||
selection_.clear();
|
||||
anchor_index_ = BinaryIndex();
|
||||
resize_sig_ = nullptr;
|
||||
}
|
||||
|
||||
void BinaryView::setMessage(const MessageId &message_id) {
|
||||
msg_id_ = message_id;
|
||||
scroll_to_top_ = true;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void BinaryView::refresh() {
|
||||
selection_.clear();
|
||||
anchor_index_ = BinaryIndex();
|
||||
resize_sig_ = nullptr;
|
||||
hovered_sig_ = nullptr;
|
||||
bit_flip_tracker_ = {};
|
||||
cells_.clear();
|
||||
if (auto dbc_msg = dbc()->msg(msg_id_)) {
|
||||
row_count_ = dbc_msg->size;
|
||||
cells_.resize(row_count_ * COLUMN_COUNT);
|
||||
for (auto sig : dbc_msg->getSignals()) {
|
||||
for (int j = 0; j < sig->size; ++j) {
|
||||
int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j;
|
||||
int idx = COLUMN_COUNT * (pos / 8) + pos % 8;
|
||||
if (idx >= cells_.size()) {
|
||||
fprintf(stderr, "signal %s out of bounds.start_bit: %d size: %d\n", sig->name.c_str(), sig->start_bit, sig->size);
|
||||
break;
|
||||
}
|
||||
if (j == 0) sig->is_little_endian ? cells_[idx].is_lsb = true : cells_[idx].is_msb = true;
|
||||
if (j == sig->size - 1) sig->is_little_endian ? cells_[idx].is_msb = true : cells_[idx].is_lsb = true;
|
||||
|
||||
auto &sigs = cells_[idx].sigs;
|
||||
sigs.push_back(sig);
|
||||
if (sigs.size() > 1) {
|
||||
std::sort(sigs.begin(), sigs.end(), [](auto l, auto r) { return l->size > r->size; });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row_count_ = can->lastMessage(msg_id_).dat.size();
|
||||
cells_.resize(row_count_ * COLUMN_COUNT);
|
||||
}
|
||||
updateState();
|
||||
if (under_mouse_) highlightPosition(last_mouse_pos_);
|
||||
}
|
||||
|
||||
|
||||
std::set<const cabana::Signal *> BinaryView::getOverlappingSignals() const {
|
||||
std::set<const cabana::Signal *> overlapping;
|
||||
for (const auto &item : cells_) {
|
||||
if (item.sigs.size() > 1) {
|
||||
for (auto s : item.sigs) {
|
||||
if (s->type == cabana::Signal::Type::Normal) overlapping.insert(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return overlapping;
|
||||
}
|
||||
|
||||
std::tuple<int, int, bool> BinaryView::getSelection(BinaryIndex index) {
|
||||
if (index.column == HEX_COLUMN) {
|
||||
index = {index.row, 7};
|
||||
}
|
||||
bool is_lb = true;
|
||||
if (resize_sig_) {
|
||||
is_lb = resize_sig_->is_little_endian;
|
||||
} else if (settings.drag_direction == Settings::DragDirection::MsbFirst) {
|
||||
is_lb = index < anchor_index_;
|
||||
} else if (settings.drag_direction == Settings::DragDirection::LsbFirst) {
|
||||
is_lb = !(index < anchor_index_);
|
||||
} else if (settings.drag_direction == Settings::DragDirection::AlwaysLE) {
|
||||
is_lb = true;
|
||||
} else if (settings.drag_direction == Settings::DragDirection::AlwaysBE) {
|
||||
is_lb = false;
|
||||
}
|
||||
|
||||
int cur_bit_pos = get_bit_pos(index);
|
||||
int anchor_bit_pos = get_bit_pos(anchor_index_);
|
||||
int start_bit = is_lb ? std::min(cur_bit_pos, anchor_bit_pos) : get_bit_pos(std::min(index, anchor_index_));
|
||||
int size = is_lb ? std::abs(cur_bit_pos - anchor_bit_pos) + 1 : std::abs(flipBitPos(cur_bit_pos) - flipBitPos(anchor_bit_pos)) + 1;
|
||||
return {start_bit, size, is_lb};
|
||||
}
|
||||
|
||||
BinaryIndex BinaryView::indexAt(const ImVec2 &pos) const {
|
||||
if (column_width_ <= 0 || pos.x < grid_pos_.x + VERTICAL_HEADER_WIDTH || pos.y < grid_pos_.y) return {};
|
||||
int column = static_cast<int>((pos.x - grid_pos_.x - VERTICAL_HEADER_WIDTH) / column_width_);
|
||||
int row = static_cast<int>((pos.y - grid_pos_.y) / CELL_HEIGHT);
|
||||
if (column >= COLUMN_COUNT || row >= row_count_) return {};
|
||||
return {row, column};
|
||||
}
|
||||
|
||||
ImRect BinaryView::visualRect(const BinaryIndex &index) const {
|
||||
|
||||
|
||||
const float x0 = grid_pos_.x + VERTICAL_HEADER_WIDTH + IM_ROUND(index.column * column_width_);
|
||||
const float x1 = grid_pos_.x + VERTICAL_HEADER_WIDTH + IM_ROUND((index.column + 1) * column_width_);
|
||||
const float y = grid_pos_.y + index.row * CELL_HEIGHT;
|
||||
return ImRect(x0, y, x1, y + CELL_HEIGHT);
|
||||
}
|
||||
|
||||
void BinaryView::draw() {
|
||||
is_message_active_ = can->isMessageActive(msg_id_);
|
||||
if (scroll_to_top_) {
|
||||
ImGui::SetScrollY(0.0f);
|
||||
scroll_to_top_ = false;
|
||||
}
|
||||
|
||||
const int rows = row_count_;
|
||||
const float width = ImGui::GetContentRegionAvail().x;
|
||||
column_width_ = std::max(1.0f, (width - VERTICAL_HEADER_WIDTH) / COLUMN_COUNT);
|
||||
grid_pos_ = ImGui::GetCursorScreenPos();
|
||||
ImGui::InvisibleButton("##binary_view", ImVec2(std::max(width, 1.0f), std::max(static_cast<float>(rows * CELL_HEIGHT), 1.0f)));
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
|
||||
for (int row = 0; row < rows; ++row) {
|
||||
const ImRect r(grid_pos_.x, grid_pos_.y + row * CELL_HEIGHT, grid_pos_.x + VERTICAL_HEADER_WIDTH, grid_pos_.y + (row + 1) * CELL_HEIGHT);
|
||||
painter->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_WindowBg));
|
||||
drawText(painter, r, std::to_string(row).c_str(), ImGui::GetColorU32(ImGuiCol_Text));
|
||||
}
|
||||
for (int row = 0; row < rows; ++row) {
|
||||
for (int column = 0; column < COLUMN_COUNT; ++column) {
|
||||
const BinaryIndex index = {row, column};
|
||||
paintCell(painter, visualRect(index), index);
|
||||
}
|
||||
}
|
||||
|
||||
const ImVec2 mouse = ImGui::GetMousePos();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
const bool active = ImGui::IsItemActive();
|
||||
const bool under_mouse = (hovered || active) && ImGui::IsMouseHoveringRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), false);
|
||||
if (hovered || active) {
|
||||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) handleMousePress(mouse);
|
||||
const ImVec2 delta = ImGui::GetIO().MouseDelta;
|
||||
if (delta.x != 0.0f || delta.y != 0.0f) {
|
||||
handleMouseMove(mouse);
|
||||
} else {
|
||||
|
||||
|
||||
highlightPosition(last_mouse_pos_ = mouse);
|
||||
}
|
||||
}
|
||||
|
||||
if (std::exchange(under_mouse_, under_mouse) && !under_mouse) highlight(nullptr);
|
||||
if (ImGui::IsItemDeactivated()) handleMouseRelease(mouse);
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) {
|
||||
if (auto index = indexAt(mouse); index.isValid() && !cellAt(index).sigs.empty()) {
|
||||
ImGui::SetTooltip("%s", utils::stripHtml(utils::signalToolTip(cellAt(index).sigs.back())).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
addShortcuts();
|
||||
}
|
||||
|
||||
void BinaryView::setCell(int row, int col, uint8_t val, const CabanaColor &color) {
|
||||
auto &item = cells_[row * COLUMN_COUNT + col];
|
||||
item.valid = true;
|
||||
item.val = val;
|
||||
item.bg_color = color;
|
||||
}
|
||||
|
||||
void BinaryView::updateState() {
|
||||
const auto &last_msg = can->lastMessage(msg_id_);
|
||||
const auto &binary = last_msg.dat;
|
||||
if (binary.size() > row_count_) {
|
||||
row_count_ = binary.size();
|
||||
cells_.resize(row_count_ * COLUMN_COUNT);
|
||||
}
|
||||
|
||||
auto &bit_flips = heatmap_live_mode_ ? last_msg.bit_flip_counts : bitFlipChanges(binary.size());
|
||||
uint32_t max_bit_flip_count = 1;
|
||||
for (const auto &row : bit_flips) {
|
||||
for (uint32_t count : row) {
|
||||
max_bit_flip_count = std::max(max_bit_flip_count, count);
|
||||
}
|
||||
}
|
||||
|
||||
const bool dark = isDarkTheme();
|
||||
const double max_alpha = 255.0;
|
||||
const double min_alpha_with_signal = dark ? 70.0 : 25.0;
|
||||
const double min_alpha_no_signal = dark ? 28.0 : 10.0;
|
||||
const double alpha_gamma = dark ? 0.6 : 1.0;
|
||||
const double log_factor = 1.0 + 0.2;
|
||||
const double log_scaler = max_alpha / log2(log_factor * max_bit_flip_count);
|
||||
|
||||
for (size_t i = 0; i < binary.size(); ++i) {
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
auto &item = cells_[i * COLUMN_COUNT + j];
|
||||
int bit_val = (binary[i] >> (7 - j)) & 1;
|
||||
|
||||
double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal;
|
||||
uint32_t flip_count = bit_flips[i][j];
|
||||
if (flip_count > 0) {
|
||||
double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler;
|
||||
normalized_alpha = max_alpha * std::pow(std::clamp(normalized_alpha / max_alpha, 0.0, 1.0), alpha_gamma);
|
||||
double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal;
|
||||
alpha = std::clamp(normalized_alpha, min_alpha, max_alpha);
|
||||
}
|
||||
|
||||
auto color = item.bg_color;
|
||||
color.a = static_cast<uint8_t>(alpha);
|
||||
setCell(i, j, bit_val, color);
|
||||
}
|
||||
setCell(i, HEX_COLUMN, binary[i], last_msg.colors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::array<uint32_t, 8>> &BinaryView::bitFlipChanges(size_t msg_size) {
|
||||
auto time_range = can->timeRange();
|
||||
if (bit_flip_tracker_.time_range == time_range && !bit_flip_tracker_.flip_counts.empty())
|
||||
return bit_flip_tracker_.flip_counts;
|
||||
|
||||
bit_flip_tracker_.time_range = time_range;
|
||||
bit_flip_tracker_.flip_counts.assign(msg_size, std::array<uint32_t, 8>{});
|
||||
|
||||
auto [first, last] = can->eventsInRange(msg_id_, time_range);
|
||||
if (std::distance(first, last) <= 1) return bit_flip_tracker_.flip_counts;
|
||||
|
||||
std::vector<uint8_t> prev_values((*first)->dat, (*first)->dat + (*first)->size);
|
||||
for (auto it = std::next(first); it != last; ++it) {
|
||||
const CanEvent *event = *it;
|
||||
int size = std::min<int>(msg_size, event->size);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
const uint8_t diff = event->dat[i] ^ prev_values[i];
|
||||
if (!diff) continue;
|
||||
|
||||
auto &bit_flips = bit_flip_tracker_.flip_counts[i];
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
if (diff & (1u << bit)) ++bit_flips[7 - bit];
|
||||
}
|
||||
prev_values[i] = event->dat[i];
|
||||
}
|
||||
}
|
||||
|
||||
return bit_flip_tracker_.flip_counts;
|
||||
}
|
||||
|
||||
bool BinaryView::hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const {
|
||||
if (!index.isValid()) return false;
|
||||
int idx = (index.row + dy) * COLUMN_COUNT + index.column + dx;
|
||||
if (idx < 0 || idx >= (int)cells_.size()) return false;
|
||||
auto &s = cells_[idx].sigs;
|
||||
return std::find(s.begin(), s.end(), sig) != s.end();
|
||||
}
|
||||
|
||||
void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index) const {
|
||||
auto item = &cellAt(index);
|
||||
ImFont *font = ImGui::GetFont();
|
||||
float font_size = ImGui::GetFontSize();
|
||||
ImU32 pen = paletteText(is_message_active_);
|
||||
|
||||
if (index.column == HEX_COLUMN) {
|
||||
if (item->valid) {
|
||||
pushMonoFont();
|
||||
font = ImGui::GetFont();
|
||||
font_size = ImGui::GetFontSize();
|
||||
popMonoFont();
|
||||
painter->AddRectFilled(rect.Min, rect.Max, toImU32(item->bg_color));
|
||||
}
|
||||
} else if (isSelected(index)) {
|
||||
auto color = resize_sig_ ? toImU32(resize_sig_->color) : paletteHighlight();
|
||||
painter->AddRectFilled(rect.Min, rect.Max, color);
|
||||
pen = paletteBrightText();
|
||||
} else if (!hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), resize_sig_) == item->sigs.end()) {
|
||||
if (item->sigs.size() > 0) {
|
||||
for (auto &s : item->sigs) {
|
||||
if (s == hovered_sig_) {
|
||||
painter->AddRectFilled(rect.Min, rect.Max, toImU32(s->color.darker(125)));
|
||||
} else {
|
||||
drawSignalCell(painter, rect, index, s);
|
||||
}
|
||||
}
|
||||
} else if (item->valid && item->bg_color.alpha() > 0) {
|
||||
painter->AddRectFilled(rect.Min, rect.Max, toImU32(item->bg_color));
|
||||
}
|
||||
bool bright = std::find(item->sigs.begin(), item->sigs.end(), hovered_sig_) != item->sigs.end();
|
||||
pen = bright ? paletteBrightText() : paletteText(is_message_active_);
|
||||
}
|
||||
|
||||
if (item->sigs.size() > 1) {
|
||||
fillDense7Pattern(painter, rect, DARK_GRAY);
|
||||
} else if (!item->valid) {
|
||||
fillBDiagPattern(painter, rect, DARK_GRAY);
|
||||
}
|
||||
if (item->valid) {
|
||||
if (index.column == HEX_COLUMN) {
|
||||
drawBoldText(painter, rect, utils::hexByte(item->val), pen, font, font_size);
|
||||
} else {
|
||||
drawText(painter, rect, item->val ? "1" : "0", pen, font, font_size);
|
||||
}
|
||||
}
|
||||
if (item->is_msb || item->is_lsb) {
|
||||
const ImRect marker_rect(rect.Min, ImVec2(rect.Max.x - 8, rect.Max.y - 3));
|
||||
drawText(painter, marker_rect, item->is_msb ? "M" : "L", pen, nullptr, SMALL_FONT_SIZE, ImVec2(1.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const {
|
||||
bool draw_left = !hasSignal(index, -1, 0, sig);
|
||||
bool draw_top = !hasSignal(index, 0, -1, sig);
|
||||
bool draw_right = !hasSignal(index, 1, 0, sig);
|
||||
bool draw_bottom = !hasSignal(index, 0, 1, sig);
|
||||
|
||||
const int spacing = 2;
|
||||
ImRect rc(rect.Min.x + draw_left * 3, rect.Min.y + draw_top * spacing, rect.Max.x - draw_right * 3, rect.Max.y - draw_bottom * spacing);
|
||||
std::vector<ImRect> subtract;
|
||||
if (!draw_top) {
|
||||
if (!draw_left && !hasSignal(index, -1, -1, sig)) {
|
||||
subtract.emplace_back(rc.Min.x, rc.Min.y, rc.Min.x + 3, rc.Min.y + spacing);
|
||||
} else if (!draw_right && !hasSignal(index, 1, -1, sig)) {
|
||||
subtract.emplace_back(rc.Max.x - 3, rc.Min.y, rc.Max.x, rc.Min.y + spacing);
|
||||
}
|
||||
}
|
||||
if (!draw_bottom) {
|
||||
if (!draw_left && !hasSignal(index, -1, 1, sig)) {
|
||||
subtract.emplace_back(rc.Min.x, rc.Max.y - spacing, rc.Min.x + 3, rc.Max.y);
|
||||
} else if (!draw_right && !hasSignal(index, 1, 1, sig)) {
|
||||
subtract.emplace_back(rc.Max.x - 3, rc.Max.y - spacing, rc.Max.x, rc.Max.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const ImRect *top_notch = !subtract.empty() && subtract.front().Min.y == rc.Min.y ? &subtract.front() : nullptr;
|
||||
const ImRect *bottom_notch = !subtract.empty() && subtract.back().Min.y != rc.Min.y ? &subtract.back() : nullptr;
|
||||
std::vector<ImRect> region;
|
||||
auto band = [&](const ImRect *notch, float y0, float y1) {
|
||||
const float x0 = notch && notch->Min.x == rc.Min.x ? notch->Max.x : rc.Min.x;
|
||||
const float x1 = notch && notch->Min.x != rc.Min.x ? notch->Min.x : rc.Max.x;
|
||||
if (x1 > x0 && y1 > y0) region.emplace_back(x0, y0, x1, y1);
|
||||
};
|
||||
if (top_notch) band(top_notch, rc.Min.y, rc.Min.y + spacing);
|
||||
band(nullptr, rc.Min.y + (top_notch ? spacing : 0), rc.Max.y - (bottom_notch ? spacing : 0));
|
||||
if (bottom_notch) band(bottom_notch, rc.Max.y - spacing, rc.Max.y);
|
||||
|
||||
auto item = &cellAt(index);
|
||||
CabanaColor color = sig->color;
|
||||
color.a = item->bg_color.alpha();
|
||||
const ImU32 edge = toImU32(sig->color.darker(125));
|
||||
|
||||
for (const ImRect &clip : region) {
|
||||
painter->PushClipRect(clip.Min, clip.Max, true);
|
||||
|
||||
painter->AddRectFilled(rc.Min, rc.Max, paletteBase());
|
||||
painter->AddRectFilled(rc.Min, rc.Max, toImU32(color));
|
||||
|
||||
if (draw_left) painter->AddLine(ImVec2(rc.Min.x + 0.5f, rc.Min.y), ImVec2(rc.Min.x + 0.5f, rc.Max.y), edge, 1.0f);
|
||||
if (draw_right) painter->AddLine(ImVec2(rc.Max.x - 0.5f, rc.Min.y), ImVec2(rc.Max.x - 0.5f, rc.Max.y), edge, 1.0f);
|
||||
if (draw_bottom) painter->AddLine(ImVec2(rc.Min.x, rc.Max.y - 0.5f), ImVec2(rc.Max.x, rc.Max.y - 0.5f), edge, 1.0f);
|
||||
if (draw_top) painter->AddLine(ImVec2(rc.Min.x, rc.Min.y + 0.5f), ImVec2(rc.Max.x, rc.Min.y + 0.5f), edge, 1.0f);
|
||||
|
||||
|
||||
for (auto &r : subtract) {
|
||||
painter->AddRect(r.Min, r.Max, edge, 0.0f, 0, 2.0f);
|
||||
}
|
||||
painter->PopClipRect();
|
||||
}
|
||||
}
|
||||
98
iqpilot/tools/cabana/ui/widgets/binaryview.h
Normal file
98
iqpilot/tools/cabana/ui/widgets/binaryview.h
Normal file
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
|
||||
struct BinaryIndex {
|
||||
int row = -1;
|
||||
int column = -1;
|
||||
bool isValid() const { return row >= 0 && column >= 0; }
|
||||
bool operator==(const BinaryIndex &o) const { return row == o.row && column == o.column; }
|
||||
bool operator<(const BinaryIndex &o) const { return std::tie(row, column) < std::tie(o.row, o.column); }
|
||||
};
|
||||
|
||||
class BinaryView {
|
||||
public:
|
||||
static constexpr int COLUMN_COUNT = 9;
|
||||
static constexpr int HEX_COLUMN = 8;
|
||||
|
||||
BinaryView();
|
||||
void setMessage(const MessageId &message_id);
|
||||
void highlight(const cabana::Signal *sig);
|
||||
std::set<const cabana::Signal *> getOverlappingSignals() const;
|
||||
void updateState();
|
||||
|
||||
void draw();
|
||||
ImVec2 minimumSizeHint() const;
|
||||
void setHeatmapLiveMode(bool live) { heatmap_live_mode_ = live; updateState(); }
|
||||
std::string whatsThis() const;
|
||||
|
||||
Observable<const cabana::Signal *> signalClicked;
|
||||
Observable<const cabana::Signal *> signalHovered;
|
||||
Observable<const cabana::Signal *, cabana::Signal &> editSignal;
|
||||
Observable<const MessageId &, const cabana::Signal *, bool, bool> showChart;
|
||||
|
||||
private:
|
||||
struct Cell {
|
||||
CabanaColor bg_color = CabanaColor(102, 86, 169, 255);
|
||||
bool is_msb = false;
|
||||
bool is_lsb = false;
|
||||
uint8_t val;
|
||||
std::vector<const cabana::Signal *> sigs;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
void refresh();
|
||||
void setCell(int row, int col, uint8_t val, const CabanaColor &color);
|
||||
const std::vector<std::array<uint32_t, 8>> &bitFlipChanges(size_t msg_size);
|
||||
Cell &cellAt(const BinaryIndex &index) { return cells_[index.row * COLUMN_COUNT + index.column]; }
|
||||
const Cell &cellAt(const BinaryIndex &index) const { return cells_[index.row * COLUMN_COUNT + index.column]; }
|
||||
|
||||
void addShortcuts();
|
||||
std::tuple<int, int, bool> getSelection(BinaryIndex index);
|
||||
void setSelection();
|
||||
void handleMousePress(const ImVec2 &pos);
|
||||
void handleMouseMove(const ImVec2 &pos);
|
||||
void handleMouseRelease(const ImVec2 &pos);
|
||||
void highlightPosition(const ImVec2 &pt);
|
||||
BinaryIndex indexAt(const ImVec2 &pos) const;
|
||||
ImRect visualRect(const BinaryIndex &index) const;
|
||||
bool hasSelection() const { return !selection_.empty(); }
|
||||
bool isSelected(const BinaryIndex &index) const { return selection_.count(index) > 0; }
|
||||
|
||||
void paintCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index) const;
|
||||
bool hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const;
|
||||
void drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const;
|
||||
|
||||
MessageId msg_id_;
|
||||
std::vector<Cell> cells_;
|
||||
int row_count_ = 0;
|
||||
bool heatmap_live_mode_ = true;
|
||||
struct BitFlipTracker {
|
||||
std::optional<std::pair<double, double>> time_range;
|
||||
std::vector<std::array<uint32_t, 8>> flip_counts;
|
||||
} bit_flip_tracker_;
|
||||
|
||||
BinaryIndex anchor_index_;
|
||||
ImVec2 last_mouse_pos_{-1, -1};
|
||||
bool is_message_active_ = false;
|
||||
const cabana::Signal *resize_sig_ = nullptr;
|
||||
const cabana::Signal *hovered_sig_ = nullptr;
|
||||
std::set<BinaryIndex> selection_;
|
||||
ImVec2 grid_pos_;
|
||||
float column_width_ = 0;
|
||||
bool under_mouse_ = false;
|
||||
bool scroll_to_top_ = false;
|
||||
Connections connections_;
|
||||
};
|
||||
176
iqpilot/tools/cabana/ui/widgets/cameraview.cc
Normal file
176
iqpilot/tools/cabana/ui/widgets/cameraview.cc
Normal file
@@ -0,0 +1,176 @@
|
||||
#include "tools/cabana/ui/widgets/cameraview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include "imgui_impl_opengl3_loader.h"
|
||||
|
||||
#include "common/yuv.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
constexpr GLenum GL_LINEAR_MIPMAP_LINEAR_ = 0x2703;
|
||||
|
||||
void generateMipmap() {
|
||||
static auto fn = (void (*)(GLenum))glfwGetProcAddress("glGenerateMipmap");
|
||||
if (fn) fn(GL_TEXTURE_2D);
|
||||
}
|
||||
}
|
||||
|
||||
void GlTexture::upload(const RgbImage &image) {
|
||||
if (id == 0) {
|
||||
glGenTextures(1, &id);
|
||||
glBindTexture(GL_TEXTURE_2D, id);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mipmap ? GL_LINEAR_MIPMAP_LINEAR_ : GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
} else {
|
||||
glBindTexture(GL_TEXTURE_2D, id);
|
||||
}
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
if (width != image.width || height != image.height) {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
|
||||
width = image.width;
|
||||
height = image.height;
|
||||
} else {
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
|
||||
}
|
||||
if (mipmap) generateMipmap();
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
void GlTexture::destroy() {
|
||||
if (id != 0) {
|
||||
glDeleteTextures(1, &id);
|
||||
}
|
||||
id = 0;
|
||||
width = height = 0;
|
||||
key = 0;
|
||||
}
|
||||
|
||||
CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type)
|
||||
: stream_name_(stream_name), active_stream_type_(type), requested_stream_type_(type) {}
|
||||
|
||||
CameraWidget::~CameraWidget() {
|
||||
stopVipcThread();
|
||||
}
|
||||
|
||||
void CameraWidget::startVipcThread() {
|
||||
if (!vipc_thread_.joinable()) {
|
||||
clearFrames();
|
||||
vipc_exit_ = false;
|
||||
vipc_thread_ = std::thread(&CameraWidget::vipcThread, this);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::stopVipcThread() {
|
||||
vipc_exit_ = true;
|
||||
if (vipc_thread_.joinable()) {
|
||||
vipc_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::setVisible(bool visible) {
|
||||
if (visible == visible_) return;
|
||||
visible_ = visible;
|
||||
visible ? startVipcThread() : stopVipcThread();
|
||||
}
|
||||
|
||||
void CameraWidget::draw(const ImVec2 &size) {
|
||||
setVisible(true);
|
||||
ImGui::InvisibleButton("##camera", ImVec2(std::max(1.0f, size.x), std::max(1.0f, size.y)),
|
||||
ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle);
|
||||
rect_ = ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||
paint();
|
||||
if (ImGui::IsItemDeactivated()) clicked();
|
||||
}
|
||||
|
||||
float CameraWidget::frameAspectRatio() const {
|
||||
if (frame_texture_.width > 0 && frame_texture_.height > 0) {
|
||||
return (float)frame_texture_.width / frame_texture_.height;
|
||||
}
|
||||
return 1928.0f / 1208.0f;
|
||||
}
|
||||
|
||||
void CameraWidget::paint() {
|
||||
ImDrawList *p = ImGui::GetWindowDrawList();
|
||||
p->AddRectFilled(rect_.Min, rect_.Max, bg_);
|
||||
|
||||
std::lock_guard lk(frame_lock_);
|
||||
if (rgb_frame_.isNull()) return;
|
||||
if (frame_updated_) {
|
||||
frame_texture_.upload(rgb_frame_);
|
||||
frame_updated_ = false;
|
||||
}
|
||||
|
||||
|
||||
float widget_ratio = (float)width() / height();
|
||||
float frame_ratio = (float)rgb_frame_.width / rgb_frame_.height;
|
||||
int w = std::lround(width() * std::min(frame_ratio / widget_ratio, 1.0f));
|
||||
int h = std::lround(height() * std::min(widget_ratio / frame_ratio, 1.0f));
|
||||
ImVec2 video_min(rect_.Min.x + (int)(width() - w) / 2, rect_.Min.y + (int)(height() - h) / 2);
|
||||
ImVec2 video_max(video_min.x + w, video_min.y + h);
|
||||
|
||||
ImVec2 uv0(0, 0), uv1(1, 1);
|
||||
if (active_stream_type_ == VISION_STREAM_DRIVER) {
|
||||
|
||||
uv0.x = 1;
|
||||
uv1.x = 0;
|
||||
}
|
||||
p->AddImage(frame_texture_.ref(), video_min, video_max, uv0, uv1);
|
||||
}
|
||||
|
||||
void CameraWidget::vipcThread() {
|
||||
VisionStreamType cur_stream = requested_stream_type_;
|
||||
std::unique_ptr<VisionIpcClient> vipc_client;
|
||||
VisionIpcBufExtra frame_meta = {};
|
||||
|
||||
while (!vipc_exit_) {
|
||||
if (!vipc_client || cur_stream != requested_stream_type_) {
|
||||
clearFrames();
|
||||
cur_stream = requested_stream_type_;
|
||||
vipc_client.reset(new VisionIpcClient(stream_name_, cur_stream, false));
|
||||
}
|
||||
active_stream_type_ = cur_stream;
|
||||
|
||||
if (!vipc_client->connected) {
|
||||
clearFrames();
|
||||
auto streams = VisionIpcClient::getAvailableStreams(stream_name_, false);
|
||||
if (streams.empty()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
utils::runOnMainThread(utils::guarded(alive_, [this, streams]() { availableStreamsUpdated(streams); }));
|
||||
|
||||
if (!vipc_client->connect(false)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) {
|
||||
|
||||
if (rgb_back_.width != (int)buf->width || rgb_back_.height != (int)buf->height) {
|
||||
rgb_back_.resize(buf->width, buf->height);
|
||||
}
|
||||
yuv::nv12_to_rgba(buf->y, buf->stride, buf->uv, buf->stride,
|
||||
rgb_back_.data.data(), rgb_back_.bytesPerLine(), buf->width, buf->height);
|
||||
{
|
||||
std::lock_guard lk(frame_lock_);
|
||||
rgb_frame_.swap(rgb_back_);
|
||||
frame_updated_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::clearFrames() {
|
||||
std::lock_guard lk(frame_lock_);
|
||||
rgb_frame_.reset();
|
||||
rgb_back_.reset();
|
||||
frame_updated_ = false;
|
||||
}
|
||||
88
iqpilot/tools/cabana/ui/widgets/cameraview.h
Normal file
88
iqpilot/tools/cabana/ui/widgets/cameraview.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "cereal/visionstream.h"
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "msgq/visionipc/visionipc_client.h"
|
||||
|
||||
|
||||
struct RgbImage {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<uint8_t> data;
|
||||
bool isNull() const { return data.empty(); }
|
||||
void reset() { width = height = 0; data.clear(); }
|
||||
void resize(int w, int h) { width = w; height = h; data.resize(size_t(w) * h * 4); }
|
||||
int bytesPerLine() const { return width * 4; }
|
||||
void swap(RgbImage &other) { std::swap(width, other.width); std::swap(height, other.height); data.swap(other.data); }
|
||||
};
|
||||
|
||||
|
||||
struct GlTexture {
|
||||
GlTexture() = default;
|
||||
GlTexture(const GlTexture &) = delete;
|
||||
GlTexture &operator=(const GlTexture &) = delete;
|
||||
~GlTexture() { destroy(); }
|
||||
void upload(const RgbImage &image);
|
||||
void destroy();
|
||||
ImTextureRef ref() const { return ImTextureRef((ImTextureID)(uintptr_t)id); }
|
||||
|
||||
unsigned int id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t key = 0;
|
||||
bool mipmap = false;
|
||||
};
|
||||
|
||||
class CameraWidget {
|
||||
public:
|
||||
explicit CameraWidget(std::string stream_name, VisionStreamType stream_type);
|
||||
~CameraWidget();
|
||||
void setStreamType(VisionStreamType type) { requested_stream_type_ = type; }
|
||||
void stopVipcThread();
|
||||
|
||||
|
||||
void setVisible(bool visible);
|
||||
|
||||
void draw(const ImVec2 &size);
|
||||
const ImRect &rect() const { return rect_; }
|
||||
float frameAspectRatio() const;
|
||||
float width() const { return rect_.GetWidth(); }
|
||||
float height() const { return rect_.GetHeight(); }
|
||||
|
||||
Observable<> clicked;
|
||||
Observable<std::set<VisionStreamType>> availableStreamsUpdated;
|
||||
|
||||
private:
|
||||
void paint();
|
||||
void startVipcThread();
|
||||
void vipcThread();
|
||||
void clearFrames();
|
||||
|
||||
ImU32 bg_ = IM_COL32(0, 0, 0, 255);
|
||||
RgbImage rgb_frame_;
|
||||
RgbImage rgb_back_;
|
||||
bool frame_updated_ = false;
|
||||
GlTexture frame_texture_;
|
||||
ImRect rect_;
|
||||
bool visible_ = false;
|
||||
|
||||
std::string stream_name_;
|
||||
std::atomic<VisionStreamType> active_stream_type_;
|
||||
std::atomic<VisionStreamType> requested_stream_type_;
|
||||
std::thread vipc_thread_;
|
||||
std::atomic<bool> vipc_exit_ = false;
|
||||
std::mutex frame_lock_;
|
||||
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
|
||||
};
|
||||
439
iqpilot/tools/cabana/ui/widgets/detailwidget.cc
Normal file
439
iqpilot/tools/cabana/ui/widgets/detailwidget.cc
Normal file
@@ -0,0 +1,439 @@
|
||||
#include "tools/cabana/ui/widgets/detailwidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool iequals(const std::string &a, const std::string &b) {
|
||||
return a.size() == b.size() &&
|
||||
std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) { return std::tolower((unsigned char)x) == std::tolower((unsigned char)y); });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ElidedLabel::ElidedLabel(const std::string &text) : text_(utils::trimmed(text)) {}
|
||||
|
||||
void ElidedLabel::draw(float width) {
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
const ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
|
||||
const ImRect bb(pos, ImVec2(pos.x + width, pos.y + ImGui::GetTextLineHeight()));
|
||||
ImGui::ItemSize(bb.GetSize(), 0.0f);
|
||||
if (ImGui::ItemAdd(bb, 0)) {
|
||||
ImGui::RenderTextEllipsis(window->DrawList, bb.Min, bb.Max, bb.Max.x, text_.c_str(), nullptr, nullptr);
|
||||
}
|
||||
if (!tooltip_.empty()) ImGui::SetItemTooltip("%s", tooltip_.c_str());
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
clicked();
|
||||
}
|
||||
}
|
||||
|
||||
DetailWidget::DetailWidget(ChartsWidget *charts) : charts_(charts) {
|
||||
tabbar_.setUsesScrollButtons(true);
|
||||
tabbar_.setAutoHide(true);
|
||||
tabbar_.setTabsClosable(true);
|
||||
connections_.push_back(tabbar_.currentChanged.connect([this](int index) {
|
||||
if (index >= 0) setMessage(MessageId::fromString(tabbar_.tabText(index)));
|
||||
}));
|
||||
connections_.push_back(tabbar_.tabCloseRequested.connect([this](int index) { tabbar_.removeTab(index); }));
|
||||
connections_.push_back(tabbar_.tabContextMenu.connect([this](int index) { showTabBarContextMenu(index); }));
|
||||
binary_view_ = std::make_unique<BinaryView>();
|
||||
signal_view_ = std::make_unique<SignalView>(charts);
|
||||
|
||||
history_log_ = std::make_unique<LogsWidget>();
|
||||
|
||||
connections_.push_back(binary_view_->signalHovered.connect([this](const cabana::Signal *s) { signal_view_->signalHovered(s); }));
|
||||
connections_.push_back(binary_view_->signalClicked.connect([this](const cabana::Signal *s) { signal_view_->selectSignal(s, true); }));
|
||||
connections_.push_back(binary_view_->editSignal.connect([this](const cabana::Signal *origin_s, cabana::Signal &s) { signal_view_->saveSignal(origin_s, s); }));
|
||||
connections_.push_back(binary_view_->showChart.connect([this](const MessageId &id, const cabana::Signal *sig, bool show, bool merge) { charts_->showChart(id, sig, show, merge); }));
|
||||
connections_.push_back(signal_view_->showChart.connect([this](const MessageId &id, const cabana::Signal *sig, bool show, bool merge) { charts_->showChart(id, sig, show, merge); }));
|
||||
connections_.push_back(signal_view_->highlight.connect([this](const cabana::Signal *sig) { binary_view_->highlight(sig); }));
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool) { updateState(msgs); }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(charts->seriesChanged.connect([this]() { signal_view_->updateChartState(); }));
|
||||
connections_.push_back(can->timeRangeChanged.connect([this](const std::optional<std::pair<double, double>> &range) {
|
||||
char text[64];
|
||||
if (range) snprintf(text, sizeof(text), "%.3f - %.3f", range->first, range->second);
|
||||
heatmap_all_text_ = range ? text : "All";
|
||||
const bool live = !range;
|
||||
if (std::exchange(heatmap_live_, live) != live) binary_view_->setHeatmapLiveMode(live);
|
||||
}));
|
||||
}
|
||||
|
||||
void DetailWidget::drawToolBar() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
auto radio_width = [&](const char *label) { return ImGui::GetFrameHeight() + style.ItemInnerSpacing.x + ImGui::CalcTextSize(label).x; };
|
||||
auto button_width = [&](const char *label) { return ImGui::CalcTextSize(label).x + style.FramePadding.x * 2; };
|
||||
const float right_width = ImGui::CalcTextSize("Heatmap:").x + style.ItemSpacing.x + radio_width("Live") + style.ItemSpacing.x +
|
||||
radio_width(heatmap_all_text_.c_str()) + style.ItemSpacing.x * 3 + 1.0f +
|
||||
button_width(icon::PENCIL) + style.ItemSpacing.x + button_width(icon::X_LG);
|
||||
const float avail = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
pushBoldFont();
|
||||
name_label_.draw(std::max(1.0f, avail - right_width - style.ItemSpacing.x));
|
||||
popBoldFont();
|
||||
|
||||
alignRight(right_width);
|
||||
ImGui::TextUnformatted("Heatmap:");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::RadioButton("Live##heatmap_live_", heatmap_live_) && !heatmap_live_) {
|
||||
heatmap_live_ = true;
|
||||
binary_view_->setHeatmapLiveMode(true);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::RadioButton((heatmap_all_text_ + "##heatmap_all").c_str(), !heatmap_live_) && heatmap_live_) {
|
||||
heatmap_live_ = false;
|
||||
binary_view_->setHeatmapLiveMode(false);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(icon::PENCIL)) editMsg();
|
||||
ImGui::SetItemTooltip("Edit Message");
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!action_remove_msg_enabled_);
|
||||
if (ImGui::Button(icon::X_LG)) UndoStack::instance()->push(new RemoveMsgCommand(msg_id_));
|
||||
ImGui::EndDisabled();
|
||||
disabledItemTooltip("Remove Message");
|
||||
}
|
||||
|
||||
void DetailWidget::showTabBarContextMenu(int index) {
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::MenuItem("Close Other Tabs")) {
|
||||
tabbar_.moveTab(index, 0);
|
||||
tabbar_.setCurrentIndex(0);
|
||||
while (tabbar_.count() > 1) tabbar_.removeTab(1);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
int DetailWidget::findOrAddTab(const MessageId &message_id) {
|
||||
const std::string text = message_id.toString();
|
||||
int index = tabbar_.count() - 1;
|
||||
for ( ; index >= 0; --index) {
|
||||
if (tabbar_.tabText(index) == text) break;
|
||||
}
|
||||
if (index == -1) {
|
||||
index = tabbar_.addTab(text);
|
||||
tabbar_.setTabToolTip(index, msgName(message_id));
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void DetailWidget::setMessage(const MessageId &message_id) {
|
||||
if (std::exchange(msg_id_, message_id) == message_id) return;
|
||||
|
||||
tabbar_.setCurrentIndex(findOrAddTab(message_id));
|
||||
|
||||
signal_view_->setMessage(msg_id_);
|
||||
binary_view_->setMessage(msg_id_);
|
||||
history_log_->setMessage(msg_id_);
|
||||
refresh();
|
||||
}
|
||||
|
||||
std::pair<std::string, std::vector<std::string>> DetailWidget::serializeMessageIds() const {
|
||||
std::vector<std::string> msgs;
|
||||
for (int i = 0; i < tabbar_.count(); ++i) msgs.push_back(tabbar_.tabText(i));
|
||||
return std::make_pair(msg_id_.toString(), msgs);
|
||||
}
|
||||
|
||||
void DetailWidget::restoreTabs(const std::string &active_msg_id, const std::vector<std::string>& msg_ids) {
|
||||
for (const auto& str_id : msg_ids) {
|
||||
MessageId id = MessageId::fromString(str_id);
|
||||
if (dbc()->msg(id) != nullptr)
|
||||
findOrAddTab(id);
|
||||
}
|
||||
|
||||
auto active_id = MessageId::fromString(active_msg_id);
|
||||
if (dbc()->msg(active_id) != nullptr)
|
||||
setMessage(active_id);
|
||||
}
|
||||
|
||||
void DetailWidget::refresh() {
|
||||
std::vector<std::string> warnings;
|
||||
auto msg = dbc()->msg(msg_id_);
|
||||
if (msg) {
|
||||
if (msg_id_.source == INVALID_SOURCE) {
|
||||
warnings.push_back("No messages received.");
|
||||
} else if (msg->size != can->lastMessage(msg_id_).dat.size()) {
|
||||
warnings.push_back("Message size (" + std::to_string(msg->size) + ") is incorrect.");
|
||||
}
|
||||
for (auto s : binary_view_->getOverlappingSignals()) {
|
||||
warnings.push_back(s->name + " has overlapping bits.");
|
||||
}
|
||||
}
|
||||
std::string msg_name = msg ? msg->name + " (" + msg->transmitter + ")" : msgName(msg_id_);
|
||||
name_label_.setText(msg_name);
|
||||
name_label_.setToolTip(msg_name);
|
||||
action_remove_msg_enabled_ = msg != nullptr;
|
||||
|
||||
if (!warnings.empty()) {
|
||||
warning_label_.clear();
|
||||
for (size_t i = 0; i < warnings.size(); ++i) {
|
||||
if (i) warning_label_ += '\n';
|
||||
warning_label_ += warnings[i];
|
||||
}
|
||||
warning_icon_ = msg ? icon::EXCLAMATION_TRIANGLE : icon::INFO_CIRCLE;
|
||||
}
|
||||
warning_widget_visible_ = !warnings.empty();
|
||||
}
|
||||
|
||||
void DetailWidget::updateState(const std::set<MessageId> *msgs) {
|
||||
if ((msgs && !msgs->count(msg_id_)))
|
||||
return;
|
||||
|
||||
if (tab_widget_index_ == 0)
|
||||
binary_view_->updateState();
|
||||
else
|
||||
history_log_->updateState();
|
||||
}
|
||||
|
||||
void DetailWidget::editMsg() {
|
||||
auto msg = dbc()->msg(msg_id_);
|
||||
int size = msg ? msg->size : can->lastMessage(msg_id_).dat.size();
|
||||
edit_dlg_ = std::make_unique<EditMessageDialog>(msg_id_, msgName(msg_id_), size, ImGui::GetWindowWidth());
|
||||
}
|
||||
|
||||
void DetailWidget::drawTabWidget() {
|
||||
|
||||
const float tab_height = ImGui::GetFrameHeight();
|
||||
const float content_height = ImGui::GetContentRegionAvail().y - tab_height - ImGui::GetStyle().ItemSpacing.y;
|
||||
ImGui::BeginChild("tab_widget", ImVec2(0, std::max(content_height, 1.0f)), ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
if (tab_widget_index_ == 0) {
|
||||
|
||||
const float min_height = binary_view_->minimumSizeHint().y;
|
||||
const float avail = ImGui::GetContentRegionAvail().y;
|
||||
const float max_height = std::max(avail - 6.0f - ImGui::GetStyle().ItemSpacing.y * 2 - 1.0f, 1.0f);
|
||||
const float height = std::clamp(min_height, 1.0f, max_height);
|
||||
ImGui::BeginChild("binary_view", ImVec2(0, height));
|
||||
binary_view_rect_ = ImGui::GetCurrentWindow()->Rect();
|
||||
binary_view_->draw();
|
||||
ImGui::EndChild();
|
||||
ImGui::Dummy(ImVec2(0.0f, 6.0f));
|
||||
const float spacing = ImGui::GetStyle().ItemSpacing.y;
|
||||
const ImRect child_rect = ImGui::GetCurrentWindow()->Rect();
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(child_rect.Min.x, ImGui::GetItemRectMin().y - spacing),
|
||||
ImVec2(child_rect.Max.x, ImGui::GetItemRectMax().y + spacing),
|
||||
ImGui::GetColorU32(ImGuiCol_WindowBg));
|
||||
ImGui::BeginChild("signal_view", ImVec2(0, 0));
|
||||
signal_view_rect_ = ImGui::GetCurrentWindow()->Rect();
|
||||
signal_view_->draw();
|
||||
ImGui::EndChild();
|
||||
} else {
|
||||
history_log_->draw();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
const std::string labels[] = {std::string(icon::FILE_EARMARK_RULED) + " Messages", std::string(icon::STOPWATCH) + " Logs"};
|
||||
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
float tabs_width = 0.0f;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
tabs_width += ImGui::TabItemCalcSize(labels[i].c_str(), false).x + (i ? style.ItemInnerSpacing.x : 0.0f);
|
||||
}
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
const float separator_y = ImGui::GetCursorScreenPos().y + ImGui::GetFrameHeight() - 1.0f;
|
||||
window->DrawList->AddLine(ImVec2(window->WorkRect.Min.x, separator_y), ImVec2(window->WorkRect.Max.x, separator_y),
|
||||
ImGui::GetColorU32(ImGuiCol_TabSelected), style.TabBarBorderSize);
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - tabs_width) * 0.5f));
|
||||
|
||||
if (ImGui::BeginTabBar("tab_widget_tabs")) {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
if (ImGui::BeginTabItem(labels[i].c_str())) {
|
||||
if (tab_widget_index_ != i) {
|
||||
tab_widget_index_ = i;
|
||||
if (i == 1) history_log_->onShown();
|
||||
updateState();
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
void DetailWidget::draw() {
|
||||
tabbar_.draw();
|
||||
drawToolBar();
|
||||
|
||||
if (warning_widget_visible_) {
|
||||
ImGui::TextUnformatted(warning_icon_);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(warning_label_.c_str());
|
||||
}
|
||||
|
||||
drawTabWidget();
|
||||
|
||||
if (edit_dlg_ && !edit_dlg_->draw()) {
|
||||
if (edit_dlg_->accepted()) {
|
||||
const auto r = edit_dlg_->result();
|
||||
UndoStack::instance()->push(new EditMsgCommand(r.msg_id, r.name, r.size, r.node, r.comment));
|
||||
}
|
||||
edit_dlg_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::pair<std::string, ImRect>> DetailWidget::helpRects() const {
|
||||
std::vector<std::pair<std::string, ImRect>> rects;
|
||||
if (tab_widget_index_ == 0) {
|
||||
rects.emplace_back(binary_view_->whatsThis(), binary_view_rect_);
|
||||
rects.emplace_back(signal_view_->whatsThis(), signal_view_rect_);
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const std::string &title, int size, float parent_width)
|
||||
: msg_id_(msg_id), original_name_(title), name_edit_(title), size_spin_(size), width_(parent_width * 0.9f) {
|
||||
window_title_ = "Edit message: " + msg_id.toString();
|
||||
|
||||
if (auto msg = dbc()->msg(msg_id)) {
|
||||
node_ = msg->transmitter;
|
||||
comment_edit_ = msg->comment;
|
||||
}
|
||||
validateName(name_edit_);
|
||||
}
|
||||
|
||||
EditMessageDialog::Result EditMessageDialog::result() const {
|
||||
return {msg_id_, utils::trimmed(name_edit_), utils::trimmed(node_), utils::trimmed(comment_edit_), size_spin_};
|
||||
}
|
||||
|
||||
bool EditMessageDialog::draw() {
|
||||
if (closed_) return false;
|
||||
if (!opened_) {
|
||||
ImGui::OpenPopup(window_title_.c_str());
|
||||
opened_ = true;
|
||||
}
|
||||
setNextDialogWindow(ImVec2(0.0f, 0.0f));
|
||||
ImGui::SetNextWindowSize(ImVec2(width_, 0.0f), ImGuiCond_Always);
|
||||
bool open = true;
|
||||
if (ImGui::BeginPopupModal(window_title_.c_str(), &open)) {
|
||||
const float label_width = ImGui::CalcTextSize("Comment").x + ImGui::GetStyle().ItemSpacing.x * 2;
|
||||
auto row = [&](const char *label) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine(label_width);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
};
|
||||
|
||||
if (!error_label_.empty()) {
|
||||
row("");
|
||||
ImGui::TextUnformatted(error_label_.c_str());
|
||||
}
|
||||
row("Name");
|
||||
if (validatedInput("##name", &name_edit_, nameValidator)) {
|
||||
validateName(name_edit_);
|
||||
}
|
||||
|
||||
row("Size");
|
||||
if (ImGui::InputInt("##size", &size_spin_)) size_spin_ = std::clamp(size_spin_, 1, CAN_MAX_DATA_BYTES);
|
||||
|
||||
row("Node");
|
||||
validatedInput("##node", &node_, nameValidator);
|
||||
row("Comment");
|
||||
inputTextMultiline("##comment", &comment_edit_, ImVec2(-FLT_MIN, 192.0f));
|
||||
const bool comment_active = ImGui::IsItemActive();
|
||||
|
||||
bool accept = false, reject = false;
|
||||
if (dialogButtons("OK", &accept, &reject, ok_enabled_)) {
|
||||
accepted_ = accept;
|
||||
closed_ = true;
|
||||
}
|
||||
|
||||
if (!closed_ && ok_enabled_ && !comment_active && ImGui::IsKeyPressed(ImGuiKey_Enter, false)) {
|
||||
accepted_ = true;
|
||||
closed_ = true;
|
||||
}
|
||||
if (!open) closed_ = true;
|
||||
if (closed_) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
} else {
|
||||
closed_ = true;
|
||||
}
|
||||
return !closed_;
|
||||
}
|
||||
|
||||
void EditMessageDialog::validateName(const std::string &text) {
|
||||
bool valid = !iequals(text, UNTITLED);
|
||||
error_label_.clear();
|
||||
if (!text.empty() && valid && text != original_name_) {
|
||||
valid = dbc()->msg(msg_id_.source, text) == nullptr;
|
||||
if (!valid) error_label_ = "Name already exists";
|
||||
}
|
||||
ok_enabled_ = valid;
|
||||
}
|
||||
|
||||
DetailWidget* CenterWidget::ensureDetailWidget() {
|
||||
if (!detail_widget) {
|
||||
detail_widget = std::make_unique<DetailWidget>(charts_);
|
||||
}
|
||||
return detail_widget.get();
|
||||
}
|
||||
|
||||
void CenterWidget::clear() {
|
||||
detail_widget.reset();
|
||||
charts_ = nullptr;
|
||||
}
|
||||
|
||||
void CenterWidget::draw() {
|
||||
if (detail_widget) {
|
||||
detail_widget->draw();
|
||||
} else {
|
||||
drawWelcomeWidget();
|
||||
}
|
||||
}
|
||||
|
||||
void CenterWidget::drawWelcomeWidget() {
|
||||
const ImVec2 win_pos = ImGui::GetWindowPos(), win_size = ImGui::GetWindowSize();
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(win_pos, ImVec2(win_pos.x + win_size.x, win_pos.y + win_size.y), ImGui::GetColorU32(ImGuiCol_ChildBg));
|
||||
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const ImVec2 origin = ImGui::GetCursorPos();
|
||||
auto centered = [&](const char *text, float y) {
|
||||
const ImVec2 size = ImGui::CalcTextSize(text);
|
||||
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - size.x) * 0.5f, y));
|
||||
ImGui::TextUnformatted(text);
|
||||
};
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colorRgb(169, 169, 169));
|
||||
float y = origin.y + avail.y * 0.5f - 90.0f;
|
||||
pushLargeFont();
|
||||
centered("CABANA", y);
|
||||
y += ImGui::GetTextLineHeightWithSpacing();
|
||||
popLargeFont();
|
||||
|
||||
auto newShortcutRow = [&](const char *title, const char *key) {
|
||||
const float w = ImGui::CalcTextSize(title).x + ImGui::CalcTextSize(key).x + 40.0f;
|
||||
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - w) * 0.5f, y));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(title);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled();
|
||||
ImGui::SmallButton(key);
|
||||
ImGui::EndDisabled();
|
||||
y += ImGui::GetFrameHeightWithSpacing();
|
||||
};
|
||||
|
||||
centered("<-Select a message to view details", y);
|
||||
y += ImGui::GetTextLineHeightWithSpacing();
|
||||
newShortcutRow("Pause", "Space");
|
||||
newShortcutRow("Help", "F1");
|
||||
newShortcutRow("WhatsThis", "Shift+F1");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
112
iqpilot/tools/cabana/ui/widgets/detailwidget.h
Normal file
112
iqpilot/tools/cabana/ui/widgets/detailwidget.h
Normal file
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/ui/widgets/binaryview.h"
|
||||
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||
#include "tools/cabana/ui/widgets/historylog.h"
|
||||
#include "tools/cabana/ui/widgets/signalview.h"
|
||||
#include "tools/cabana/ui/widgets/tabbar.h"
|
||||
|
||||
|
||||
class ElidedLabel {
|
||||
public:
|
||||
explicit ElidedLabel(const std::string &text = {});
|
||||
void setText(const std::string &text) { text_ = text; }
|
||||
void setToolTip(const std::string &tip) { tooltip_ = tip; }
|
||||
void draw(float width);
|
||||
|
||||
Observable<> clicked;
|
||||
|
||||
private:
|
||||
std::string text_, tooltip_;
|
||||
};
|
||||
|
||||
|
||||
class EditMessageDialog {
|
||||
public:
|
||||
struct Result {
|
||||
MessageId msg_id;
|
||||
std::string name, node, comment;
|
||||
int size;
|
||||
};
|
||||
|
||||
EditMessageDialog(const MessageId &msg_id, const std::string &title, int size, float parent_width);
|
||||
bool draw();
|
||||
bool accepted() const { return accepted_; }
|
||||
Result result() const;
|
||||
|
||||
private:
|
||||
void validateName(const std::string &text);
|
||||
|
||||
MessageId msg_id_;
|
||||
std::string original_name_;
|
||||
std::string name_edit_;
|
||||
std::string node_;
|
||||
std::string comment_edit_;
|
||||
std::string error_label_;
|
||||
int size_spin_;
|
||||
bool ok_enabled_ = true;
|
||||
std::string window_title_;
|
||||
float width_;
|
||||
bool opened_ = false;
|
||||
bool accepted_ = false;
|
||||
bool closed_ = false;
|
||||
};
|
||||
|
||||
class DetailWidget {
|
||||
public:
|
||||
DetailWidget(ChartsWidget *charts);
|
||||
void setMessage(const MessageId &message_id);
|
||||
void refresh();
|
||||
void draw();
|
||||
std::pair<std::string, std::vector<std::string>> serializeMessageIds() const;
|
||||
void restoreTabs(const std::string &active_msg_id, const std::vector<std::string> &msg_ids);
|
||||
std::vector<std::pair<std::string, ImRect>> helpRects() const;
|
||||
|
||||
private:
|
||||
void drawToolBar();
|
||||
void drawTabWidget();
|
||||
int findOrAddTab(const MessageId& message_id);
|
||||
void showTabBarContextMenu(int index);
|
||||
void editMsg();
|
||||
void updateState(const std::set<MessageId> *msgs = nullptr);
|
||||
|
||||
MessageId msg_id_;
|
||||
const char *warning_icon_ = nullptr;
|
||||
std::string warning_label_;
|
||||
ElidedLabel name_label_;
|
||||
bool warning_widget_visible_ = false;
|
||||
TabBar tabbar_;
|
||||
int tab_widget_index_ = 0;
|
||||
bool action_remove_msg_enabled_ = false;
|
||||
bool heatmap_live_ = true;
|
||||
std::string heatmap_all_text_ = "All";
|
||||
ImRect binary_view_rect_, signal_view_rect_;
|
||||
std::unique_ptr<LogsWidget> history_log_;
|
||||
std::unique_ptr<BinaryView> binary_view_;
|
||||
std::unique_ptr<SignalView> signal_view_;
|
||||
ChartsWidget *charts_;
|
||||
std::unique_ptr<EditMessageDialog> edit_dlg_;
|
||||
Connections connections_;
|
||||
};
|
||||
|
||||
class CenterWidget {
|
||||
public:
|
||||
CenterWidget() = default;
|
||||
void setChartsWidget(ChartsWidget *charts) { charts_ = charts; }
|
||||
void setMessage(const MessageId &message_id) { ensureDetailWidget()->setMessage(message_id); }
|
||||
DetailWidget* getDetailWidget() { return detail_widget.get(); }
|
||||
DetailWidget* ensureDetailWidget();
|
||||
void clear();
|
||||
void draw();
|
||||
|
||||
private:
|
||||
void drawWelcomeWidget();
|
||||
std::unique_ptr<DetailWidget> detail_widget;
|
||||
ChartsWidget *charts_ = nullptr;
|
||||
};
|
||||
307
iqpilot/tools/cabana/ui/widgets/historylog.cc
Normal file
307
iqpilot/tools/cabana/ui/widgets/historylog.cc
Normal file
@@ -0,0 +1,307 @@
|
||||
#include "tools/cabana/ui/widgets/historylog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <iterator>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/dialogs/filedialog.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/ui/widgets/messagebytes.h"
|
||||
#include "tools/cabana/utils/export.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int BATCH_SIZE = 50;
|
||||
constexpr float DISPLAY_TYPE_WIDTH = 90.0f;
|
||||
constexpr float SIGNALS_WIDTH = 160.0f;
|
||||
constexpr float COMPARE_WIDTH = 50.0f;
|
||||
|
||||
std::string formatTime(uint64_t mono_time) {
|
||||
char buf[32] = {};
|
||||
snprintf(buf, sizeof(buf), "%.3f", can->toSeconds(mono_time));
|
||||
return buf;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LogsWidget::LogsWidget() {
|
||||
connections_.push_back(can->seekedTo.connect([this](double) { reset(); }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { reset(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { reset(); }));
|
||||
}
|
||||
|
||||
void LogsWidget::setMessage(const MessageId &message_id) {
|
||||
msg_id_ = message_id;
|
||||
reset();
|
||||
}
|
||||
|
||||
void LogsWidget::reset() {
|
||||
sigs_.clear();
|
||||
if (auto dbc_msg = dbc()->msg(msg_id_)) sigs_ = dbc_msg->getSignals();
|
||||
messages_.clear();
|
||||
hex_colors_ = {};
|
||||
signals_cb_ = comp_box_ = 0;
|
||||
value_edit_.clear();
|
||||
value_edit_modified_ = false;
|
||||
export_btn_enabled_ = false;
|
||||
selected_row_ = selected_col_ = -1;
|
||||
setFilter(0, "", nullptr);
|
||||
}
|
||||
|
||||
void LogsWidget::setFilter(int sig_idx, const std::string &value, std::function<bool(double, double)> cmp) {
|
||||
filter_sig_idx_ = sig_idx;
|
||||
filter_value_ = utils::toDouble(value);
|
||||
filter_cmp_ = value.empty() ? nullptr : cmp;
|
||||
load(true);
|
||||
}
|
||||
|
||||
void LogsWidget::load(bool clear) {
|
||||
if (clear && !messages_.empty()) {
|
||||
messages_.clear();
|
||||
selected_row_ = selected_col_ = -1;
|
||||
}
|
||||
const uint64_t current_time = can->toMonoTime(can->lastMessage(msg_id_).ts) + 1;
|
||||
fetch(messages_.begin(), current_time, messages_.empty() ? 0 : messages_.front().mono_time);
|
||||
}
|
||||
|
||||
bool LogsWidget::canFetchMore() const {
|
||||
const auto &events = can->events(msg_id_);
|
||||
return !events.empty() && !messages_.empty() && messages_.back().mono_time > events.front()->mono_time;
|
||||
}
|
||||
|
||||
void LogsWidget::fetch(std::deque<Message>::iterator insert_pos, uint64_t from_time, uint64_t min_time) {
|
||||
const auto &events = can->events(msg_id_);
|
||||
auto first = std::upper_bound(events.rbegin(), events.rend(), from_time, [](uint64_t ts, auto e) { return ts > e->mono_time; });
|
||||
|
||||
std::vector<Message> msgs;
|
||||
std::vector<double> values(sigs_.size());
|
||||
msgs.reserve(BATCH_SIZE);
|
||||
for (; first != events.rend() && (*first)->mono_time > min_time; ++first) {
|
||||
const CanEvent *e = *first;
|
||||
for (int i = 0; i < sigs_.size(); ++i) {
|
||||
sigs_[i]->getValue(e->dat, e->size, &values[i]);
|
||||
}
|
||||
if (!filter_cmp_ || filter_cmp_(values[filter_sig_idx_], filter_value_)) {
|
||||
msgs.emplace_back(Message{e->mono_time, values, {e->dat, e->dat + e->size}});
|
||||
if (msgs.size() >= BATCH_SIZE && min_time == 0) break;
|
||||
}
|
||||
}
|
||||
if (msgs.empty()) return;
|
||||
|
||||
if (hexMode() && (min_time > 0 || messages_.empty())) {
|
||||
const auto freq = can->lastMessage(msg_id_).freq;
|
||||
const std::vector<uint8_t> no_mask;
|
||||
for (auto &m : msgs) {
|
||||
hex_colors_.compute(msg_id_, m.data.data(), m.data.size(), m.mono_time / (double)1e9, can->getSpeed(), no_mask, freq);
|
||||
m.colors = hex_colors_.colors;
|
||||
}
|
||||
}
|
||||
const int pos = std::distance(messages_.begin(), insert_pos);
|
||||
messages_.insert(insert_pos, std::move_iterator(msgs.begin()), std::move_iterator(msgs.end()));
|
||||
export_btn_enabled_ = true;
|
||||
|
||||
if (selected_row_ >= pos) selected_row_ += msgs.size();
|
||||
}
|
||||
|
||||
void LogsWidget::filterChanged() {
|
||||
if (value_edit_.empty() && !value_edit_modified_) return;
|
||||
|
||||
std::function<bool(double, double)> cmp = nullptr;
|
||||
switch (comp_box_) {
|
||||
case 0: cmp = std::greater<double>{}; break;
|
||||
case 1: cmp = std::equal_to<double>{}; break;
|
||||
case 2: cmp = [](double l, double r) { return l != r; }; break;
|
||||
case 3: cmp = std::less<double>{}; break;
|
||||
}
|
||||
setFilter(signals_cb_, value_edit_, cmp);
|
||||
}
|
||||
|
||||
void LogsWidget::exportToCSV() {
|
||||
std::string dir = settings.last_dir + "/" + can->routeName() + "_" + msgName(msg_id_) + ".csv";
|
||||
FileDialog::getSaveFileName("Export " + msgName(msg_id_) + " to CSV file", dir, ".csv", [this](const std::string &fn) {
|
||||
if (!fn.empty()) {
|
||||
hexMode() ? utils::exportToCSV(fn, msg_id_) : utils::exportSignalsToCSV(fn, msg_id_);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void LogsWidget::draw() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
|
||||
const float export_w = ImGui::CalcTextSize(icon::FILETYPE_CSV).x + style.FramePadding.x * 2;
|
||||
if (!sigs_.empty()) {
|
||||
const float clear_w = value_edit_.empty() ? 0.0f : ImGui::CalcTextSize(icon::X).x + style.FramePadding.x * 2;
|
||||
const float fixed = DISPLAY_TYPE_WIDTH + SIGNALS_WIDTH + COMPARE_WIDTH + clear_w + style.ItemSpacing.x * 4 + export_w;
|
||||
const float value_w = std::clamp(ImGui::GetContentRegionAvail().x - fixed, 30.0f, 120.0f);
|
||||
|
||||
ImGui::SetNextItemWidth(DISPLAY_TYPE_WIDTH);
|
||||
if (ImGui::Combo("##display_type", &display_type_cb_, "Signal\0Hex\0")) {
|
||||
hex_mode_ = display_type_cb_;
|
||||
reset();
|
||||
}
|
||||
ImGui::SetItemTooltip("Display signal value or raw hex value");
|
||||
ImGui::SameLine();
|
||||
std::string sig_items;
|
||||
for (auto s : sigs_) {
|
||||
sig_items += s->name;
|
||||
sig_items += '\0';
|
||||
}
|
||||
sig_items += '\0';
|
||||
ImGui::SetNextItemWidth(SIGNALS_WIDTH);
|
||||
if (ImGui::Combo("##signals", &signals_cb_, sig_items.c_str())) filterChanged();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(COMPARE_WIDTH);
|
||||
if (ImGui::Combo("##comp", &comp_box_, ">\0=\0!=\0<\0")) filterChanged();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(value_w);
|
||||
if (clearableInput("##value", &value_edit_, "", doubleValidator)) {
|
||||
value_edit_modified_ = true;
|
||||
filterChanged();
|
||||
}
|
||||
}
|
||||
alignRight(export_w);
|
||||
ImGui::BeginDisabled(!export_btn_enabled_);
|
||||
if (ImGui::Button(icon::FILETYPE_CSV)) exportToCSV();
|
||||
ImGui::EndDisabled();
|
||||
disabledItemTooltip("Export to CSV file...");
|
||||
|
||||
ImGui::Separator();
|
||||
drawTable();
|
||||
}
|
||||
|
||||
std::string LogsWidget::headerText(int column) const {
|
||||
if (column == 0) return "Time";
|
||||
if (hexMode()) return "Data";
|
||||
std::string text = sigs_[column - 1]->name;
|
||||
if (!sigs_[column - 1]->unit.empty()) text += " (" + sigs_[column - 1]->unit + ")";
|
||||
std::replace(text.begin(), text.end(), '_', ' ');
|
||||
return text;
|
||||
}
|
||||
|
||||
ImVec2 LogsWidget::headerSize(int column, float viewport_width) const {
|
||||
const ImVec2 time_text_size = ImGui::CalcTextSize("000000.000");
|
||||
const ImVec2 time_col_size(time_text_size.x + 10, time_text_size.y + 6);
|
||||
if (column == 0) return time_col_size;
|
||||
const int default_size = std::max(100, (int)((viewport_width - time_col_size.x) / (columnCount() - 1)));
|
||||
const ImVec2 rect = ImGui::CalcTextSize(headerText(column).c_str(), nullptr, false, default_size);
|
||||
return ImVec2{std::max(rect.x + 10, (float)default_size), rect.y + 6};
|
||||
}
|
||||
|
||||
void LogsWidget::drawHeaderCell(ImDrawList *dl, const ImRect &rect, int column) const {
|
||||
if (column > 0 && !hexMode()) {
|
||||
CabanaColor bg = sigs_[column - 1]->color;
|
||||
bg.a = 128;
|
||||
dl->AddRectFilled(rect.Min, rect.Max, toImU32(bg));
|
||||
}
|
||||
const std::string text = headerText(column);
|
||||
const ImU32 color = isDarkTheme() ? toImU32(DarkTheme::bright_text) : ImGui::GetColorU32(ImGuiCol_Text);
|
||||
|
||||
const ImRect r(rect.Min.x + 5, rect.Min.y + 3, rect.Max.x - 5, rect.Max.y - 3);
|
||||
ImFont *font = ImGui::GetFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float wrap_width = std::max(1.0f, r.GetWidth());
|
||||
const char *s = text.c_str();
|
||||
const char *end = s + text.size();
|
||||
float y = r.Min.y;
|
||||
dl->PushClipRect(rect.Min, rect.Max, true);
|
||||
while (s < end) {
|
||||
const char *line_end = font->CalcWordWrapPosition(font_size, s, end, wrap_width);
|
||||
if (line_end == s) line_end = s + 1;
|
||||
const float w = ImGui::CalcTextSize(s, line_end).x;
|
||||
dl->AddText(font, font_size, ImVec2(r.Max.x - w, y), color, s, line_end);
|
||||
y += ImGui::GetTextLineHeight();
|
||||
s = line_end;
|
||||
while (s < end && ImCharIsBlankA(*s)) s++;
|
||||
if (s < end && *s == '\n') s++;
|
||||
}
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
void LogsWidget::drawTable() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const int cols = columnCount();
|
||||
|
||||
|
||||
const float header_width = ImGui::GetContentRegionAvail().x - style.CellPadding.x * 2 * cols -
|
||||
(vscrollbar_visible_ ? style.ScrollbarSize : 0.0f);
|
||||
|
||||
std::vector<ImVec2> sizes(cols);
|
||||
float header_height = 0;
|
||||
for (int i = 0; i < cols; ++i) {
|
||||
sizes[i] = headerSize(i, header_width);
|
||||
header_height = std::max(header_height, sizes[i].y);
|
||||
}
|
||||
if (hexMode() && !messages_.empty()) {
|
||||
sizes[1].x = std::max(sizes[1].x, bytesCellSize(messages_.front().data.size(), false).x);
|
||||
}
|
||||
const float row_height = bytesCellSize(8, false).y;
|
||||
|
||||
|
||||
|
||||
ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX | ImGuiTableFlags_BordersInner |
|
||||
ImGuiTableFlags_SizingFixedFit;
|
||||
|
||||
if (messages_.empty()) flags &= ~ImGuiTableFlags_BordersInnerV;
|
||||
|
||||
float inner_width = 0;
|
||||
for (int i = 0; i < cols; ++i) inner_width += sizes[i].x + style.CellPadding.x * 2;
|
||||
|
||||
bool fetch_more = false;
|
||||
if (ImGui::BeginTable("logs", cols, flags, ImVec2(0, 0), inner_width)) {
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
for (int i = 0; i < cols; ++i) {
|
||||
ImGui::TableSetupColumn(headerText(i).c_str(), ImGuiTableColumnFlags_WidthFixed, sizes[i].x);
|
||||
}
|
||||
ImGuiTable *table = ImGui::GetCurrentTable();
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_Headers, header_height);
|
||||
for (int i = 0; i < cols; ++i) {
|
||||
if (!ImGui::TableSetColumnIndex(i)) continue;
|
||||
drawHeaderCell(painter, ImGui::TableGetCellBgRect(table, i), i);
|
||||
ImGui::Dummy(ImVec2(0, header_height - style.CellPadding.y * 2));
|
||||
}
|
||||
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin(messages_.size(), row_height);
|
||||
while (clipper.Step()) {
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
|
||||
const auto &m = messages_[row];
|
||||
ImGui::TableNextRow(0, row_height);
|
||||
|
||||
ImGui::PushID((void *)(uintptr_t)m.mono_time);
|
||||
for (int col = 0; col < cols; ++col) {
|
||||
if (!ImGui::TableSetColumnIndex(col)) continue;
|
||||
|
||||
const bool cell_selected = selected_row_ == row && selected_col_ == col;
|
||||
ImGui::PushID(col);
|
||||
if (viewSelectable("##cell", cell_selected, ImGuiSelectableFlags_AllowOverlap, ImVec2(0, row_height - style.CellPadding.y * 2))) {
|
||||
selected_row_ = row;
|
||||
selected_col_ = col;
|
||||
}
|
||||
ImGui::PopID();
|
||||
const ImRect rect = ImGui::TableGetCellBgRect(table, col);
|
||||
if (col == 0) {
|
||||
drawTextCell(painter, rect, formatTime(m.mono_time), cell_selected, false);
|
||||
} else if (hexMode()) {
|
||||
drawBytesCell(painter, rect, m.data, &m.colors, cell_selected, false, false);
|
||||
} else {
|
||||
drawTextCell(painter, rect, sigs_[col - 1]->formatValue(m.sig_values[col - 1], false), cell_selected, false);
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (clipper.DisplayEnd >= (int)messages_.size()) fetch_more = true;
|
||||
}
|
||||
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) fetch_more = true;
|
||||
vscrollbar_visible_ = table->InnerWindow->ScrollbarY;
|
||||
ImGui::EndTable();
|
||||
}
|
||||
if (fetch_more && canFetchMore()) fetch(messages_.end(), messages_.back().mono_time, 0);
|
||||
}
|
||||
59
iqpilot/tools/cabana/ui/widgets/historylog.h
Normal file
59
iqpilot/tools/cabana/ui/widgets/historylog.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
|
||||
class LogsWidget {
|
||||
public:
|
||||
LogsWidget();
|
||||
void setMessage(const MessageId &message_id);
|
||||
void updateState() { load(false); }
|
||||
void onShown() { load(true); }
|
||||
void draw();
|
||||
|
||||
private:
|
||||
struct Message {
|
||||
uint64_t mono_time = 0;
|
||||
std::vector<double> sig_values;
|
||||
std::vector<uint8_t> data;
|
||||
std::vector<CabanaColor> colors;
|
||||
};
|
||||
|
||||
bool hexMode() const { return sigs_.empty() || hex_mode_; }
|
||||
int columnCount() const { return hexMode() ? 2 : (int)sigs_.size() + 1; }
|
||||
void reset();
|
||||
void setFilter(int sig_idx, const std::string &value, std::function<bool(double, double)> cmp);
|
||||
void load(bool clear);
|
||||
bool canFetchMore() const;
|
||||
void fetch(std::deque<Message>::iterator insert_pos, uint64_t from_time, uint64_t min_time);
|
||||
void filterChanged();
|
||||
void exportToCSV();
|
||||
void drawTable();
|
||||
std::string headerText(int column) const;
|
||||
ImVec2 headerSize(int column, float viewport_width) const;
|
||||
void drawHeaderCell(ImDrawList *dl, const ImRect &rect, int column) const;
|
||||
|
||||
MessageId msg_id_;
|
||||
std::vector<cabana::Signal *> sigs_;
|
||||
std::deque<Message> messages_;
|
||||
CanData hex_colors_;
|
||||
bool hex_mode_ = false;
|
||||
int filter_sig_idx_ = -1;
|
||||
double filter_value_ = 0;
|
||||
std::function<bool(double, double)> filter_cmp_;
|
||||
int signals_cb_ = 0, comp_box_ = 0, display_type_cb_ = 0;
|
||||
std::string value_edit_;
|
||||
bool value_edit_modified_ = false;
|
||||
bool export_btn_enabled_ = false;
|
||||
int selected_row_ = -1, selected_col_ = -1;
|
||||
bool vscrollbar_visible_ = false;
|
||||
Connections connections_;
|
||||
};
|
||||
60
iqpilot/tools/cabana/ui/widgets/messagebytes.cc
Normal file
60
iqpilot/tools/cabana/ui/widgets/messagebytes.cc
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "tools/cabana/ui/widgets/messagebytes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
ImVec2 byteCellSize() {
|
||||
pushMonoFont();
|
||||
|
||||
const ImFontBaked *baked = ImGui::GetFontBaked();
|
||||
const ImVec2 size(ImGui::CalcTextSize("00 ").x, std::ceil(baked->Ascent) - std::floor(baked->Descent) + 1 + 2);
|
||||
popMonoFont();
|
||||
return size;
|
||||
}
|
||||
|
||||
ImVec2 bytesCellSize(int n, bool multiple_lines) {
|
||||
const ImVec2 byte_size = byteCellSize();
|
||||
const int rows = multiple_lines ? std::max(1, (n + 7) / 8) : 1;
|
||||
const int columns = multiple_lines ? std::min(n, 8) : n;
|
||||
const ImVec2 margin = ImGui::GetStyle().CellPadding;
|
||||
return {columns * byte_size.x + (margin.x + 1) * 2, rows * byte_size.y + (margin.y + 1) * 2};
|
||||
}
|
||||
|
||||
ImU32 cellTextColor(bool selected, bool inactive) {
|
||||
if (selected) return inactive ? withAlpha(highlightedTextColor(), 100) : highlightedTextColor();
|
||||
return ImGui::GetColorU32(inactive ? ImGuiCol_TextDisabled : ImGuiCol_Text);
|
||||
}
|
||||
|
||||
void drawTextCell(ImDrawList *dl, const ImRect &rect, const std::string &text, bool selected, bool inactive) {
|
||||
drawElidedText(dl, rect, text, cellTextColor(selected, inactive));
|
||||
}
|
||||
|
||||
void drawBytesCell(ImDrawList *dl, const ImRect &rect, const std::vector<uint8_t> &bytes, const std::vector<CabanaColor> *colors,
|
||||
bool selected, bool inactive, bool multiple_lines) {
|
||||
const ImU32 text_pen = cellTextColor(selected, inactive);
|
||||
const ImVec2 byte_size = byteCellSize();
|
||||
pushMonoFont();
|
||||
ImFont *font = ImGui::GetFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
for (int i = 0; i < (int)bytes.size(); ++i) {
|
||||
const int row = multiple_lines ? i / 8 : 0;
|
||||
const int column = multiple_lines ? i % 8 : i;
|
||||
const ImVec2 min(rect.Min.x + column * byte_size.x, rect.Min.y + row * byte_size.y);
|
||||
const ImRect r(min, ImVec2(min.x + byte_size.x, min.y + byte_size.y));
|
||||
|
||||
|
||||
ImU32 pen = text_pen;
|
||||
if (colors && i < (int)colors->size() && (*colors)[i].alpha() > 0) {
|
||||
if (selected) {
|
||||
pen = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
dl->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_WindowBg));
|
||||
}
|
||||
dl->AddRectFilled(r.Min, r.Max, toImU32((*colors)[i]));
|
||||
}
|
||||
drawText(dl, r, utils::hexByte(bytes[i]), pen, font, font_size);
|
||||
}
|
||||
popMonoFont();
|
||||
}
|
||||
20
iqpilot/tools/cabana/ui/widgets/messagebytes.h
Normal file
20
iqpilot/tools/cabana/ui/widgets/messagebytes.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/core/color.h"
|
||||
|
||||
|
||||
|
||||
|
||||
ImVec2 byteCellSize();
|
||||
ImVec2 bytesCellSize(int n, bool multiple_lines);
|
||||
ImU32 cellTextColor(bool selected, bool inactive);
|
||||
|
||||
void drawTextCell(ImDrawList *dl, const ImRect &rect, const std::string &text, bool selected, bool inactive);
|
||||
void drawBytesCell(ImDrawList *dl, const ImRect &rect, const std::vector<uint8_t> &bytes, const std::vector<CabanaColor> *colors,
|
||||
bool selected, bool inactive, bool multiple_lines);
|
||||
512
iqpilot/tools/cabana/ui/widgets/messageswidget.cc
Normal file
512
iqpilot/tools/cabana/ui/widgets/messageswidget.cc
Normal file
@@ -0,0 +1,512 @@
|
||||
#include "tools/cabana/ui/widgets/messageswidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <utility>
|
||||
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/ui/widgets/messagebytes.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char *COLUMN_TITLES[MessageList::COLUMN_COUNT] = {"Name", "Bus", "ID", "Node", "Freq", "Count", "Bytes"};
|
||||
constexpr float DEFAULT_SECTION_SIZE = 100.0f;
|
||||
|
||||
|
||||
unsigned int toUInt(const std::string &s, bool *ok, int base) {
|
||||
const char *b = s.data(), *e = b + s.size();
|
||||
while (b < e && std::isspace((unsigned char)*b)) ++b;
|
||||
while (e > b && std::isspace((unsigned char)e[-1])) --e;
|
||||
unsigned int v = 0;
|
||||
auto [p, ec] = std::from_chars(b, e, v, base);
|
||||
*ok = b < e && p == e && ec == std::errc();
|
||||
return *ok ? v : 0;
|
||||
}
|
||||
|
||||
bool parseRange(const std::string &filter, uint32_t value, int base = 10) {
|
||||
|
||||
unsigned int min = std::numeric_limits<unsigned int>::min();
|
||||
unsigned int max = std::numeric_limits<unsigned int>::max();
|
||||
auto s = utils::split(filter, '-');
|
||||
bool ok = s.size() >= 1 && s.size() <= 2;
|
||||
if (ok && !s[0].empty()) min = toUInt(s[0], &ok, base);
|
||||
if (ok && s.size() == 1) {
|
||||
max = min;
|
||||
} else if (ok && s.size() == 2 && !s[1].empty()) {
|
||||
max = toUInt(s[1], &ok, base);
|
||||
}
|
||||
return ok && value >= min && value <= max;
|
||||
}
|
||||
|
||||
|
||||
|
||||
inline ImGuiSortDirection flipSortDirection(ImGuiSortDirection dir) {
|
||||
return dir == ImGuiSortDirection_Ascending ? ImGuiSortDirection_Descending : ImGuiSortDirection_Ascending;
|
||||
}
|
||||
|
||||
std::string formatFreq(float freq) {
|
||||
if (freq <= 0) return "--";
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), freq >= 0.95 ? "%.0f" : "%.2f", freq >= 0.95 ? std::nearbyint(freq) : freq);
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
std::string cellText(const MessageList::Item &item, int column) {
|
||||
const bool seen = item.id.source != INVALID_SOURCE;
|
||||
switch (column) {
|
||||
case MessageList::NAME: return item.name;
|
||||
case MessageList::SOURCE: return seen ? std::to_string(item.id.source) : "N/A";
|
||||
case MessageList::ADDRESS: return utils::toHexString(item.id.address);
|
||||
case MessageList::NODE: return item.node;
|
||||
case MessageList::FREQ: return seen ? formatFreq(can->lastMessage(item.id).freq) : "N/A";
|
||||
case MessageList::COUNT: return seen ? std::to_string(can->lastMessage(item.id).count) : "N/A";
|
||||
case MessageList::DATA: return seen ? "" : "N/A";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
MessageList::MessageList() {
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool has_new_ids) { msgsReceived(msgs, has_new_ids); }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { dbcModified(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { dbcModified(); }));
|
||||
}
|
||||
|
||||
void MessageList::setFilters(const std::map<int, std::string> &filters) {
|
||||
filters_ = filters;
|
||||
filterAndSort();
|
||||
}
|
||||
|
||||
void MessageList::showInactiveMessages(bool show) {
|
||||
show_inactive_messages = show;
|
||||
filterAndSort();
|
||||
}
|
||||
|
||||
void MessageList::dbcModified() {
|
||||
dbc_messages_.clear();
|
||||
for (const auto &[_, m] : dbc()->getMessages(-1)) {
|
||||
dbc_messages_.insert(MessageId{.source = INVALID_SOURCE, .address = m.address});
|
||||
}
|
||||
filterAndSort();
|
||||
}
|
||||
|
||||
void MessageList::sortItems(std::vector<Item> &list) {
|
||||
auto compare = [this](const auto &l, const auto &r) {
|
||||
switch (sort_column_) {
|
||||
case NAME: return std::tie(l.name, l.id) < std::tie(r.name, r.id);
|
||||
case SOURCE: return std::tie(l.id.source, l.id.address) < std::tie(r.id.source, r.id.address);
|
||||
case ADDRESS: return std::tie(l.id.address, l.id.source) < std::tie(r.id.address, r.id.source);
|
||||
case NODE: return std::tie(l.node, l.id) < std::tie(r.node, r.id);
|
||||
case FREQ: return std::tie(can->lastMessage(l.id).freq, l.id) < std::tie(can->lastMessage(r.id).freq, r.id);
|
||||
case COUNT: return std::tie(can->lastMessage(l.id).count, l.id) < std::tie(can->lastMessage(r.id).count, r.id);
|
||||
default: return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (sort_order_ == ImGuiSortDirection_Descending)
|
||||
std::stable_sort(list.rbegin(), list.rend(), compare);
|
||||
else
|
||||
std::stable_sort(list.begin(), list.end(), compare);
|
||||
}
|
||||
|
||||
bool MessageList::match(const Item &item) {
|
||||
if (filters_.empty()) return true;
|
||||
|
||||
bool match = true;
|
||||
const auto &data = can->lastMessage(item.id);
|
||||
for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) {
|
||||
const std::string &txt = it->second;
|
||||
switch (it->first) {
|
||||
case NAME: {
|
||||
match = utils::containsCI(item.name, txt);
|
||||
if (!match) {
|
||||
const auto m = dbc()->msg(item.id);
|
||||
match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(),
|
||||
[&txt](const auto &s) { return utils::containsCI(s->name, txt); });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SOURCE:
|
||||
match = parseRange(txt, item.id.source);
|
||||
break;
|
||||
case ADDRESS:
|
||||
match = utils::containsCI(utils::toHexString(item.id.address), txt);
|
||||
match = match || parseRange(txt, item.id.address, 16);
|
||||
break;
|
||||
case NODE:
|
||||
match = utils::containsCI(item.node, txt);
|
||||
break;
|
||||
case FREQ:
|
||||
match = parseRange(txt, data.freq);
|
||||
break;
|
||||
case COUNT:
|
||||
match = parseRange(txt, data.count);
|
||||
break;
|
||||
case DATA:
|
||||
match = utils::containsCI(utils::toHex(data.dat), txt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
bool MessageList::filterAndSort() {
|
||||
|
||||
std::vector<MessageId> all_messages;
|
||||
all_messages.reserve(can->lastMessages().size() + dbc_messages_.size());
|
||||
auto dbc_msgs = dbc_messages_;
|
||||
for (const auto &[id, m] : can->lastMessages()) {
|
||||
all_messages.push_back(id);
|
||||
dbc_msgs.erase(MessageId{.source = INVALID_SOURCE, .address = id.address});
|
||||
}
|
||||
all_messages.insert(all_messages.end(), dbc_msgs.begin(), dbc_msgs.end());
|
||||
|
||||
std::vector<Item> new_items;
|
||||
new_items.reserve(all_messages.size());
|
||||
for (const auto &id : all_messages) {
|
||||
if (show_inactive_messages || can->isMessageActive(id)) {
|
||||
auto msg = dbc()->msg(id);
|
||||
Item item = {.id = id, .name = msg ? msg->name : UNTITLED, .node = msg ? msg->transmitter : std::string()};
|
||||
if (match(item)) new_items.emplace_back(item);
|
||||
}
|
||||
}
|
||||
sortItems(new_items);
|
||||
|
||||
if (items != new_items) {
|
||||
items = std::move(new_items);
|
||||
changed();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MessageList::msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids) {
|
||||
if (has_new_ids || ((filters_.count(FREQ) || filters_.count(COUNT) || filters_.count(DATA)) &&
|
||||
++sort_threshold_ == STREAM_UPDATE_FPS)) {
|
||||
sort_threshold_ = 0;
|
||||
filterAndSort();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageList::sort(int column, ImGuiSortDirection order) {
|
||||
if (column != DATA) {
|
||||
sort_column_ = column;
|
||||
sort_order_ = order;
|
||||
filterAndSort();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
MessagesWidget::MessagesWidget() {
|
||||
std::iota(display_order_.begin(), display_order_.end(), 0);
|
||||
list_.sort(MessageList::NAME, ImGuiSortDirection_Ascending);
|
||||
|
||||
connections_.push_back(list_.changed.connect([this]() {
|
||||
current_row_ = -1;
|
||||
if (current_msg_id_) selectMessage(*current_msg_id_);
|
||||
updateBytesSectionSize();
|
||||
updateTitle();
|
||||
}));
|
||||
|
||||
suppressHighlighted();
|
||||
}
|
||||
|
||||
std::string MessagesWidget::whatsThis() const {
|
||||
return R"(
|
||||
<b>Message View</b><br/>
|
||||
<span style="color:gray">Byte color</span><br />
|
||||
<span style="color:gray;">■ </span> constant changing<br />
|
||||
<span style="color:blue;">■ </span> increasing<br />
|
||||
<span style="color:red;">■ </span> decreasing<br />
|
||||
<span style="color:gray">Shortcuts</span><br />
|
||||
Horizontal Scrolling: <span style="background-color:lightGray;color:gray"> shift+wheel </span>
|
||||
)";
|
||||
}
|
||||
|
||||
void MessagesWidget::drawToolBar() {
|
||||
ImGui::Dummy(ImVec2(0, std::max(0.0f, 9 - ImGui::GetStyle().ItemSpacing.y)));
|
||||
if (ImGui::Button("Suppress Highlighted")) suppressHighlighted(true);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!suppress_clear_enabled_);
|
||||
const std::string clear_label = suppress_clear_text_ + "##suppress_clear";
|
||||
if (ImGui::Button(clear_label.c_str())) suppressHighlighted(false);
|
||||
ImGui::EndDisabled();
|
||||
disabledItemTooltip("Clear suppressed");
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float checkbox_width = ImGui::CalcTextSize("Suppress Signals").x + ImGui::GetFrameHeight() + style.ItemInnerSpacing.x;
|
||||
const float view_button_width = ImGui::CalcTextSize(icon::THREE_DOTS).x + style.FramePadding.x * 2;
|
||||
alignRight(checkbox_width + style.ItemSpacing.x + view_button_width);
|
||||
|
||||
bool suppress_defined_signals = settings.suppress_defined_signals;
|
||||
if (checkBox("Suppress Signals", &suppress_defined_signals)) can->suppressDefinedSignals(suppress_defined_signals);
|
||||
ImGui::SetItemTooltip("Suppress defined signals");
|
||||
ImGui::SameLine();
|
||||
|
||||
if (toolButton("view_btn", icon::THREE_DOTS, "View...")) ImGui::OpenPopup("menu");
|
||||
}
|
||||
|
||||
void MessagesWidget::updateTitle() {
|
||||
auto stats = std::accumulate(
|
||||
list_.items.begin(), list_.items.end(), std::pair<size_t, size_t>(),
|
||||
[](const auto &pair, const auto &item) {
|
||||
auto m = dbc()->msg(item.id);
|
||||
return m ? std::make_pair(pair.first + 1, pair.second + m->sigs.size()) : pair;
|
||||
});
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "%zu Messages (%zu DBC Messages, %zu Signals)", list_.items.size(), stats.first, stats.second);
|
||||
title_ = buf;
|
||||
}
|
||||
|
||||
void MessagesWidget::selectMessage(const MessageId &msg_id) {
|
||||
auto it = std::find_if(list_.items.cbegin(), list_.items.cend(), [&msg_id](auto &item) { return item.id == msg_id; });
|
||||
if (it != list_.items.cend()) setCurrentRow(std::distance(list_.items.cbegin(), it));
|
||||
}
|
||||
|
||||
void MessagesWidget::setCurrentRow(int row) {
|
||||
if (row < 0 || row >= (int)list_.items.size()) return;
|
||||
current_row_ = row;
|
||||
scroll_to_current_ = true;
|
||||
const auto &id = list_.items[row].id;
|
||||
if (!current_msg_id_ || id != *current_msg_id_) {
|
||||
current_msg_id_ = id;
|
||||
msgSelectionChanged(*current_msg_id_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesWidget::suppressHighlighted(bool from_suppress_add) {
|
||||
int n = from_suppress_add ? can->suppressHighlighted() : (can->clearSuppressed(), 0);
|
||||
suppress_clear_text_ = n > 0 ? "Clear (" + std::to_string(n) + ")" : "Clear";
|
||||
suppress_clear_enabled_ = n > 0;
|
||||
}
|
||||
|
||||
void MessagesWidget::drawContextMenu() {
|
||||
if (!ImGui::BeginPopup("menu")) return;
|
||||
for (int i = 0; i < MessageList::COLUMN_COUNT; ++i) {
|
||||
const int column = display_order_[i];
|
||||
|
||||
if (ImGui::MenuItem(COLUMN_TITLES[column], nullptr, !hidden_[column], column > 0)) {
|
||||
pending_hidden_.emplace_back(column, !hidden_[column]);
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Multi-Line bytes", nullptr, settings.multiple_lines_hex)) {
|
||||
setMultiLineBytes(!settings.multiple_lines_hex);
|
||||
}
|
||||
if (ImGui::MenuItem("Show inactive messages", nullptr, list_.show_inactive_messages)) {
|
||||
list_.showInactiveMessages(!list_.show_inactive_messages);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void MessagesWidget::setMultiLineBytes(bool multi) {
|
||||
settings.multiple_lines_hex = multi;
|
||||
updateBytesSectionSize();
|
||||
}
|
||||
|
||||
void MessagesWidget::updateBytesSectionSize() {
|
||||
int max_bytes = 8;
|
||||
if (!settings.multiple_lines_hex) {
|
||||
for (const auto &[_, m] : can->lastMessages()) {
|
||||
max_bytes = std::max<int>(max_bytes, m.dat.size());
|
||||
}
|
||||
}
|
||||
bytes_section_bytes_ = max_bytes;
|
||||
}
|
||||
|
||||
void MessagesWidget::draw() {
|
||||
drawToolBar();
|
||||
drawTable();
|
||||
if (std::exchange(header_menu_requested_, false)) ImGui::OpenPopup("menu");
|
||||
drawContextMenu();
|
||||
}
|
||||
|
||||
void MessagesWidget::handleKeys() {
|
||||
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) || ImGui::IsAnyItemActive()) return;
|
||||
const int last = (int)list_.items.size() - 1;
|
||||
if (last < 0) return;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && current_row_ > 0) {
|
||||
setCurrentRow(current_row_ - 1);
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && current_row_ < last) {
|
||||
setCurrentRow(current_row_ + 1);
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_Home)) {
|
||||
setCurrentRow(0);
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_End)) {
|
||||
setCurrentRow(last);
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
|
||||
setCurrentRow(std::max(current_row_ - visible_rows_, 0));
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
|
||||
setCurrentRow(std::min(current_row_ + visible_rows_, last));
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesWidget::drawTable() {
|
||||
handleKeys();
|
||||
const bool multiple_lines = settings.multiple_lines_hex;
|
||||
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable |
|
||||
ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders |
|
||||
ImGuiTableFlags_Hideable;
|
||||
|
||||
const float bytes_width = bytesCellSize(bytes_section_bytes_, multiple_lines).x;
|
||||
const float avail_width = ImGui::GetContentRegionAvail().x - (has_scrollbar_y_ ? ImGui::GetStyle().ScrollbarSize : 0);
|
||||
const float inner_width = std::max(avail_width, fixed_columns_width_ + bytes_width);
|
||||
if (!ImGui::BeginTable("messages", MessageList::COLUMN_COUNT, flags, ImVec2(0, 0), inner_width)) return;
|
||||
|
||||
|
||||
ImGui::TableSetupScrollFreeze(0, 2);
|
||||
|
||||
|
||||
ImGuiTable *table = ImGui::GetCurrentTable();
|
||||
table->DisableDefaultContextMenu = true;
|
||||
table->IsContextPopupOpen = false;
|
||||
for (int i = 0; i < MessageList::COLUMN_COUNT; ++i) {
|
||||
|
||||
ImGuiTableColumnFlags column_flags = ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_PreferSortDescending;
|
||||
float width = DEFAULT_SECTION_SIZE;
|
||||
if (i == MessageList::NAME) {
|
||||
column_flags |= ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_NoHide;
|
||||
} else if (i == MessageList::DATA) {
|
||||
column_flags = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_NoResize;
|
||||
width = 0;
|
||||
}
|
||||
ImGui::TableSetupColumn(COLUMN_TITLES[i], column_flags, width);
|
||||
}
|
||||
for (const auto &[column, hide] : pending_hidden_) ImGui::TableSetColumnEnabled(column, !hide);
|
||||
pending_hidden_.clear();
|
||||
|
||||
if (ImGuiTableSortSpecs *specs = ImGui::TableGetSortSpecs(); specs && specs->SpecsDirty) {
|
||||
if (specs->SpecsCount > 0) list_.sort(specs->Specs[0].ColumnIndex, flipSortDirection(specs->Specs[0].SortDirection));
|
||||
specs->SpecsDirty = false;
|
||||
if (current_row_ >= 0) scroll_to_current_ = true;
|
||||
}
|
||||
|
||||
drawHeader();
|
||||
|
||||
const int rows = list_.items.size();
|
||||
if (!multiple_lines) {
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin(rows);
|
||||
if (scroll_to_current_ && current_row_ >= 0) clipper.IncludeItemByIndex(current_row_);
|
||||
while (clipper.Step()) {
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) drawRow(row);
|
||||
}
|
||||
} else {
|
||||
|
||||
for (int row = 0; row < rows; ++row) drawRow(row);
|
||||
}
|
||||
|
||||
|
||||
const ImGuiTableColumn &data_column = table->Columns[MessageList::DATA];
|
||||
fixed_columns_width_ = data_column.IsEnabled ? table->ColumnsGivenWidth - data_column.WidthGiven : 0;
|
||||
has_scrollbar_y_ = table->InnerWindow->ScrollbarY;
|
||||
visible_rows_ = std::max(1, (int)(table->InnerWindow->InnerRect.GetHeight() / bytesCellSize(0, multiple_lines).y) - 2);
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
void MessagesWidget::drawHeader() {
|
||||
if (tableHeadersRow() >= 0) header_menu_requested_ = true;
|
||||
|
||||
ImGuiTable *table = ImGui::GetCurrentTable();
|
||||
for (int i = 0; i < MessageList::COLUMN_COUNT; i++) {
|
||||
display_order_[i] = table->DisplayOrderToIndex[i];
|
||||
hidden_[i] = !(ImGui::TableGetColumnFlags(i) & ImGuiTableColumnFlags_IsEnabled);
|
||||
}
|
||||
|
||||
|
||||
const float clear_width = ImGui::CalcTextSize(icon::X).x + ImGui::GetStyle().FramePadding.x * 2;
|
||||
ImGui::TableNextRow();
|
||||
for (int i = 0; i < MessageList::COLUMN_COUNT; i++) {
|
||||
if (!ImGui::TableSetColumnIndex(i)) continue;
|
||||
ImGui::PushID(i);
|
||||
ImGui::SetNextItemWidth(filters_[i].empty() ? -FLT_MIN : std::max(1.0f, ImGui::GetContentRegionAvail().x - clear_width));
|
||||
const std::string placeholder = std::string("Filter ") + COLUMN_TITLES[i];
|
||||
if (clearableInput("##filter", &filters_[i], placeholder.c_str())) {
|
||||
std::map<int, std::string> filters;
|
||||
for (int c = 0; c < MessageList::COLUMN_COUNT; ++c) {
|
||||
if (!filters_[c].empty()) filters[c] = filters_[c];
|
||||
}
|
||||
list_.setFilters(filters);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesWidget::drawRow(int row) {
|
||||
const auto &item = list_.items[row];
|
||||
const bool selected = row == current_row_;
|
||||
const bool inactive = !can->isMessageActive(item.id);
|
||||
const auto &m = can->lastMessage(item.id);
|
||||
const bool seen = item.id.source != INVALID_SOURCE;
|
||||
const bool multiple_lines = settings.multiple_lines_hex;
|
||||
const float row_height = bytesCellSize(seen ? m.dat.size() : 0, multiple_lines).y - ImGui::GetStyle().CellPadding.y * 2;
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(row);
|
||||
|
||||
bool row_item_submitted = false;
|
||||
for (int column = 0; column < MessageList::COLUMN_COUNT; ++column) {
|
||||
if (!ImGui::TableSetColumnIndex(column)) continue;
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
const float width = ImGui::GetContentRegionAvail().x;
|
||||
const ImRect rect(pos, ImVec2(pos.x + width, pos.y + row_height));
|
||||
|
||||
const bool row_item = !row_item_submitted;
|
||||
if (row_item) {
|
||||
|
||||
|
||||
row_item_submitted = true;
|
||||
|
||||
if (viewSelectable("##row", selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SelectOnClick, ImVec2(0, row_height))) {
|
||||
setCurrentRow(row);
|
||||
}
|
||||
if (selected && scroll_to_current_) {
|
||||
|
||||
const ImGuiWindow *inner = ImGui::GetCurrentTable()->InnerWindow;
|
||||
const float view_top = inner->InnerClipRect.Min.y + inner->DecoInnerSizeY1;
|
||||
const float view_bottom = inner->InnerClipRect.Max.y;
|
||||
if (ImGui::GetItemRectMin().y < view_top) {
|
||||
ImGui::SetScrollHereY(0.0f);
|
||||
} else if (ImGui::GetItemRectMax().y > view_bottom) {
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
}
|
||||
scroll_to_current_ = false;
|
||||
}
|
||||
|
||||
const ImGuiTableColumn &name_col = ImGui::GetCurrentTable()->Columns[MessageList::NAME];
|
||||
const float mouse_x = ImGui::GetIO().MousePos.x;
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && mouse_x >= name_col.MinX && mouse_x < name_col.MaxX) {
|
||||
auto msg = dbc()->msg(item.id);
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(item.name.c_str());
|
||||
if (msg && !msg->comment.empty()) ImGui::TextDisabled("%s", msg->comment.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
if (column == MessageList::DATA && seen) {
|
||||
drawBytesCell(ImGui::GetWindowDrawList(), rect, m.dat, &m.colors, selected, inactive, multiple_lines);
|
||||
} else {
|
||||
drawTextCell(ImGui::GetWindowDrawList(), rect, cellText(item, column), selected, inactive);
|
||||
}
|
||||
|
||||
if (!row_item) ImGui::Dummy(ImVec2(width, row_height));
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
93
iqpilot/tools/cabana/ui/widgets/messageswidget.h
Normal file
93
iqpilot/tools/cabana/ui/widgets/messageswidget.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
|
||||
|
||||
class MessageList {
|
||||
public:
|
||||
enum Column { NAME = 0, SOURCE, ADDRESS, NODE, FREQ, COUNT, DATA, COLUMN_COUNT };
|
||||
|
||||
struct Item {
|
||||
MessageId id;
|
||||
std::string name;
|
||||
std::string node;
|
||||
bool operator==(const Item &other) const { return id == other.id && name == other.name && node == other.node; }
|
||||
};
|
||||
|
||||
MessageList();
|
||||
void sort(int column, ImGuiSortDirection order);
|
||||
void setFilters(const std::map<int, std::string> &filters);
|
||||
void showInactiveMessages(bool show);
|
||||
bool filterAndSort();
|
||||
|
||||
std::vector<Item> items;
|
||||
bool show_inactive_messages = true;
|
||||
Observable<> changed;
|
||||
|
||||
private:
|
||||
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
|
||||
void dbcModified();
|
||||
void sortItems(std::vector<Item> &list);
|
||||
bool match(const Item &item);
|
||||
|
||||
std::map<int, std::string> filters_;
|
||||
std::set<MessageId> dbc_messages_;
|
||||
int sort_column_ = NAME;
|
||||
ImGuiSortDirection sort_order_ = ImGuiSortDirection_Ascending;
|
||||
int sort_threshold_ = 0;
|
||||
Connections connections_;
|
||||
};
|
||||
|
||||
class MessagesWidget {
|
||||
public:
|
||||
MessagesWidget();
|
||||
void draw();
|
||||
void selectMessage(const MessageId &message_id);
|
||||
void suppressHighlighted(bool from_suppress_add = false);
|
||||
const std::string &title() const { return title_; }
|
||||
std::string whatsThis() const;
|
||||
|
||||
Observable<const MessageId &> msgSelectionChanged;
|
||||
|
||||
private:
|
||||
void drawToolBar();
|
||||
void drawTable();
|
||||
void drawHeader();
|
||||
void drawRow(int row);
|
||||
void drawContextMenu();
|
||||
void handleKeys();
|
||||
void setCurrentRow(int row);
|
||||
void updateBytesSectionSize();
|
||||
void updateTitle();
|
||||
void setMultiLineBytes(bool multi);
|
||||
|
||||
MessageList list_;
|
||||
std::optional<MessageId> current_msg_id_;
|
||||
int current_row_ = -1;
|
||||
bool scroll_to_current_ = false;
|
||||
int bytes_section_bytes_ = 8;
|
||||
float fixed_columns_width_ = 0;
|
||||
bool has_scrollbar_y_ = false;
|
||||
int visible_rows_ = 1;
|
||||
std::array<std::string, MessageList::COLUMN_COUNT> filters_;
|
||||
std::array<bool, MessageList::COLUMN_COUNT> hidden_ = {};
|
||||
std::array<int, MessageList::COLUMN_COUNT> display_order_;
|
||||
std::vector<std::pair<int, bool>> pending_hidden_;
|
||||
bool header_menu_requested_ = false;
|
||||
std::string suppress_clear_text_;
|
||||
bool suppress_clear_enabled_ = false;
|
||||
std::string title_ = "MESSAGES";
|
||||
Connections connections_;
|
||||
};
|
||||
87
iqpilot/tools/cabana/ui/widgets/scrollabletabbar.cc
Normal file
87
iqpilot/tools/cabana/ui/widgets/scrollabletabbar.cc
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "tools/cabana/ui/widgets/scrollabletabbar.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui_internal.h"
|
||||
|
||||
namespace {
|
||||
float scrollButtonsWidth() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
return ImGui::GetFrameHeight() * 2.0f + style.ItemInnerSpacing.x + style.ItemSpacing.x * 2.0f;
|
||||
}
|
||||
|
||||
void drawScrollButtons(ImGuiTabBar *tab_bar) {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float size = ImGui::GetFrameHeight();
|
||||
const float max_scroll = std::max(0.0f, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
|
||||
const float start_x = tab_bar->BarRect.Max.x + style.ItemSpacing.x;
|
||||
const ImVec2 backup_pos = ImGui::GetCursorScreenPos();
|
||||
|
||||
ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const bool left = i == 0;
|
||||
ImGui::SetCursorScreenPos(ImVec2(start_x + i * (size + style.ItemInnerSpacing.x), tab_bar->BarRect.Min.y));
|
||||
ImGui::BeginDisabled(left ? tab_bar->ScrollingTarget <= 0.0f : tab_bar->ScrollingTarget >= max_scroll);
|
||||
if (ImGui::Button(left ? "###scroll_left" : "###scroll_right", ImVec2(size, size))) {
|
||||
const float step = (left ? -4.0f : 4.0f) * ImGui::GetFontSize();
|
||||
tab_bar->ScrollingTarget = std::clamp(tab_bar->ScrollingTarget + step, 0.0f, max_scroll);
|
||||
tab_bar->ScrollingAnim = tab_bar->ScrollingTarget;
|
||||
}
|
||||
|
||||
const ImVec2 c((ImGui::GetItemRectMin().x + ImGui::GetItemRectMax().x) * 0.5f,
|
||||
(ImGui::GetItemRectMin().y + ImGui::GetItemRectMax().y) * 0.5f);
|
||||
const float h = std::round(ImGui::GetFontSize() * 0.25f);
|
||||
const float dx = left ? h * 0.5f : -h * 0.5f;
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
painter->PathLineTo(ImVec2(c.x + dx, c.y - h));
|
||||
painter->PathLineTo(ImVec2(c.x - dx, c.y));
|
||||
painter->PathLineTo(ImVec2(c.x + dx, c.y + h));
|
||||
painter->PathStroke(ImGui::GetColorU32(ImGuiCol_Text), ImDrawFlags_None, 1.5f);
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::PopItemFlag();
|
||||
ImGui::SetCursorScreenPos(backup_pos);
|
||||
}
|
||||
|
||||
struct ScrollableTabBar { ImGuiTabBar *tab_bar; bool overflowing; };
|
||||
std::vector<ScrollableTabBar> scrollable_tab_bars;
|
||||
}
|
||||
|
||||
bool beginScrollableTabBar(const char *str_id, ImGuiTabBarFlags flags) {
|
||||
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
ImGuiTabBar *prev_tab_bar = ImGui::TabBarFindByID(window->GetID(str_id));
|
||||
const bool overflowing = prev_tab_bar && prev_tab_bar->WidthAllTabsIdeal > prev_tab_bar->BarRect.GetWidth() + 1.0f;
|
||||
const float backup_work_max_x = window->WorkRect.Max.x;
|
||||
if (overflowing) window->WorkRect.Max.x -= scrollButtonsWidth();
|
||||
const bool open = ImGui::BeginTabBar(str_id, flags | ImGuiTabBarFlags_FittingPolicyScroll | ImGuiTabBarFlags_NoTabListScrollingButtons);
|
||||
window->WorkRect.Max.x = backup_work_max_x;
|
||||
if (open) scrollable_tab_bars.push_back({ImGui::GetCurrentTabBar(), overflowing});
|
||||
return open;
|
||||
}
|
||||
|
||||
void endScrollableTabBar() {
|
||||
ImGui::EndTabBar();
|
||||
const ScrollableTabBar bar = scrollable_tab_bars.back();
|
||||
scrollable_tab_bars.pop_back();
|
||||
if (!bar.overflowing) return;
|
||||
drawScrollButtons(bar.tab_bar);
|
||||
|
||||
|
||||
|
||||
ImGuiTabBar *tab_bar = bar.tab_bar;
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max)) {
|
||||
ImGui::SetKeyOwner(ImGuiKey_MouseWheelX, tab_bar->ID);
|
||||
ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, tab_bar->ID);
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
const float wheel = io.MouseWheelH + io.MouseWheel;
|
||||
if (wheel != 0.0f) {
|
||||
const float max_scroll = std::max(0.0f, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
|
||||
const float step = std::floor(ImGui::GetFontSize() * 2.0f);
|
||||
tab_bar->ScrollingTarget = std::clamp(tab_bar->ScrollingTarget - wheel * step, 0.0f, max_scroll);
|
||||
tab_bar->ScrollingAnim = tab_bar->ScrollingTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
iqpilot/tools/cabana/ui/widgets/scrollabletabbar.h
Normal file
8
iqpilot/tools/cabana/ui/widgets/scrollabletabbar.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
|
||||
|
||||
bool beginScrollableTabBar(const char *str_id, ImGuiTabBarFlags flags = 0);
|
||||
void endScrollableTabBar();
|
||||
975
iqpilot/tools/cabana/ui/widgets/signalview.cc
Normal file
975
iqpilot/tools/cabana/ui/widgets/signalview.cc
Normal file
@@ -0,0 +1,975 @@
|
||||
#include "tools/cabana/ui/widgets/signalview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <future>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/ui/threadpool.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
|
||||
namespace {
|
||||
constexpr float INDENTATION = 20.0f;
|
||||
constexpr float H_MARGIN = 3.0f;
|
||||
constexpr float V_MARGIN = 2.0f;
|
||||
|
||||
constexpr float SIGNAL_ROW_EXTRA = 5.0f;
|
||||
constexpr float SIGNAL_ROW_SCALE = 1.25f;
|
||||
constexpr float FILTER_WIDTH = 160.0f;
|
||||
constexpr float SPARKLINE_SLIDER_WIDTH = 120.0f;
|
||||
constexpr float COLLAPSE_ICON_SIZE = 12.0f;
|
||||
|
||||
|
||||
constexpr int SPARKLINE_RANGE_MAX = 30;
|
||||
constexpr float LABEL_FONT = 12.0f;
|
||||
constexpr float MINMAX_FONT = 10.0f;
|
||||
constexpr int COLOR_LABEL_WIDTH = 18;
|
||||
|
||||
std::string signalTypeToString(cabana::Signal::Type type) {
|
||||
if (type == cabana::Signal::Type::Multiplexor) return "Multiplexor Signal";
|
||||
else if (type == cabana::Signal::Type::Multiplexed) return "Multiplexed Signal";
|
||||
else return "Normal Signal";
|
||||
}
|
||||
|
||||
std::string multiplexIndicator(const cabana::Signal *sig) {
|
||||
return sig->type == cabana::Signal::Type::Multiplexor ? std::string(" M ") : " m" + std::to_string(sig->multiplex_value) + " ";
|
||||
}
|
||||
|
||||
std::string nameText(const SignalModel::Item *item) {
|
||||
return item->type == SignalModel::Item::Sig ? item->sig->name : item->title;
|
||||
}
|
||||
|
||||
float rowHeight() {
|
||||
return ImGui::GetFrameHeight();
|
||||
}
|
||||
|
||||
|
||||
float signalRowHeight() {
|
||||
return std::floor((ImGui::GetFrameHeight() + SIGNAL_ROW_EXTRA) * SIGNAL_ROW_SCALE);
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool valueDescriptionEditor(int column, std::string *text) {
|
||||
ImGui::PushID(column);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
validatedInput("##edit", text, column == 0 ? doubleValidator : nullptr);
|
||||
ImGui::PopStyleVar();
|
||||
const bool clicked = ImGui::IsItemActivated() || ImGui::IsItemClicked();
|
||||
ImGui::PopID();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SignalModel::SignalModel() : root_(new Item) {
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { handleMsgChanged(id); }));
|
||||
connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { handleMsgChanged(id); }));
|
||||
connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); }));
|
||||
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); }));
|
||||
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { handleSignalRemoved(sig); }));
|
||||
}
|
||||
|
||||
void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) {
|
||||
Item *parent_item = new Item{.type = Item::Sig, .parent = root_item, .sig = sig, .title = sig->name};
|
||||
root_item->children.insert(root_item->children.begin() + pos, parent_item);
|
||||
std::string titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type",
|
||||
"Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"};
|
||||
for (int i = 0; i < std::size(titles); ++i) {
|
||||
auto item = new Item{.type = (Item::Type)(i + Item::Name), .parent = parent_item, .sig = sig, .title = titles[i]};
|
||||
parent_item->children.push_back(item);
|
||||
if (item->type == Item::ExtraInfo) {
|
||||
parent_item = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalModel::setMessage(const MessageId &id) {
|
||||
msg_id_ = id;
|
||||
filter_str_ = "";
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SignalModel::setFilter(const std::string &txt) {
|
||||
filter_str_ = txt;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SignalModel::refresh() {
|
||||
root_.reset(new SignalModel::Item);
|
||||
if (auto msg = dbc()->msg(msg_id_)) {
|
||||
for (auto s : msg->getSignals()) {
|
||||
if (filter_str_.empty() || utils::containsCI(s->name, filter_str_)) {
|
||||
insertItem(root_.get(), root_->children.size(), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
modelReset();
|
||||
rowsChanged();
|
||||
}
|
||||
|
||||
bool SignalModel::isEnabled(const Item *item) {
|
||||
return !(item->type == Item::MultiplexValue && item->sig->type != cabana::Signal::Type::Multiplexed);
|
||||
}
|
||||
|
||||
bool SignalModel::isCheckable(const Item *item) {
|
||||
return item->type == Item::Endian || item->type == Item::Signed;
|
||||
}
|
||||
|
||||
bool SignalModel::isEditable(const Item *item) {
|
||||
return item->children.empty() && !isCheckable(item);
|
||||
}
|
||||
|
||||
int SignalModel::signalRow(const cabana::Signal *sig) const {
|
||||
for (int i = 0; i < root_->children.size(); ++i) {
|
||||
if (root_->children[i]->sig == sig) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string SignalModel::valueText(const Item *item) const {
|
||||
switch (item->type) {
|
||||
case Item::Sig: return item->sig_val;
|
||||
case Item::Name: return item->sig->name;
|
||||
case Item::Size: return std::to_string(item->sig->size);
|
||||
case Item::Node: return item->sig->receiver_name;
|
||||
case Item::SignalType: return signalTypeToString(item->sig->type);
|
||||
case Item::MultiplexValue: return std::to_string(item->sig->multiplex_value);
|
||||
case Item::Offset: return doubleToString(item->sig->offset);
|
||||
case Item::Factor: return doubleToString(item->sig->factor);
|
||||
case Item::Unit: return item->sig->unit;
|
||||
case Item::Comment: return item->sig->comment;
|
||||
case Item::Min: return doubleToString(item->sig->min);
|
||||
case Item::Max: return doubleToString(item->sig->max);
|
||||
case Item::Desc: {
|
||||
std::string val_desc;
|
||||
for (auto &[val, desc] : item->sig->val_desc) {
|
||||
if (!val_desc.empty()) val_desc += " ";
|
||||
val_desc += utils::toString(val) + " \"" + desc + "\"";
|
||||
}
|
||||
return val_desc;
|
||||
}
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
||||
bool SignalModel::setData(Item *item, const ItemValue &value) {
|
||||
cabana::Signal s = *item->sig;
|
||||
switch (item->type) {
|
||||
case Item::Name: s.name = value.toString(); break;
|
||||
case Item::Size: s.size = value.toInt(); break;
|
||||
case Item::Node: s.receiver_name = utils::trimmed(value.toString()); break;
|
||||
case Item::SignalType: s.type = (cabana::Signal::Type)value.toInt(); break;
|
||||
case Item::MultiplexValue: s.multiplex_value = value.toInt(); break;
|
||||
case Item::Endian: s.is_little_endian = value.toBool(); break;
|
||||
case Item::Signed: s.is_signed = value.toBool(); break;
|
||||
case Item::Offset: s.offset = value.toDouble(); break;
|
||||
case Item::Factor: s.factor = value.toDouble(); break;
|
||||
case Item::Unit: s.unit = value.toString(); break;
|
||||
case Item::Comment: s.comment = value.toString(); break;
|
||||
case Item::Min: s.min = value.toDouble(); break;
|
||||
case Item::Max: s.max = value.toDouble(); break;
|
||||
case Item::Desc: s.val_desc = value.toValueDescription(); break;
|
||||
default: return false;
|
||||
}
|
||||
return saveSignal(item->sig, s);
|
||||
}
|
||||
|
||||
bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) {
|
||||
auto msg = dbc()->msg(msg_id_);
|
||||
if (s.name != origin_s->name && msg->sig(s.name) != nullptr) {
|
||||
std::string text = "There is already a signal with the same name '" + s.name + "'";
|
||||
MessageBox::warning("Failed to save signal", text);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s.is_little_endian != origin_s->is_little_endian) {
|
||||
s.start_bit = flipBitPos(s.start_bit);
|
||||
}
|
||||
UndoStack::instance()->push(new EditSignalCommand(msg_id_, origin_s, s));
|
||||
return true;
|
||||
}
|
||||
|
||||
void SignalModel::handleMsgChanged(MessageId id) {
|
||||
if (id.address == msg_id_.address) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
|
||||
if (id == msg_id_) {
|
||||
if (filter_str_.empty()) {
|
||||
int i = dbc()->msg(msg_id_)->indexOf(sig);
|
||||
insertItem(root_.get(), i, sig);
|
||||
rowsChanged();
|
||||
} else if (utils::containsCI(sig->name, filter_str_)) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
|
||||
if (int row = signalRow(sig); row != -1) {
|
||||
if (filter_str_.empty()) {
|
||||
|
||||
int to = dbc()->msg(msg_id_)->indexOf(sig);
|
||||
if (to != row) {
|
||||
auto item = root_->children[row];
|
||||
root_->children.erase(root_->children.begin() + row);
|
||||
root_->children.insert(root_->children.begin() + to, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalModel::handleSignalRemoved(const cabana::Signal *sig) {
|
||||
if (int row = signalRow(sig); row != -1) {
|
||||
delete root_->children[row];
|
||||
root_->children.erase(root_->children.begin() + row);
|
||||
rowsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
float SignalView::textWidth(const std::string &text, float font_size) {
|
||||
ImFont *font = ImGui::GetFont();
|
||||
if (!font || ImGui::GetFontSize() <= 0) return 0;
|
||||
return font->CalcTextSizeA(font_size > 0 ? font_size : ImGui::GetFontSize(), FLT_MAX, 0.0f, text.c_str()).x;
|
||||
}
|
||||
|
||||
float SignalView::nameColumnWidth(const SignalModel::Item *item, float widget_width, const std::string &text) const {
|
||||
float spacing = INDENTATION + COLOR_LABEL_WIDTH + 8;
|
||||
std::string txt = text;
|
||||
if (item->type == SignalModel::Item::Sig && item->sig->type != cabana::Signal::Type::Normal) {
|
||||
txt += multiplexIndicator(item->sig);
|
||||
spacing += H_MARGIN * 2;
|
||||
}
|
||||
return std::min<float>(widget_width / 3.0, textWidth(txt) + spacing);
|
||||
}
|
||||
|
||||
void SignalView::paintCell(ImDrawList *painter, const ImRect &option_rect, const SignalModel::Item *item, int column,
|
||||
bool selected, const std::string &text, float viewport_x) const {
|
||||
const float h_margin = H_MARGIN;
|
||||
const float v_margin = V_MARGIN;
|
||||
|
||||
ImRect rect(option_rect.Min.x + h_margin, option_rect.Min.y + v_margin, option_rect.Max.x - h_margin, option_rect.Max.y - v_margin);
|
||||
|
||||
const ImU32 text_color = selected ? highlightedTextColor() : ImGui::GetColorU32(ImGuiCol_Text);
|
||||
|
||||
if (column == 0) {
|
||||
if (item->type == SignalModel::Item::Sig) {
|
||||
|
||||
ImRect icon_rect(rect.Min.x, rect.Min.y, rect.Min.x + COLOR_LABEL_WIDTH, rect.Max.y);
|
||||
painter->AddRectFilled(icon_rect.Min, icon_rect.Max, toImU32(item->sig->color.darker(item->highlight ? 125 : 0)), 3.0f);
|
||||
drawText(painter, icon_rect, std::to_string(item->row() + 1).c_str(), item->highlight ? IM_COL32_WHITE : IM_COL32_BLACK,
|
||||
nullptr, LABEL_FONT);
|
||||
|
||||
rect.Min.x = icon_rect.Max.x + h_margin * 2;
|
||||
|
||||
if (item->sig->type != cabana::Signal::Type::Normal) {
|
||||
const std::string indicator = multiplexIndicator(item->sig);
|
||||
ImRect indicator_rect(rect.Min.x, rect.Min.y, rect.Min.x + ImGui::CalcTextSize(indicator.c_str()).x, rect.Max.y);
|
||||
painter->AddRectFilled(indicator_rect.Min, indicator_rect.Max, IM_COL32(160, 160, 164, 255), 3.0f);
|
||||
drawElidedText(painter, indicator_rect, indicator, IM_COL32_WHITE, false);
|
||||
rect.Min.x = indicator_rect.Max.x + h_margin * 2;
|
||||
}
|
||||
} else {
|
||||
rect.Min.x = viewport_x + INDENTATION + COLOR_LABEL_WIDTH + h_margin * 3;
|
||||
}
|
||||
|
||||
|
||||
if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, false);
|
||||
} else if (column == 1) {
|
||||
if (!item->sparkline.isEmpty()) {
|
||||
const ImVec2 sparkline_size = item->sparkline.size;
|
||||
item->sparkline.draw(painter, rect.Min);
|
||||
|
||||
rect.Min.x += sparkline_size.x + 1;
|
||||
float value_adjust = 10;
|
||||
if (item->highlight || selected) {
|
||||
painter->AddLine(rect.Min, ImVec2(rect.Min.x, rect.Max.y), text_color);
|
||||
rect.Min.x += 5;
|
||||
rect.Min.y -= v_margin;
|
||||
rect.Max.y += v_margin;
|
||||
std::string min = utils::toString(item->sparkline.min_val);
|
||||
std::string max = utils::toString(item->sparkline.max_val);
|
||||
drawText(painter, rect, max.c_str(), text_color, nullptr, MINMAX_FONT, ImVec2(0.0f, 0.0f));
|
||||
drawText(painter, rect, min.c_str(), text_color, nullptr, MINMAX_FONT, ImVec2(0.0f, 1.0f));
|
||||
value_adjust = std::max(textWidth(min, MINMAX_FONT), textWidth(max, MINMAX_FONT)) + 5;
|
||||
} else if (item->sig->type == cabana::Signal::Type::Multiplexed) {
|
||||
|
||||
char freq[64];
|
||||
snprintf(freq, sizeof(freq), "%.2g hz", item->sparkline.freq());
|
||||
ImRect freq_rect(rect.Min.x + 5, rect.Min.y, rect.Max.x, rect.Max.y);
|
||||
drawText(painter, freq_rect, freq, text_color, nullptr, LABEL_FONT, ImVec2(0.0f, 0.5f));
|
||||
value_adjust = textWidth(freq, LABEL_FONT) + 10;
|
||||
}
|
||||
|
||||
rect.Min.x += value_adjust;
|
||||
rect.Max.x -= button_size_.x;
|
||||
if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, true);
|
||||
} else {
|
||||
|
||||
rect.Max.x -= button_size_.x;
|
||||
if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::drawEditor(SignalModel::Item *item) {
|
||||
const bool take_focus = focus_item_ == item;
|
||||
if (take_focus) focus_item_ = nullptr;
|
||||
if (item->type == SignalModel::Item::Name || item->type == SignalModel::Item::Node || item->type == SignalModel::Item::Offset ||
|
||||
item->type == SignalModel::Item::Factor || item->type == SignalModel::Item::MultiplexValue ||
|
||||
item->type == SignalModel::Item::Min || item->type == SignalModel::Item::Max) {
|
||||
ImGuiInputTextCallback validator = nullptr;
|
||||
if (item->type == SignalModel::Item::Name) validator = nameValidator;
|
||||
else if (item->type == SignalModel::Item::Node) validator = nodeValidator;
|
||||
else validator = doubleValidator;
|
||||
|
||||
drawLineEditor(item, validator, take_focus);
|
||||
} else if (item->type == SignalModel::Item::Size) {
|
||||
int v = item->sig->size;
|
||||
if (take_focus) ImGui::SetKeyboardFocusHere();
|
||||
bool changed = ImGui::InputInt("##editor", &v, 1, 100, ImGuiInputTextFlags_AutoSelectAll);
|
||||
if (ImGui::IsItemDeactivated() && ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
|
||||
open_item_ = nullptr;
|
||||
return;
|
||||
}
|
||||
if (ImGui::IsItemDeactivatedAfterEdit() || (changed && !ImGui::IsItemActive())) {
|
||||
queueCommit(item, std::clamp(v, 1, CAN_MAX_DATA_BYTES));
|
||||
}
|
||||
|
||||
if (ImGui::IsItemDeactivated() && (!ImGui::IsItemHovered() || ImGui::IsKeyPressed(ImGuiKey_Enter, false) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false) || ImGui::IsKeyPressed(ImGuiKey_Escape, false))) {
|
||||
open_item_ = nullptr;
|
||||
}
|
||||
} else if (item->type == SignalModel::Item::SignalType) {
|
||||
|
||||
if (combo_focused_ && (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || ImGui::IsKeyPressed(ImGuiKey_Enter, false) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false))) {
|
||||
open_item_ = nullptr;
|
||||
combo_focused_ = false;
|
||||
return;
|
||||
}
|
||||
std::vector<std::pair<std::string, int>> items;
|
||||
items.emplace_back(signalTypeToString(cabana::Signal::Type::Normal), (int)cabana::Signal::Type::Normal);
|
||||
if (!dbc()->msg(model_.msgId())->multiplexor) {
|
||||
items.emplace_back(signalTypeToString(cabana::Signal::Type::Multiplexor), (int)cabana::Signal::Type::Multiplexor);
|
||||
} else if (item->sig->type != cabana::Signal::Type::Multiplexor) {
|
||||
items.emplace_back(signalTypeToString(cabana::Signal::Type::Multiplexed), (int)cabana::Signal::Type::Multiplexed);
|
||||
}
|
||||
std::vector<const char *> names;
|
||||
int current = -1;
|
||||
for (int i = 0; i < items.size(); ++i) {
|
||||
names.push_back(items[i].first.c_str());
|
||||
if (items[i].second == (int)item->sig->type) current = i;
|
||||
}
|
||||
const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, ImGui::GetID("##editor"));
|
||||
if (take_focus) ImGui::SetKeyboardFocusHere();
|
||||
if (ImGui::Combo("##editor", ¤t, names.data(), names.size())) {
|
||||
queueCommit(item, items[current].second);
|
||||
open_item_ = nullptr;
|
||||
}
|
||||
combo_focused_ = ImGui::IsItemFocused() || ImGui::IsPopupOpen(popup_id, ImGuiPopupFlags_None);
|
||||
if (!take_focus && !combo_focused_) open_item_ = nullptr;
|
||||
} else if (item->type == SignalModel::Item::Desc) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, (ImU32)0);
|
||||
const bool clicked = ImGui::Selectable("##editor", false, 0, ImVec2(0, rowHeight()));
|
||||
ImGui::PopStyleColor();
|
||||
drawElidedText(ImGui::GetWindowDrawList(), ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()), model_.valueText(item),
|
||||
highlightedTextColor(), false);
|
||||
if (clicked || take_focus) {
|
||||
desc_dlg_ = std::make_unique<ValueDescriptionDlg>(item->sig->val_desc);
|
||||
desc_dlg_->title = item->sig->name;
|
||||
desc_sig_ = item->sig;
|
||||
}
|
||||
} else {
|
||||
|
||||
drawLineEditor(item, nullptr, take_focus);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::commitEditor() {
|
||||
SignalModel::Item *item = editing_item_;
|
||||
std::string text = edit_text_;
|
||||
closeEditor();
|
||||
if (item && validateEditor(item, text) == ValidState::Acceptable) {
|
||||
queueCommit(item, text);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::closeEditor() {
|
||||
editing_item_ = open_item_ = focus_item_ = nullptr;
|
||||
editor_active_ = refocus_editor_ = enter_pressed_ = combo_focused_ = false;
|
||||
pending_commit_ = nullptr;
|
||||
}
|
||||
|
||||
|
||||
ValidState SignalView::validateEditor(const SignalModel::Item *item, std::string &text) {
|
||||
if (item->type == SignalModel::Item::Name) return validateName(text);
|
||||
if (item->type == SignalModel::Item::Node) return validateNodes(text);
|
||||
if (item->type == SignalModel::Item::Offset || item->type == SignalModel::Item::Factor ||
|
||||
item->type == SignalModel::Item::MultiplexValue || item->type == SignalModel::Item::Min ||
|
||||
item->type == SignalModel::Item::Max) {
|
||||
return validateDouble(text);
|
||||
}
|
||||
return ValidState::Acceptable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void SignalView::drawLineEditor(SignalModel::Item *item, ImGuiInputTextCallback validator, bool take_focus) {
|
||||
const bool editing = editing_item_ == item;
|
||||
const bool was_active = editing && editor_active_;
|
||||
|
||||
std::string text = editing ? edit_text_ : model_.valueText(item);
|
||||
if (take_focus) ImGui::SetKeyboardFocusHere();
|
||||
if (editing && refocus_editor_) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
refocus_editor_ = false;
|
||||
}
|
||||
validatedInput("##editor", &text, validator, "", ImGuiInputTextFlags_AutoSelectAll);
|
||||
if (ImGui::IsItemActivated()) editing_item_ = item;
|
||||
if (editing_item_ != item) return;
|
||||
|
||||
edit_text_ = text;
|
||||
editor_active_ = ImGui::IsItemActive();
|
||||
if (was_active) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
|
||||
|
||||
editing_item_ = open_item_ = nullptr;
|
||||
enter_pressed_ = false;
|
||||
return;
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false)) {
|
||||
enter_pressed_ = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemDeactivated()) {
|
||||
const bool by_enter = std::exchange(enter_pressed_, false);
|
||||
if (!ImGui::IsItemDeactivatedAfterEdit()) {
|
||||
editing_item_ = open_item_ = nullptr;
|
||||
} else if (validateEditor(item, edit_text_) == ValidState::Acceptable) {
|
||||
queueCommit(item, edit_text_);
|
||||
editing_item_ = open_item_ = nullptr;
|
||||
} else if (by_enter) {
|
||||
refocus_editor_ = true;
|
||||
} else {
|
||||
editing_item_ = open_item_ = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::queueCommit(SignalModel::Item *item, const ItemValue &value) {
|
||||
pending_commit_ = [this, item, value]() { model_.setData(item, value); };
|
||||
}
|
||||
|
||||
void SignalView::drawValueDescriptionDlg() {
|
||||
if (!desc_dlg_) return;
|
||||
if (desc_dlg_->draw()) return;
|
||||
|
||||
if (desc_dlg_->accepted) {
|
||||
|
||||
for (auto sig_item : model_.root()->children) {
|
||||
if (sig_item->sig != desc_sig_) continue;
|
||||
for (auto child : sig_item->children) {
|
||||
if (child->type != SignalModel::Item::ExtraInfo) continue;
|
||||
for (auto extra : child->children) {
|
||||
if (extra->type == SignalModel::Item::Desc) queueCommit(extra, desc_dlg_->val_desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
desc_dlg_.reset();
|
||||
desc_sig_ = nullptr;
|
||||
}
|
||||
|
||||
SignalView::SignalView(ChartsWidget *charts) : charts_(charts) {
|
||||
settings.sparkline_range = std::clamp(settings.sparkline_range, 1, SPARKLINE_RANGE_MAX);
|
||||
|
||||
|
||||
|
||||
button_size_ = ImVec2(22 * 2 + TOOLBAR_ITEM_SPACING, 22);
|
||||
updateToolBar();
|
||||
|
||||
connections_.push_back(model_.rowsChanged.connect([this]() { rowsChanged(); }));
|
||||
|
||||
connections_.push_back(model_.modelReset.connect([this]() {
|
||||
closeEditor();
|
||||
|
||||
|
||||
if (first_visible_row_ != -1) {
|
||||
last_visible_row_ = std::min(model_.rowCount() - 1, last_visible_row_ - first_visible_row_);
|
||||
first_visible_row_ = 0;
|
||||
}
|
||||
}));
|
||||
connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); }));
|
||||
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); }));
|
||||
|
||||
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) {
|
||||
if (desc_sig_ == sig) desc_sig_ = nullptr;
|
||||
if ((editing_item_ && editing_item_->sig == sig) || (open_item_ && open_item_->sig == sig) ||
|
||||
(focus_item_ && focus_item_->sig == sig)) closeEditor();
|
||||
handleSignalRemoved(sig);
|
||||
}));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() {
|
||||
desc_sig_ = nullptr;
|
||||
closeEditor();
|
||||
handleSignalRemoved(nullptr);
|
||||
}));
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool) { updateState(msgs); }));
|
||||
}
|
||||
|
||||
std::string SignalView::whatsThis() const {
|
||||
return R"(
|
||||
<b>Signal view</b><br />
|
||||
)";
|
||||
}
|
||||
|
||||
void SignalView::setMessage(const MessageId &id) {
|
||||
filter_edit_.clear();
|
||||
model_.setMessage(id);
|
||||
}
|
||||
|
||||
void SignalView::rowsChanged() {
|
||||
updateToolBar();
|
||||
updateChartState();
|
||||
updateState();
|
||||
}
|
||||
|
||||
void SignalView::rowClicked(SignalModel::Item *item) {
|
||||
if (item->type == SignalModel::Item::Sig || item->type == SignalModel::Item::ExtraInfo) {
|
||||
item->expanded = !item->expanded;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::selectSignal(const cabana::Signal *sig, bool expand) {
|
||||
if (int row = model_.signalRow(sig); row != -1) {
|
||||
auto item = model_.root()->children[row];
|
||||
if (expand) {
|
||||
item->expanded = !item->expanded;
|
||||
}
|
||||
scroll_to_sig_ = sig;
|
||||
current_sig_ = sig;
|
||||
current_type_ = SignalModel::Item::Sig;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::updateChartState() {
|
||||
for (auto item : model_.root()->children) {
|
||||
item->chart_opened = charts_->hasSignal(model_.msgId(), item->sig);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::signalHovered(const cabana::Signal *sig) {
|
||||
auto &children = model_.root()->children;
|
||||
for (int i = 0; i < children.size(); ++i) {
|
||||
children[i]->highlight = children[i]->sig == sig;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::updateToolBar() {
|
||||
signal_count_lb_ = "Signals: " + std::to_string(model_.rowCount());
|
||||
sparkline_label_ = utils::formatSeconds(settings.sparkline_range);
|
||||
}
|
||||
|
||||
void SignalView::setSparklineRange(int value) {
|
||||
settings.sparkline_range = value;
|
||||
updateToolBar();
|
||||
updateState();
|
||||
}
|
||||
|
||||
void SignalView::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
|
||||
if (id.address == model_.msgId().address) {
|
||||
selectSignal(sig);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::handleSignalUpdated(const cabana::Signal *sig) {
|
||||
if (int row = model_.signalRow(sig); row != -1)
|
||||
updateState();
|
||||
}
|
||||
|
||||
void SignalView::handleSignalRemoved(const cabana::Signal *sig) {
|
||||
if (!sig || current_sig_ == sig) {
|
||||
|
||||
current_sig_ = nullptr;
|
||||
auto &children = model_.root()->children;
|
||||
if (sig && !children.empty() && current_row_ >= 0) {
|
||||
current_sig_ = children[std::min<int>(current_row_, children.size() - 1)]->sig;
|
||||
current_type_ = SignalModel::Item::Sig;
|
||||
}
|
||||
}
|
||||
if (!sig || scroll_to_sig_ == sig) scroll_to_sig_ = nullptr;
|
||||
if (!sig || hovered_sig_ == sig) hovered_sig_ = nullptr;
|
||||
}
|
||||
|
||||
float SignalView::widestValueWidth(const cabana::Signal *sig) {
|
||||
const double raw_max = sig->is_signed ? std::ldexp(1.0, sig->size - 1) - 1 : std::ldexp(1.0, sig->size) - 1;
|
||||
const double raw_min = sig->is_signed ? -std::ldexp(1.0, sig->size - 1) : 0.0;
|
||||
float width = 0;
|
||||
for (double raw : {raw_min, raw_max}) {
|
||||
width = std::max(width, textWidth(sig->formatValue(raw * sig->factor + sig->offset)));
|
||||
}
|
||||
for (const auto &[_, desc] : sig->val_desc) {
|
||||
width = std::max(width, textWidth(desc));
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
void SignalView::updateState(const std::set<MessageId> *msgs) {
|
||||
const auto &last_msg = can->lastMessage(model_.msgId());
|
||||
if (model_.rowCount() == 0 || (msgs && !msgs->count(model_.msgId())) || last_msg.dat.size() == 0) return;
|
||||
|
||||
|
||||
|
||||
float max_value_width = 0;
|
||||
for (auto item : model_.root()->children) {
|
||||
double value = 0;
|
||||
if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) {
|
||||
item->sig_val = item->sig->formatValue(value);
|
||||
}
|
||||
max_value_width = std::max(max_value_width, widestValueWidth(item->sig));
|
||||
}
|
||||
|
||||
if (first_visible_row_ != -1 && last_visible_row_ != -1 && last_visible_row_ < model_.rowCount()) {
|
||||
const float min_max_width = textWidth("-000.00", MINMAX_FONT) + 5;
|
||||
float available_width = value_column_width_ - button_size_.x;
|
||||
float value_width = std::min<float>(max_value_width + min_max_width, available_width / 2);
|
||||
ImVec2 size(std::floor(available_width - value_width),
|
||||
std::floor(signalRowHeight() - V_MARGIN * 2));
|
||||
|
||||
|
||||
|
||||
|
||||
const double window_end = can->currentSec();
|
||||
|
||||
|
||||
const double lead_in = settings.sparkline_range * 0.05;
|
||||
|
||||
const auto range = can->eventsInRange(model_.msgId(), std::make_pair(window_end - settings.sparkline_range - lead_in, window_end));
|
||||
const CanEventIter first = range.first, last = range.second;
|
||||
std::vector<std::future<void>> futures;
|
||||
for (int i = first_visible_row_; i <= last_visible_row_; ++i) {
|
||||
auto item = model_.root()->children[i];
|
||||
futures.push_back(ThreadPool::instance().run([item, first, last, size, window_end]() {
|
||||
item->sparkline.update(item->sig, first, last, settings.sparkline_range, size, window_end);
|
||||
}));
|
||||
}
|
||||
for (auto &f : futures) f.get();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float SignalView::toolBarRightWidth(const std::string &range_label) {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
return ImGui::CalcTextSize(range_label.c_str()).x + style.ItemSpacing.x + SPARKLINE_SLIDER_WIDTH + style.ItemSpacing.x +
|
||||
ImGui::GetFont()->CalcTextSizeA(COLLAPSE_ICON_SIZE, FLT_MAX, 0.0f, icon::DASH_SQUARE).x + style.FramePadding.x * 2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
float SignalView::minimumWidth() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float left_width = ImGui::CalcTextSize("Signals: 000").x + style.ItemSpacing.x + FILTER_WIDTH;
|
||||
|
||||
return left_width + style.ItemSpacing.x + toolBarRightWidth("00:00") + (style.WindowPadding.x + style.ChildBorderSize) * 2;
|
||||
}
|
||||
|
||||
void SignalView::draw() {
|
||||
if (!ImGui::BeginChild("SignalView", ImVec2(0, 0), ImGuiChildFlags_Borders)) {
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(signal_count_lb_.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(FILTER_WIDTH);
|
||||
if (clearableInput("##filter_edit", &filter_edit_, "Filter Signal", nonWhitespaceValidator)) {
|
||||
model_.setFilter(filter_edit_);
|
||||
}
|
||||
|
||||
|
||||
alignRight(toolBarRightWidth(sparkline_label_));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(sparkline_label_.c_str());
|
||||
ImGui::SameLine();
|
||||
int range = settings.sparkline_range;
|
||||
if (fusionSliderInt("##sparkline_range_slider", &range, 1, SPARKLINE_RANGE_MAX, SPARKLINE_SLIDER_WIDTH)) {
|
||||
setSparklineRange(range);
|
||||
}
|
||||
ImGui::SetItemTooltip("Sparkline time range");
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), COLLAPSE_ICON_SIZE);
|
||||
const bool collapse = toolButton("collapse_all", icon::DASH_SQUARE, "Collapse All");
|
||||
ImGui::PopFont();
|
||||
if (collapse) collapseAll();
|
||||
|
||||
drawTree();
|
||||
drawValueDescriptionDlg();
|
||||
|
||||
if (pending_commit_) std::exchange(pending_commit_, nullptr)();
|
||||
if (pending_action_) std::exchange(pending_action_, nullptr)();
|
||||
current_row_ = model_.signalRow(current_sig_);
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void SignalView::collapseAll() {
|
||||
commitEditor();
|
||||
for (auto item : model_.root()->children) {
|
||||
item->expanded = false;
|
||||
for (auto child : item->children) child->expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::drawTree() {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
const float min_height = std::max(ImGui::GetContentRegionAvail().y, 300.0f);
|
||||
const bool visible = ImGui::BeginChild("tree", ImVec2(0, min_height), ImGuiChildFlags_None);
|
||||
ImGui::PopStyleVar();
|
||||
if (visible) {
|
||||
DrawContext ctx{ImGui::GetWindowDrawList(), ImGui::GetCursorScreenPos().x, ImGui::GetContentRegionAvail().x, rowHeight()};
|
||||
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) editor_open_on_press_ = open_item_ != nullptr;
|
||||
|
||||
int first_visible = -1, last_visible = -1;
|
||||
auto &children = model_.root()->children;
|
||||
for (int i = 0; i < children.size(); ++i) {
|
||||
ctx.any_visible = false;
|
||||
const bool header_visible = drawItem(children[i], 0, ctx);
|
||||
if (header_visible && first_visible == -1) first_visible = i;
|
||||
if (ctx.any_visible) last_visible = i;
|
||||
}
|
||||
if (first_visible == -1 && last_visible != -1) last_visible = -1;
|
||||
|
||||
bool changed = first_visible != first_visible_row_ || last_visible != last_visible_row_;
|
||||
first_visible_row_ = first_visible;
|
||||
last_visible_row_ = last_visible;
|
||||
scroll_to_sig_ = nullptr;
|
||||
|
||||
if (ctx.name_width > 0) name_column_width_ = ctx.name_width;
|
||||
if (ctx.value_column_width > 0 && ctx.value_column_width != value_column_width_) {
|
||||
value_column_width_ = ctx.value_column_width;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) updateState();
|
||||
|
||||
|
||||
|
||||
if (!ctx.mouse_on_row && ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
current_sig_ = nullptr;
|
||||
current_type_ = SignalModel::Item::Root;
|
||||
}
|
||||
|
||||
if (ctx.hovered_sig != hovered_sig_) {
|
||||
hovered_sig_ = ctx.hovered_sig;
|
||||
highlight(hovered_sig_);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
bool SignalView::drawItem(SignalModel::Item *item, int depth, DrawContext &ctx) {
|
||||
const bool selected = item->sig == current_sig_ && item->type == current_type_;
|
||||
const float row_height = item->type == SignalModel::Item::Sig ? signalRowHeight() : ctx.row_height;
|
||||
const ImVec2 row_min = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 row_max(row_min.x + ctx.width, row_min.y + row_height);
|
||||
const bool row_visible = ImGui::IsRectVisible(row_min, row_max);
|
||||
ctx.any_visible |= row_visible;
|
||||
|
||||
ImGui::PushID(item);
|
||||
ImGui::BeginDisabled(!SignalModel::isEnabled(item));
|
||||
const bool row_clicked = viewSelectable("##row", selected, ImGuiSelectableFlags_AllowOverlap, ImVec2(0, row_height));
|
||||
|
||||
|
||||
const float branch_x = row_min.x + depth * INDENTATION;
|
||||
const bool on_branch = !item->children.empty() && ImGui::GetMousePos().x >= branch_x &&
|
||||
ImGui::GetMousePos().x < branch_x + INDENTATION;
|
||||
if (row_clicked && on_branch) {
|
||||
item->expanded = !item->expanded;
|
||||
} else if (row_clicked) {
|
||||
current_sig_ = item->sig;
|
||||
current_type_ = item->type;
|
||||
|
||||
|
||||
closeEditor();
|
||||
if (SignalModel::isEditable(item) && ImGui::GetMousePos().x >= row_min.x + name_column_width_) {
|
||||
focus_item_ = open_item_ = item;
|
||||
}
|
||||
rowClicked(item);
|
||||
}
|
||||
if (item->type == SignalModel::Item::Sig && item->sig == scroll_to_sig_) {
|
||||
ImGui::SetScrollHereY(0.0f);
|
||||
scroll_to_sig_ = nullptr;
|
||||
}
|
||||
if (ImGui::IsMouseHoveringRect(row_min, row_max)) {
|
||||
ctx.mouse_on_row = true;
|
||||
if (ImGui::IsWindowHovered()) ctx.hovered_sig = item->sig;
|
||||
}
|
||||
|
||||
if (!item->children.empty()) {
|
||||
const float arrow_size = ImGui::GetFontSize() * 0.7f;
|
||||
ImGui::RenderArrow(ctx.draw_list, ImVec2(row_min.x + depth * INDENTATION + 4.0f, row_min.y + (row_height - arrow_size) * 0.5f),
|
||||
ImGui::GetColorU32(ImGuiCol_Text), item->expanded ? ImGuiDir_Down : ImGuiDir_Right, 0.7f);
|
||||
}
|
||||
|
||||
|
||||
const std::string text0 = nameText(item);
|
||||
ctx.name_width = std::max(ctx.name_width, nameColumnWidth(item, ctx.width, text0));
|
||||
const ImRect rect1(ImVec2(row_min.x + name_column_width_, row_min.y), row_max);
|
||||
ctx.value_column_width = rect1.GetWidth();
|
||||
|
||||
|
||||
|
||||
const bool editor_open = selected && open_item_ == item;
|
||||
if (row_visible || editor_open) {
|
||||
const ImRect rect0(ImVec2(row_min.x + (depth + 1) * INDENTATION, row_min.y), ImVec2(row_min.x + name_column_width_, row_max.y));
|
||||
paintCell(ctx.draw_list, rect0, item, 0, selected, text0, ctx.viewport_x);
|
||||
if (item->type == SignalModel::Item::Sig && ImGui::IsMouseHoveringRect(ImVec2(row_min.x, row_min.y), rect0.Max) &&
|
||||
ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && ImGui::BeginTooltip()) {
|
||||
ImGui::TextUnformatted(utils::stripHtml(utils::signalToolTip(item->sig)).c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
if (item->type == SignalModel::Item::Sig) {
|
||||
paintCell(ctx.draw_list, rect1, item, 1, selected, item->sig_val, ctx.viewport_x);
|
||||
drawIndexWidget(item, rect1);
|
||||
} else if (SignalModel::isCheckable(item)) {
|
||||
bool checked = item->type == SignalModel::Item::Endian ? item->sig->is_little_endian : item->sig->is_signed;
|
||||
ImGui::SetCursorScreenPos(ImVec2(rect1.Min.x + H_MARGIN, rect1.Min.y));
|
||||
if (checkBox("##check", &checked)) queueCommit(item, checked);
|
||||
} else if (SignalModel::isEditable(item) && editor_open) {
|
||||
|
||||
ImGui::SetCursorScreenPos(rect1.Min);
|
||||
ImGui::SetNextItemWidth(rect1.GetWidth());
|
||||
drawEditor(item);
|
||||
} else {
|
||||
paintCell(ctx.draw_list, rect1, item, 1, selected, model_.valueText(item), ctx.viewport_x);
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopID();
|
||||
ImGui::SetCursorScreenPos(ImVec2(row_min.x, row_max.y));
|
||||
|
||||
if (item->expanded) {
|
||||
for (auto child : item->children) drawItem(child, depth + 1, ctx);
|
||||
}
|
||||
return row_visible;
|
||||
}
|
||||
|
||||
void SignalView::drawIndexWidget(SignalModel::Item *item, const ImRect &rect) {
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f, 2.0f));
|
||||
const ImVec2 btn_size(ImGui::CalcTextSize(icon::GRAPH_UP).x + 6.0f, ImGui::GetFrameHeight());
|
||||
const ImVec2 size(btn_size.x * 2 + TOOLBAR_ITEM_SPACING, btn_size.y);
|
||||
ImGui::SetCursorScreenPos(ImVec2(rect.Max.x - size.x, rect.Min.y + (rect.GetHeight() - size.y) * 0.5f));
|
||||
|
||||
const auto sig = item->sig;
|
||||
const bool checked = item->chart_opened;
|
||||
if (checked) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
|
||||
if (ImGui::Button((std::string(icon::GRAPH_UP) + "##plot").c_str(), btn_size) && !editor_open_on_press_) {
|
||||
item->chart_opened = !checked;
|
||||
showChart(model_.msgId(), sig, item->chart_opened, ImGui::GetIO().KeyShift);
|
||||
}
|
||||
if (checked) ImGui::PopStyleColor();
|
||||
ImGui::SetItemTooltip("%s", checked ? "Close Plot" : "Show Plot\nSHIFT click to add to previous opened plot");
|
||||
ImGui::SameLine(0.0f, TOOLBAR_ITEM_SPACING);
|
||||
if (ImGui::Button((std::string(icon::X) + "##remove").c_str(), btn_size) && !editor_open_on_press_) {
|
||||
pending_action_ = [this, sig]() { UndoStack::instance()->push(new RemoveSigCommand(model_.msgId(), sig)); };
|
||||
}
|
||||
ImGui::SetItemTooltip("Remove signal");
|
||||
ImGui::PopStyleVar();
|
||||
button_size_ = size;
|
||||
}
|
||||
|
||||
ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions) {
|
||||
for (auto &[val, desc] : descriptions) {
|
||||
table_.emplace_back(utils::toString(val), desc);
|
||||
}
|
||||
}
|
||||
|
||||
bool ValueDescriptionDlg::draw() {
|
||||
const std::string popup_id = title + "###ValueDescriptionDlg";
|
||||
if (!opened_) {
|
||||
ImGui::OpenPopup(popup_id.c_str());
|
||||
opened_ = true;
|
||||
}
|
||||
setNextDialogWindow(ImVec2(500.0f, 0.0f));
|
||||
bool open = true;
|
||||
|
||||
if (!ImGui::BeginPopupModal(popup_id.c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) return ImGui::IsPopupOpen(popup_id.c_str());
|
||||
|
||||
bool closing = false;
|
||||
if (ImGui::Button(icon::PLUS)) {
|
||||
table_.emplace_back("", "");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(current_row_ == -1);
|
||||
if (ImGui::Button(icon::DASH) && current_row_ < table_.size()) {
|
||||
table_.erase(table_.begin() + current_row_);
|
||||
current_row_ = -1;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY;
|
||||
if (ImGui::BeginTable("table", 3, flags, ImVec2(0.0f, 300.0f))) {
|
||||
ImGui::TableSetupScrollFreeze(1, 1);
|
||||
|
||||
ImGui::TableSetupColumn("##row_number", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHeaderLabel,
|
||||
ImGui::CalcTextSize("000").x + ImGui::GetStyle().CellPadding.x * 2);
|
||||
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
||||
ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableHeadersRow();
|
||||
for (int row = 0; row < table_.size(); ++row) {
|
||||
ImGui::PushID(row);
|
||||
ImGui::TableNextRow();
|
||||
if (row == current_row_) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImGuiCol_Header));
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(std::to_string(row + 1).c_str());
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (valueDescriptionEditor(0, &table_[row].first)) current_row_ = row;
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (valueDescriptionEditor(1, &table_[row].second)) current_row_ = row;
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
bool accept = false, reject = false;
|
||||
if (dialogButtons("OK", &accept, &reject)) {
|
||||
if (accept) save();
|
||||
closing = true;
|
||||
}
|
||||
if (!open) closing = true;
|
||||
if (closing) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
return !closing;
|
||||
}
|
||||
|
||||
void ValueDescriptionDlg::save() {
|
||||
for (int i = 0; i < table_.size(); ++i) {
|
||||
std::string val = utils::trimmed(table_[i].first);
|
||||
std::string desc = utils::trimmed(table_[i].second);
|
||||
if (!val.empty() && !desc.empty()) {
|
||||
val_desc.push_back({utils::toDouble(val), desc});
|
||||
}
|
||||
}
|
||||
accepted = true;
|
||||
}
|
||||
201
iqpilot/tools/cabana/ui/widgets/signalview.h
Normal file
201
iqpilot/tools/cabana/ui/widgets/signalview.h
Normal file
@@ -0,0 +1,201 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||
#include "tools/cabana/ui/chart/sparkline.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
|
||||
class ItemValue {
|
||||
public:
|
||||
ItemValue(const std::string &s) : str_(s) {}
|
||||
ItemValue(int v) : str_(std::to_string(v)) {}
|
||||
ItemValue(bool v) : str_(v ? "1" : "0") {}
|
||||
ItemValue(const ValueDescription &v) : val_desc_(v) {}
|
||||
ItemValue(const char *) = delete;
|
||||
std::string toString() const { return str_; }
|
||||
int toInt() const { return utils::toInt(str_); }
|
||||
bool toBool() const { return str_ == "1"; }
|
||||
double toDouble() const { return utils::toDouble(str_); }
|
||||
const ValueDescription &toValueDescription() const { return val_desc_; }
|
||||
|
||||
private:
|
||||
std::string str_;
|
||||
ValueDescription val_desc_;
|
||||
};
|
||||
|
||||
class SignalModel {
|
||||
public:
|
||||
struct Item {
|
||||
enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc };
|
||||
~Item() { for (auto c : children) delete c; }
|
||||
inline int row() const {
|
||||
auto it = std::find(parent->children.begin(), parent->children.end(), this);
|
||||
return it != parent->children.end() ? std::distance(parent->children.begin(), it) : -1;
|
||||
}
|
||||
|
||||
Type type = Type::Root;
|
||||
Item *parent = nullptr;
|
||||
std::vector<Item *> children;
|
||||
|
||||
const cabana::Signal *sig = nullptr;
|
||||
std::string title;
|
||||
bool highlight = false;
|
||||
std::string sig_val = "-";
|
||||
Sparkline sparkline;
|
||||
bool expanded = false;
|
||||
bool chart_opened = false;
|
||||
};
|
||||
|
||||
SignalModel();
|
||||
Item *root() const { return root_.get(); }
|
||||
const MessageId &msgId() const { return msg_id_; }
|
||||
int rowCount() const { return root_->children.size(); }
|
||||
static bool isEnabled(const Item *item);
|
||||
static bool isEditable(const Item *item);
|
||||
static bool isCheckable(const Item *item);
|
||||
std::string valueText(const Item *item) const;
|
||||
bool setData(Item *item, const ItemValue &value);
|
||||
void setMessage(const MessageId &id);
|
||||
void setFilter(const std::string &txt);
|
||||
bool saveSignal(const cabana::Signal *origin_s, cabana::Signal &s);
|
||||
int signalRow(const cabana::Signal *sig) const;
|
||||
|
||||
Observable<> rowsChanged;
|
||||
Observable<> modelReset;
|
||||
|
||||
private:
|
||||
void insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig);
|
||||
void handleSignalAdded(MessageId id, const cabana::Signal *sig);
|
||||
void handleSignalUpdated(const cabana::Signal *sig);
|
||||
void handleSignalRemoved(const cabana::Signal *sig);
|
||||
void handleMsgChanged(MessageId id);
|
||||
void refresh();
|
||||
|
||||
MessageId msg_id_;
|
||||
std::string filter_str_;
|
||||
std::unique_ptr<Item> root_;
|
||||
Connections connections_;
|
||||
};
|
||||
|
||||
|
||||
class ValueDescriptionDlg {
|
||||
public:
|
||||
ValueDescriptionDlg(const ValueDescription &descriptions);
|
||||
bool draw();
|
||||
ValueDescription val_desc;
|
||||
std::string title;
|
||||
bool accepted = false;
|
||||
|
||||
private:
|
||||
void save();
|
||||
std::vector<std::pair<std::string, std::string>> table_;
|
||||
int current_row_ = -1;
|
||||
bool opened_ = false;
|
||||
};
|
||||
|
||||
class SignalView {
|
||||
public:
|
||||
SignalView(ChartsWidget *charts);
|
||||
void setMessage(const MessageId &id);
|
||||
void draw();
|
||||
static float minimumWidth();
|
||||
void signalHovered(const cabana::Signal *sig);
|
||||
void updateChartState();
|
||||
void selectSignal(const cabana::Signal *sig, bool expand = false);
|
||||
bool saveSignal(const cabana::Signal *origin, cabana::Signal &s) { return model_.saveSignal(origin, s); }
|
||||
std::string whatsThis() const;
|
||||
|
||||
Observable<const cabana::Signal *> highlight;
|
||||
Observable<const MessageId &, const cabana::Signal *, bool, bool> showChart;
|
||||
|
||||
private:
|
||||
void rowsChanged();
|
||||
void rowClicked(SignalModel::Item *item);
|
||||
static float toolBarRightWidth(const std::string &range_label);
|
||||
void updateToolBar();
|
||||
void setSparklineRange(int value);
|
||||
void handleSignalAdded(MessageId id, const cabana::Signal *sig);
|
||||
void handleSignalUpdated(const cabana::Signal *sig);
|
||||
void handleSignalRemoved(const cabana::Signal *sig);
|
||||
void updateState(const std::set<MessageId> *msgs = nullptr);
|
||||
|
||||
struct DrawContext {
|
||||
ImDrawList *draw_list;
|
||||
float viewport_x;
|
||||
float width;
|
||||
float row_height;
|
||||
float name_width = 0;
|
||||
float value_column_width = 0;
|
||||
bool any_visible = false;
|
||||
bool mouse_on_row = false;
|
||||
const cabana::Signal *hovered_sig = nullptr;
|
||||
};
|
||||
void drawTree();
|
||||
bool drawItem(SignalModel::Item *item, int depth, DrawContext &ctx);
|
||||
void drawIndexWidget(SignalModel::Item *item, const ImRect &rect);
|
||||
void collapseAll();
|
||||
static float widestValueWidth(const cabana::Signal *sig);
|
||||
|
||||
|
||||
void paintCell(ImDrawList *painter, const ImRect &rect, const SignalModel::Item *item, int column, bool selected,
|
||||
const std::string &text, float viewport_x) const;
|
||||
float nameColumnWidth(const SignalModel::Item *item, float widget_width, const std::string &text) const;
|
||||
|
||||
void drawEditor(SignalModel::Item *item);
|
||||
|
||||
|
||||
void queueCommit(SignalModel::Item *item, const ItemValue &value);
|
||||
void drawValueDescriptionDlg();
|
||||
static float textWidth(const std::string &text, float font_size = 0);
|
||||
void closeEditor();
|
||||
void commitEditor();
|
||||
|
||||
void drawLineEditor(SignalModel::Item *item, ImGuiInputTextCallback validator, bool take_focus);
|
||||
static ValidState validateEditor(const SignalModel::Item *item, std::string &text);
|
||||
|
||||
float value_column_width_ = 0;
|
||||
float name_column_width_ = 150;
|
||||
bool editor_open_on_press_ = false;
|
||||
|
||||
|
||||
int first_visible_row_ = -1;
|
||||
int last_visible_row_ = -1;
|
||||
const cabana::Signal *current_sig_ = nullptr;
|
||||
int current_row_ = -1;
|
||||
SignalModel::Item::Type current_type_ = SignalModel::Item::Root;
|
||||
const cabana::Signal *scroll_to_sig_ = nullptr;
|
||||
const cabana::Signal *hovered_sig_ = nullptr;
|
||||
std::function<void()> pending_action_;
|
||||
std::string sparkline_label_;
|
||||
std::string filter_edit_;
|
||||
ChartsWidget *charts_;
|
||||
std::string signal_count_lb_;
|
||||
|
||||
ImVec2 button_size_ = {};
|
||||
|
||||
SignalModel::Item *focus_item_ = nullptr;
|
||||
|
||||
SignalModel::Item *open_item_ = nullptr;
|
||||
std::function<void()> pending_commit_;
|
||||
SignalModel::Item *editing_item_ = nullptr;
|
||||
std::string edit_text_;
|
||||
bool editor_active_ = false;
|
||||
bool refocus_editor_ = false;
|
||||
bool enter_pressed_ = false;
|
||||
bool combo_focused_ = false;
|
||||
std::unique_ptr<ValueDescriptionDlg> desc_dlg_;
|
||||
const cabana::Signal *desc_sig_ = nullptr;
|
||||
SignalModel model_;
|
||||
Connections connections_;
|
||||
};
|
||||
92
iqpilot/tools/cabana/ui/widgets/tabbar.cc
Normal file
92
iqpilot/tools/cabana/ui/widgets/tabbar.cc
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "tools/cabana/ui/widgets/tabbar.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "tools/cabana/ui/widgets/scrollabletabbar.h"
|
||||
|
||||
int TabBar::addTab(const std::string &text) {
|
||||
tabs_.push_back({text, 0, next_id_++});
|
||||
int index = count() - 1;
|
||||
if (current_index_ == -1) {
|
||||
current_index_ = index;
|
||||
select_current_ = true;
|
||||
currentChanged(index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void TabBar::setCurrentIndex(int index) {
|
||||
if (index == current_index_ || index < -1 || index >= count()) return;
|
||||
current_index_ = index;
|
||||
select_current_ = true;
|
||||
currentChanged(index);
|
||||
}
|
||||
|
||||
int TabBar::tabAt(const ImVec2 &pos) const {
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (tabs_[i].rect.Contains(pos)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TabBar::removeTab(int index) {
|
||||
tabs_.erase(tabs_.begin() + index);
|
||||
if (index == current_index_) {
|
||||
|
||||
current_index_ = count() ? std::min(index, count() - 1) : -1;
|
||||
select_current_ = true;
|
||||
currentChanged(current_index_);
|
||||
} else if (index < current_index_) {
|
||||
--current_index_;
|
||||
}
|
||||
}
|
||||
|
||||
void TabBar::moveTab(int from, int to) {
|
||||
if (from == to || from < 0 || from >= count() || to < 0 || to >= count()) return;
|
||||
const int current_id = current_index_ >= 0 ? tabs_[current_index_].id : -1;
|
||||
Tab tab = std::move(tabs_[from]);
|
||||
tabs_.erase(tabs_.begin() + from);
|
||||
tabs_.insert(tabs_.begin() + to, std::move(tab));
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (tabs_[i].id == current_id) current_index_ = i;
|
||||
}
|
||||
select_current_ = true;
|
||||
}
|
||||
|
||||
void TabBar::draw() {
|
||||
if (auto_hide_ && count() < 2) return;
|
||||
ImGui::PushID(this);
|
||||
|
||||
if (!(scroll_buttons_ ? beginScrollableTabBar("##tabbar", ImGuiTabBarFlags_NoTooltip) : ImGui::BeginTabBar("##tabbar", ImGuiTabBarFlags_NoTooltip))) {
|
||||
ImGui::PopID();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float close_button_min_width = tabs_closable_ ? std::exchange(style.TabCloseButtonMinWidthUnselected, -1.0f) : 0.0f;
|
||||
|
||||
const bool select_current = std::exchange(select_current_, false);
|
||||
int close_index = -1;
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
bool open = true;
|
||||
const std::string label = tabs_[i].text + "###tab" + std::to_string(tabs_[i].id);
|
||||
const ImGuiTabItemFlags flags = (select_current && i == current_index_) ? ImGuiTabItemFlags_SetSelected : 0;
|
||||
if (ImGui::BeginTabItem(label.c_str(), tabs_closable_ ? &open : nullptr, flags)) {
|
||||
|
||||
if (!select_current && i != current_index_) {
|
||||
current_index_ = i;
|
||||
currentChanged(i);
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
tabs_[i].rect = ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||
if (!tabs_[i].tooltip.empty()) ImGui::SetItemTooltip("%s", tabs_[i].tooltip.c_str());
|
||||
tabContextMenu(i);
|
||||
if (!open) close_index = i;
|
||||
}
|
||||
if (tabs_closable_) style.TabCloseButtonMinWidthUnselected = close_button_min_width;
|
||||
scroll_buttons_ ? endScrollableTabBar() : ImGui::EndTabBar();
|
||||
ImGui::PopID();
|
||||
if (close_index >= 0) tabCloseRequested(close_index);
|
||||
}
|
||||
45
iqpilot/tools/cabana/ui/widgets/tabbar.h
Normal file
45
iqpilot/tools/cabana/ui/widgets/tabbar.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
#include "tools/cabana/core/observable.h"
|
||||
|
||||
|
||||
class TabBar {
|
||||
public:
|
||||
TabBar() = default;
|
||||
int addTab(const std::string &text);
|
||||
int count() const { return (int)tabs_.size(); }
|
||||
void setTabText(int index, const std::string &text) { if (index >= 0 && index < count()) tabs_[index].text = text; }
|
||||
const std::string &tabText(int index) const { return tabs_[index].text; }
|
||||
void setTabToolTip(int index, const std::string &tip) { if (index >= 0 && index < count()) tabs_[index].tooltip = tip; }
|
||||
void setTabData(int index, int data) { if (index >= 0 && index < count()) tabs_[index].data = data; }
|
||||
int tabData(int index) const { return index >= 0 && index < count() ? tabs_[index].data : 0; }
|
||||
int currentIndex() const { return current_index_; }
|
||||
void setCurrentIndex(int index);
|
||||
int tabAt(const ImVec2 &pos) const;
|
||||
void removeTab(int index);
|
||||
void moveTab(int from, int to);
|
||||
void setAutoHide(bool hide) { auto_hide_ = hide; }
|
||||
void setTabsClosable(bool closable) { tabs_closable_ = closable; }
|
||||
void setUsesScrollButtons(bool use) { scroll_buttons_ = use; }
|
||||
void draw();
|
||||
|
||||
Observable<int> currentChanged;
|
||||
Observable<int> tabCloseRequested;
|
||||
Observable<int> tabContextMenu;
|
||||
|
||||
private:
|
||||
struct Tab { std::string text; int data = 0; int id = 0; std::string tooltip; ImRect rect; };
|
||||
std::vector<Tab> tabs_;
|
||||
int current_index_ = -1;
|
||||
int next_id_ = 0;
|
||||
bool select_current_ = false;
|
||||
bool auto_hide_ = false;
|
||||
bool tabs_closable_ = false;
|
||||
bool scroll_buttons_ = false;
|
||||
};
|
||||
612
iqpilot/tools/cabana/ui/widgets/videowidget.cc
Normal file
612
iqpilot/tools/cabana/ui/widgets/videowidget.cc
Normal file
@@ -0,0 +1,612 @@
|
||||
#include "tools/cabana/ui/widgets/videowidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
}
|
||||
#include <capnp/serialize.h>
|
||||
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/threadpool.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
const int MIN_VIDEO_HEIGHT = 100;
|
||||
const int THUMBNAIL_MARGIN = 3;
|
||||
const float POINT_10_FONT_SIZE = 13.0f;
|
||||
const float POINT_16_FONT_SIZE = 21.0f;
|
||||
const float TOOLBAR_MARGIN_Y = 6.0f;
|
||||
const float TOOLBAR_SEPARATOR_EXTENT = 6.0f;
|
||||
const float SLIDER_HEIGHT = 15.0f;
|
||||
|
||||
|
||||
static const ImU32 timeline_colors[] = {
|
||||
IM_COL32(111, 143, 175, 255),
|
||||
IM_COL32(0, 163, 108, 255),
|
||||
IM_COL32(0, 255, 0, 255),
|
||||
IM_COL32(255, 195, 0, 255),
|
||||
IM_COL32(199, 0, 57, 255),
|
||||
IM_COL32(255, 0, 255, 255),
|
||||
};
|
||||
|
||||
static const float speeds[] = {0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 0.8, 1., 2., 3., 5.};
|
||||
static const int NORMAL_SPEED_INDEX = std::find(std::begin(speeds), std::end(speeds), 1.0f) - std::begin(speeds);
|
||||
|
||||
static Replay *getReplay() {
|
||||
auto stream = dynamic_cast<ReplayStream *>(can);
|
||||
return stream ? stream->getReplay() : nullptr;
|
||||
}
|
||||
|
||||
static std::string colorName(ImU32 c) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "#%02x%02x%02x", (c >> IM_COL32_R_SHIFT) & 0xff, (c >> IM_COL32_G_SHIFT) & 0xff, (c >> IM_COL32_B_SHIFT) & 0xff);
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
static std::pair<double, double> displayedTimeRange() {
|
||||
return can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds()));
|
||||
}
|
||||
|
||||
|
||||
static bool decodeJpeg(const uint8_t *data, size_t size, RgbImage *out) {
|
||||
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG);
|
||||
AVCodecContext *context = codec ? avcodec_alloc_context3(codec) : nullptr;
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
bool ok = false;
|
||||
if (context && frame && packet && size > 0 && size <= (size_t)INT32_MAX && av_new_packet(packet, (int)size) >= 0) {
|
||||
std::copy(data, data + size, packet->data);
|
||||
ok = avcodec_open2(context, codec, nullptr) >= 0 && avcodec_send_packet(context, packet) >= 0 &&
|
||||
avcodec_receive_frame(context, frame) >= 0 && frame->width > 0 && frame->height > 0;
|
||||
}
|
||||
int chroma_x_shift = 0, chroma_y_shift = 0;
|
||||
if (ok) {
|
||||
switch ((AVPixelFormat)frame->format) {
|
||||
case AV_PIX_FMT_YUV420P: case AV_PIX_FMT_YUVJ420P: chroma_x_shift = chroma_y_shift = 1; break;
|
||||
case AV_PIX_FMT_YUV422P: case AV_PIX_FMT_YUVJ422P: chroma_x_shift = 1; break;
|
||||
case AV_PIX_FMT_YUV444P: case AV_PIX_FMT_YUVJ444P: break;
|
||||
default: ok = false; break;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
out->resize(frame->width, frame->height);
|
||||
const bool full_range = frame->color_range == AVCOL_RANGE_JPEG || frame->format == AV_PIX_FMT_YUVJ420P ||
|
||||
frame->format == AV_PIX_FMT_YUVJ422P || frame->format == AV_PIX_FMT_YUVJ444P;
|
||||
const float y_scale = full_range ? 1.0f : 1.164383f;
|
||||
const float y_offset = full_range ? 0.0f : 16.0f;
|
||||
const float kr = full_range ? 1.402f : 1.596027f;
|
||||
const float kgu = full_range ? 0.344136f : 0.391762f;
|
||||
const float kgv = full_range ? 0.714136f : 0.812968f;
|
||||
const float kb = full_range ? 1.772f : 2.017232f;
|
||||
for (int y = 0; y < frame->height; ++y) {
|
||||
const uint8_t *y_row = frame->data[0] + y * frame->linesize[0];
|
||||
const uint8_t *u_row = frame->data[1] + (y >> chroma_y_shift) * frame->linesize[1];
|
||||
const uint8_t *v_row = frame->data[2] + (y >> chroma_y_shift) * frame->linesize[2];
|
||||
uint8_t *dst = out->data.data() + (size_t)y * out->bytesPerLine();
|
||||
for (int x = 0; x < frame->width; ++x) {
|
||||
const float luma = y_scale * ((float)y_row[x] - y_offset);
|
||||
const float u = (float)u_row[x >> chroma_x_shift] - 128.0f;
|
||||
const float v = (float)v_row[x >> chroma_x_shift] - 128.0f;
|
||||
const float r = luma + kr * v;
|
||||
const float g = luma - kgu * u - kgv * v;
|
||||
const float b = luma + kb * u;
|
||||
dst[x * 4 + 0] = (uint8_t)std::clamp(std::lround(r), 0L, 255L);
|
||||
dst[x * 4 + 1] = (uint8_t)std::clamp(std::lround(g), 0L, 255L);
|
||||
dst[x * 4 + 2] = (uint8_t)std::clamp(std::lround(b), 0L, 255L);
|
||||
dst[x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return ok;
|
||||
}
|
||||
|
||||
VideoWidget::VideoWidget() {
|
||||
if (!can->liveStreaming())
|
||||
createCameraWidget();
|
||||
|
||||
createSpeedDropdown();
|
||||
|
||||
connections_.push_back(can->timeRangeChanged.connect([this](const auto &) { timeRangeChanged(); }));
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *, bool) { msgs_received_ = true; }));
|
||||
}
|
||||
|
||||
std::string VideoWidget::whatsThis() const {
|
||||
|
||||
return "<b>Video</b><br />\n"
|
||||
"<span style=\"color:gray\">Timeline color</span><br />\n" +
|
||||
colorName(timeline_colors[(int)TimelineType::None]) + " Disengaged " +
|
||||
colorName(timeline_colors[(int)TimelineType::Engaged]) + " Engaged<br />\n" +
|
||||
colorName(timeline_colors[(int)TimelineType::UserBookmark]) + " User Flag " +
|
||||
colorName(timeline_colors[(int)TimelineType::AlertInfo]) + " Info<br />\n" +
|
||||
colorName(timeline_colors[(int)TimelineType::AlertWarning]) + " Warning " +
|
||||
colorName(timeline_colors[(int)TimelineType::AlertCritical]) + " Critical<br />\n"
|
||||
"<span style=\"color:gray\">Shortcuts</span><br />\n"
|
||||
"Pause/Resume: <span style=\"background-color:lightGray;color:gray\"> space </span>";
|
||||
}
|
||||
|
||||
static float toolbarHeight() { return TOOLBAR_MARGIN_Y + ImGui::GetFrameHeight(); }
|
||||
|
||||
void VideoWidget::drawPlaybackController() {
|
||||
beginToolbar();
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + TOOLBAR_MARGIN_Y);
|
||||
const float speed_width = menuButtonWidth("0.05x ", true);
|
||||
|
||||
const char *play_icon = can->isPaused() ? icon::PLAY : icon::PAUSE;
|
||||
const char *play_tooltip = can->isPaused() ? "Play" : "Pause";
|
||||
const char *loop_icon = getReplay() && getReplay()->loop() ? icon::REPEAT : icon::REPEAT_1;
|
||||
const std::string time_text = slider_ ? formatTime(can->currentSec(), true) + " / " + formatTime(slider_->maximum() / slider_->factor)
|
||||
: formatTime(can->currentSec(), true);
|
||||
const char *time_tooltip = settings.absolute_time ? "Elapsed time" : "Absolute time";
|
||||
|
||||
auto seek_backward = []() { can->seekTo(can->currentSec() - 1); };
|
||||
auto toggle_play = []() { can->pause(!can->isPaused()); };
|
||||
auto seek_forward = []() { can->seekTo(can->currentSec() + 1); };
|
||||
|
||||
std::vector<ToolbarItem> items = {
|
||||
{toolbarButtonWidth(icon::REWIND), [&]() { if (toolButton("rewind", icon::REWIND, "Seek backward")) seek_backward(); },
|
||||
"Seek backward", seek_backward},
|
||||
{toolbarButtonWidth(play_icon), [&]() { if (toolButton("play", play_icon, play_tooltip)) toggle_play(); },
|
||||
play_tooltip, toggle_play},
|
||||
{toolbarButtonWidth(icon::FAST_FORWARD), [&]() { if (toolButton("fast-forward", icon::FAST_FORWARD, "Seek forward")) seek_forward(); },
|
||||
"Seek forward", seek_forward},
|
||||
};
|
||||
if (can->liveStreaming()) {
|
||||
items.push_back({toolbarButtonWidth(icon::SKIP_END), [&]() {
|
||||
ImGui::BeginDisabled(!skip_to_end_enabled_);
|
||||
if (toolButton("skip-end", icon::SKIP_END, "Skip to the end")) skipToEnd();
|
||||
ImGui::EndDisabled();
|
||||
}, "Skip to the end", [this]() { skipToEnd(); }, skip_to_end_enabled_});
|
||||
}
|
||||
if (slider_ || msgs_received_) {
|
||||
|
||||
pushMonoFont(ImGui::GetFontSize());
|
||||
const float time_width = toolbarButtonWidth(time_text);
|
||||
popMonoFont();
|
||||
items.push_back({time_width,
|
||||
[&]() {
|
||||
pushMonoFont(ImGui::GetFontSize());
|
||||
if (toolButton("time_display", time_text.c_str(), time_tooltip)) toggleTimeDisplay();
|
||||
popMonoFont();
|
||||
},
|
||||
time_text, [this]() { toggleTimeDisplay(); }});
|
||||
}
|
||||
|
||||
const size_t spacer_index = items.size();
|
||||
if (!can->liveStreaming()) {
|
||||
items.push_back({toolbarButtonWidth(loop_icon), [&]() { if (toolButton("loop", loop_icon, "Loop playback")) loopPlaybackClicked(); },
|
||||
"Loop playback", [this]() { loopPlaybackClicked(); }});
|
||||
}
|
||||
items.push_back({speed_width, [&]() { drawSpeedDropdown(speed_width); }});
|
||||
if (!can->liveStreaming()) {
|
||||
ToolbarItem separator{TOOLBAR_SEPARATOR_EXTENT, []() {
|
||||
|
||||
const ImVec2 min = ImGui::GetCursorScreenPos();
|
||||
ImGui::Dummy(ImVec2(TOOLBAR_SEPARATOR_EXTENT, ImGui::GetFrameHeight()));
|
||||
const float x = std::floor(min.x + TOOLBAR_SEPARATOR_EXTENT * 0.5f);
|
||||
ImGui::GetWindowDrawList()->AddLine(ImVec2(x, min.y + 4.0f), ImVec2(x, min.y + ImGui::GetFrameHeight() - 4.0f), ImGui::GetColorU32(ImGuiCol_Separator));
|
||||
}};
|
||||
separator.in_menu = false;
|
||||
items.push_back(std::move(separator));
|
||||
items.push_back({toolbarButtonWidth(icon::INFO_CIRCLE),
|
||||
[&]() { if (toolButton("route_info", icon::INFO_CIRCLE, "View route details")) showRouteInfo(); },
|
||||
"View route details", [this]() { showRouteInfo(); }});
|
||||
}
|
||||
|
||||
drawToolbar(items, spacer_index);
|
||||
endToolbar();
|
||||
}
|
||||
|
||||
void VideoWidget::skipToEnd() {
|
||||
|
||||
speed_index_ = NORMAL_SPEED_INDEX;
|
||||
can->pause(false);
|
||||
can->seekTo(can->maxSeconds() + 1);
|
||||
}
|
||||
|
||||
void VideoWidget::toggleTimeDisplay() {
|
||||
settings.absolute_time = !settings.absolute_time;
|
||||
}
|
||||
|
||||
static std::string speedText(float speed, const char *suffix) {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%gx%s", speed, suffix);
|
||||
return buf;
|
||||
}
|
||||
|
||||
void VideoWidget::createSpeedDropdown() {
|
||||
speed_index_ = NORMAL_SPEED_INDEX;
|
||||
can->setSpeed(speeds[speed_index_]);
|
||||
speed_text_ = speedText(speeds[speed_index_], " ");
|
||||
}
|
||||
|
||||
void VideoWidget::drawSpeedDropdown(float width) {
|
||||
menuButton("speed_btn", speed_text_, "speed_menu", true, width);
|
||||
if (ImGui::BeginPopup("speed_menu")) {
|
||||
drawSpeedMenuItems();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void VideoWidget::drawSpeedMenuItems() {
|
||||
|
||||
|
||||
const float indent = ImGui::GetFontSize();
|
||||
float label_width = 0;
|
||||
for (int i = 0; i < (int)std::size(speeds); ++i) {
|
||||
label_width = std::max(label_width, ImGui::CalcTextSize(speedText(speeds[i], "").c_str()).x);
|
||||
}
|
||||
for (int i = 0; i < (int)std::size(speeds); ++i) {
|
||||
const float speed = speeds[i];
|
||||
if (radioMenuItem(speedText(speed, "").c_str(), speed_index_ == i, indent + label_width + indent)) {
|
||||
speed_index_ = i;
|
||||
can->setSpeed(speed);
|
||||
speed_text_ = speedText(speed, " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VideoWidget::createCameraWidget() {
|
||||
camera_tab_ = std::make_unique<TabBar>();
|
||||
camera_tab_->setAutoHide(true);
|
||||
|
||||
cam_widget_ = std::make_unique<StreamCameraView>("camerad", VISION_STREAM_ROAD);
|
||||
|
||||
slider_ = std::make_unique<Slider>();
|
||||
slider_->setTimeRange(can->minSeconds(), can->maxSeconds());
|
||||
|
||||
connections_.push_back(slider_->sliderReleased.connect([this]() { can->seekTo(slider_->currentSecond()); }));
|
||||
connections_.push_back(cam_widget_->clicked.connect([]() { can->pause(!can->isPaused()); }));
|
||||
connections_.push_back(cam_widget_->availableStreamsUpdated.connect([this](std::set<VisionStreamType> streams) { vipcAvailableStreamsUpdated(streams); }));
|
||||
connections_.push_back(camera_tab_->currentChanged.connect([this](int index) {
|
||||
if (index != -1) cam_widget_->setStreamType((VisionStreamType)camera_tab_->tabData(index));
|
||||
}));
|
||||
connections_.push_back(static_cast<ReplayStream *>(can)->qLogLoaded.connect([this](std::shared_ptr<LogReader> qlog) { cam_widget_->parseQLog(qlog); }));
|
||||
}
|
||||
|
||||
void VideoWidget::drawCameraWidget() {
|
||||
camera_tab_->draw();
|
||||
|
||||
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const float cam_height = std::max((float)MIN_VIDEO_HEIGHT, avail.y - SLIDER_HEIGHT - toolbarHeight());
|
||||
cam_widget_->draw(ImVec2(avail.x, cam_height), thumbnail_display_time_);
|
||||
|
||||
if (!slider_->isSliderDown()) slider_->setCurrentSecond(can->currentSec());
|
||||
slider_->draw(thumbnail_display_time_);
|
||||
updateSliderThumbnail();
|
||||
}
|
||||
|
||||
void VideoWidget::vipcAvailableStreamsUpdated(std::set<VisionStreamType> streams) {
|
||||
static const std::string stream_names[] = {"Road camera", "Driver camera", "Wide road camera"};
|
||||
for (int i = 0; i < streams.size(); ++i) {
|
||||
if (camera_tab_->count() <= i) {
|
||||
camera_tab_->addTab(std::string());
|
||||
}
|
||||
int type = *std::next(streams.begin(), i);
|
||||
camera_tab_->setTabText(i, stream_names[type]);
|
||||
camera_tab_->setTabData(i, type);
|
||||
}
|
||||
while (camera_tab_->count() > streams.size()) {
|
||||
camera_tab_->removeTab(camera_tab_->count() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoWidget::loopPlaybackClicked() {
|
||||
getReplay()->setLoop(!getReplay()->loop());
|
||||
}
|
||||
|
||||
void VideoWidget::timeRangeChanged() {
|
||||
const auto time_range = can->timeRange();
|
||||
if (can->liveStreaming()) {
|
||||
skip_to_end_enabled_ = !time_range.has_value();
|
||||
return;
|
||||
}
|
||||
time_range ? slider_->setTimeRange(time_range->first, time_range->second)
|
||||
: slider_->setTimeRange(can->minSeconds(), can->maxSeconds());
|
||||
}
|
||||
|
||||
std::string VideoWidget::formatTime(double sec, bool include_milliseconds) {
|
||||
if (settings.absolute_time)
|
||||
sec += std::chrono::duration<double>(can->beginDateTime().time_since_epoch()).count();
|
||||
return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time);
|
||||
}
|
||||
|
||||
void VideoWidget::setVisible(bool visible) {
|
||||
if (cam_widget_) cam_widget_->setVisible(visible);
|
||||
}
|
||||
|
||||
void VideoWidget::showThumbnail(double seconds) {
|
||||
if (can->liveStreaming()) return;
|
||||
thumbnail_display_time_ = seconds;
|
||||
}
|
||||
|
||||
void VideoWidget::showRouteInfo() {
|
||||
|
||||
route_info_dlgs_.push_back(std::make_unique<RouteInfoDlg>());
|
||||
}
|
||||
|
||||
void VideoWidget::updateSliderThumbnail() {
|
||||
if (slider_->underMouse()) {
|
||||
auto [min_sec, max_sec] = displayedTimeRange();
|
||||
showThumbnail(min_sec + (ImGui::GetMousePos().x - slider_->rect().Min.x) * (max_sec - min_sec) / slider_->width());
|
||||
} else if (slider_->mouseLeft()) {
|
||||
showThumbnail(-1);
|
||||
}
|
||||
}
|
||||
|
||||
float VideoWidget::sizeHintHeight() const {
|
||||
|
||||
return MIN_VIDEO_HEIGHT + SLIDER_HEIGHT + toolbarHeight();
|
||||
}
|
||||
|
||||
|
||||
float VideoWidget::defaultHeight(float width) const {
|
||||
if (!cam_widget_) return toolbarHeight();
|
||||
const float cam_height = std::max((float)MIN_VIDEO_HEIGHT, width / cam_widget_->frameAspectRatio());
|
||||
const float tab_height = camera_tab_->count() >= 2 ? ImGui::GetFrameHeight() : 0.0f;
|
||||
return cam_height + tab_height + SLIDER_HEIGHT + toolbarHeight();
|
||||
}
|
||||
|
||||
void VideoWidget::draw() {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f));
|
||||
if (!can->liveStreaming())
|
||||
drawCameraWidget();
|
||||
|
||||
drawPlaybackController();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
for (auto it = route_info_dlgs_.begin(); it != route_info_dlgs_.end();) {
|
||||
it = (*it)->draw() ? it + 1 : route_info_dlgs_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Slider::draw(double thumbnail_time) {
|
||||
ImGui::InvisibleButton("##slider", ImVec2(std::max(1.0f, ImGui::GetContentRegionAvail().x), SLIDER_HEIGHT));
|
||||
rect_ = ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
left_ = hovered_ && !hovered;
|
||||
hovered_ = hovered;
|
||||
|
||||
if (ImGui::IsItemActivated()) handleMousePress();
|
||||
if (slider_down_) {
|
||||
if (ImGui::IsItemActive()) {
|
||||
|
||||
setValue(pixelPosToRangeValue(ImGui::GetMousePos().x - click_offset_));
|
||||
} else {
|
||||
slider_down_ = false;
|
||||
sliderReleased();
|
||||
}
|
||||
}
|
||||
paint(thumbnail_time);
|
||||
}
|
||||
|
||||
ImRect Slider::handleRect() const {
|
||||
const float handle_width = SLIDER_LENGTH;
|
||||
const float handle_height = std::min(SLIDER_THICKNESS, rect_.GetHeight());
|
||||
const int range = std::max(1, maximum() - minimum());
|
||||
const float x = rect_.Min.x + (float)(value() - minimum()) / range * std::max(0.0f, width() - handle_width);
|
||||
const float y = rect_.GetCenter().y - handle_height / 2;
|
||||
return ImRect(ImVec2(x, y), ImVec2(x + handle_width, y + handle_height));
|
||||
}
|
||||
|
||||
|
||||
int Slider::pixelPosToRangeValue(float x) const {
|
||||
const float handle_width = SLIDER_LENGTH;
|
||||
const float span = std::max(1.0f, width() - handle_width);
|
||||
return minimum() + (int)std::lround((maximum() - minimum()) * std::clamp((x - rect_.Min.x) / span, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
void Slider::paint(double thumbnail_time) {
|
||||
ImDrawList *p = ImGui::GetWindowDrawList();
|
||||
|
||||
ImRect handle_rect = handleRect();
|
||||
ImRect groove_rect = rect_;
|
||||
|
||||
|
||||
float handle_height = handle_rect.GetHeight();
|
||||
const float groove_height = std::ceil(handle_height * 0.5f);
|
||||
const float center_y = rect_.GetCenter().y;
|
||||
groove_rect.Min.y = std::floor(center_y - groove_height / 2);
|
||||
groove_rect.Max.y = groove_rect.Min.y + groove_height;
|
||||
|
||||
p->AddRectFilled(groove_rect.Min, groove_rect.Max, timeline_colors[(int)TimelineType::None]);
|
||||
|
||||
double min = minimum() / factor;
|
||||
double max = maximum() / factor;
|
||||
const double span = std::max(max - min, 1e-9);
|
||||
|
||||
auto fillRange = [&](double begin, double end, ImU32 color) {
|
||||
if (begin > max || end < min) return;
|
||||
|
||||
|
||||
|
||||
ImRect r = groove_rect;
|
||||
r.Min.x = rect_.Min.x + std::floor(((std::max(min, begin) - min) / span) * width());
|
||||
r.Max.x = rect_.Min.x + std::floor(((std::min(max, end) - min) / span) * width()) + 1.0f;
|
||||
p->AddRectFilled(r.Min, r.Max, color);
|
||||
};
|
||||
|
||||
if (auto replay = getReplay()) {
|
||||
for (const auto &entry : *replay->getTimeline()) {
|
||||
fillRange(entry.start_time, entry.end_time, timeline_colors[(int)entry.type]);
|
||||
}
|
||||
|
||||
ImU32 empty_color = ImGui::GetColorU32(ImGuiCol_WindowBg, 160 / 255.0f);
|
||||
const auto event_data = replay->getEventData();
|
||||
for (const auto &[n, _] : replay->route().segments()) {
|
||||
if (!event_data->isSegmentLoaded(n))
|
||||
fillRange(n * 60.0, (n + 1) * 60.0, empty_color);
|
||||
}
|
||||
}
|
||||
|
||||
drawSliderHandle(p, handle_rect);
|
||||
|
||||
if (thumbnail_time >= 0) {
|
||||
float left = rect_.Min.x + (float)((thumbnail_time - min) * width() / span) - 1;
|
||||
ImRect rc(ImVec2(left, rect_.Min.y + 1), ImVec2(left + 2, rect_.Max.y - 1));
|
||||
p->AddRectFilled(rc.Min, rc.Max, ImGui::GetColorU32(ImGuiCol_Header), 1.5f);
|
||||
}
|
||||
}
|
||||
|
||||
void Slider::handleMousePress() {
|
||||
|
||||
const ImRect handle_rect = handleRect();
|
||||
if (handle_rect.Contains(ImGui::GetMousePos())) {
|
||||
slider_down_ = true;
|
||||
click_offset_ = ImGui::GetMousePos().x - handle_rect.Min.x;
|
||||
return;
|
||||
}
|
||||
setValue(minimum() + (int)(((maximum() - minimum()) * (ImGui::GetMousePos().x - rect_.Min.x)) / width()));
|
||||
sliderReleased();
|
||||
}
|
||||
|
||||
StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type)
|
||||
: CameraWidget(stream_name, stream_type) {
|
||||
big_thumbnail_texture_.mipmap = true;
|
||||
}
|
||||
|
||||
StreamCameraView::~StreamCameraView() {
|
||||
for (auto &pending : pending_thumbnails_) pending.done.wait();
|
||||
}
|
||||
|
||||
void StreamCameraView::parseQLog(std::shared_ptr<LogReader> qlog) {
|
||||
auto thumbnails = std::make_shared<std::map<uint64_t, RgbImage>>();
|
||||
auto done = ThreadPool::instance().run([qlog, thumbnails]() {
|
||||
for (const Event &e : qlog->events) {
|
||||
if (e.which != cereal::Event::Which::THUMBNAIL) continue;
|
||||
capnp::FlatArrayMessageReader reader(e.data);
|
||||
auto thumb_data = reader.getRoot<cereal::Event>().getThumbnail();
|
||||
auto image_data = thumb_data.getThumbnail();
|
||||
if (RgbImage thumb; decodeJpeg(image_data.begin(), image_data.size(), &thumb)) {
|
||||
(*thumbnails)[thumb_data.getTimestampEof()] = std::move(thumb);
|
||||
}
|
||||
}
|
||||
});
|
||||
pending_thumbnails_.push_back({std::move(done), std::move(thumbnails)});
|
||||
}
|
||||
|
||||
void StreamCameraView::collectThumbnails() {
|
||||
for (auto it = pending_thumbnails_.begin(); it != pending_thumbnails_.end();) {
|
||||
if (it->done.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
for (auto &[ts, thumb] : *it->thumbnails) big_thumbnails_[ts] = std::move(thumb);
|
||||
it = pending_thumbnails_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void StreamCameraView::draw(const ImVec2 &size, double thumbnail_time) {
|
||||
collectThumbnails();
|
||||
CameraWidget::draw(size);
|
||||
|
||||
ImDrawList *p = ImGui::GetWindowDrawList();
|
||||
bool scrubbing = false;
|
||||
if (thumbnail_time >= 0) {
|
||||
scrubbing = can->isPaused();
|
||||
scrubbing ? drawScrubThumbnail(p, thumbnail_time) : drawThumbnail(p, thumbnail_time);
|
||||
}
|
||||
if (auto alert = getReplay()->findAlertAtTime(scrubbing ? thumbnail_time : can->currentSec())) {
|
||||
drawAlert(p, rect(), *alert, ImGui::GetFontSize());
|
||||
}
|
||||
|
||||
if (can->isPaused()) {
|
||||
ImFont *font = boldFont();
|
||||
const char *text = "PAUSED";
|
||||
const ImVec2 text_size = font->CalcTextSizeA(POINT_16_FONT_SIZE, FLT_MAX, 0.0f, text);
|
||||
const ImVec2 center = rect().GetCenter();
|
||||
p->AddText(font, POINT_16_FONT_SIZE, ImVec2(center.x - text_size.x / 2, center.y - text_size.y / 2),
|
||||
IM_COL32(200, 200, 200, static_cast<int>(255 * 0.7f)), text);
|
||||
}
|
||||
}
|
||||
|
||||
const RgbImage *StreamCameraView::thumbnailAt(double sec, uint64_t *mono_time) {
|
||||
auto it = big_thumbnails_.lower_bound(can->toMonoTime(sec));
|
||||
if (it == big_thumbnails_.end()) return nullptr;
|
||||
if (big_thumbnail_texture_.id == 0 || big_thumbnail_texture_.key != it->first) {
|
||||
big_thumbnail_texture_.upload(it->second);
|
||||
big_thumbnail_texture_.key = it->first;
|
||||
}
|
||||
if (mono_time) *mono_time = it->first;
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
void StreamCameraView::drawScrubThumbnail(ImDrawList *p, double sec) {
|
||||
p->AddRectFilled(rect().Min, rect().Max, IM_COL32(0, 0, 0, 255));
|
||||
if (const RgbImage *image = thumbnailAt(sec, nullptr)) {
|
||||
|
||||
const float scale = std::min(width() / image->width, height() / image->height);
|
||||
const ImVec2 scaled_size(std::floor(image->width * scale), std::floor(image->height * scale));
|
||||
const ImVec2 center = rect().GetCenter();
|
||||
const ImVec2 thumb_min(center.x - (int)(scaled_size.x / 2), center.y - (int)(scaled_size.y / 2));
|
||||
ImRect thumb_rect(thumb_min, ImVec2(thumb_min.x + scaled_size.x, thumb_min.y + scaled_size.y));
|
||||
p->AddImage(big_thumbnail_texture_.ref(), thumb_rect.Min, thumb_rect.Max);
|
||||
drawTime(p, thumb_rect, sec);
|
||||
}
|
||||
}
|
||||
|
||||
void StreamCameraView::drawThumbnail(ImDrawList *p, double sec) {
|
||||
uint64_t mono_time = 0;
|
||||
if (const RgbImage *image = thumbnailAt(sec, &mono_time)) {
|
||||
|
||||
const int h = MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2;
|
||||
const int w = std::max(1, (int)std::lround((double)image->width * h / image->height));
|
||||
auto [min_sec, max_sec] = displayedTimeRange();
|
||||
int pos = (sec - min_sec) * width() / (max_sec - min_sec);
|
||||
const int max_x = (int)width() - w - THUMBNAIL_MARGIN + 1;
|
||||
int x = std::clamp(pos - w / 2, THUMBNAIL_MARGIN, std::max(THUMBNAIL_MARGIN, max_x));
|
||||
int y = height() - h - THUMBNAIL_MARGIN;
|
||||
|
||||
ImRect thumb_rect(ImVec2(rect().Min.x + x, rect().Min.y + y), ImVec2(rect().Min.x + x + w, rect().Min.y + y + h));
|
||||
p->AddImage(big_thumbnail_texture_.ref(), thumb_rect.Min, thumb_rect.Max);
|
||||
p->AddRect(thumb_rect.Min, thumb_rect.Max, paletteBrightText(), 0.0f, 0, 2.0f);
|
||||
if (auto alert = getReplay()->findAlertAtTime(can->toSeconds(mono_time))) {
|
||||
drawAlert(p, thumb_rect, *alert, POINT_10_FONT_SIZE);
|
||||
}
|
||||
drawTime(p, thumb_rect, sec);
|
||||
}
|
||||
}
|
||||
|
||||
void StreamCameraView::drawTime(ImDrawList *p, const ImRect &rect, double seconds) {
|
||||
char text[32];
|
||||
snprintf(text, sizeof(text), "%.3f", seconds);
|
||||
ImFont *font = ImGui::GetFont();
|
||||
const ImVec2 text_size = font->CalcTextSizeA(POINT_10_FONT_SIZE, FLT_MAX, 0.0f, text);
|
||||
|
||||
p->AddText(font, POINT_10_FONT_SIZE, ImVec2(rect.GetCenter().x - text_size.x / 2, rect.Max.y - THUMBNAIL_MARGIN - text_size.y),
|
||||
paletteBrightText(), text);
|
||||
}
|
||||
|
||||
void StreamCameraView::drawAlert(ImDrawList *p, const ImRect &rect, const Timeline::Entry &alert, float font_size) {
|
||||
const ImU32 pen = paletteBrightText();
|
||||
ImU32 color = withAlpha(timeline_colors[int(alert.type)], 128);
|
||||
std::string text = alert.text1;
|
||||
if (!alert.text2.empty()) text += "\n" + alert.text2;
|
||||
|
||||
ImRect text_rect(ImVec2(rect.Min.x + 1, rect.Min.y + 1), ImVec2(rect.Max.x - 1, rect.Max.y - 1));
|
||||
ImFont *font = ImGui::GetFont();
|
||||
const float wrap_width = std::max(1.0f, text_rect.GetWidth());
|
||||
const ImVec2 r = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text.c_str());
|
||||
p->AddRectFilled(ImVec2(text_rect.Min.x, text_rect.Min.y), ImVec2(text_rect.Max.x, text_rect.Min.y + r.y), color);
|
||||
|
||||
float y = text_rect.Min.y;
|
||||
for (const auto &line : utils::split(text, '\n')) {
|
||||
const ImVec2 line_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, line.c_str());
|
||||
p->AddText(font, font_size, ImVec2(text_rect.Min.x + (text_rect.GetWidth() - line_size.x) / 2, y), pen, line.c_str(), nullptr, wrap_width);
|
||||
y += line_size.y;
|
||||
}
|
||||
}
|
||||
121
iqpilot/tools/cabana/ui/widgets/videowidget.h
Normal file
121
iqpilot/tools/cabana/ui/widgets/videowidget.h
Normal file
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
#include "tools/cabana/ui/widgets/cameraview.h"
|
||||
#include "tools/cabana/ui/widgets/tabbar.h"
|
||||
#include "tools/cabana/ui/tools/routeinfo.h"
|
||||
#include "tools/replay/logreader.h"
|
||||
#include "tools/cabana/streams/replaystream.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
|
||||
class Slider {
|
||||
public:
|
||||
Slider() = default;
|
||||
double currentSecond() const { return value() / factor; }
|
||||
void setCurrentSecond(double sec) { setValue(sec * factor); }
|
||||
void setTimeRange(double min, double max) { setRange(min * factor, max * factor); }
|
||||
int value() const { return value_; }
|
||||
void setValue(int v) { value_ = std::clamp(v, minimum_, maximum_); }
|
||||
void setRange(int min, int max) { minimum_ = min; maximum_ = std::max(min, max); setValue(value_); }
|
||||
int minimum() const { return minimum_; }
|
||||
int maximum() const { return maximum_; }
|
||||
bool isSliderDown() const { return slider_down_; }
|
||||
float width() const { return rect_.GetWidth(); }
|
||||
const ImRect &rect() const { return rect_; }
|
||||
bool underMouse() const { return hovered_; }
|
||||
bool mouseLeft() const { return left_; }
|
||||
void draw(double thumbnail_time);
|
||||
static constexpr double factor = 1000.0;
|
||||
|
||||
Observable<> sliderReleased;
|
||||
|
||||
private:
|
||||
void handleMousePress();
|
||||
void paint(double thumbnail_time);
|
||||
ImRect handleRect() const;
|
||||
int pixelPosToRangeValue(float x) const;
|
||||
int minimum_ = 0;
|
||||
int maximum_ = 99;
|
||||
int value_ = 0;
|
||||
bool slider_down_ = false;
|
||||
float click_offset_ = 0;
|
||||
bool hovered_ = false;
|
||||
bool left_ = false;
|
||||
ImRect rect_;
|
||||
};
|
||||
|
||||
class StreamCameraView : public CameraWidget {
|
||||
public:
|
||||
StreamCameraView(std::string stream_name, VisionStreamType stream_type);
|
||||
~StreamCameraView();
|
||||
void draw(const ImVec2 &size, double thumbnail_time);
|
||||
void parseQLog(std::shared_ptr<LogReader> qlog);
|
||||
|
||||
private:
|
||||
struct PendingThumbnails {
|
||||
std::future<void> done;
|
||||
std::shared_ptr<std::map<uint64_t, RgbImage>> thumbnails;
|
||||
};
|
||||
void collectThumbnails();
|
||||
|
||||
const RgbImage *thumbnailAt(double sec, uint64_t *mono_time);
|
||||
void drawAlert(ImDrawList *p, const ImRect &rect, const Timeline::Entry &alert, float font_size);
|
||||
void drawThumbnail(ImDrawList *p, double sec);
|
||||
void drawScrubThumbnail(ImDrawList *p, double sec);
|
||||
void drawTime(ImDrawList *p, const ImRect &rect, double seconds);
|
||||
|
||||
std::map<uint64_t, RgbImage> big_thumbnails_;
|
||||
GlTexture big_thumbnail_texture_;
|
||||
std::vector<PendingThumbnails> pending_thumbnails_;
|
||||
};
|
||||
|
||||
class VideoWidget {
|
||||
public:
|
||||
VideoWidget();
|
||||
void draw();
|
||||
float sizeHintHeight() const;
|
||||
float defaultHeight(float width) const;
|
||||
|
||||
|
||||
void setVisible(bool visible);
|
||||
void showThumbnail(double seconds);
|
||||
std::string whatsThis() const;
|
||||
|
||||
private:
|
||||
void updateSliderThumbnail();
|
||||
std::string formatTime(double sec, bool include_milliseconds = false);
|
||||
void timeRangeChanged();
|
||||
void createCameraWidget();
|
||||
void drawCameraWidget();
|
||||
void drawPlaybackController();
|
||||
void skipToEnd();
|
||||
void toggleTimeDisplay();
|
||||
void createSpeedDropdown();
|
||||
void drawSpeedDropdown(float width);
|
||||
void drawSpeedMenuItems();
|
||||
void loopPlaybackClicked();
|
||||
void vipcAvailableStreamsUpdated(std::set<VisionStreamType> streams);
|
||||
void showRouteInfo();
|
||||
|
||||
std::unique_ptr<StreamCameraView> cam_widget_;
|
||||
std::string speed_text_;
|
||||
int speed_index_ = -1;
|
||||
bool skip_to_end_enabled_ = true;
|
||||
bool msgs_received_ = false;
|
||||
double thumbnail_display_time_ = -1;
|
||||
std::unique_ptr<Slider> slider_;
|
||||
std::unique_ptr<TabBar> camera_tab_;
|
||||
std::vector<std::unique_ptr<RouteInfoDlg>> route_info_dlgs_;
|
||||
Connections connections_;
|
||||
};
|
||||
Reference in New Issue
Block a user