IQ.Pilot Release Commit @ 0b96bd5

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-02 16:46:45 -05:00
parent 4fb3da761f
commit 7745f48100
162 changed files with 12835 additions and 9476 deletions

View 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;
}
}

View 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();

View 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;
}

View 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_;
};

View 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;
}

View 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 *> &currentCharts() { 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;
};

View 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);
}
}
}

View 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;
};

View 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();
}

View 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;
};

View 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);
}

View 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;
};

View 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);
}
}
}

View 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();
}

View 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);
}
}
}

View 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();
}

View 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);
}

View 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_;
};

View 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();
}

View 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_;
};

View 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_);
}
}

View 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_;
};

View 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[] = {{"&nbsp;", " "}};
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, "&#9632;") == 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;
}

View 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;
};

View 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";
}

View 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());
}
}

View 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();
}

View 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);
}

View 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, &center);
ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.4f, &right, &center);
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);
}
}

View 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_;
};

View 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(); }

View 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();
}

View 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();
}
}

View 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;
};

View 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;
}

View 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;
};

View 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();
}

View 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;
};

View 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;
};

View 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;
}

View 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();

View 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">&nbsp;x&nbsp;</span>,
<span style="background-color:lightGray;color:gray">&nbsp;Backspace&nbsp;</span>,
<span style="background-color:lightGray;color:gray">&nbsp;Delete&nbsp;</span><br />
Change endianness: <span style="background-color:lightGray;color:gray">&nbsp;e&nbsp; </span><br />
Change signedness: <span style="background-color:lightGray;color:gray">&nbsp;s&nbsp;</span><br />
Open chart:
<span style="background-color:lightGray;color:gray">&nbsp;c&nbsp;</span>,
<span style="background-color:lightGray;color:gray">&nbsp;p&nbsp;</span>,
<span style="background-color:lightGray;color:gray">&nbsp;g&nbsp;</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();
}
}

View 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_;
};

View 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;
}

View 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);
};

View 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();
}

View 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;
};

View 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);
}

View 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_;
};

View 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();
}

View 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);

View 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;">&#9632; </span> constant changing<br />
<span style="color:blue;">&#9632; </span> increasing<br />
<span style="color:red;">&#9632; </span> decreasing<br />
<span style="color:gray">Shortcuts</span><br />
Horizontal Scrolling: <span style="background-color:lightGray;color:gray">&nbsp;shift+wheel&nbsp;</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();
}

View 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_;
};

View 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;
}
}
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include "imgui.h"
bool beginScrollableTabBar(const char *str_id, ImGuiTabBarFlags flags = 0);
void endScrollableTabBar();

View 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", &current, 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;
}

View 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_;
};

View 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);
}

View 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;
};

View 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&nbsp;&nbsp;&nbsp;" +
colorName(timeline_colors[(int)TimelineType::Engaged]) + " Engaged<br />\n" +
colorName(timeline_colors[(int)TimelineType::UserBookmark]) + " User Flag&nbsp;&nbsp;&nbsp;" +
colorName(timeline_colors[(int)TimelineType::AlertInfo]) + " Info<br />\n" +
colorName(timeline_colors[(int)TimelineType::AlertWarning]) + " Warning&nbsp;&nbsp;&nbsp;" +
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\">&nbsp;space&nbsp;</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;
}
}

View 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_;
};