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